Spring Security 4.X(XML文件配置session超时,单点登录-session并发控制,退出/logout)

目录

前言

一、Java web设置session超时

二、session并发控制

三、退出/logout设置


前言

        本文是继SSM项目集成Spring Security 4.X版本(使用spring-security.xml 配置文件方式)_spring security4.x 会话管理配置文件版-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/u011529483/article/details/135699004?spm=1001.2014.3001.5501文章之后的配置,继续实现了 session超时,单点登录-session并发控制,退出/logout 三项配置。

在配置过程中遇到了一些问题。正因如此才使我想要写文章记录。


一、Java web设置session超时

java web应用设置session超时(默认是 30 分钟),也就是失效时间的方法有:

按优先执行权从高到底是:

1. 容器的配置中设置 如:tomcat的web.xml中设置

<session-config>
<session-timeout> 30 </session-timeout>
</session-config>

2. java项目的web.xml中设置(本文中采用了此种方式):

  <!--设置session超时,单位分钟-->
  <session-config>
    <session-timeout>2</session-timeout>
  </session-config>

3. 通过java代码设置

        HttpSession session = request.getSession();
        session.setMaxInactiveInterval(120); //单位秒

Spring Security 中给我们提供了security:session-management标签进行session的配置管理,spring-security.xml 配置文件截图如下:

先来看看 invalid-session-url="/s_timeout.jsp" 配置的运行效果,手动设置session超时时间为 2 分钟,项目的web.xml文件中设置:

运行项目,登录成功后,等待2分钟后访问服务器。2分钟后session超时失效。

登录成功后访问了菜单查询:

2 分钟后再次访问菜单查询跳转到了超时页面,说明我们的 invalid-session-url="/s_timeout.jsp"  配置生效了。

二、session并发控制

        什么是session并发控制,就是同一个账号多处客户端同一时间段发起的请求,也就生成了多个session会话。session并发控制就是控制这些session会话的数量。现在我们将session会话数量设置为 1 ,即同时段同账号只能有一个用户登录成功,后面登录的挤掉前面登录的。spring-security.xml 配置如下:

        <!-- session管理,
        invalid-session-url:指定使用已经超时的sessionId(保存在Cookie中)进行请求需要重定向的页面或路径。
        max-sessions:默认值为1,session并发数量控制。控制同一用户在系统中同时允许存在的已经通过认证的session数量。当超过这个值时,
        Spring Security的默认策略是将先前的设为无效。如果要限制用户再次登录可以设置concurrency-control的error-if-maximum-exceeded的值为true。
        -->
        <security:session-management invalid-session-url="/s_timeout.jsp">
            <security:concurrency-control max-sessions="1" error-if-maximum-exceeded="false"/>
        </security:session-management>

max-sessions="1" 设置session并发数量为 1。

error-if-maximum-exceeded="false" 设置为false后面登录的挤掉前面登录的。设置为true表示前面登录的保留,后面登录的被拒绝。

要使以上设置生效还需要在项目的web.xml文件中添加监听

  <!--加载SpringSecurity,Session并发控制-->
  <listener>
    <listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class>
  </listener>

网上很多都说了这两步配置,但是我配置后不生效。

后面我在网上查了原因,经过调试后实现了单点登录(同一个账号只能有一处登录在线)的效果。需要在 SysUser 实体类中(或自定义用户详情类中)重写equals和hashCode方法:

package com.wqbr.wqdemotwo.domain;


import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.*;

/**
 * 系统用户,封装用户数据,实现 UserDetails 接口
 * @author lv
 * @date 2024年1月11日
 */
public class SysUser implements UserDetails {

  private static final long serialVersionUID = 1L;

  private String id;
  private String username; //从UserDetails的重写方法中返回
  private String password; //从UserDetails的重写方法中返回
  private Date addtime;
  private boolean accountnonexpired; //账户是否过期,从UserDetails的重写方法中返回
  private boolean accountnonlocked; //账户是否锁定,从UserDetails的重写方法中返回
  private boolean credentialsnonexpired; //密码是否过期,从UserDetails的重写方法中返回
  private boolean enabled; //账户是否可用,从UserDetails的重写方法中返回

  // 储存用户拥有的所有权限
  private List<GrantedAuthority> authorities = new ArrayList<>(); //从UserDetails的重写方法中返回


  public String getId() {
    return id;
  }

  public void setId(String id) {
    this.id = id;
  }

  public void setUsername(String username) {
    this.username = username;
  }

  public void setPassword(String password) {
    this.password = password;
  }

