升级至Spring Security 6及SAS 1.0.1遇401错误:会话问题排查
背景
将代码从Spring Authorization Server 0.3.0升级至1.0.1(同步升级至Spring Security 6.x),原有自定义流程要求先验证JWT令牌,再进入主OAuth流程,该流程在旧版本中运行正常。
原有配置(Spring Security 5 + Authorization Server 0.3.0)
@Bean @Order(1) public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http); http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.ALWAYS); return http.build(); } @Bean @Order(2) public SecurityFilterChain standardSecurityFilterChain(HttpSecurity http) throws Exception { http.authorizeRequests( authorize -> authorize .antMatchers(publicRoutes.toArray(new String[0])) .permitAll() .anyRequest() .authenticated()) .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt); http.csrf().disable(); http.headers().frameOptions().disable(); return http.build(); }
原有/login端点逻辑
@GetMapping("/login") public String login( @RequestParam("response_type") String responseType, @RequestParam("client_id") String clientId, @RequestParam("scope") String scope, @RequestParam("state") String state, @RequestParam("redirect_uri") String redirectUri, @RequestParam("nonce") String nonce, Authentication authentication, HttpServletRequest httpServletRequest, HttpSession httpSession) { return String.format( "redirect:%s/oauth2/authorize?response_type=%s&client_id=%s&scope=%s&state=%s&nonce=%s&redirect_uri=%s", serverFqdn, responseType, clientId, scope, state, nonce, redirectUri); }
升级后的尝试及问题
升级后调用/oauth2/authorize出现401错误,了解到Spring Security 6会话持久化机制有变化,尝试修改配置:
@Bean @Order(1) public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http); http.getConfigurer( OAuth2AuthorizationServerConfigurer.class) .oidc( Customizer.withDefaults()); http .sessionManagement().sessionCreationPolicy( SessionCreationPolicy.ALWAYS ) .and() .securityContext(securityContext -> securityContext .requireExplicitSave( false ) .securityContextRepository( patchedDelegatingSecurityContextRepository ) // or can this be any securityContextRepository that's here.. I assumed it needs to be a bean? ); return http.build(); }
同时修改/login方法尝试手动保存会话:
@GetMapping("/login") public String login( @RequestParam("response_type") String responseType, @RequestParam("client_id") String clientId, @RequestParam("scope") String scope, @RequestParam("state") String state, @RequestParam("redirect_uri") String redirectUri, @RequestParam("nonce") String nonce, Authentication authentication, HttpServletRequest httpServletRequest, HttpSession httpSession) { SecurityContext context = securityContextHolderStrategy.getContext(); context.setAuthentication(authentication); SecurityContextHolder.setContext(context); securityContextHolderStrategy.setContext( context ); sessionSecurityContextRepository.saveContext(context, httpServletRequest,response); return String.format( "redirect:%s/oauth2/authorize?response_type=%s&client_id=%s&scope=%s&state=%s&nonce=%s&redirect_uri=%s", serverFqdn, responseType, clientId, scope, state, nonce, redirectUri); }
但会话仍未正确保存或被读取,询问遗漏的配置项。
解决方案
1. 为两个SecurityFilterChain配置共享的SecurityContextRepository
Spring Security 6中,不同的SecurityFilterChain默认使用独立的SecurityContextRepository,导致/login端点所在链保存的会话无法被授权服务器链读取。需显式配置共享实例:
首先定义共享Bean:
@Bean public SecurityContextRepository securityContextRepository() { return new HttpSessionSecurityContextRepository(); }
然后修改两个过滤器链配置:
@Bean @Order(1) public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http, SecurityContextRepository securityContextRepository) throws Exception { OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http); http.getConfigurer(OAuth2AuthorizationServerConfigurer.class) .oidc(Customizer.withDefaults()); http.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.ALWAYS)) .securityContext(securityContext -> securityContext .securityContextRepository(securityContextRepository) .requireExplicitSave(false)); // 由框架自动处理上下文保存,无需显式调用 return http.build(); } @Bean @Order(2) public SecurityFilterChain standardSecurityFilterChain(HttpSecurity http, SecurityContextRepository securityContextRepository) throws Exception { // 注意:Spring Security 6中authorizeRequests已被authorizeHttpRequests替代,antMatchers替换为requestMatchers http.authorizeHttpRequests( authorize -> authorize .requestMatchers(publicRoutes.toArray(new String[0])) .permitAll() .anyRequest() .authenticated()) .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults())) .csrf(csrf -> csrf.disable()) .headers(headers -> headers.frameOptions(frame -> frame.disable())) .securityContext(securityContext -> securityContext .securityContextRepository(securityContextRepository)); // 绑定共享的上下文仓库 return http.build(); }
2. 简化/login端点的会话处理
只要共享的SecurityContextRepository配置正确,无需手动调用saveContext,框架会自动将SecurityContext同步到会话中。简化后的/login方法:
@GetMapping("/login") public String login( @RequestParam("response_type") String responseType, @RequestParam("client_id") String clientId, @RequestParam("scope") String scope, @RequestParam("state") String state, @RequestParam("redirect_uri") String redirectUri, @RequestParam("nonce") String nonce, Authentication authentication) { // Authentication已由oauth2ResourceServer的JWT验证逻辑自动填充,无需手动处理上下文 return String.format( "redirect:%s/oauth2/authorize?response_type=%s&client_id=%s&scope=%s&state=%s&nonce=%s&redirect_uri=%s", serverFqdn, responseType, clientId, scope, state, nonce, redirectUri); }
3. 统一会话Cookie配置
确保两个过滤器链使用相同的会话Cookie名称,避免因Cookie不一致导致会话无法共享:
在两个SecurityFilterChain配置中添加:
http.sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.ALWAYS) .sessionCookie(cookie -> cookie.name("JSESSIONID"))); // 使用统一的Cookie名称
4. 验证/oauth2/authorize的访问规则
Spring Authorization Server 1.0.x中,/oauth2/authorize端点默认要求已认证用户,需确保会话中的Authentication能被授权服务器链正确识别。若存在自定义拦截器,需排查是否拦截了合法的会话请求。
内容的提问来源于stack exchange,提问作者Colin Riddell

