Spring Boot 6 + Angular CSRF令牌无效触发401错误求助
Spring Boot 6 + Angular 集成 Spring Security 时 CSRF 令牌验证失败问题
问题现象
调用/api/login接口(已排除CSRF验证)返回200,能成功获取XSRF-TOKEN Cookie;但携带该令牌和Basic Auth信息调用/api/customers接口时,返回401错误,服务器日志提示无效的CSRF令牌。
期望实现:
- 从
/api/login的响应Cookie中获取CSRF令牌 - 携带该令牌成功调用
/api/customers接口
相关代码
Spring Security 配置
@Configuration public class SecurityConfiguration { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf(c -> c.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) .csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler()) .ignoringRequestMatchers("api/login", "api/register")) .authorizeHttpRequests(a -> a .requestMatchers("api/customers").hasRole("sample") .requestMatchers("api/login", "api/register").permitAll()) .httpBasic(Customizer.withDefaults()) .cors(Customizer.withDefaults()) .addFilterAfter(new CsrfCustomFilter(), BasicAuthenticationFilter.class); return http.build(); } @Bean CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowCredentials(true); configuration.addAllowedOrigin("http://localhost:4200"); configuration.addAllowedMethod("GET"); configuration.addAllowedMethod("POST"); configuration.addAllowedHeader("*"); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); return source; } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
CsrfCustomFilter 类
public class CsrfCustomFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName()); System.out.println(csrfToken); System.out.println(csrfToken.getHeaderName()); if (csrfToken.getHeaderName() != null) { response.setHeader(csrfToken.getHeaderName(), csrfToken.getToken()); } filterChain.doFilter(request, response); } }
login.component.ts 代码
... import { AuthService } from 'src/app/service/auth.service'; import { getCookie } from 'typescript-cookie'; @Component({ selector: 'app-login', templateUrl: './login.component.html', styleUrls: ['./login.component.css'], }) export class LoginComponent implements OnDestroy { message = "no" subs = new Subscription() validateForm: FormGroup<{ username: FormControl<string>; password: FormControl<string>; remember: FormControl<boolean>; }> = this.fb.group({ username: ['', [Validators.required]], password: ['', [Validators.required]], remember: [true], }); constructor(private fb: NonNullableFormBuilder, private router: Router, private authService: AuthService) {} submitForm(): void { const userName = this.validateForm.get('username')?.value; const password = this.validateForm.get('password')?.value; localStorage.setItem('username', userName == undefined ? '' : userName); localStorage.setItem('password', password == undefined ? '' : password); this.subs = this.authService.basicAuthTestLogin().subscribe(res=>{ console.log(getCookie("XSRF-TOKEN")) }) } post() { this.authService.basiccsrfTest().subscribe((r) => { this.message = r.body || ""; console.log(r.headers) }); } ngOnDestroy(): void { this.subs.unsubscribe() } }
auth.service.ts 代码
@Injectable({ providedIn: 'root', }) export class AuthService { constructor(private httpClient: HttpClient) {} public basiccsrfTest = () => { return this.httpClient .post('http://localhost:8080/api/customers', null, { observe: 'response', responseType: 'text' }); }; public basicAuthTestLogin = () => { return this.httpClient .post('http://localhost:8080/api/login', null, { observe: 'response', responseType: 'text', withCredentials:true }); }; }
auth-interceptor.service.ts 代码
... @Injectable({ providedIn: 'root', }) export class AuthInterceptorService implements HttpInterceptor { constructor( private auth: AuthService, private tokenExtractor: HttpXsrfTokenExtractor ) {} intercept(req: HttpRequest<any>, next: HttpHandler) { const cookieheaderName = 'X-XSRF-TOKEN'; let csrfToken = this.tokenExtractor.getToken() as string; const user = localStorage.getItem('username'); const pass = localStorage.getItem('password'); const csrf = getCookie('XSRF-TOKEN'); let headers = {}; if (user != undefined && pass != undefined) { headers = { ...headers, Authorization: `Basic ${window.btoa(user + ':' + pass)}`, }; if (csrfToken !== null && !req.headers.has(cookieheaderName)) { headers = { ...headers, 'X-XSRF-TOKEN': csrfToken }; } const authReq = req.clone({setHeaders:headers}); return next.handle(authReq); } return next.handle(req); } }
app.module.ts 代码
@NgModule({ declarations: [ ... ], imports: [ ... HttpClientXsrfModule.withOptions({ cookieName: 'XSRF-TOKEN', headerName: 'X-XSRF-TOKEN', }), RouterModule.forRoot(routes), ], providers: [ { provide: NZ_I18N, useValue: en_US }, { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptorService, multi: true, }, ], bootstrap: [AppComponent], }) export class AppModule {}
问题原因及解决方案
核心原因
- Angular请求未携带Credentials:
/api/customers请求未设置withCredentials:true,导致请求时未携带XSRF-TOKENCookie,服务器无法验证令牌有效性(CSRF验证要求Cookie令牌与请求头令牌匹配)。 - 拦截器逻辑冗余:同时使用
HttpXsrfTokenExtractor和手动获取Cookie,可能导致令牌不匹配;且未确保所有请求都携带Credentials。 - CSRF过滤器顺序错误:
CsrfCustomFilter放在BasicAuthenticationFilter之后,/login请求无需Basic Auth,过滤器未触发,令牌生成逻辑不完整。
修复步骤
1. 确保Angular请求携带Credentials
修改auth.service.ts的basiccsrfTest方法,添加withCredentials:true:
public basiccsrfTest = () => { return this.httpClient .post('http://localhost:8080/api/customers', null, { observe: 'response', responseType: 'text', withCredentials: true // 新增此行 }); };
2. 简化拦截器逻辑
调整拦截器,统一使用HttpXsrfTokenExtractor获取令牌,并确保所有请求携带Credentials:
@Injectable({ providedIn: 'root', }) export class AuthInterceptorService implements HttpInterceptor { constructor( private tokenExtractor: HttpXsrfTokenExtractor ) {} intercept(req: HttpRequest<any>, next: HttpHandler) { let authReq = req; // 添加Basic Auth头 const user = localStorage.getItem('username'); const pass = localStorage.getItem('password'); if (user && pass) { authReq = authReq.clone({ setHeaders: { Authorization: `Basic ${window.btoa(user + ':' + pass)}` } }); } // 添加CSRF令牌头 const csrfToken = this.tokenExtractor.getToken(); if (csrfToken && !authReq.headers.has('X-XSRF-TOKEN')) { authReq = authReq.clone({ setHeaders: { 'X-XSRF-TOKEN': csrfToken } }); } // 强制携带Credentials if (!authReq.has('withCredentials')) { authReq = authReq.clone({ withCredentials: true }); } return next.handle(authReq); } }
3. 调整CSRF过滤器顺序
在SecurityConfiguration的filterChain方法中,将CsrfCustomFilter移至CsrfFilter之后,确保令牌生成后再添加到响应头:
.addFilterAfter(new CsrfCustomFilter(), CsrfFilter.class); // 替换原BasicAuthenticationFilter.class
4. 验证用户角色
确保调用/api/customers的用户已被正确赋予sample角色,401错误也可能因角色权限不足导致。
内容的提问来源于stack exchange,提问作者Kimin Lee
相关产品推荐
相关产品推荐

