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

Spring Security集成JWT时如何接入HttpOnly Cookie实现鉴权

核心逻辑就是用HttpOnly Cookie直接包裹JWT令牌,无需额外新增独立的Cookie鉴权过滤器,在你现有代码基础上改3个位置即可,完全兼容原有JWT校验逻辑


修改CustomAuthenticationFilter的successfulAuthentication方法,原来直接把令牌返回在响应体里的逻辑,改成把令牌写入带安全属性的HttpOnly Cookie,从根源避免XSS读取令牌:

@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException {
    User springUserDetails = (User) authentication.getPrincipal();
    Algorithm algorithm = Algorithm.HMAC256("secret".getBytes());
    
    // 生成Access Token,有效期2小时
    String access_token = JWT.create()
            .withSubject(springUserDetails.getUsername())
            .withExpiresAt(new Date(System.currentTimeMillis() + 120 * 60 * 1000))
            .withIssuer(request.getRequestURI().toString())
            .withClaim("roles", springUserDetails.getAuthorities()
                    .stream()
                    .map(GrantedAuthority::getAuthority)
                    .collect(Collectors.toList()))
            .sign(algorithm);

    // 生成Refresh Token,有效期7天(不要和Access Token同有效期)
    String refresh_token = JWT.create()
            .withSubject(springUserDetails.getUsername())
            .withExpiresAt(new Date(System.currentTimeMillis() + 7 * 24 * 60 * 60 * 1000))
            .withIssuer(request.getRequestURI().toString())
            .withClaim("roles", springUserDetails.getAuthorities()
                    .stream()
                    .map(GrantedAuthority::getAuthority)
                    .collect(Collectors.toList()))
            .sign(algorithm);

    // 写入Access Token Cookie
    Cookie accessCookie = new Cookie("access_token", access_token);
    accessCookie.setHttpOnly(true);
    accessCookie.setPath("/");
    accessCookie.setMaxAge(120 * 60);
    // 生产环境改为true,仅允许HTTPS传输
    accessCookie.setSecure(false);
    // 跨域场景必须配置,降低CSRF风险
    accessCookie.setAttribute("SameSite", "Lax");
    response.addCookie(accessCookie);

    // 写入Refresh Token Cookie,仅允许刷新接口读取
    Cookie refreshCookie = new Cookie("refresh_token", refresh_token);
    refreshCookie.setHttpOnly(true);
    refreshCookie.setPath("/User/refreshToken");
    refreshCookie.setMaxAge(7 * 24 * 60 * 60);
    refreshCookie.setSecure(false);
    refreshCookie.setAttribute("SameSite", "Lax");
    response.addCookie(refreshCookie);

    // 可保留简单成功响应,无需再把令牌返回给前端
    Map<String, String> resp = new HashMap<>();
    resp.put("msg", "login success");
    response.setContentType(APPLICATION_JSON_VALUE);
    new ObjectMapper().writeValue(response.getOutputStream(), resp);
}

2. 改造令牌校验过滤器,整合Cookie读取逻辑

不需要额外新增你参考的那个CookieAuthenticationFilter——那个实现直接把Cookie值塞入认证上下文,没有做JWT合法性校验,存在严重安全漏洞。直接把Cookie读取逻辑整合到现有CustomAuthorizationFilter里即可,优先读Cookie中的令牌,同时兼容原有Authorization头传参的模式方便调试:

public class CustomAuthorizationFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 修正原路径匹配bug,原写法的/**匹配不生效
        if (request.getServletPath().equals("/login") || request.getServletPath().startsWith("/User/refreshToken")) {
            filterChain.doFilter(request,response);
            return;
        }

        String token = null;
        // 第一步:优先从HttpOnly Cookie取Access Token
        Cookie[] cookies = request.getCookies();
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if ("access_token".equals(cookie.getName())) {
                    token = cookie.getValue();
                    break;
                }
            }
        }

        // 第二步:Cookie无令牌时,兼容从Authorization头取Bearer Token
        if (token == null) {
            String authorizationHeader = request.getHeader(AUTHORIZATION);
            if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
                token = authorizationHeader.substring("Bearer ".length());
            }
        }

        // 取到令牌后走原有JWT校验逻辑
        if (token != null) {
            try {
                Algorithm algorithm = Algorithm.HMAC256("secret".getBytes());
                JWTVerifier verifier = JWT.require(algorithm).build();
                DecodedJWT decodedJWT = verifier.verify(token);
                String email = decodedJWT.getSubject();
                String[] roles = decodedJWT.getClaim("roles").asArray(String.class);
                Collection<SimpleGrantedAuthority> authorities = new ArrayList<>();
                stream(roles).forEach(role -> authorities.add(new SimpleGrantedAuthority(role)));
                UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(email, null, authorities);
                SecurityContextHolder.getContext().setAuthentication(authToken);
                filterChain.doFilter(request, response);
            } catch (Exception e) {
                response.setHeader("error" , e.getMessage());
                response.setStatus(FORBIDDEN.value());
                Map<String, String> error = new HashMap<>();
                error.put("error_message", e.getMessage());
                response.setContentType(APPLICATION_JSON_VALUE);
                new ObjectMapper().writeValue(response.getOutputStream(), error);
            }
        } else {
            // 无令牌直接放行,由Spring Security拦截返回未认证错误
            filterChain.doFilter(request, response);
        }
    }
}

3. 调整SecurityConfiguration配置

补上跨域、登出、路径匹配的必要配置,否则Cookie会在跨域场景下失效:

@Override
protected void configure(HttpSecurity http) throws Exception {
    // 配置CORS允许跨域携带Cookie
    http.cors().configurationSource(request -> {
        CorsConfiguration config = new CorsConfiguration();
        // 替换为实际前端域名,不要用*通配符
        config.setAllowedOrigins(List.of("http://localhost:8080"));
        config.setAllowedMethods(List.of("GET","POST","PUT","DELETE","OPTIONS"));
        config.setAllowedHeaders(List.of("*"));
        // 核心配置:允许跨域携带凭证
        config.setAllowCredentials(true);
        return config;
    }).and().csrf().disable();

    http.addFilter(new CustomAuthenticationFilter(authenticationManagerBean()));
    http.addFilterBefore(new CustomAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class);
    http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and().logout()
            // 登出时清除两个令牌Cookie
            .deleteCookies("access_token", "refresh_token")
            .logoutSuccessHandler((req, res, auth) -> res.setStatus(HttpServletResponse.SC_OK))
        .and().authorizeRequests()
            // 修正原路径匹配规则,放开refreshToken的所有子路径
            .antMatchers("/login/**", "/User/refreshToken/**", "/User/add").permitAll()
            .antMatchers(GET, "/**").hasAnyAuthority("STUDENT")
            .anyRequest().authenticated();
}

注意事项

  • 前端请求时不需要手动在代码里存取令牌,浏览器会自动携带同域的HttpOnly Cookie,跨域场景下需要将前端请求的withCredentials配置设为true
  • 代码中硬编码的JWT签名密钥secret生产环境必须替换为从配置中心/环境变量读取的强随机字符串,禁止硬编码
  • 刷新Token的接口逻辑需要同步改造,从Cookie中读取refresh_token做校验,生成新的Access Token后写回Cookie即可
  • 生产环境必须将所有Cookie的setSecure属性设为true,强制HTTPS传输,避免令牌被明文窃听
  • 正式上线前建议开启CSRF防护,进一步降低跨站请求伪造风险

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.30 05:57:17