Spring Security是如何储存认证用户信息的
创始人
2025-05-30 05:01:31
0

Spring Security是如何储存认证用户信息的

文章目录

  • Spring Security是如何储存认证用户信息的
    • 前言剧透
    • 有着用户信息的SecurityContext一定是被ThreadLocal管理的吗?
      • ThreadLocalSecurityContextHolderStrategy类
    • 请求时源码流程
      • 获取包含着认证信息的context
      • 创建进入repo寻找context
      • 解析request的session
      • 根据key拿到session中的SecurityContext对象
      • 将SecurityContext交给holder进行管理
    • 结束请求时源码
    • 疑问:将认证信息放在session里不会被篡改吗?
    • 自动登录:过滤器识别cookie存的用户信息
      • 解析cookie变成token
        • SpringSecurity存储token的方式
      • token获取用户信息并进行认证
        • 用户认证(验证账号是否正常)
        • 返回认证实例

前言剧透

总结:Spring Security会将信息储存在SecurityContext中,请求过程中会被SecurityContextHolder进行管理,底层是基于的ThreadLocal(当然还有很多种策略)
问题:SpringBoot底层的servlet会将每个请求分配一个线程,用ThreadLocal能拿到数据?

单纯的用ThreadLocal当然不够,其自身有一个小“仓库”
而SecurityContext被“保存”于这个仓库SecurityContextRepository中(实际上是放在session里的)

  • 请求时从repo中拿出来context,并交给给SecurityContextHolder管理
  • 结束时把context放回repo

这样每次请求都能从仓库拿到之前的context,也就知道用户到底认证没有了

有着用户信息的SecurityContext一定是被ThreadLocal管理的吗?

我们上文提到“SecurityContext请求过程中会被SecurityContextHolder进行管理,底层是基于的ThreadLocal”
实际并不严谨,因为SecurityContextHolder底层有多种实现
上代码

我们可以看到第一个if里就判断了spring.security.strategy配置里是否有值

  • 如果配置里没有主动指定holder的策略名称,那么默认是MODE_THREADLOCAL——ThreadLocalSecurityContextHolderStrategy类,也就是ThreadLocal
  • 如果指定了策略那就获取指定的策略

所以我们需要纠正为“在Spring Security中,用户的信息默认被ThreadLocal储存,这不是绝对的”

	private static String strategyName = System.getProperty("spring.security.strategy");private static void initialize() {if (!StringUtils.hasText(strategyName)) {// Set defaultstrategyName = MODE_THREADLOCAL;}if (strategyName.equals(MODE_THREADLOCAL)) {strategy = new ThreadLocalSecurityContextHolderStrategy();}else if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) {strategy = new InheritableThreadLocalSecurityContextHolderStrategy();}else if (strategyName.equals(MODE_GLOBAL)) {strategy = new GlobalSecurityContextHolderStrategy();}else {// Try to load a custom strategytry {Class clazz = Class.forName(strategyName);Constructor customStrategy = clazz.getConstructor();strategy = (SecurityContextHolderStrategy) customStrategy.newInstance();}catch (Exception ex) {ReflectionUtils.handleReflectionException(ex);}}initializeCount++;}

ThreadLocalSecurityContextHolderStrategy类

可以看到底层就是new了一个ThreadLocal

final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {private static final ThreadLocal contextHolder = new ThreadLocal<>();
}

请求时源码流程

SecurityContextPersistenceFilter核心过滤器:org.springframework.security.web.context.SecurityContextPersistenceFilter#doFilter(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, javax.servlet.FilterChain)

注意:如果是忽略路径的话,是不会走这个过滤器的

获取包含着认证信息的context

在filter第一句就是if判断语句:用于确保每个请求只应用一次筛选器

	static final String FILTER_APPLIED = "__spring_security_scpf_applied";if (request.getAttribute(FILTER_APPLIED) != null) {chain.doFilter(request, response);return;}

创建进入repo寻找context

根据request和response新建HttpRequestResponseHolder,然后将holder放入repo寻找其context

HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);

解析request的session

获取holder中的request,并拿到request的session,然后解析session拿到SecurityContext

		HttpServletRequest request = requestResponseHolder.getRequest();HttpServletResponse response = requestResponseHolder.getResponse();HttpSession httpSession = request.getSession(false);SecurityContext context = readSecurityContextFromSession(httpSession);

HttpSessionSecurityContextRepository根据session获取SecurityContext:org.springframework.security.web.context.HttpSessionSecurityContextRepository#readSecurityContextFromSession

根据key拿到session中的SecurityContext对象

可以看到从session取出了一个名为“SPRING_SECURITY_CONTEXT_KEY”的属性,并转化为了SecurityContext对象(判空等代码已省略)

private String springSecurityContextKey = SPRING_SECURITY_CONTEXT_KEY;private SecurityContext readSecurityContextFromSession(HttpSession httpSession) {Object contextFromSession = httpSession.getAttribute(this.springSecurityContextKey);return (SecurityContext) contextFromSession;
}

将SecurityContext交给holder进行管理

至此,请求时的SecurityContext获取到此结束
(省略了一些打log逻辑)

		SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);SecurityContextHolder.setContext(contextBeforeChainExecution);chain.doFilter(holder.getRequest(), holder.getResponse());

