Spring Security OAuth2 Client结合Angular:获取并维护OAuth2令牌
在Angular中结合Spring Security OAuth2 Client实现授权码模式的令牌管理
一、后端暴露令牌信息接口
Spring Security OAuth2 Client会在用户认证成功后,将令牌数据存储在SecurityContextHolder中。你可以编写一个接口,将令牌信息返回给前端:
@RestController @RequestMapping("/api/auth") public class AuthController { @GetMapping("/token") public ResponseEntity<OAuth2TokenInfo> getCurrentToken(Authentication authentication) { if (authentication instanceof OAuth2AuthenticationToken oauthToken) { OAuth2AuthorizedClient client = oauthToken.getAuthorizedClient(); OAuth2AccessToken accessToken = client.getAccessToken(); OAuth2RefreshToken refreshToken = client.getRefreshToken(); OAuth2TokenInfo tokenInfo = new OAuth2TokenInfo(); tokenInfo.setAccessToken(accessToken.getTokenValue()); tokenInfo.setRefreshToken(refreshToken != null ? refreshToken.getTokenValue() : null); tokenInfo.setExpiresAt(accessToken.getExpiresAt()); return ResponseEntity.ok(tokenInfo); } return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } // 定义令牌信息DTO public static class OAuth2TokenInfo { private String accessToken; private String refreshToken; private Instant expiresAt; // 省略getter、setter方法 } }
该接口仅对已认证用户开放,Spring Security会自动拦截未认证请求,无需额外配置权限。
二、Angular初始化时获取并存储令牌
在Angular应用启动阶段,调用后端令牌接口获取数据,并存储到sessionStorage(安全性优于localStorage,避免跨页面共享风险):
- 创建Auth服务管理令牌生命周期:
import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable, BehaviorSubject, tap } from 'rxjs'; export interface OAuth2TokenInfo { accessToken: string; refreshToken: string; expiresAt: Date; } @Injectable({ providedIn: 'root' }) export class AuthService { private tokenSubject = new BehaviorSubject<OAuth2TokenInfo | null>(null); public token$ = this.tokenSubject.asObservable(); constructor(private http: HttpClient) {} loadToken(): Observable<OAuth2TokenInfo> { return this.http.get<OAuth2TokenInfo>('/api/auth/token').pipe( tap(token => { this.tokenSubject.next(token); sessionStorage.setItem('authToken', JSON.stringify(token)); }) ); } getCurrentToken(): OAuth2TokenInfo | null { const storedToken = sessionStorage.getItem('authToken'); return storedToken ? JSON.parse(storedToken) : null; } clearToken(): void { this.tokenSubject.next(null); sessionStorage.removeItem('authToken'); } }
- 在AppComponent中触发令牌加载:
import { Component, OnInit } from '@angular/core'; import { AuthService } from './auth.service'; @Component({ selector: 'app-root', templateUrl: './app.component.html' }) export class AppComponent implements OnInit { constructor(private authService: AuthService) {} ngOnInit(): void { this.authService.loadToken().subscribe({ error: () => this.authService.clearToken() }); } }
三、Angular请求自动携带令牌
创建HTTP拦截器,为所有API请求自动添加Authorization请求头:
import { Injectable } from '@angular/core'; import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http'; import { Observable } from 'rxjs'; import { AuthService } from './auth.service'; @Injectable() export class AuthInterceptor implements HttpInterceptor { constructor(private authService: AuthService) {} intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> { const currentToken = this.authService.getCurrentToken(); if (currentToken) { request = request.clone({ setHeaders: { Authorization: `Bearer ${currentToken.accessToken}` } }); } return next.handle(request); } }
在app.module.ts中注册拦截器:
import { HTTP_INTERCEPTORS } from '@angular/common/http'; import { AuthInterceptor } from './auth.interceptor'; @NgModule({ providers: [ { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true } ] }) export class AppModule {}
四、令牌刷新与过期处理
- 后端实现令牌刷新接口:
@GetMapping("/refresh-token") public ResponseEntity<OAuth2TokenInfo> refreshToken(Authentication authentication, OAuth2AuthorizedClientService clientService) { if (authentication instanceof OAuth2AuthenticationToken oauthToken) { OAuth2AuthorizedClient client = clientService.loadAuthorizedClient( oauthToken.getAuthorizedClientRegistrationId(), oauthToken.getName() ); OAuth2RefreshToken refreshToken = client.getRefreshToken(); OAuth2AuthorizationContext context = OAuth2AuthorizationContext.builder() .registeredClient(client.getRegisteredClient()) .principal(authentication) .refreshToken(refreshToken) .build(); OAuth2AuthorizedClient refreshedClient = clientService.authorize(context); OAuth2AccessToken newAccessToken = refreshedClient.getAccessToken(); OAuth2TokenInfo tokenInfo = new OAuth2TokenInfo(); tokenInfo.setAccessToken(newAccessToken.getTokenValue()); tokenInfo.setRefreshToken(refreshedClient.getRefreshToken().getTokenValue()); tokenInfo.setExpiresAt(newAccessToken.getExpiresAt()); return ResponseEntity.ok(tokenInfo); } return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); }
- Angular端处理令牌过期:
在AuthService中添加刷新方法,并在拦截器中捕获401错误触发刷新:
// AuthService中新增方法 refreshToken(): Observable<OAuth2TokenInfo> { return this.http.get<OAuth2TokenInfo>('/api/auth/refresh-token').pipe( tap(token => { this.tokenSubject.next(token); sessionStorage.setItem('authToken', JSON.stringify(token)); }) ); }
修改拦截器逻辑:
import { catchError, switchMap, throwError } from 'rxjs'; // 拦截器intercept方法更新 intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> { const currentToken = this.authService.getCurrentToken(); if (currentToken) { request = request.clone({ setHeaders: { Authorization: `Bearer ${currentToken.accessToken}` } }); } return next.handle(request).pipe( catchError(error => { if (error.status === 401 && currentToken?.refreshToken) { return this.authService.refreshToken().pipe( switchMap(newToken => { const refreshedRequest = request.clone({ setHeaders: { Authorization: `Bearer ${newToken.accessToken}` } }); return next.handle(refreshedRequest); }), catchError(() => { this.authService.clearToken(); // 可在此处添加路由跳转至登录页逻辑 return throwError(() => new Error('令牌刷新失败')); }) ); } return throwError(() => error); }) ); }
五、安全注意事项
- 优先使用
sessionStorage存储令牌,避免localStorage的XSS攻击风险; - 合理设置令牌过期时间:access_token有效期不宜过长,refresh_token需妥善存储并限制使用次数;
- 开启CSRF防护:Spring Security默认启用CSRF防护,Angular可通过
HttpClientXsrfModule自动携带CSRF令牌。
内容的提问来源于stack exchange,提问作者aditya chava
相关产品推荐
相关产品推荐

