spring security 会话管理
目录
一、会话管理(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 实现了具体的退出逻辑
当退出操作出发时,将发生:
- 销毁 httpSession 对象
- 清除认证状态
- 跳转到 /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 会话管理相关推荐
- REST + Spring Security会话问题
REST , 会话 ..等待. REST应用程序中没有会话,对吗? 好吧,那是真的. 如果我们可以避免会议,我们应该这样做. REST是无状态的 . 有关无状态性的主要问题是身份验证. 在通常的Web ...
- 6.Spring Security Session 管理
用户登录成功后,信息保存在服务器Session中,这节学习下如何管理这些Session.这节将在Spring Security短信验证码登录的基础上继续扩展. Session超时设置 Session超 ...
- spring Security 权限管理
文章来源:http://hotstrong.iteye.com/blog/1160153 1.技术目标 了解并创建Security框架所需数据表 为项目添加Spring Security框架 掌握Se ...
- Spring Security 权限管理的投票器与表决机制
今天咱们来聊一聊 Spring Security 中的表决机制与投票器. 当用户想访问 Spring Security 中一个受保护的资源时,用户具备一些角色,该资源的访问也需要一些角色,在比对用户具 ...
- SpringBoot整合Spring Security——登录管理
文章目录 一.自定义认证成功.失败处理 1.1 CustomAuthenticationSuccessHandle 1.2 CustomAuthenticationFailureHandler 1.3 ...
- 基于Spring Security 的Java SaaS应用的权限管理
1. 概述 权限管理,一般指根据系统设置的安全规则或者安全策略,用户可以访问而且只能访问自己被授权的资源.资源包括访问的页面,访问的数据等,这在传统的应用系统中比较常见.本文介绍的则是基于Saas系统 ...
- Spring Security和Angular教程
Spring Security和Angular教程 (一)安全的单页应用程序 在本教程中,我们展示了Spring Security,Spring Boot和Angular的一些很好的功能,它们协同工作 ...
- 【开发技术】2万字分析shiro、spring security两大安全框架,spring session,OAuth2 入门级教程
SpringBoot 内容管理 Shiro 创建demo 用户[用户包括token ,角色,权限] :fist_oncoming: Shiro配置 配置5个基本对象 + 3额外对象 路径匹配有先后 : ...
- spring security基于数据库的安全认证 配置
创建数据库 /* Navicat MySQL Data TransferSource Server : mysql3306 Source Server Version : 50542 Source H ...
最新文章
- python encodings模块_ImportError:没有名为'encodings'的模块
- linux dbus-daemon进程 消息转发 简介
- Scala样例类及密封类
- elasticsearch6.x {error:Content-Type header [application/x-www-form-urlencoded] is not supported
- visualvm远程监控jvm_大型企业JVM实战:优化及面试热点分析
- python常用算法有哪些_python常见的排序算法有哪些?
- python槽怎么用_【Python成长之路】从零学GUI -- 多窗口跳转(信号与槽函数用法)...
- RateLimiter
- ssis sql_SSIS OLE DB来源:SQL命令与表或视图
- vue生命周期整理学习
- android长截图工具下载,长截图拼接app下载
- DIY智能小车篇(四):常见问题 BUG汇总
- 泛在电力物联网建设大纲ppt
- C# 曲线控件 曲线绘制 实时曲线 多曲线控件 开发
- 到底什么是CE、C++、C+L波段?
- C# eval()函数浅谈
- npm/package.json/package-lock.json文件
- CSS3图片边框四个角剪切
- 世纪互联加入云计算专委会 推动应用进程
- 紫薇星上的数据结构(7)
热门文章
- 118.杨辉三角 java求解
- PyQt(Python+Qt)学习随笔:QScrollArea的widgetResizable属性
- 市场调查Market Survey
- 倾向值分析(协变量选择)
- 微信小程序商机_微信小程序提供的创业商机有哪些
- 安卓逆向笔记--apk加固
- 《HP大中华区总裁孙振耀退休感言》-写给迷茫时看的信
- 微信技术总监:11亿日活的超大型系统架构之道!13页ppt详解
- android河流曲线控件,WWF Free Rivers
- 备案接口 php,​分享一个备案查询的api接口源码,可自行开发对接备案API接口...