结束请求时源码

这段很简略

  1. 从holder中拿回context
  2. holder清除context
  3. 将context放回repo
  4. request删除之前放置的属性“FILTER_APPLIED”(防止请求的重复过滤)
	static final String FILTER_APPLIED = "__spring_security_scpf_applied";finally {SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();SecurityContextHolder.clearContext();this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());request.removeAttribute(FILTER_APPLIED);}

疑问:将认证信息放在session里不会被篡改吗?

session保存在服务器,等于用户的认证信息实际上还是被保存在了服务端

 1. 打开浏览器,在浏览器上发送首次请求2. 服务器会创建一个HttpSession对象,该对象代表一次会话3. 同时生成HttpSession对象对应的Cookie对象,并且Cookie对象的name是jsessionid,Cookie的value是32位长度的字符串(jsessionid=xxxx)4. 服务器将Cookie的value和HttpSession对象绑定到session列表中5. 服务器将Cookie完整发送给浏览器客户端6. 浏览器客户端将Cookie保存到缓存中7. 只要浏览器不关闭,Cookie就不会消失8. 当再次发送请求的时候,会自动提交缓存中当的Cookie9. 服务器接收到Cookie,验证该Cookie的name是否是jsessionid,然后获取该Cookie的value10. 通过Cookie的value去session列表中检索对应的HttpSession对象 

需要知道的是:浏览器关闭之后,服务器不会销毁session对象
HTTP协议是一种无连接/无状态的协议
当一段时间后,用户没有再访问session对象,此时session对象超时,web服务器会自动回收session对象

在SpringBoot中,session默认存储30分钟

	@DurationUnit(ChronoUnit.SECONDS)private Duration timeout = Duration.ofMinutes(30);

配置

server:servlet:session:timeout: 30m

自动登录:过滤器识别cookie存的用户信息

自动登录过滤器:RememberMeAuthenticationFilter
当用户没有登录时,会读取request里的cookie来进行自动登录认证

解析cookie变成token

读取request里的cookie后遍历cookie,判断有没有一个名字为“remember-me”的cookie,若有则取出

    protected String extractRememberMeCookie(HttpServletRequest request) {Cookie[] cookies = request.getCookies();if ((cookies == null) || (cookies.length == 0)) {return null;}for (Cookie cookie : cookies) {if (this.cookieName.equals(cookie.getName())) {return cookie.getValue();}}return null;}

如果有这个“记住我”cookie,那么将其base64解码
解码后的字符串将其转换为“分隔列表字符串数组”,变成token
看代码吧

	protected String[] decodeCookie(String cookieValue) throws InvalidCookieException {for (int j = 0; j < cookieValue.length() % 4; j++) {cookieValue = cookieValue + "=";}String cookieAsPlainText;try {cookieAsPlainText = new String(Base64.getDecoder().decode(cookieValue.getBytes()));}catch (IllegalArgumentException ex) {throw new InvalidCookieException("Cookie token was not Base64 encoded; value was '" + cookieValue + "'");}String[] tokens = StringUtils.delimitedListToStringArray(cookieAsPlainText, DELIMITER);for (int i = 0; i < tokens.length; i++) {try {tokens[i] = URLDecoder.decode(tokens[i], StandardCharsets.UTF_8.toString());}catch (UnsupportedEncodingException ex) {this.logger.error(ex.getMessage(), ex);}}return tokens;}

SpringSecurity存储token的方式

有趣的是,SpringSecurity有两种存储token的方式
一种是HashMap的内存储存:InMemoryTokenRepositoryImpl

	private final Map seriesTokens = new HashMap<>();@Overridepublic synchronized PersistentRememberMeToken getTokenForSeries(String seriesId) {return this.seriesTokens.get(seriesId);}

一种是jdbc的磁盘储存:JdbcTokenRepositoryImpl

	@Overridepublic PersistentRememberMeToken getTokenForSeries(String seriesId) {return getJdbcTemplate().queryForObject(this.tokensBySeriesSql, this::createRememberMeToken, seriesId);}

token获取用户信息并进行认证

“解析”完cookie变成cookieTokens后,利用这个token获取用户信息并进行校验

	public final Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) {String[] cookieTokens = decodeCookie(rememberMeCookie);UserDetails user = processAutoLoginCookie(cookieTokens, request, response);this.userDetailsChecker.check(user);return createSuccessfulAuthentication(request, user);}

获取token后,根据username获取User信息(UserDetails)

PersistentRememberMeToken token = this.tokenRepository.getTokenForSeries(presentedSeries);
return getUserDetailsService().loadUserByUsername(token.getUsername());

用户认证(验证账号是否正常)

