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

Angular 14发送POST请求时出现401状态码问题排查

Spring Boot Basic Auth在Postman正常,Angular请求返回401问题解决

问题场景

启用安全机制的Spring Boot后端,向/snippetcollection发送POST请求时,Postman配置Basic Auth凭证可获得201创建成功响应,但Angular代码发起请求时始终返回401未授权错误。

相关代码

Spring Boot安全配置

@Configuration
public class SecurityConfig {

@Bean
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
    http.cors().configurationSource(new CorsConfigurationSource() {
        @Override
        public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
            CorsConfiguration config = new CorsConfiguration();
            config.setAllowedOrigins(Collections.singletonList("http://localhost:4200"));
            config.setAllowedMethods(Collections.singletonList("*"));
            config.setAllowCredentials(true);
            config.setAllowedHeaders(Collections.singletonList("*"));
            config.setMaxAge(3600L);
            return config;
        }
    }).and().csrf().ignoringAntMatchers("/register", "/snippetcollection")
            .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
            .and().authorizeRequests()
            .antMatchers("/snippetcollection").hasAnyRole(Role.FREE_USER.name(), Role.PRO_USER.name())
            .antMatchers("/dashboard").hasAnyRole(Role.FREE_USER.name(), Role.PRO_USER.name())
            .antMatchers("/profile").hasAnyRole(Role.FREE_USER.name(), Role.PRO_USER.name())
            .antMatchers("/tags").hasAnyRole(Role.FREE_USER.name(), Role.PRO_USER.name())
            .antMatchers("/shared").hasRole(Role.PRO_USER.name())
            .antMatchers("/settings").hasAnyRole(Role.FREE_USER.name(), Role.PRO_USER.name())
            .antMatchers("/user").authenticated()
            .antMatchers("/home", "/documentation", "/pricing", "/register", "/share", "/login").permitAll()
            .and().httpBasic()
            .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    
    return http.build();
}

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

Angular代码

HTTP拦截器

@Injectable()
export class AjaxhttpInterceptor implements HttpInterceptor {
  user = new User();

  constructor(private router: Router) {}

  intercept(req: HttpRequest<any>, next: HttpHandler) {
    let httpHeaders = new HttpHeaders();

    if (sessionStorage.getItem('userdetails')) {
      this.user = JSON.parse(sessionStorage.getItem('userdetails')!);
    }

    if (this.user && this.user.password && this.user.email) {
      httpHeaders = httpHeaders.append(
        'Authorization',
        'Basic ' + window.btoa(this.user.email + ':' + this.user.password)
      );
    }

    let xsrf = sessionStorage.getItem('XSRF-TOKEN');
    if (xsrf) {
      httpHeaders = httpHeaders.append('X-XSRF-TOKEN', xsrf);
    }

    httpHeaders = httpHeaders.append('X-Requested-With', 'XMLHttpRequest');
    const xhr = req.clone({
      headers: httpHeaders,
    });

    return next.handle(xhr).pipe(
      tap((err: any) => {
        if (err instanceof HttpErrorResponse) {
          if (err.status !== 401) {
            return;
          }
          this.router.navigate(['/dashboard']);
        }
      })
    );
  }
}

服务类

export class DashboardService {
constructor(private http: HttpClient) {}

createSnippetCollection(snippetcollection: SnippetCollection) {
   return this.http.post(
     environment.rooturl + '/snippetcollection',
     snippetcollection,
     { observe: 'response', withCredentials: true }
    );
 }
}

组件调用

this.dashboardService
    .createSnippetCollection({
      title: 'noasdsadasde export link',
      description: 'Cerasdasdvus Lorem ipsom dolor sadasdamet',
      programmingLanguage: 'nasdasdde',
    })
    .subscribe((responseData) => {
      console.log(responseData);
    });

问题排查与解决

1. 密码存储与凭证生成问题

Angular中用window.btoa(user.email + ':' + user.password)生成Basic Auth凭证,要求password是明文密码。但Spring Boot用BCryptPasswordEncoder加密存储密码,若sessionStorage中存的是加密后的密码,生成的Authorization头必然无效。

解决方式:

  • 登录接口返回用户信息时,不要返回加密密码,仅保留email等标识
  • 登录成功后直接生成完整的Auth凭证并存入sessionStorage:
    // 登录成功时执行
    const authToken = 'Basic ' + window.btoa(email + ':' + plainPassword);
    sessionStorage.setItem('authToken', authToken);
    
  • 拦截器中直接读取该凭证添加到请求头:
    const authToken = sessionStorage.getItem('authToken');
    if (authToken) {
      httpHeaders = httpHeaders.append('Authorization', authToken);
    }
    

2. 拦截器逻辑漏洞

原拦截器仅当user对象同时存在password和email时才添加Auth头,若sessionStorage中的userdetails未存储password(比如登录后只存了用户基本信息),则不会携带凭证。

修改拦截器:移除对user.password的依赖,直接使用预先生成的authToken(如上一步)。

3. 角色权限匹配问题

Spring Security的hasAnyRole方法会自动给角色名添加ROLE_前缀。例如Role.FREE_USER.name()若为FREE_USER,则系统实际校验的是ROLE_FREE_USER权限。

解决方式:

  • 方式一:改用hasAnyAuthority(不会自动添加前缀):
    .antMatchers("/snippetcollection").hasAnyAuthority(Role.FREE_USER.name(), Role.PRO_USER.name())
    
  • 方式二:确保用户实体中存储的角色带有ROLE_前缀(如ROLE_FREE_USER)。

4. 调试验证步骤

  1. 打开浏览器开发者工具的Network面板,查看POST请求的Request Headers,确认是否包含正确的Authorization: Basic xxx头
  2. 查看Response Headers中的WWW-Authenticate字段,获取具体未授权原因
  3. 查看Spring Boot后台日志,日志会输出权限校验失败的详细信息,快速定位问题

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.16 08:15:37