Skip to content

Latest commit

 

History

History
196 lines (145 loc) · 8.18 KB

File metadata and controls

196 lines (145 loc) · 8.18 KB

Spring Security Session 管理

用户登录成功后,信息保存在服务器 Session 中,这节学习下如何管理这些 Session。这节将在 Spring Security 短信验证码登录的基础上继续扩展。

Session 超时设置

Session 超时时间也就是用户登录的有效时间。要设置 Session 超时时间很简单,只需要在配置文件中添加:

server:
  session:
    timeout: 3600

单位为秒,通过上面的配置,Session 的有效期为一个小时。

值得注意的是,Session 的最小有效期为 60 秒,也就是说即使你设置为小于 60 秒的值,其有效期还是为 60 秒。查看 TomcatEmbeddedServletContainerFactory 的源码即可发现原因:

QQ截图20190618100327.png

Session 失效后,刷新页面后将跳转到认证页面,我们可以再添加一些配置,自定义 Session 失效后的一些行为。

在 Spring Security 中配置 Session 管理器,并配置 Session 失效后要跳转的 URL:

@Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class) // 添加验证码校验过滤器
.addFilterBefore(smsCodeFilter,UsernamePasswordAuthenticationFilter.class) // 添加短信验证码校验过滤器
.formLogin() // 表单登录
.loginPage("/authentication/require") // 登录跳转 URL
.loginProcessingUrl("/login") // 处理表单登录 URL
.successHandler(authenticationSucessHandler) // 处理登录成功
.failureHandler(authenticationFailureHandler) // 处理登录失败
.and()
.authorizeRequests() // 授权配置
.antMatchers("/authentication/require",
"/login.html", "/code/image","/code/sms","/session/invalid").permitAll() // 无需认证的请求路径
.anyRequest() // 所有请求
.authenticated() // 都需要认证
.and()
.sessionManagement() // 添加 Session 管理器
.invalidSessionUrl("/session/invalid") // Session 失效后跳转到这个链接
......
}

上面配置了 Session 失效后跳转到/session/invalid,并且将这个 URL 添加到了免认证路径中。

在 Controller 里添加一个方法,映射该请求:

@GetMapping("session/invalid")
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public String sessionInvalid(){
return "session 已失效,请重新认证";
}

为了演示,我们将 Session 的超时时间设置为最小值 60 秒,重启项目,认证后等待 60 秒并刷新页面:

QQ 截图 20190618104337.png

可看到请求跳转到了我们自定义的/session/invalidURL 上。

Session 并发控制 Session 并发控制可以控制一个账号同一时刻最多能登录多少个。我们在 Spring Security 配置中继续添加 Session 相关配置:

@Autowired
private MySessionExpiredStrategy sessionExpiredStrategy;

@Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class) // 添加验证码校验过滤器
.addFilterBefore(smsCodeFilter,UsernamePasswordAuthenticationFilter.class) // 添加短信验证码校验过滤器
.formLogin() // 表单登录
.loginPage("/authentication/require") // 登录跳转 URL
.loginProcessingUrl("/login") // 处理表单登录 URL
.successHandler(authenticationSucessHandler) // 处理登录成功
.failureHandler(authenticationFailureHandler) // 处理登录失败
.and()
.authorizeRequests() // 授权配置
.antMatchers("/authentication/require",
"/login.html", "/code/image","/code/sms","/session/invalid").permitAll() // 无需认证的请求路径
.anyRequest() // 所有请求
.authenticated() // 都需要认证
.and()
.sessionManagement() // 添加 Session 管理器
.invalidSessionUrl("/session/invalid") // Session 失效后跳转到这个链接
.maximumSessions(1)
.expiredSessionStrategy(sessionExpiredStrategy)
.and()
......

maximumSessions 配置了最大 Session 并发数量为 1 个,如果 mrbird 这个账户登录后,在另一个客户端也使用 mrbird 账户登录,那么第一个使用 mrbird 登录的账户将会失效,类似于一个先入先出队列。expiredSessionStrategy 配置了 Session 在并发下失效后的处理策略,这里为我们自定义的策略 MySessionExpiredStrategy。

MySessionExpiredStrategy 实现 SessionInformationExpiredStrategy:

@Component
public class MySessionExpiredStrategy implements SessionInformationExpiredStrategy {

    @Override
    public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
        HttpServletResponse response = event.getResponse();
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        response.setContentType("application/json;charset=utf-8");
        response.getWriter().write("您的账号已经在别的地方登录,当前登录已失效。如果密码遭到泄露,请立即修改密码!");
    }

}

为了演示这个效果,我们先将 Session 超时时间设置久一点,比如 3600 秒,然后重启项目,在 Chrome 里使用 mrbird 账户登录。

登录成功后,在 firefox 上也是用 mrbird 账户登录,登录成功后回到 chrome,刷新页面,效果如下所示:

QQ 截图 20190618105958.png

除了后者将前者踢出的策略,我们也可以控制当 Session 达到最大有效数的时候,不再允许相同的账户登录。

要实现这个功能只需要在上面的配置中添加:

......
.and()
.sessionManagement() // 添加 Session 管理器
.invalidSessionUrl("/session/invalid") // Session 失效后跳转到这个链接
.maximumSessions(1)
.maxSessionsPreventsLogin(true)
.expiredSessionStrategy(sessionExpiredStrategy)
.and()
......

重启系统,在 chrome 上登录 mrbird 账户后,在 firefox 上尝试使用 mrbird 账户登录:

QQ 截图 20190618112432.png

可以看到登录受限。

在实际开发中,发现 Session 并发控制只对 Spring Security 默认的登录方式——账号密码登录有效,而像短信验证码登录,社交账号登录并不生效,解决方案可以参考我的开源项目https://github.com/wuyouzhuguli/FEBS-Security

Session 集群处理

Session 集群听着高大上,其实实现起来很简单。当我们登录成功后,用户认证的信息存储在 Session 中,而这些 Session 默认是存储在运行运用的服务器上的,比如 Tomcat,netty 等。当应用集群部署的时候,用户在 A 应用上登录认证了,后续通过负载均衡可能会把请求发送到 B 应用,而 B 应用服务器上并没有与该请求匹配的认证 Session 信息,所以用户就需要重新进行认证。要解决这个问题,我们可以把 Session 信息存储在第三方容器里(如 Redis 集群),而不是各自的服务器,这样应用集群就可以通过第三方容器来共享 Session 了。

我们引入 Redis 和 Spring Session 依赖:

<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

然后在 yml 中配置 Session 存储方式为 Redis:

spring:
session:
store-type: redis

为了方便,Redis 配置采用默认配置即可。

开启 Redis,并且启动两个应用实例,一个端口为 8080,另一个端口为 9090。

我们现在 8080 端口应用上登录:

QQ 截图 20190618142823.png

然后访问 9090 端口应用的主页:

QQ 截图 20190618142905.png

可以看到登录也是生效的。这就实现了集群化 Session 管理。

其他操作 SessionRegistry 包含了一些使用的操作 Session 的方法,比如:

踢出用户(让 Session 失效):

String currentSessionId = request.getRequestedSessionId();
sessionRegistry.getSessionInformation(sessionId).expireNow();
获取所有 Session 信息:

List<Object> principals = sessionRegistry.getAllPrincipals();