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

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,避免跨页面共享风险):

  1. 创建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');
  }
}
  1. 在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 {}

四、令牌刷新与过期处理

  1. 后端实现令牌刷新接口:
@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();
}
  1. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.16 09:01:00