Spring Security+Spring Boot中会话管理与AccessToken校验优化
会话与AccessToken混合认证的实现疑问
业务需求
- AccessToken完成身份验证后即可过期,用户基于会话生命周期配置继续访问资源;
- 携带AccessToken的首次请求,Spring Security默认执行Token校验;
- 校验通过后,用户详情及其他信息存入Tomcat内存会话;
- 后续携带JSESSIONID与AccessToken的请求,Spring Security优先检查内存中是否存在有效会话,存在则跳过AccessToken校验,否则执行校验。
设计原因
- 优化性能,避免同一用户每次请求重复校验AccessToken,首次校验后认证信息已存储在会话中。
疑问
- 将AccessToken详情存储在会话中,待AccessToken过期后使用会话中的认证信息是否合理?
- Spring Security针对该场景有何推荐方案,或更优实现方式?
资源服务器实现细节
Spring Security适配器配置
package org.cfx.resouce.server; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.jwt.JwtDecoders; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter; import org.springframework.security.web.SecurityFilterChain; @Configuration public class ResouceServerAdapter { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.addFilterBefore(new CookieAuthenticationFilter(), BearerTokenAuthenticationFilter.class); http.addFilterAfter(new SecurityContextHolderSetterAfterAccessTokenValidation(), BearerTokenAuthenticationFilter.class); http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED).and().csrf().disable(); http.authorizeRequests().antMatchers("/msg").authenticated() .antMatchers("/test").permitAll().anyRequest().denyAll().and().oauth2ResourceServer().jwt() .jwtAuthenticationConverter(jwtAuthenticationConverter()); return http.build(); } @Bean public JwtDecoder jwtDecoder() { return JwtDecoders.fromIssuerLocation("http://host:port/openam/oauth2/Test"); } @Bean public JwtAuthenticationConverter jwtAuthenticationConverter() { JwtGrantedAuthoritiesConverter converter = new JwtGrantedAuthoritiesConverter(); converter.setAuthoritiesClaimName("groups"); converter.setAuthorityPrefix("ROLE_"); JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(converter); return jwtAuthenticationConverter; } }
自定义过滤器实现
CookieAuthenticationFilter
package org.cfx.resouce.server; import java.io.IOException; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.apache.commons.lang3.tuple.ImmutablePair; import org.springframework.http.HttpHeaders; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.filter.OncePerRequestFilter; public class CookieAuthenticationFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { ImmutablePair<String, String> resolvedHeaders = Utils.resolveHeaders(request); //String accessToken = resolvedHeaders.left; String cookieValue = resolvedHeaders.right; //BFAA1E95A63DA333CDBFACE6786FCC26 // if cookie present then need to check inside session if (cookieValue != null) { System.out.println("Cookie Value Found Inside Header........"); HttpSession session = request.getSession(false); String sessionId = session != null ? session.getId() : null; System.out.println("------Session Found with ID: --------: " + sessionId); System.out.println("Session :: " + (session != null ? session.getAttribute("SPRING_SECURITY_CONTEXT") : null)); if (session != null && session.getAttribute("SPRING_SECURITY_CONTEXT") != null) { System.out.println("--------------- Session ID Found ------------ "); SecurityContext context = SecurityContextHolder.createEmptyContext(); context.setAuthentication((Authentication) session.getAttribute("SPRING_SECURITY_CONTEXT")); SecurityContextHolder.setContext(context); System.out.println("Removing the Authorization Header......"); // Remove the header for Bearer Token Validation to skip BearerTokenAuthenticationFilter. HttpServletRequestWrapper requestWrapper = new HttpServletRequestWrapper(request) { @Override public String getHeader(String name) { if (name.equalsIgnoreCase(HttpHeaders.AUTHORIZATION)) { // Remove the header return null; } return super.getHeader(name); } }; filterChain.doFilter(requestWrapper, response); } else { System.out.println("Go for Bearer token validation because session not found....."); filterChain.doFilter(request, response); } } else { System.out.println("Go for Bearer token validation....."); filterChain.doFilter(request, response); } } }
SecurityContextHolderSetterAfterAccessTokenValidation
package org.cfx.resouce.server; import java.io.IOException; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.apache.commons.lang3.tuple.ImmutablePair; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.filter.OncePerRequestFilter; public class SecurityContextHolderSetterAfterAccessTokenValidation extends OncePerRequestFilter { protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { ImmutablePair<String, String> resolvedHeaders = Utils.resolveHeaders(request); String accessToken = resolvedHeaders.left; // if cookie present then need to check inside session if (accessToken != null) { // Session doesn't exist, fall back to token-based authentication Authentication context = SecurityContextHolder.getContext().getAuthentication(); System.out.println("Context :: " + context); HttpSession newSession = request.getSession(true); newSession.setAttribute("SPRING_SECURITY_CONTEXT", context); // SPRING_SECURITY_CONTEXT System.out.println( "Session ID for Newly Created Session::<<<<<< " + newSession.getId() + " >>>>>>>>>>>>>> "); filterChain.doFilter(request, response); } else { System.out.println("Auth Done By Cookie Value..."); filterChain.doFilter(request, response); } } }
应用主类
package org.cfx.resouce.server; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; @SpringBootApplication public class ResouceServerMainApp extends SpringBootServletInitializer { public static void main(String[] args) { SpringApplication.run(ResouceServerMainApp.class, args); } }
application.properties配置
server.port=8080 server.servlet.context-path=/server server.shutdown=graceful #logging.level.org.quartz=DEBUG #logging.level.org.apache.tomcat.util=DEBUG #logging.level.org.springframework=DEBUG logging.level.org.springframework.security=TRACE #logging.level.com.zaxxer.hikari=DEBUG #server.tomcat.accesslog.enabled=true #logging.level.org.apache.tomcat=DEBUG #logging.level.org.apache.catalina=DEBUG #org.apache.catalina.session.StandardSession.ACTIVITY_CHECK=true
测试Curl命令
curl --location 'http://localhost:8080/server/msg' --header 'Cookie: JSESSIONID=<JSESSION ID GOES HERE>' --header 'Authorization: Bearer <ACCESS_TOKEN GOES HERE>'
疑问解答
1. AccessToken过期后使用会话中存储的认证信息是否合理?
这种做法存在安全与业务逻辑上的权衡:
- 风险点:AccessToken的过期机制是为了限制凭证的有效时间,降低被盗用后的影响范围。如果AccessToken过期后仍依赖会话中的认证信息,相当于绕过了这一安全机制——若会话被盗(比如JSESSIONID泄露),攻击者可在会话有效期内持续访问资源,不受AccessToken过期的限制。此外,若用户权限在会话有效期内发生变更,会话中存储的旧认证信息无法及时同步,会导致权限校验失效。
- 合理性场景:如果业务场景对性能的优先级远高于短期凭证的安全性,且能通过其他手段保障会话安全(比如启用HTTPS、设置HttpOnly/Secure Cookie、合理配置会话超时时间、限制会话并发数),这种方式可以接受。但需明确业务风险并做好相应防护。
2. Spring Security的推荐方案与更优实现
(1)利用Spring Security内置的会话管理能力
Spring Security默认通过SessionSecurityContextRepository将SecurityContext存储到会话中,无需手动编写过滤器存储SPRING_SECURITY_CONTEXT。你可以简化现有实现:
- 移除自定义的
SecurityContextHolderSetterAfterAccessTokenValidation过滤器,因为SessionManagementFilter会自动在认证成功后将SecurityContext存入会话。 - 调整
CookieAuthenticationFilter,直接从会话中获取SecurityContext并设置到SecurityContextHolder,无需手动移除Authorization header——只要SecurityContextHolder中已有有效认证信息,BearerTokenAuthenticationFilter会自动跳过校验。
(2)缓存Token校验结果
如果不想依赖会话,可通过缓存(比如Redis、Caffeine)存储已校验通过的Token及其对应的认证信息,设置缓存过期时间与AccessToken过期时间同步。Spring Security支持通过自定义AuthenticationManager或JwtDecoder结合缓存实现,避免重复校验。
(3)分布式会话支持
若服务是集群部署,Tomcat内存会话无法共享,此时推荐使用Spring Session整合Redis等存储,实现会话的分布式共享,同时保持会话与认证信息的一致性。
(4)优化现有过滤器逻辑
现有自定义过滤器中直接移除Authorization header的方式不够优雅,可通过判断SecurityContextHolder的状态来决定是否执行Token校验:如果SecurityContextHolder中已有有效认证信息,BearerTokenAuthenticationFilter会自动跳过校验,无需修改请求头。
内容的提问来源于stack exchange,提问作者Ravi Ranjan
相关产品推荐
相关产品推荐

