Angular无法登录Spring Boot OAuth2 Resource Server问题排查
Spring Boot OAuth2资源服务器Angular登录401问题
我用spring-boot-starter-oauth2-resource-server搭建了Spring Boot应用,Postman测试能正常获取token,但Angular前端调用时返回401 Unauthorized,后端token()方法接收的Authentication对象为null。之前用spring-boot-starter-security时Angular登录正常。
后端代码
pom.xml依赖
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.0.3</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.devskills</groupId> <artifactId>oauth2api</artifactId> <version>0.0.1-SNAPSHOT</version> <name>oauth2api</name> <description>Oauth2 security security with Angular in frontend</description> <properties> <java.version>17</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-resource-server</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
安全配置类
package com.devskills.oauth2api.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.userdetails.User; import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.jwt.JwtEncoder; import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; import org.springframework.security.oauth2.jwt.NimbusJwtEncoder; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.SecurityFilterChain; import com.nimbusds.jose.jwk.JWK; import com.nimbusds.jose.jwk.JWKSet; import com.nimbusds.jose.jwk.RSAKey; import com.nimbusds.jose.jwk.source.ImmutableJWKSet; import com.nimbusds.jose.jwk.source.JWKSource; import com.nimbusds.jose.proc.SecurityContext; import static org.springframework.security.config.Customizer.*; @Configuration @EnableWebSecurity public class SecurityConfig { private final RsaKeyProperties rsaKeys; public SecurityConfig(RsaKeyProperties rsaKeys) { this.rsaKeys = rsaKeys; } @Bean InMemoryUserDetailsManager user() { return new InMemoryUserDetailsManager( User.withUsername("keen") .password("{noop}password") .authorities("read") .build() ); } @Bean SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { return http .csrf(csrf -> csrf.disable()) .authorizeHttpRequests() .requestMatchers("/token").permitAll() .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() .anyRequest().authenticated() .and() .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .httpBasic(withDefaults()) .build(); } @Bean JwtDecoder jwtDecoder() { return NimbusJwtDecoder.withPublicKey(rsaKeys.publicKey()).build(); } @Bean JwtEncoder jwtEncoder() { JWK jwk = new RSAKey.Builder(rsaKeys.publicKey()).privateKey(rsaKeys.privateKey()).build(); JWKSource<SecurityContext> jwks = new ImmutableJWKSet<>(new JWKSet(jwk)); return new NimbusJwtEncoder(jwks); } }
认证控制器
package com.devskills.oauth2api.controller; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; import com.devskills.oauth2api.service.TokenService; @RestController public class AuthController { private static final Logger LOG = LoggerFactory.getLogger(AuthController.class); private final TokenService tokenService; public AuthController(TokenService tokenService) { this.tokenService = tokenService; } @PostMapping("/token") public String token(Authentication authentication) { LOG.warn(authentication.getPrincipal().toString()); LOG.debug("Token requested for user: '{}'", authentication.getName()); String token = tokenService.generateToken(authentication); LOG.debug("Token granted {}", token); return token; } }
CORS配置(主类中)
@Bean CorsFilter corsFilter() { CorsConfiguration corsConfiguration = new CorsConfiguration(); corsConfiguration.setAllowCredentials(true); corsConfiguration.setAllowedOrigins(Arrays.asList("http://localhost:4200")); corsConfiguration.setAllowedHeaders(Arrays.asList("Origin", "Access-Control-Allow-Origin", "Content-Type", "Accept", "Authorization", "Origin, Accept", "X-Requested-With", "Access-Control-Request-Method", "Access-Control-Request-Headers")); corsConfiguration.setExposedHeaders(Arrays.asList("Origin", "Content-Type", "Accept", "Authorization", "Access-Control-Allow-Origin", "Access-Control-Allow-Credentials")); corsConfiguration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource(); urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration); return new CorsFilter(urlBasedCorsConfigurationSource); }
前端代码
AuthService
import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; import { map } from 'rxjs/operators'; @Injectable({ providedIn: 'root' }) export class AuthService { constructor(private httpClient: HttpClient) { } login() { const username = 'keen'; const password = 'password'; const headers = new HttpHeaders({Authorization: 'Basic ' + btoa(username + ':' + password)}); return this.httpClient.post<any>('http://localhost:8080/token', {headers}) .pipe( map( userData => { sessionStorage.setItem('username', username); } ) ); } isUserLoggedIn() { let user = sessionStorage.getItem('username'); return !(user === null); } logout() { sessionStorage.removeItem('username'); } }
登录组件
import { Component, OnInit } from '@angular/core'; import { AuthService } from '../service/auth.service'; @Component({ selector: 'app-login', templateUrl: './login.component.html', styleUrls: ['./login.component.scss'] }) export class LoginComponent implements OnInit { constructor(private authService: AuthService) { } ngOnInit(): void { } login() { this.authService.login().subscribe( response => { alert('login successfully'); } ); } }
错误信息
- 前端返回401 Unauthorized
- 后端报错:
java.lang.NullPointerException: Cannot invoke "org.springframework.security.core.Authentication.getPrincipal()" because "authentication" is null at com.devskills.oauth2api.controller.AuthController.token(AuthController.java:24) ~[classes/:na]
问题原因及解决方法
核心问题
Angular的httpClient.post方法调用错误:第二个参数是请求体,而不是请求头配置。当前代码把headers放在了请求体里,导致后端没有收到Basic认证头,无法解析出Authentication对象。
修复代码
修改AuthService中的login方法,正确传递请求头:
login() { const username = 'keen'; const password = 'password'; const headers = new HttpHeaders({Authorization: 'Basic ' + btoa(username + ':' + password)}); // 把headers作为第三个参数(options)传递,而不是请求体 return this.httpClient.post<any>('http://localhost:8080/token', {}, { headers }) .pipe( map( userData => { sessionStorage.setItem('username', username); // 保存返回的token到sessionStorage,供后续请求使用 sessionStorage.setItem('token', userData); } ) ); }
额外优化
- 后端
token()方法建议添加空指针检查,避免崩溃:
@PostMapping("/token") public String token(Authentication authentication) { if (authentication == null) { throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid credentials"); } LOG.warn(authentication.getPrincipal().toString()); LOG.debug("Token requested for user: '{}'", authentication.getName()); String token = tokenService.generateToken(authentication); LOG.debug("Token granted {}", token); return token; }
- Angular后续请求需携带返回的JWT token,通过
Authorization: Bearer {token}头传递。
内容的提问来源于stack exchange,提问作者DONGMO BERNARD GERAUD
相关产品推荐
相关产品推荐

