Spring Boot与Angular5环境下HttpOnly Cookie存储JWT认证问题
嘿,这个问题我之前做前后端分离项目时也碰到过,把JWT放进HttpOnly + Secure的Cookie里确实是比localStorage安全得多的方案,刚好适配你的Angular 5 + Spring Boot场景,我给你一步步拆解解决方案:
因为Angular和Spring Boot是前后端分离架构,跨域是必须解决的前提,而且要确保允许请求携带Cookie,这是Cookie传递的基础。
Spring Boot端全局跨域配置
别用@CrossOrigin的通配符配置,一定要指定具体的Angular域名,否则允许携带Cookie的设置会失效:
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("http://localhost:4200") // 替换成你的Angular实际域名+端口 .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) // 核心:允许请求携带Cookie .maxAge(3600); // 预检请求缓存时间 } }
Angular端全局请求配置
通过Http拦截器给所有请求加上withCredentials: true,这样请求会自动携带Cookie:
import { Injectable } from '@angular/core'; import { HttpInterceptor, HttpRequest, HttpHandler } from '@angular/common/http'; @Injectable() export class AuthInterceptor implements HttpInterceptor { intercept(req: HttpRequest<any>, next: HttpHandler) { // 克隆请求并开启携带Cookie的配置 const authRequest = req.clone({ withCredentials: true }); return next.handle(authRequest); } } // 在AppModule里注册拦截器 @NgModule({ providers: [ { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true } ] }) export class AppModule { }
用户登录验证通过后,不要把JWT直接返回给前端,而是封装进HttpOnly、Secure的Cookie中:
@PostMapping("/login") public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) { // 1. 验证邮箱和密码,生成JWT令牌 Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(loginRequest.getEmail(), loginRequest.getPassword()) ); String jwt = jwtTokenProvider.generateToken(authentication); // 2. 创建JWT Cookie Cookie jwtCookie = new Cookie("JWT_TOKEN", jwt); jwtCookie.setHttpOnly(true); // 关键:禁止JavaScript访问,彻底防范XSS jwtCookie.setSecure(true); // 生产环境必须开启:只在HTTPS下传输Cookie jwtCookie.setPath("/"); // 让整个应用路径都能访问该Cookie jwtCookie.setMaxAge(86400); // 设置有效期,比如24小时 // 3. 将Cookie放入响应头 HttpHeaders headers = new HttpHeaders(); headers.add(HttpHeaders.SET_COOKIE, jwtCookie.toString()); return ResponseEntity.ok().headers(headers).body("登录成功"); }
注意:开发环境如果用HTTP协议,
setSecure(true)会导致Cookie不生效,可以根据环境动态设置:jwtCookie.setSecure(env.acceptsProfiles("prod"))
写一个过滤器,从请求Cookie中提取JWT并完成身份验证:
@Component public class JwtAuthFilter extends OncePerRequestFilter { @Autowired private JwtTokenProvider jwtTokenProvider; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 从Cookie中获取JWT String jwt = extractJwtFromCookie(request); // 验证JWT并设置认证上下文 if (StringUtils.hasText(jwt) && jwtTokenProvider.validateToken(jwt)) { Authentication auth = jwtTokenProvider.getAuthentication(jwt); SecurityContextHolder.getContext().setAuthentication(auth); } filterChain.doFilter(request, response); } private String extractJwtFromCookie(HttpServletRequest request) { Cookie[] cookies = request.getCookies(); if (cookies != null) { for (Cookie cookie : cookies) { if ("JWT_TOKEN".equals(cookie.getName())) { return cookie.getValue(); } } } return null; } }
然后把这个过滤器加到Spring Security配置中:
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private JwtAuthFilter jwtAuthFilter; @Override protected void configure(HttpSecurity http) throws Exception { http.cors() .and() .csrf().disable() // 临时关闭,后面会讲CSRF防护 .authorizeRequests() .antMatchers("/login").permitAll() // 登录接口放行 .anyRequest().authenticated() .and() .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); } }
用Cookie存JWT能防XSS,但CSRF攻击依然存在,必须补上防护:
- Spring Boot端:开启CSRF防护(默认是开启的,把上面配置里的
csrf().disable()删掉即可),Spring Boot会自动把CSRF令牌存入名为XSRF-TOKEN的HttpOnly Cookie中。 - Angular端:HttpClient会自动读取
XSRF-TOKENCookie,并在请求头中添加X-XSRF-TOKEN,只要你之前配置了withCredentials: true,这个过程完全自动,不用额外写代码。
如果JWT有过期时间,建议额外生成一个刷新令牌(同样存入HttpOnly Cookie),当JWT过期时,前端调用刷新接口获取新的JWT:
@PostMapping("/refresh-token") public ResponseEntity<?> refreshToken(HttpServletRequest request) { String refreshToken = extractRefreshTokenFromCookie(request); if (StringUtils.hasText(refreshToken) && jwtTokenProvider.validateRefreshToken(refreshToken)) { String newJwt = jwtTokenProvider.generateNewJwt(refreshToken); // 更新JWT Cookie Cookie jwtCookie = new Cookie("JWT_TOKEN", newJwt); jwtCookie.setHttpOnly(true); jwtCookie.setSecure(true); jwtCookie.setPath("/"); jwtCookie.setMaxAge(86400); HttpHeaders headers = new HttpHeaders(); headers.add(HttpHeaders.SET_COOKIE, jwtCookie.toString()); return ResponseEntity.ok().headers(headers).body("令牌已刷新"); } return ResponseEntity.badRequest().body("刷新令牌无效"); }
Angular端可以在拦截器中捕获401错误,自动调用刷新令牌接口,然后重试原请求。
内容的提问来源于stack exchange,提问作者Miha Jamsek

