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

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 {}

问题原因及解决方案

核心原因

  1. Angular请求未携带Credentials:/api/customers请求未设置withCredentials:true,导致请求时未携带XSRF-TOKEN Cookie,服务器无法验证令牌有效性(CSRF验证要求Cookie令牌与请求头令牌匹配)。
  2. 拦截器逻辑冗余:同时使用HttpXsrfTokenExtractor和手动获取Cookie,可能导致令牌不匹配;且未确保所有请求都携带Credentials。
  3. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 05:04:51