  public Date getAddtime() {
    return addtime;
  }

  public void setAddtime(Date addtime) {
    this.addtime = addtime;
  }

  // 返回用户权限,上面声明了权限集合对象 authorities
  @Override
  public Collection<? extends GrantedAuthority> getAuthorities() {
    return this.authorities;
  }

  public void setAuthorities(List<GrantedAuthority> authorities) {
    this.authorities = authorities;
  }

  // 返回用户密码,上面声明了属性 password
  @Override
  public String getPassword() {
    return password;
  }

  // 返回用户名,上面声明了属性 username
  @Override
  public String getUsername() {
    return username;
  }

  @Override
  public boolean isAccountNonExpired() {
    return accountnonexpired;
  }

  public void setAccountnonexpired(boolean accountnonexpired) {
    this.accountnonexpired = accountnonexpired;
  }

  @Override
  public boolean isAccountNonLocked() {
    return accountnonlocked;
  }

  public void setAccountnonlocked(boolean accountnonlocked) {
    this.accountnonlocked = accountnonlocked;
  }

  @Override
  public boolean isCredentialsNonExpired() {
    return credentialsnonexpired;
  }

  public void setCredentialsnonexpired(boolean credentialsnonexpired) {
    this.credentialsnonexpired = credentialsnonexpired;
  }

  @Override
  public boolean isEnabled() {
    return enabled;
  }

  public void setEnabled(boolean enabled) {
    this.enabled = enabled;
  }

  /*重写equals和hashCode方法,因为如果是自定义的UserDetails 则需要重定义equal和hashcode。
  另外Spring Security 中通过 SessionRegistryImpl 类来实现对会话信息的统一管理,而 SessionRegistryImpl 类中定义了
  private final ConcurrentMap<Object, Set<String>> principals; 是一个Map集合,其中的key是指定的对象。
  在 JavaSE 中用对象做 key,需要重写 equals 方法和 hashCode 方法,否则第一次存完数据,下次就找不到了。*/
  @Override
  public boolean equals(Object obj) {
    if (obj instanceof SysUser) {
      return username.equals(((SysUser) obj).username);
    }
    return false;
  }
  @Override
  public int hashCode() {
    return username.hashCode();
  }

}

为什么要重写这两个方法呢?因为自己动手创建用户实体类实现自定义用户详情验证,需要显式实现equals和hashCode方法。而我的用户实体类 SysUser 实现了 UserDetails接口。没有重写equals和hashCode方法,所以

导致这样配置的效果没有实现。另外Spring Security 中通过 SessionRegistryImpl 类来实现对会话信息的统一管理,而 SessionRegistryImpl 类中定义了 private final ConcurrentMap<Object, Set<String>> principals; 是一个Map集合,其中的key是指定的对象。 在 JavaSE 中用对象做 key,需要重写 equals 方法和 hashCode 方法。

现在来看看我们重写 equals 方法和 hashCode 方法后项目的运行效果:项目启动完毕后先在谷歌浏览器登录,登录成功后我访问了菜单请求如图:

 说明登录成功,并顺利访问资源。且idea控制台打印的session信息:

然后我们在另外一种浏览器(360)中再次登录,

登录成功,顺利请求到资源。且idea控制台打印的session信息:

可以看到360浏览器和谷歌浏览器访问的IDEA控制台打印出的session ID 不同,说明同一个用户发起了两次请求。此时再访问谷歌浏览器去请求一次资源得到如下结果:

这段英文翻译如下:

继续刷新请求一次就重定向到会话超时页面:

此时360浏览器登录的用户是可以继续请求资源的。spring-security控制session并发数量,实现单点登录就实现了。

三、退出/logout设置

spring-security.xml 文件中的配置:

<!--<security:logout/>:注销功能
        logout-url="/logout":退出过滤器默认的拦截路径,springSecurity内LogoutFilter要拦截的url(向这/logout发送请求来注销)
        logout-success-url:用户退出后要被重定向的url
        invalidate-session:默认为true,用户在退出后Http session失效
        success-handler-ref:指定一个bean(需要实现LogoutSuccessHandler接口),用来自定义退出成功后的操作
        delete-cookies="JSESSIONID":删除session对应的cookie
        -->
        <security:logout logout-url="/logout" logout-success-url="/login" invalidate-session="true" delete-cookies="JSESSIONID"/>

配置后运行项目,点击 退出 后 logout-success-url="/login" 重定向配置没有生效。查到原因:不能

<security:intercept-url pattern="/login" access="permitAll()"/> 这样配置/login路径的放行策略。需要放到spring security过滤链策略外,配置如下:

