我目前正在开发一个 Angular 应用程序,该应用程序与 Spring Boot API 进行通信,以通过 JWT 进行身份验证和用户管理。但是,我在前端和后端使用 JWT 令牌处理身份验证时遇到了问题。
背景:
前端:使用 JwtInterceptor 的 Angular 在请求标头中添加 JWT 令牌。 后端:带有 Spring Security 的 Spring Boot,用于生成和验证 JWT 令牌。 我已经配置了部分代码来处理注册、登录、密码更改和帐户删除。这是我迄今为止所做的总结。
前端(角度)
JwtInterceptor:从 localStorage 检索令牌并将其添加到每个 HTTP 请求中。 AccountsService:管理注册、登录和用户管理的 API 调用。令牌存储在 localStorage 中。 JwtInterceptor.ts代码:
@Injectable()
export class JwtInterceptor implements HttpInterceptor {
constructor(private accountsService: AccountsService) { }
intercept(req: HttpRequest<any>, next: HttpHandler) {
const token = this.accountsService.getToken();
if (token) {
console.log('JWT Token:', token);
const cloned = req.clone({
headers: req.headers.set('Authorization', `Bearer ${token}`)
});
return next.handle(cloned);
}
return next.handle(req);
}
}
账户.service.ts
export class AccountsService {
private apiUrl = 'http://localhost:8080';
constructor(private http: HttpClient) { }
register(user: { username: string; password: string }): Observable<string> {
return this.http.post<string>(`${this.apiUrl}/auth/register`, user, { responseType: 'text' as 'json' })
.pipe(
tap(response => console.log('User registered:', response)),
catchError(this.handleError)
);
}
login(credentials: { username: string; password: string }): Observable<string> {
return this.http.post<string>(`${this.apiUrl}/auth/login`, credentials, { responseType: 'text' as 'json' })
.pipe(
tap(response => console.log('Login successful:', response)),
catchError(this.handleError)
);
}
saveToken(token: string) {
localStorage.setItem('jwtToken', token);
}
getToken() {
return localStorage.getItem('jwtToken');
}
logout() {
localStorage.removeItem('jwtToken');
console.log('User logged out, token removed');
}
}
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.cors().and() // Adding CORS support
.authorizeRequests()
.requestMatchers("/submit", "/auth/register", "/auth/login").permitAll() // Public routes
.anyRequest().authenticated() // Authentication required for other routes
.and()
.addFilterBefore(jwtRequestFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public JwtRequestFilter jwtRequestFilter() {
return new JwtRequestFilter();
}
}
@Component
public class JwtTokenUtil {
private static final String SECRET_KEY = "mysecret";
public String generateToken(String username) {
Map<String, Object> claims = new HashMap<>();
return Jwts.builder()
.setClaims(claims)
.setSubject(username)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10)) // 10 hours
.signWith(SignatureAlgorithm.HS256, SECRET_KEY)
.compact();
}
public boolean validateToken(String token, String username) {
final String tokenUsername = extractUsername(token);
return (tokenUsername.equals(username) && !isTokenExpired(token));
}
public String extractUsername(String token) {
return extractAllClaims(token).getSubject();
}
private Claims extractAllClaims(String token) {
return Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();
}
private boolean isTokenExpired(String token) {
return extractAllClaims(token).getExpiration().before(new Date());
}
}
public class JwtRequestFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
final String authorizationHeader = request.getHeader("Authorization");
String username = null;
String jwt = null;
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
jwt = authorizationHeader.substring(7);
username = jwtTokenUtil.extractUsername(jwt);
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
username, null, new ArrayList<>());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
chain.doFilter(request, response);
}
}
问题:
JWT 令牌已正确生成并在登录期间发送到前端。但是,当我使用授权标头中的令牌从 Angular 发送经过身份验证的请求时,后端似乎没有正确验证令牌。受保护的请求返回 401(未经授权)错误。
我检查过的内容:
令牌由 Angular 拦截器正确存储和发送。 JwtRequestFilter 拦截请求,但身份验证失败。 我检查了 Spring Security 配置,受保护的路由已正确指定。 我在寻找什么:
有没有人遇到过类似的问题,并且可以提供有关在 Spring Boot 中进行 JWT 令牌验证期间解决此 401 错误的见解?问题可能与 Spring Security 配置、令牌签名或 Angular 发送请求的方式有关吗?
预先感谢您的帮助!
在 Spring Security 6 中,需要显式保存安全上下文。
就好像有如下配置:
http
// ...
.securityContext(securityContext -> securityContext.requireExplicitSave(true));
要保留安全上下文,首先公开
SecurityContextRepository
bean。
@Bean
public SecurityContextRepository securityContextRepository() {
return new DelegatingSecurityContextRepository(
new RequestAttributeSecurityContextRepository(),
new HttpSessionSecurityContextRepository()
); // expose the default one
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// other configuration...
.securityContext(securityContext -> securityContext
.securityContextRepository(securityContextRepository())
);
return http.build();
}
然后,在需要更新安全上下文时自动装配它。
public classJwtRequestFilter extends OncePerRequestFilter {
@Autowired
private SecurityContextRepository securityContextRepository;
// ...
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
// ...
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
securityContextRepository.saveContext(securityContext, request, response);
// ...
}
}