Spring Security集成JWT时如何接入HttpOnly Cookie实现鉴权
核心逻辑就是用HttpOnly Cookie直接包裹JWT令牌,无需额外新增独立的Cookie鉴权过滤器,在你现有代码基础上改3个位置即可,完全兼容原有JWT校验逻辑
1. 改造登录成功逻辑,将JWT写入HttpOnly Cookie
修改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
相关产品推荐
相关产品推荐

