总结:Spring Security会将信息储存在SecurityContext中,请求过程中会被SecurityContextHolder进行管理,底层是基于的ThreadLocal(当然还有很多种策略)
问题:SpringBoot底层的servlet会将每个请求分配一个线程,用ThreadLocal能拿到数据?
单纯的用ThreadLocal当然不够,其自身有一个小“仓库”
而SecurityContext被“保存”于这个仓库SecurityContextRepository中(实际上是放在session里的)
这样每次请求都能从仓库拿到之前的context,也就知道用户到底认证没有了
我们上文提到“SecurityContext请求过程中会被SecurityContextHolder进行管理,底层是基于的ThreadLocal”
实际并不严谨,因为SecurityContextHolder底层有多种实现
上代码
我们可以看到第一个if里就判断了spring.security.strategy配置里是否有值
所以我们需要纠正为“在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++;}
可以看到底层就是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)
注意:如果是忽略路径的话,是不会走这个过滤器的
在filter第一句就是if判断语句:用于确保每个请求只应用一次筛选器
static final String FILTER_APPLIED = "__spring_security_scpf_applied";if (request.getAttribute(FILTER_APPLIED) != null) {chain.doFilter(request, response);return;}
根据request和response新建HttpRequestResponseHolder,然后将holder放入repo寻找其context
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);
获取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
可以看到从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获取到此结束
(省略了一些打log逻辑)
SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);SecurityContextHolder.setContext(contextBeforeChainExecution);chain.doFilter(holder.getRequest(), holder.getResponse());
这段很简略
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保存在服务器,等于用户的认证信息实际上还是被保存在了服务端
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
自动登录过滤器:RememberMeAuthenticationFilter
当用户没有登录时,会读取request里的cookie来进行自动登录认证
读取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的方式
一种是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);}
“解析”完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;}
至此自动登录的认证完成
上一篇:最新或2023(历届)二年级防溺水手抄报的文字图片_小学生防溺水手抄报精美图片 最新或2023(历届)二年级防溺水手抄报的文字图片_小学生防溺水手抄报精美图片
下一篇:最新或2023(历届)关于安全简单又漂亮的手抄报图片 安全手抄报简单又漂亮4开纸图文 关于安全的手抄报简单又漂亮图片