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

Spring Boot Security:Cookie传JWT替代Authorization头遇认证失败

从Cookie读取JWT实现Spring Security认证的解决方案

问题背景

正在从Spring Security会话认证迁移到JWT认证,为避免XSS风险选择将JWT存储在HttpOnly Cookie中,而非localStorage。但集成测试时持续报错,提示找不到Bearer token:

main] .s.r.w.a.BearerTokenAuthenticationFilter : Did not process request since did not find bearer token

核心问题是默认的BearerTokenAuthenticationFilter只会从Authorization请求头读取Bearer格式的token,无法识别Cookie中的JWT。

问题分析

你之前尝试的JwtFilter存在两个关键问题:

  1. 提取Cookie值错误:代码中通过map(Cookie::getName)拿到的是Cookie名称而非token值,导致添加到Header的是Cookie名而非实际JWT。
  2. 过滤器逻辑冗余:无需对所有请求都包装Request,仅当存在目标Cookie时才需要添加Header。

解决方案

提供两种可行方案,推荐第二种更贴合Spring Security规范的实现:

方案一:修复自定义Jwt过滤器

调整过滤器逻辑,正确提取Cookie中的JWT并注入到Authorization头,同时对无需认证的请求直接放行:

@Component
@Slf4j
public class JwtFilter extends OncePerRequestFilter {

    @Value("${your.jwt.cookie.name}") // 替换为你的JWT Cookie名称配置
    private String JWT_COOKIE_NAME;

    @Override
    protected void doFilterInternal(
            @NotNull HttpServletRequest request,
            @NotNull HttpServletResponse response,
            @NotNull FilterChain filterChain
    ) throws ServletException, IOException {
        // 对公开路径(如登录)直接放行,无需处理
        if (isPublicRequest(request)) {
            filterChain.doFilter(request, response);
            return;
        }

        Cookie[] cookies = request.getCookies();
        if (cookies != null) {
            Optional<Cookie> jwtCookie = Arrays.stream(cookies)
                    .filter(cookie -> JWT_COOKIE_NAME.equals(cookie.getName()))
                    .findFirst();

            jwtCookie.ifPresent(cookie -> {
                // 包装请求,添加Authorization头
                HeaderMapRequestWrapper requestWrapper = new HeaderMapRequestWrapper(request);
                requestWrapper.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + cookie.getValue());
                try {
                    filterChain.doFilter(requestWrapper, response);
                } catch (IOException | ServletException e) {
                    log.error("处理JWT Cookie时出错", e);
                }
                return;
            });
        }

        // 未找到Cookie时继续执行,交由后续过滤器处理认证失败逻辑
        filterChain.doFilter(request, response);
    }

    // 判断是否为公开请求,可复用你的publicRoutes()逻辑
    private boolean isPublicRequest(HttpServletRequest request) {
        String path = request.getRequestURI();
        return path.startsWith("/login") || // 根据实际公开路径调整
               path.startsWith("/public");
    }
}

然后在SecurityFilterChain中启用过滤器,确保它在BearerTokenAuthenticationFilter之前执行:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    return http
            .csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))
            .cors(Customizer.withDefaults())
            .authorizeHttpRequests(auth -> {
                auth.requestMatchers(publicRoutes()).permitAll();
                auth.anyRequest().authenticated();
            })
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
            .exceptionHandling(ex -> ex.authenticationEntryPoint(this.authEntryPoint))
            .addFilterBefore(jwtFilter, BearerTokenAuthenticationFilter.class) // 添加过滤器
            .logout(out -> out
                    .logoutUrl("/logout")
                    .deleteCookies(COOKIE_NAME, LOGGEDSESSION)
                    .logoutSuccessHandler((request, response, authentication) ->
                            SecurityContextHolder.clearContext()
                    )
            )
            .build();
}

方案二:自定义JWT Token解析器(推荐)

通过Spring Security提供的JwtBearerTokenResolver扩展,直接从Cookie读取JWT,无需手动修改请求头,更符合框架规范:

@Bean
public JwtBearerTokenResolver jwtBearerTokenResolver() {
    JwtBearerTokenResolver resolver = new JwtBearerTokenResolver();
    // 自定义从Cookie提取token的逻辑
    resolver.setBearerTokenExtractor(request -> {
        Cookie[] cookies = request.getCookies();
        if (cookies != null) {
            return Arrays.stream(cookies)
                    .filter(cookie -> COOKIE_NAME.equals(cookie.getName())) // COOKIE_NAME为你的JWT Cookie名
                    .map(Cookie::getValue)
                    .findFirst()
                    .orElse(null);
        }
        return null;
    });
    return resolver;
}

然后在SecurityFilterChain中配置该解析器:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    return http
            .csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))
            .cors(Customizer.withDefaults())
            .authorizeHttpRequests(auth -> {
                auth.requestMatchers(publicRoutes()).permitAll();
                auth.anyRequest().authenticated();
            })
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .oauth2ResourceServer(oauth2 -> oauth2
                    .jwt(jwt -> jwt.bearerTokenResolver(jwtBearerTokenResolver())) // 配置自定义解析器
            )
            .exceptionHandling(ex -> ex.authenticationEntryPoint(this.authEntryPoint))
            .logout(out -> out
                    .logoutUrl("/logout")
                    .deleteCookies(COOKIE_NAME, LOGGEDSESSION)
                    .logoutSuccessHandler((request, response, authentication) ->
                            SecurityContextHolder.clearContext()
                    )
            )
            .build();
}

集成测试验证

你的现有测试代码逻辑正确,只需确保COOKIE_NAME与实际JWT Cookie名称一致,运行测试即可验证认证是否正常通过。

内容的提问来源于stack exchange,提问作者iTchTheRightSpot

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.17 06:30:47