目录

一、会话管理(Session)

1、获取用户信息身份

2、会话控制

3、会话超时

4、会话并发控制

5、集群 session

二、RememberMe 实现

RememberMe 源码分析

三、退出登录


一、会话管理(Session)

用户认证通过后,为了避免用户的每次操作都进行认证可将用户的信息保存在会话中。spring security 提供会话管理,认证通过后将身份信息放入SecurityContextHolder上下文,SecurityContext与当前线程进行绑定,方便获取用户身份。

1、获取用户信息身份

private String getUsername() {// 从 SecurityContext 中获取当前登录的用户信息Authentication authentication = SecurityContextHolder.getContext().getAuthentication();if (!authentication.isAuthenticated()) {return null;}Object principal = authentication.getPrincipal();String username = null;if (principal instanceof UserDetails) {username = ((UserDetails) principal).getUsername();} else {username = principal.toString();}return username;}

2、会话控制

我们可以通过以下选项准确控制会话何时创建以及Spring Security如何与之交互:

机制 描述
always 如果session不存在总是需要创建
ifRequired 如果需要就创建一个session(默认)登录时
never Spring Security 将不会创建session,但是如果应用中其他地方创建了session,那么Spring Security将会使用它
stateless Spring Security将绝对不会创建session,也不使用session。并且它会暗示不使用 cookie,所以每个请求都需要重新进行身份验证。这种无状态架构适用于REST API 及其无状态认证机制。
    @Overrideprotected void configure(HttpSecurity http) throws Exception {http.formLogin() //表单提交.successHandler(new MyAuthenticationSuccessHandler("/main.html"));http.sessionManagement() // session 策略.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);http.authorizeRequests().antMatchers("/error.html","/main.html").permitAll() // 不需要认证.anyRequest().authenticated() // 认证拦截.and().csrf().disable(); //关闭csrf防护}

默认情况下,Spring Security 会为每个登录成功的用户会新建一个Session,就是ifRequired 。在执行认证过程之前,spring security将运行SecurityContextPersistenceFilter过滤器负责存储安全请求上下文,上下文根据策略进行存储,默认为HttpSessionSecurityContextRepository ,其使用http session 作为存储器。

3、会话超时

可以在 sevlet 容器中设置 Session 的超时时间,如下设置 Session 有效期为 600s ;

spring boot配置文件:

server:servlet:session:timeout: 60s

注意:session最低60s,参考源码 TomcatServletWebServerFactory#configureSession:

    private void configureSession(Context context) {// 设置超时时间long sessionTimeout = getSessionTimeoutInMinutes();context.setSessionTimeout((int) sessionTimeout);Boolean httpOnly = getSession().getCookie().getHttpOnly();if (httpOnly != null) {context.setUseHttpOnly(httpOnly);}if (getSession().isPersistent()) {Manager manager = context.getManager();if (manager == null) {manager = new StandardManager();context.setManager(manager);}configurePersistSession(manager);}else {context.addLifecycleListener(new DisablePersistSessionListener());}}

设置超时时间,最小超时时间为 1 分钟

    private long getSessionTimeoutInMinutes() {Duration sessionTimeout = getSession().getTimeout();if (isZeroOrLess(sessionTimeout)) {return 0;}// 比较取最大值return Math.max(sessionTimeout.toMinutes(), 1);}

session 超时之后,可以通过Spring Security 设置跳转的路径。

    http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED).invalidSessionUrl("/session/invalid");

对应路径接口的代码

@RestController
@RequestMapping("/session")
public class AdminController {@GetMapping("/invalid")@ResponseStatus(code = HttpStatus.UNAUTHORIZED)public String sessionInvalid() {return "session失效";}
}

4、会话并发控制

用户在这个手机登录后,他又在另一个手机登录相同账户,对于之前登录的账户是否需要被挤兑,或者说在第二次登录时限制它登录,更或者像腾讯视频 VIP 账号一样,最多只能五个人同时登录,第六个人将限制登录。

  • maximumSessions:最大会话数量,设置为1表示一个用户只能有一个会话
  • expiredSessionStrategy:会话过期策略
    @Overrideprotected void configure(HttpSecurity http) throws Exception {http.formLogin() //表单提交.successHandler(new MyAuthenticationSuccessHandler("/main.html"));http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED).maximumSessions(1) // 只能有一个session 在线, 最大会话数.expiredSessionStrategy(new MyExpiredSessionStrategy()); // session过期策略http.authorizeRequests().antMatchers("/error.html","/main.html").permitAll() // 不需要认证.anyRequest().authenticated() // 认证拦截.and().csrf().disable(); //关闭csrf防护}

配置 session 失效拒绝策略

import org.springframework.security.web.session.SessionInformationExpiredEvent;
import org.springframework.security.web.session.SessionInformationExpiredStrategy;import javax.servlet.http.HttpServletResponse;
import java.io.IOException;public class MyExpiredSessionStrategy implements SessionInformationExpiredStrategy {@Overridepublic void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException {HttpServletResponse response = event.getResponse();response.setContentType("application/json;charset=UTF-8");response.getWriter().write("您已被挤兑下线!");}
}

1. 使用chrome浏览器,先登录,再访问 http://localhost:8080/admin/test

2. 使用ie浏览器,再登录,再访问 http://localhost:8080/admin/test

3. 使用chrome浏览器,重新访问 http://localhost:8080/admin/test,会执行expiredSessionStrategy,页面上显示”您已被挤兑下线!“

阻止用户第二次登录

sessionManagement 也可以配置 maxSessionsPreventsLogin:boolean值,当达到maximumSessions 设置的最大会话个数时阻止登录。

    http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED).maximumSessions(1) // 只能有一个session 在线, 最大会话数.expiredSessionStrategy(new MyExpiredSessionStrategy()) // session过期策略.maxSessionsPreventsLogin(true); // 阻止 会话超过最大值,防止被踢

当限制 session 个数为 1 时,同一个账号第二次登陆,将会被阻止

5、集群 session

实际场景中一个服务会至少有两台服务器在提供服务,在服务器前面会有一个nginx做负载均衡,用户访问 nginx,nginx 再决定去访问哪一台服务器。当一台服务宕机了之后,另一台服务器也可以继续提供服务,保证服务不中断。此时,用户登录的会话信息就不能再保存到 Web 服务器中,而是保存到一个单独的库(redis、mongodb、mysql等)中,所有服务器都访问同一个库,都从同一个库来获取用户的session信息。

引入spring session依赖

<dependency><groupId>org.springframework.session</groupId><artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId>
</dependency>

修改 application.yaml 配置,spring 就会自动把 session 存入到 redis 当中

spring:datasource: # 数据库配置driver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://localhost:3306/security?useSSL=falsepassword: rootusername: rootsession: # session 配置store-type: redisredis: # redis 配置host: localhostport: 6379server:servlet:session:timeout: 60s # session 过期时间

redis 中存放的 session

再次访问时,请求头中会带上 session 信息

session 的自动存储源码

找到 SessionRepositoryFilter.java 这个过滤器,SessionRepositoryFilter#doFilterInternal 方法源码如下

    @Overrideprotected void doFilterInternal(HttpServletRequest request,HttpServletResponse response, FilterChain filterChain)throws ServletException, IOException {request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(request, response, this.servletContext);SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(wrappedRequest, response);try {filterChain.doFilter(wrappedRequest, wrappedResponse);}finally {// 提交session wrappedRequest.commitSession();}}

其中 wrappedRequest.commitSession(); 便执行了 session 存储的逻辑

    private void commitSession() {HttpSessionWrapper wrappedSession = getCurrentSession();if (wrappedSession == null) {if (isInvalidateClientSession()) {SessionRepositoryFilter.this.httpSessionIdResolver.expireSession(this,this.response);}}else {S session = wrappedSession.getSession();clearRequestedSessionCache();// 存储 sessionSessionRepositoryFilter.this.sessionRepository.save(session);String sessionId = session.getId();if (!isRequestedSessionIdValid()|| !sessionId.equals(getRequestedSessionId())) {SessionRepositoryFilter.this.httpSessionIdResolver.setSessionId(this,this.response, sessionId);}}}

其中 sessionRepository ,就是类中的以下这个属性

private final SessionRepository<S> sessionRepository;

SessionRepository 接口的其中有一个实现就是 redis 的

最终会调用 RedisOperationsSessionRepository#save 进行保存

    public void save(RedisOperationsSessionRepository.RedisSession session) {session.saveDelta();if (session.isNew()) {String sessionCreatedKey = this.getSessionCreatedChannel(session.getId());this.sessionRedisOperations.convertAndSend(sessionCreatedKey, session.delta);session.setNew(false);}}

安全会话cookie

我们可以使用 httpOnly 和 secure 标签来保护我们的会话 cookie:

  • httpOnly:如果为true,那么浏览器脚本将无法访问 cookie
  • secure:如果为true,则cookie将仅通过HTTPS连接发送

spring boot配置文件:

server.servlet.session.cookie.http-only=true
server.servlet.session.cookie.secure=true

二、RememberMe 实现

Spring Security 中 Remember Me 为“记住我”功能,用户只需要在登录时添加 remember-me复选框,取值为true。Spring Security 会自动把用户信息存储到数据源中,以后就可以不登录进行访问。

RememberMe 配置完整版

import com.swadian.userdemo.filter.MyExpiredSessionStrategy;
import com.swadian.userdemo.service.MyUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;import javax.sql.DataSource;/*** @author swadian*/
@Configuration // 标记为注解类
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {@Autowiredprivate MyUserDetailsService userService;@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {//设置UserDetailsService的实现类auth.userDetailsService(userService);}@Overrideprotected void configure(HttpSecurity http) throws Exception {http.formLogin() //表单提交.loginPage("/login.html") //自定义登录页面.loginProcessingUrl("/my-user/login");//登录访问路径,必须和表单提交接口一样// 记住我http.rememberMe().tokenRepository(persistentTokenRepository())//设置持久化仓库.tokenValiditySeconds(3600) //超时时间,单位s 默认两周.userDetailsService(userService); //设置自定义登录逻辑http.authorizeRequests().antMatchers("/login.html", "/error.html", "/main.html").permitAll() // 不需要认证.anyRequest().authenticated() // 认证拦截.and().csrf().disable(); //关闭csrf防护}@Autowired // rememberMe -> 需要引入数据源public DataSource dataSource;public PersistentTokenRepository persistentTokenRepository() {JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();// rememberMe -> 设置数据源jdbcTokenRepository.setDataSource(dataSource);return jdbcTokenRepository;}}

创建数据库表

CREATE TABLE persistent_logins (username VARCHAR ( 64 ) NOT NULL,series VARCHAR ( 64 ) PRIMARY KEY,token VARCHAR ( 64 ) NOT NULL,last_used TIMESTAMP NOT NULL
)

在客户端登录页面 login.html 中添加 remember-me 的复选框,只要用户勾选了复选框下次就不需要进行登录了。

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Title</title>
</head>
<body>
<form action="/my-user/login" method="post">用户名:<input type="text" name="username"/><br/>密码: <input type="password"name="password"/><br/><input type="checkbox" name="remember-me" value="true"/><br/><input type="submit" value="提交"/></form>
</body>
</html>

成功登陆后,我们可以看到数据库表中多了一行记录

RememberMe 源码分析

spring security 很多功能都是基于过滤器实现的,因此我们可以去代码中找 RememberMe 过滤器的代码实现。

在源码中可以找到这个方法 RememberMeAuthenticationFilter # doFilter

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)throws IOException, ServletException {HttpServletRequest request = (HttpServletRequest) req;HttpServletResponse response = (HttpServletResponse) res;if (SecurityContextHolder.getContext().getAuthentication() == null) {// 自动登陆Authentication rememberMeAuth = rememberMeServices.autoLogin(request,response);if (rememberMeAuth != null) {// Attempt authenticaton via AuthenticationManagertry {// 认证逻辑rememberMeAuth = authenticationManager.authenticate(rememberMeAuth);// Store to SecurityContextHolderSecurityContextHolder.getContext().setAuthentication(rememberMeAuth);onSuccessfulAuthentication(request, response, rememberMeAuth);if (logger.isDebugEnabled()) {logger.debug("SecurityContextHolder populated with remember-me token: '"+ SecurityContextHolder.getContext().getAuthentication()+ "'");}// Fire eventif (this.eventPublisher != null) {eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(SecurityContextHolder.getContext().getAuthentication(), this.getClass()));}if (successHandler != null) {successHandler.onAuthenticationSuccess(request, response,rememberMeAuth);return;}}catch (AuthenticationException authenticationException) {if (logger.isDebugEnabled()) {logger.debug("SecurityContextHolder not populated with remember-me token, as "+ "AuthenticationManager rejected Authentication returned by RememberMeServices: '"+ rememberMeAuth+ "'; invalidating remember-me token",authenticationException);}rememberMeServices.loginFail(request, response);onUnsuccessfulAuthentication(request, response,authenticationException);}}chain.doFilter(request, response);}else {if (logger.isDebugEnabled()) {logger.debug("SecurityContextHolder not populated with remember-me token, as it already contained: '"+ SecurityContextHolder.getContext().getAuthentication() + "'");}chain.doFilter(request, response);}}

三、退出登录

Spring security默认实现了 logout 退出,用户只需要向 Spring Security 项目中发送 /logout 退出请求即可。

默认的退出 url 为 /logout ,退出成功后跳转到 /login?logout 。进入 LogoutConfigurer.java 可以看到如下配置

自定义退出逻辑

如果不希望使用默认值,可以通过下面的方法进行修改。

    http.logout().logoutUrl("/logout").logoutSuccessUrl("/login.html"); // 退出后跳转到登陆页面

执行 http://localhost:8080/logout 可以看到退出效果

退出登录源码

同样是从过滤器开始,LogoutFilter # doFilter

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)throws IOException, ServletException {HttpServletRequest request = (HttpServletRequest) req;HttpServletResponse response = (HttpServletResponse) res;if (requiresLogout(request, response)) {// 1-获取用户信息Authentication auth = SecurityContextHolder.getContext().getAuthentication();if (logger.isDebugEnabled()) {logger.debug("Logging out user '" + auth+ "' and transferring to logout destination");}// 2-退出登陆 -> SecurityContextLogoutHandler#logoutthis.handler.logout(request, response, auth);// 3-拓展点,成功退出后的操作logoutSuccessHandler.onLogoutSuccess(request, response, auth);return;}chain.doFilter(request, response);}

SecurityContextLogoutHandler 实现了 LogoutHandler 接口

SecurityContextLogoutHandler # logout 实现了具体的退出逻辑

当退出操作出发时,将发生:

  1. 销毁 httpSession 对象
  2. 清除认证状态
  3. 跳转到 /login.html -> 配置了 logoutSuccessUrl 的处理逻辑
    public void logout(HttpServletRequest request, HttpServletResponse response,Authentication authentication) {Assert.notNull(request, "HttpServletRequest required");if (invalidateHttpSession) {HttpSession session = request.getSession(false);if (session != null) {logger.debug("Invalidating session: " + session.getId());// 1-失效 sessionsession.invalidate();}}if (clearAuthentication) {// 2-清空用户信息SecurityContext context = SecurityContextHolder.getContext();context.setAuthentication(null);}// 3-清空Security上下文SecurityContextHolder.clearContext();}

至此,退出登陆分析结束。

spring security 会话管理相关推荐

  1. REST + Spring Security会话问题

    REST , 会话 ..等待. REST应用程序中没有会话,对吗? 好吧,那是真的. 如果我们可以避免会议,我们应该这样做. REST是无状态的 . 有关无状态性的主要问题是身份验证. 在通常的Web ...

  2. 6.Spring Security Session 管理

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

  3. spring Security 权限管理

    文章来源:http://hotstrong.iteye.com/blog/1160153 1.技术目标 了解并创建Security框架所需数据表 为项目添加Spring Security框架 掌握Se ...

  4. Spring Security 权限管理的投票器与表决机制

    今天咱们来聊一聊 Spring Security 中的表决机制与投票器. 当用户想访问 Spring Security 中一个受保护的资源时,用户具备一些角色,该资源的访问也需要一些角色,在比对用户具 ...

  5. SpringBoot整合Spring Security——登录管理

    文章目录 一.自定义认证成功.失败处理 1.1 CustomAuthenticationSuccessHandle 1.2 CustomAuthenticationFailureHandler 1.3 ...

  6. 基于Spring Security 的Java SaaS应用的权限管理

    1. 概述 权限管理,一般指根据系统设置的安全规则或者安全策略,用户可以访问而且只能访问自己被授权的资源.资源包括访问的页面,访问的数据等,这在传统的应用系统中比较常见.本文介绍的则是基于Saas系统 ...

  7. Spring Security和Angular教程

    Spring Security和Angular教程 (一)安全的单页应用程序 在本教程中,我们展示了Spring Security,Spring Boot和Angular的一些很好的功能,它们协同工作 ...

  8. 【开发技术】2万字分析shiro、spring security两大安全框架,spring session,OAuth2 入门级教程

    SpringBoot 内容管理 Shiro 创建demo 用户[用户包括token ,角色,权限] :fist_oncoming: Shiro配置 配置5个基本对象 + 3额外对象 路径匹配有先后 : ...

  9. spring security基于数据库的安全认证 配置

    创建数据库 /* Navicat MySQL Data TransferSource Server : mysql3306 Source Server Version : 50542 Source H ...

最新文章

  1. python encodings模块_ImportError:没有名为'encodings'的模块
  2. linux dbus-daemon进程 消息转发 简介
  3. Scala样例类及密封类
  4. elasticsearch6.x {error:Content-Type header [application/x-www-form-urlencoded] is not supported
  5. visualvm远程监控jvm_大型企业JVM实战:优化及面试热点分析
  6. python常用算法有哪些_python常见的排序算法有哪些?
  7. python槽怎么用_【Python成长之路】从零学GUI -- 多窗口跳转(信号与槽函数用法)...
  8. RateLimiter
  9. ssis sql_SSIS OLE DB来源:SQL命令与表或视图
  10. vue生命周期整理学习
  11. android长截图工具下载,长截图拼接app下载
  12. DIY智能小车篇(四):常见问题 BUG汇总
  13. 泛在电力物联网建设大纲ppt
  14. C# 曲线控件 曲线绘制 实时曲线 多曲线控件 开发
  15. 到底什么是CE、C++、C+L波段?
  16. C# eval()函数浅谈
  17. npm/package.json/package-lock.json文件
  18. CSS3图片边框四个角剪切
  19. 世纪互联加入云计算专委会 推动应用进程
  20. 紫薇星上的数据结构(7)

热门文章

  1. 118.杨辉三角 java求解
  2. PyQt(Python+Qt)学习随笔:QScrollArea的widgetResizable属性
  3. 市场调查Market Survey
  4. 倾向值分析(协变量选择)
  5. 微信小程序商机_微信小程序提供的创业商机有哪些
  6. 安卓逆向笔记--apk加固
  7. 《HP大中华区总裁孙振耀退休感言》-写给迷茫时看的信
  8. 微信技术总监:11亿日活的超大型系统架构之道!13页ppt详解
  9. android河流曲线控件,WWF Free Rivers
  10. 备案接口 php,​分享一个备案查询的api接口源码,可自行开发对接备案API接口...