<!--security="none"指定不受Spring Security管理-->
<security:http security="none" pattern="/login" />

spring-security.xml 完整的配置代码:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:security="http://www.springframework.org/schema/security"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/security
    http://www.springframework.org/schema/security/spring-security.xsd">

    <!--指定不受Spring Security管理-->
    <security:http security="none" pattern="/login" />
    <!--spring过滤器链配置
        1) 需要拦截什么资源
        2) 什么资源对应什么角色权限
        3) 定制认证方式: HttpBasic or FormLogin
        4) 自定义登录页面,定义登录请求地址,定义错误处理方式
    -->
    <security:http>

        <!--
            使用form-login的方式进行认证
                login-page:指定获取登录页面的url(需要编写controller返回登录页面)
                login-processing-url:指定登录页面中post请求提交到哪里的url(不需要编写controller,框架已实现)
                default-target-url:指定登录成功后,跳转到哪个url(需要编写controller)
                authentication-success-handler-ref:指定登录成功后,由哪个类来进行处理
                authentication-failure-handler-ref:指定登录失败后,由哪个类来进行处理
                username-parameter:指定登录表单中用户名的input中name值,如果这里不配置,则默认为username
                password-parameter:指定登录表单中密码的input中name值,如果这里不配置,则默认为password
        -->
        <security:form-login login-page="/login" login-processing-url="/spring_security_check"
                             authentication-success-handler-ref="myAuthenticationSuccessHandler"
                             authentication-failure-handler-ref="myAuthenticationFailureHandler"/>

        <!-- 关闭csrf的保护-->
        <security:csrf disabled="true"/>

        <!-- 配置资源拦截规则
            pattern属性指定资源目录: 即需要拦截的资源  /* 代表根目录下的一级目录  /** 代表根目录下的所有目录
            access(SpEL)方法执行Spring EL表达式。提供如下表达式:
                permitALL():设置那些路径可以直接访问,不需要认证。直接返回true
                isAnonymous():只有匿名用户可以访问,登录用户不可访问
                isAuthenticated():需要身份认证成功才能访问。如果认证用户不是匿名用户,则返回true,认证通过
                isFullyAuthenticated():需要身份认证成功才能访问。如果认证用户不是匿名用户或记住我的用户,则返回true,认证通过
                其它自行查找......
        -->
        <!--开始配置拦截规则,注意拦截规则的位置顺序(如不需要身份认证的规则,要放在前面,需要身份认证的规则放在后面)-->
        <!--permitAll()不需要身份认证,无条件放行-->
        <!--<security:intercept-url pattern="/login" access="permitAll()"/>-->
        <security:intercept-url pattern="/system/index" access="permitAll()"/>
        <security:intercept-url pattern="/s_timeout.jsp" access="permitAll()"/>

        <!--进行权限划分:hasRole('ROLE_USER'):表示拥有 ROLE_USER 权限的用户可以访问
        hasRole('ROLE_ALL'):表示拥有 ROLE_ALL 权限的用户可以访问
        -->
        <security:intercept-url pattern="/system/add" access="hasAuthority('admin')"/>
        <security:intercept-url pattern="/system/list" access="hasAuthority('ROLE_ALL')"/>

        <!--permitAll()不需要身份认证,无条件放行静态资源-->
        <security:intercept-url pattern="/js/**" access="permitAll()"/>

        <!--拦截所有页面,需要身份认证成功才能访问。如果认证用户不是匿名用户或记住我的用户,则返回true,认证通过-->
        <security:intercept-url pattern="/**" access="isFullyAuthenticated()"/>
        <!--结束配置拦截规则-->

        <!-- 自定义用户访问权限不足的处理方式(需要编写controller返回权限不足的页面) -->
        <security:access-denied-handler error-page="/accessDeny"/>

        <!-- session管理,
        invalid-session-url:指定使用已经超时的sessionId(保存在Cookie中)进行请求需要重定向的页面或路径。
        max-sessions:默认值为1,session并发数量控制。控制同一用户在系统中同时允许存在的已经通过认证的session数量。当超过这个值时,
        Spring Security的默认策略是将先前的设为无效。如果要限制用户再次登录可以设置concurrency-control的error-if-maximum-exceeded的值为true。
        -->
        <security:session-management invalid-session-url="/s_timeout.jsp">
            <security:concurrency-control max-sessions="1" error-if-maximum-exceeded="false"/>
        </security:session-management>
        <!--<security:session-management session-authentication-error-url="/session" invalid-session-url="/session">
            <security:concurrency-control max-sessions="1" session-registry-alias="sessionRegistry" error-if-maximum-exceeded="false" expired-url="/session"/>
        </security:session-management>-->

        <!--加上Remember Me功能,token-validity-seconds:有效时间(秒)-->
        <!--<security:remember-me token-repository-ref="jdbcTokenRepository" token-validity-seconds="604800"/>-->

        <!--<security:logout/>:注销功能
        logout-url="/logout":退出过滤器默认的拦截路径,springSecurity内LogoutFilter要拦截的url(向这/logout发送请求来注销)
        logout-success-url:用户退出后要被重定向的url
        invalidate-session:默认为true,用户在退出后Http session失效
        success-handler-ref:指定一个bean(需要实现LogoutSuccessHandler接口),用来自定义退出成功后的操作
        delete-cookies="JSESSIONID":删除session对应的cookie
        -->
        <security:logout logout-url="/logout" logout-success-url="/login" invalidate-session="true" delete-cookies="JSESSIONID"/>
    </security:http>

    <!--身份验证管理器-->
    <security:authentication-manager>
        <!--
            自定义授权提供类MyUserDetailsService,获得登录用户的用户详情信息。此类实现UserDetailsService接口。
            user-service-ref="myUserDetailsService" : 指定 UserDetailsService 接口的实现类
            最终都要返回一个UserDetail,用户详情
        -->
        <security:authentication-provider user-service-ref="myUserDetailsService">
            <!-- 配置:加密算法对用户输入的密码进行加密,然后和数据库的密码进行配对 -->
            <!--<security:password-encoder ref="bCryptPasswordEncoder"/>-->
        </security:authentication-provider>
    </security:authentication-manager>

    <!--创建 springSecurity 密码加密工具类,使用PasswordEncoder 接口的实现,也可以使用别的-->
    <!--<bean id="bCryptPasswordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"></bean>-->


    <!--springSecurity实现 remember me 功能:
        如果用户登录选择 remember me ,springSecurity会将其cookie值存入数据库,来实现remember me 功能
        JdbcTokenRepositoryImpl 用来存取cookie值-->
    <!--<bean id="jdbcTokenRepository" class="org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl">
        <property name="dataSource" ref="dataSource"/> &lt;!&ndash;数据库数据源&ndash;&gt;
        &lt;!&ndash;<property name="createTableOnStartup" value="true"/>&ndash;&gt; &lt;!&ndash;createTableOnStartup属性是当项目启动时,springSecurity创建表存储remember me相关信息,第二次启动时要注释这个属性&ndash;&gt;
    </bean>-->

</beans>

如此配置运行效果,点击退出后重定向到了登录页面。即logout-success-url="/login"配置生效。

退出页面代码:

<div>
<%--    <form action="<c:url value='/logout'/>" method="post">
        <input id="logout" type="submit" value="退出系统">
    </form>--%>
    <a href="<c:url value='/logout'/>">退出系统</a>
</div>

另外再看看退出时的 delete-cookies="JSESSIONID" 配置,如下idea控制台的截图可以看出,退出后重定向到退出的Controller请求时session ID重新生成了,说明浏览器cookies中的上一次JSESSIONID已经失效。


好了,小伙伴们这篇文章就到这里了,希望多留下你们的足迹,谢谢。

相关推荐

  1. go-gin中session实现redis前缀和db库选择+登录

    2024-02-07 05:54:06       25 阅读
  2. <span style='color:red;'>Session</span>

    Session

    2024-02-07 05:54:06      38 阅读
  3. 微服务Redis-Session共享登录状态

    2024-02-07 05:54:06       41 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-02-07 05:54:06       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-02-07 05:54:06       101 阅读
  3. 在Django里面运行非项目文件

    2024-02-07 05:54:06       82 阅读
  4. Python语言-面向对象

    2024-02-07 05:54:06       91 阅读

热门阅读

  1. 嵌入式硬件工程师与嵌入式软件工程师

    2024-02-07 05:54:06       63 阅读
  2. Lua可变参数函数

    2024-02-07 05:54:06       55 阅读
  3. 【计算机视觉】浅谈计算机视觉中的Transformer

    2024-02-07 05:54:06       51 阅读
  4. ubuntu22.04@laptop OpenCV Get Started: 003_image_resizing

    2024-02-07 05:54:06       60 阅读
  5. mysql清空表数据后如何让自增ID仍从1开始

    2024-02-07 05:54:06       51 阅读
  6. Go语言教学(一)起源

    2024-02-07 05:54:06       45 阅读