目录
前言
一、Java web设置session超时
二、session并发控制
三、退出/logout设置
前言
本文是继SSM项目集成Spring Security 4.X版本(使用spring-security.xml 配置文件方式)_spring security4.x 会话管理配置文件版-CSDN博客https://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@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return this.authorities;}public void setAuthorities(List<GrantedAuthority> authorities) {this.authorities = authorities;}// 返回用户密码,上面声明了属性 password@Overridepublic String getPassword() {return password;}// 返回用户名,上面声明了属性 username@Overridepublic String getUsername() {return username;}@Overridepublic boolean isAccountNonExpired() {return accountnonexpired;}public void setAccountnonexpired(boolean accountnonexpired) {this.accountnonexpired = accountnonexpired;}@Overridepublic boolean isAccountNonLocked() {return accountnonlocked;}public void setAccountnonlocked(boolean accountnonlocked) {this.accountnonlocked = accountnonlocked;}@Overridepublic boolean isCredentialsNonExpired() {return credentialsnonexpired;}public void setCredentialsnonexpired(boolean credentialsnonexpired) {this.credentialsnonexpired = credentialsnonexpired;}@Overridepublic 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 方法,否则第一次存完数据,下次就找不到了。*/@Overridepublic boolean equals(Object obj) {if (obj instanceof SysUser) {return username.equals(((SysUser) obj).username);}return false;}@Overridepublic 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:用户退出后要被重定向的urlinvalidate-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/beanshttp://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/securityhttp://www.springframework.org/schema/security/spring-security.xsd"><!--指定不受Spring Security管理--><security:http security="none" pattern="/login" /><!--spring过滤器链配置1) 需要拦截什么资源2) 什么资源对应什么角色权限3) 定制认证方式: HttpBasic or FormLogin4) 自定义登录页面,定义登录请求地址,定义错误处理方式--><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值,如果这里不配置,则默认为usernamepassword-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():设置那些路径可以直接访问,不需要认证。直接返回trueisAnonymous():只有匿名用户可以访问,登录用户不可访问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:用户退出后要被重定向的urlinvalidate-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"/> <!–数据库数据源–><!–<property name="createTableOnStartup" value="true"/>–> <!–createTableOnStartup属性是当项目启动时,springSecurity创建表存储remember me相关信息,第二次启动时要注释这个属性–></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已经失效。
好了,小伙伴们这篇文章就到这里了,希望多留下你们的足迹,谢谢。