You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Security+Spring Boot中会话管理与AccessToken校验优化

会话与AccessToken混合认证的实现疑问

业务需求

  • AccessToken完成身份验证后即可过期,用户基于会话生命周期配置继续访问资源;
  • 携带AccessToken的首次请求,Spring Security默认执行Token校验;
  • 校验通过后,用户详情及其他信息存入Tomcat内存会话;
  • 后续携带JSESSIONID与AccessToken的请求,Spring Security优先检查内存中是否存在有效会话,存在则跳过AccessToken校验,否则执行校验。

设计原因

  • 优化性能,避免同一用户每次请求重复校验AccessToken,首次校验后认证信息已存储在会话中。

疑问

  1. 将AccessToken详情存储在会话中,待AccessToken过期后使用会话中的认证信息是否合理?
  2. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 23:55:53