SpringSecurity⼊门(四):Session会话管理
本⽂在⼀⽂的代码基础上介绍Spring Security的 Session 会话管理。
Session 会话管理的配置⽅法
Session 会话管理需要在configure(HttpSecurity http)⽅法中通过http.sessionManagement()开启配置。此处对http.sessionManagement()返回值的主要⽅法进⾏说明,这些⽅法涉及 Session 会话管理的配置,具体如下:invalidSessionUrl(String invalidSessionUrl):指定会话失效时(请求携带⽆效的 JSESSIONID 访问系统)重定向的 URL,默认重定向到登录页⾯。
invalidSessionStrategy(InvalidSessionStrategy invalidSessionStrategy):指定会话失效时(请求携带⽆效的 JSESSIONID 访问系统)的处理策略。
maximumSessions(int maximumSessions):指定每个⽤户的最⼤并发会话数量,-1 表⽰不限数量。
maxSessionsPreventsLogin(boolean maxSessionsPreventsLogin):如果设置为 true,表⽰某⽤户达到最⼤会话并发数后,新会话请求会被拒绝登录;如果设置为 false,表⽰某⽤户达到最⼤会话并发数后,新会话请求访问时,其最⽼会话会在下⼀次请求时失效并根据 expiredUrl() 或者 expiredSessionStrategy() ⽅法配置的会话失效策略进⾏处理,默认值为 false。
expiredUrl(String expiredUrl):如果某⽤户达到最⼤会话并发数后,新会话请求访问时,其最⽼会话会在下⼀次请求时失效并重定向到 expiredUrl。
expiredSessionStrategy(SessionInformationExpiredStrategy expiredSessionStrategy):如果某⽤户达到最⼤会话并发数后,新会话请求访问时,其最⽼会话会在下⼀次请求中失效并按照该策略处理请求。注意如果本⽅法与expiredUrl() 同时使⽤,优先使⽤ expiredUrl() 的配置。
sessionRegistry(SessionRegistry sessionRegistry):设置所要使⽤的 sessionRegistry,默认配置的是 SessionRegistryImpl 实现类。
Session 会话失效处理
当⽤户的 Session 会话失效(请求携带着⽆效的 JSESSIONID 访问系统)时,可以制定相关策略对会话失效的请求进⾏处理。
invalidSessionUrl ⽅法
☕ 修改安全配置类 SpringSecurityConfig,配置 Session 会话失效时重定向到/login/page
@EnableWebSecurity // 开启 MVC Security 安全配置
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
//...
/**
* 定制基于 HTTP 请求的⽤户访问控制
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
//...
// 开启 Session 会话管理配置
http.sessionManagement()
// 设置 Session 会话失效时重定向路径,默认为 loginPage()
.
invalidSessionUrl("/login/page");
}
//...
}
☕ 设置 Session 的失效时间
Session 的失效时间配置是 SpringBoot 原⽣⽀持的,可以在 application.properties 配置⽂件中直接配置:
# session 失效时间,单位是秒,默认为 30min
server.servlet.session.timeout=30m
# JSESSIONID (Cookie)的⽣命周期,单位是秒,默认为 -1
server.kie.max-age=-1
注意:Session 的失效时间⾄少要 1 分钟,少于 1 分钟按照 1 分钟配置,查看源码:
public class TomcatServletWebServerFactory extends AbstractServletWebServerFactory implements ConfigurableTomcatWebServerFactory, ResourceLoaderAware {
//...session和application的区别
private long getSessionTimeoutInMinutes() {
Duration sessionTimeout = Session().getTimeout();
// ⾄少 1 分钟,少于 1 分钟按照 1 分钟配置
return this.isZeroOrLess(sessionTimeout) ? 0L : Math.Minutes(), 1L);
}
//...
}
为了⽅便检验,在 application.properties 中配置 Session 的失效时间为 1 分钟:
# session 失效时间,单位是秒,默认为 30min
server.servlet.session.timeout=60
☕ 测试
浏览器访问localhost:8080/login/page,输⼊正确的⽤户名、密码(不选择“记住我”功能)成功登录后,重定向到⾸页⾯:
之后,等待 1 分钟,刷新页⾯,浏览器重定向到/login/page:
invalidSessionStrategy ⽅法
如果想要⾃定义 Session 会话失效处理策略,使⽤该⽅法传⼊⾃定义策略。
⾃定义 Session 会话失效处理策略 CustomInvalidSessionStrategy
fig.security.session;
ity.ResultData;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.session.InvalidSessionStrategy;
import org.springframework.stereotype.Component;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* ⽤户请求携带⽆效的 JSESSIONID 访问时的处理策略,即对应的 Session 会话失效
*/
@Component
public class CustomInvalidSessionStrategy implements InvalidSessionStrategy {
@Autowired
private ObjectMapper objectMapper;
private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@Override
public void onInvalidSessionDetected(HttpServletRequest request, HttpServletResponse response) throws IOException { // 清除浏览器中的⽆效的 JSESSIONID
Cookie cookie = new Cookie("JSESSIONID", null);
cookie.setPath(getCookiePath(request));
cookie.setMaxAge(0);
response.addCookie(cookie);
String xRequestedWith = Header("x-requested-with");
// 判断前端的请求是否为 ajax 请求
if ("XMLHttpRequest".equals(xRequestedWith)) {
// 响应 JSON 数据
response.setContentType("application/json;charset=utf-8");
// 重定向到登录页⾯
redirectStrategy.sendRedirect(request, response, "/login/page");
}
}
private String getCookiePath(HttpServletRequest request) {
String contextPath = ContextPath();
return contextPath.length() > 0 ? contextPath : "/";
}
}
修改安全配置类 SpringSecurityConfig,配置使⽤⾃定义的 Session 会话失效处理策略
@EnableWebSecurity // 开启 MVC Security 安全配置
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
//...
@Autowired
private CustomInvalidSessionStrategy invalidSessionStrategy; // ⾃定义 Session 会话失效策略
//...
/**
* 定制基于 HTTP 请求的⽤户访问控制
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
//...
// 开启 Session 会话管理配置
http.sessionManagement()
// 设置 Session 会话失效时重定向路径,默认为 loginPage()
// .invalidSessionUrl("/login/page")
// 配置使⽤⾃定义的 Session 会话失效处理策略
.invalidSessionStrategy(invalidSessionStrategy);
}
//...
}
测试
浏览器访问localhost:8080/login/page,输⼊正确的⽤户名、密码(不选择“记住我”功能)成功登录后,重定向到⾸页⾯:
之后,等待 1 分钟,刷新页⾯,查看响应头:
同时,浏览器重定向到/login/page:
Session 会话并发控制
Session 会话并发控制可以限制⽤户的最⼤并发会话数量,例如:只允许⼀个⽤户在⼀个地⽅登陆,也就是说每个⽤户在系统中只能有⼀个 Session 会话。为了⽅便检验,在 application.properties 中将 Session 的过期时间改回 30分钟:
# session 有效期,单位是秒,默认为 30min
server.servlet.session.timeout=30m
在使⽤ Session 会话并发控制时,最好保证⾃定义的 UserDetails 实现类重写了 equals() 和 hashCode() ⽅法:
@Data
public class User implements UserDetails {
//...
private String username; // ⽤户名
//...
@Override
public boolean equals(Object obj) { // equals() ⽅法⼀般要重写
return obj instanceof User && this.username.equals(((User) obj).username);
}
@Override
public int hashCode() { // hashCode() ⽅法⼀般要重写
return this.username.hashCode();
}
}
我们前⾯实现了两种登录⽅式:⽤户名、密码登录和⼿机短信验证码登录,需要保证两种登录⽅式使⽤的是同⼀个 SessionAuthenticationStrategy 实例,也就是 MobileAuthenticationConfig 配置类中要有(1.4)的配置:
@Component
public class MobileAuthenticationConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
//...
@Override
public void configure(HttpSecurity http) throws Exception {
//...
//(1.1) 创建⼿机短信验证码认证过滤器的实例 filer
MobileAuthenticationFilter filter = new MobileAuthenticationFilter();
//...
//(1.4) 设置 filter 使⽤ SessionAuthenticationStrategy 会话管理器
// 多种登录⽅式应该使⽤同⼀个会话管理器实例,获取 Spring 容器已经存在的 SessionAuthenticationStrategy 实例
SessionAuthenticationStrategy sessionAuthenticationStrategy = SharedObject(SessionAuthenticationStrategy.class);
filter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy);
//...
}
}
如果没有(1.4)的配置,MobileAuthenticationFilter 默认使⽤的是 NullAuthenticatedSessionStrategy 实例管理 Session,⽽ UsernamePasswordAuthenticationFilter 使⽤的是 CompositeSessionAuthenticationStrategy 实例管理Session,也就是说两种登录⽅式的 Session 管理是相互独⽴的,这是不应该出现的情况。
基本使⽤
场景⼀:如果同⼀个⽤户在第⼆个地⽅登录,则不允许他⼆次登录
✏ 修改安全配置类 SpringSecurityConfig,配置⽤户最⼤并发 Session 会话数量和限制⽤户⼆次登录
@EnableWebSecurity // 开启 MVC Security 安全配置
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
//...
/
**
* 定制基于 HTTP 请求的⽤户访问控制
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
//...
// 开启 Session 会话管理配置
http.sessionManagement()
// 设置 Session 会话失效时重定向路径,默认为 loginPage()
// .invalidSessionUrl("/login/page")
// 配置使⽤⾃定义的 Session 会话失效处理策略
.
invalidSessionStrategy(invalidSessionStrategy)
// 设置单⽤户的 Session 最⼤并发会话数量,-1 表⽰不受限制
.maximumSessions(1)
// 设置为 true,表⽰某⽤户达到最⼤会话并发数后,新会话请求会被拒绝登录
.maxSessionsPreventsLogin(true);
// 设置所要使⽤的 sessionRegistry,默认为 SessionRegistryImpl 实现类
.sessionRegistry(sessionRegistry());
}
/**
* 注册 SessionRegistry,该 Bean ⽤于管理 Session 会话并发控制
*/
@Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
/**
* 配置 Session 的(注意:如果使⽤并发 Sessoion 控制,⼀般都需要配置该)
* 解决 Session 失效后, SessionRegistry 中 SessionInformation 没有同步失效的问题
*/
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
//..
}
✏ 测试
第⼀个浏览器访问localhost:8080/login/page,输⼊正确的⽤户名、密码成功登录后,会重定向到/index:
第⼆个浏览器访问localhost:8080/login/page,输⼊相同的⽤户名、密码访问,重定向/login/page?error:
上述配置限制了同⼀个⽤户的⼆次登陆,但是不建议使⽤该配置。因为⽤户⼀旦被盗号,那真正的⽤户后续就⽆法登录,只能通过联系管理员解决,所以如果只能⼀个⽤户 Session 登录,⼀般是新会话登录并将⽼会话踢下线。场景⼆:如果同⼀个⽤户在第⼆个地⽅登录,则将第⼀个踢下线
⾃定义最⽼会话被踢时的处理策略 CustomSessionInformationExpiredStrategy:
fig.security.session;
ity.ResultData;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.AuthenticationException;
import org.userdetails.UserDetails;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.session.SessionInformationExpiredEvent;
import org.springframework.security.web.session.SessionInformationExpiredStrategy;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 前提:Session 并发处理的配置为 maxSessionsPreventsLogin(false)
* ⽤户的并发 Session 会话数量达到上限,新会话登录后,最⽼会话会在下⼀次请求中失效,并执⾏此策略
*/
@Component
public class CustomSessionInformationExpiredStrategy implements SessionInformationExpiredStrategy {
@Autowired
private ObjectMapper objectMapper;
private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@Override
public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException {
HttpServletRequest request = Request();
HttpServletResponse response = Response();
// 最⽼会话被踢下线时显⽰的信息
UserDetails userDetails = (UserDetails) SessionInformation().getPrincipal();
String msg = String.format("⽤户[%s]在另外⼀台机器登录,您已下线!", Username());
String xRequestedWith = Request().getHeader("x-requested-with");
// 判断前端的请求是否为 ajax 请求
if ("XMLHttpRequest".equals(xRequestedWith)) {
// 认证成功,响应 JSON 数据
response.setContentType("application/json;charset=utf-8");
}else {
// 返回到登录页⾯显⽰信息
AuthenticationException e = new AuthenticationServiceException(msg);
redirectStrategy.sendRedirect(request, response, "/login/page?error");
}
}
}
修改安全配置类 SpringSecurityConfig,配置最⽼会话被踢时的处理策略
@EnableWebSecurity // 开启 MVC Security 安全配置
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
//...
@Autowired
private CustomSessionInformationExpiredStrategy sessionInformationExpiredStrategy; // ⾃定义最⽼会话失效策略
//...
/**
* 定制基于 HTTP 请求的⽤户访问控制
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
//...
// 开启 Session 会话管理配置
http.sessionManagement()
// 设置 Session 会话失效时重定向路径,默认为 loginPage()
// .invalidSessionUrl("/login/page")
// 配置使⽤⾃定义的 Session 会话失效处理策略
.
invalidSessionStrategy(invalidSessionStrategy)
// 设置单⽤户的 Session 最⼤并发会话数量,-1 表⽰不受限制
.maximumSessions(1)
// 设置为 true,表⽰某⽤户达到最⼤会话并发数后,新会话请求会被拒绝登录;
// 设置为 false,表⽰某⽤户达到最⼤会话并发数后,新会话请求访问时,其最⽼会话会在下⼀次请求时失效
.maxSessionsPreventsLogin(false)
// 设置所要使⽤的 sessionRegistry,默认为 SessionRegistryImpl 实现类
.sessionRegistry(sessionRegistry())
// 最⽼会话在下⼀次请求时失效,并重定向到 /login/page
//.expiredUrl("/login/page");
// 最⽼会话在下⼀次请求时失效,并按照⾃定义策略处理
.expiredSessionStrategy(sessionInformationExpiredStrategy);
}
/**
* 注册 SessionRegistry,该 Bean ⽤于管理 Session 会话并发控制
*/
@Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
/**
* 配置 Session 的(如果使⽤并发 Sessoion 控制,⼀般都需要配置)
* 解决 Session 失效后, SessionRegistry 中 SessionInformation 没有同步失效问题
*/
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
//...
}
测试
第⼀个浏览器访问localhost:8080/login/page,输⼊正确的⽤户名、密码成功登录后,重定向到/index:
第⼆个浏览器访问localhost:8080/login/page,输⼊相同的⽤户名、密码成功登录后,重定向到/index:
刷新第⼀个浏览器页⾯,重定向到/login/page?error:
原理分析
✌ AbstractAuthenticationProcessingFilter#doFilter
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean implements ApplicationEventPublisherAware, MessageSourceAware {
private SessionAuthenticationStrategy sessionStrategy = new NullAuthenticatedSessionStrategy();
//...
// 过滤器 doFilter() ⽅法
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)res;
if (!quiresAuthentication(request, response)) {
//(1) 判断该请求是否为 POST ⽅式的登录表单提交请求,如果不是则直接放⾏,进⼊下⼀个过滤器
chain.doFilter(request, response);
} else {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Request is to process authentication");
}
// Authentication 是⽤来存储⽤户认证信息的类,后续会进⾏详细介绍
Authentication authResult;
try {
//(2) 调⽤⼦类 UsernamePasswordAuthenticationFilter 重写的⽅法进⾏⾝份认证,
// 返回的 authResult 对象封装认证后的⽤户信息
authResult = this.attemptAuthentication(request, response);
if (authResult == null) {
return;
}
//(3) Session 策略处理(如果配置了⽤户 Session 最⼤并发数,就是在此处进⾏判断并处理)
// 默认使⽤的是新创建的 NullAuthenticatedSessionStrategy 实例,⽽ UsernamePasswordAuthenticationFilter 过滤器使⽤的是 CompositeSessionAuthenticationStrategy 实例 Authentication(authResult, request, response);
} catch (InternalAuthenticationServiceException var8) {
("An internal error occurred while trying to authenticate the user.", var8);
//(4) 认证失败,调⽤认证失败的处理器
this.unsuccessfulAuthentication(request, response, var8);
return;
} catch (AuthenticationException var9) {
this.unsuccessfulAuthentication(request, response, var9);
return;
}
//(4) 认证成功的处理
if (inueChainBeforeSuccessfulAuthentication) {
// 默认的 continueChainBeforeSuccessfulAuthentication 为 false,所以认证成功之后不进⼊下⼀个过滤器
chain.doFilter(request, response);
}
// 调⽤认证成功的处理器
this.successfulAuthentication(request, response, chain, authResult);
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论