上文讲到获取token拿到username后,根据username拿到了user信息(UserDetails)
判断用户是否被锁定、账户过期等,若存在这些情况就抛出异常

	public void check(UserDetails user) {if (!user.isAccountNonLocked()) {this.logger.debug("Failed to authenticate since user account is locked");throw new LockedException(this.messages.getMessage("AccountStatusUserDetailsChecker.locked", "User account is locked"));}if (!user.isEnabled()) {this.logger.debug("Failed to authenticate since user account is disabled");throw new DisabledException(this.messages.getMessage("AccountStatusUserDetailsChecker.disabled", "User is disabled"));}if (!user.isAccountNonExpired()) {this.logger.debug("Failed to authenticate since user account is expired");throw new AccountExpiredException(this.messages.getMessage("AccountStatusUserDetailsChecker.expired", "User account has expired"));}if (!user.isCredentialsNonExpired()) {this.logger.debug("Failed to authenticate since user account credentials have expired");throw new CredentialsExpiredException(this.messages.getMessage("AccountStatusUserDetailsChecker.credentialsExpired", "User credentials have expired"));}}

返回认证实例

创建从autoLogin方法返回的最终身份验证对象。
默认情况下,它将创建RememberMeAuthenticationToken实例。

	protected Authentication createSuccessfulAuthentication(HttpServletRequest request, UserDetails user) {RememberMeAuthenticationToken auth = new RememberMeAuthenticationToken(this.key, user,this.authoritiesMapper.mapAuthorities(user.getAuthorities()));auth.setDetails(this.authenticationDetailsSource.buildDetails(request));return auth;}

至此自动登录的认证完成

相关内容

热门资讯

【Spring从成神到升仙系列... 👏作者简介:大家好,我是爱敲代码的小黄,独...
最新或2023(历届)广州小升... 最新或2023(历届)1、2月开始寒假复习,重视寒假复习,备战3月的华杯赛及希望杯竞赛备考杯赛冲刺阶...
QT 如何提高 Qt Crea... 如何提高编译速度,貌似是一个老生常谈的话题。对于Qter而言,如何提高Q...
最新或2023(历届)广州民校...   ■制图:王云涛与名校紧密挂钩的“华杯赛”参赛单位暂无广州,业内人士预测由机构承办赛事根据政策,明...
最新或2023(历届)昆明民办...   昆明新政  当民办初中学校的报名信息确认学生数大于最新或2023(历届)招生计划时,招生计划的5...
图解北京小升初一张图让你了解最...  图解北京小升初:一张图让你了解最新或2023(历届)北京小升初新政
昆明市教育局最新或2023(历... 昆明市教育局关于做好最新或2023(历届)民办初中招生工作的通知  各县区教育局,各国家级、省级开发...
最新或2023(历届)美丽的春... 冬去春来,杨柳吐绿,我盼望已久的春天来到了,我不禁欣喜若狂。春天来了,山变绿了,田野里的小草偷偷探出...
最新或2023(历届)昆明小升...  28日上午,昆明市教育局下发了《关于进一步做好昆明市义务教育阶段小学升初中工作的通知》,最新或20...
最新或2023(历届)月圆是画... 在五光十色的世界中,许多人都在为了忙碌而忙碌。乐此不疲之际,往往忽略了自己心中的追求。因此,在并非觉...
聚焦最新或2023(历届)北京...  西城区以史无前例、脱胎换骨的力度即将进行第二阶段的更深层教育改革。即便第二阶段的教改还没有通过官方...
Notes03:使用寄存器点亮... 使用寄存器点亮LED 目标 使用寄存器点亮LED_G 原理图 由原理图可知, LED_G阳极接3V...
最新或2023(历届)昆明小升...  28日上午,昆明市教育局下发了《关于进一步做好昆明市义务教育阶段小学升初中工作的通知》,最新或20...
最新或2023(历届)分享之乐... 分享,是一只活泼可爱的小精灵,把欢乐撒满人间;分享,是一台复制机,把幸福复制的到处都是;分享,又是一...
最新或2023(历届)秋天的香... 秋天来到了,地面上一片黄色,像是画家给大地添上了金黄色的地毯。爸爸妈妈要让我感受一下秋天的气息,就带...
最新或2023(历届)我心中的... 万泉河在我心中就像一首清新淡雅的小诗,流露出天地之间的灵气,诠释着岁月的深沉和古老.沧海桑田,不变的...
最新或2023(历届)冬雪飞舞... “天寒色青苍,北风叫枯桑”。当尖厉的寒风从乌黑溜光的冰面上旋起,又在笔直而光秃的杨树梢上打着呼哨而去...
改变mac的默认终端背景色显示... 什么是zsh zsh是 shell 的一种 ,但是并不是我们系统默认的 shell ,unix 衍生...
最新或2023(历届)为了自己... 每个人都有梦想,我们也曾为自己的梦想付出过,但有的人努力了很久,很久,也未曾达到。是什么原因导致了结...
python requests... python requests接口自动化测试工具类文件封装,加上中文代码注释ÿ...