Angular 18授权码认证报错:window未定义及state不存在
问题描述
- 生成的state值为
DjoOX1sfdburqP8rJjgESsfLfe3eaHhsMJb9BGoP,系统提示sessionStorage不可用,将使用内存存储 - 抛出
ERROR ReferenceError: window is not defined错误 - 获取token时提示「State not found in storage」
AuthService实现代码
import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; import { Observable, throwError } from 'rxjs'; import { catchError, map } from 'rxjs/operators'; import { JwtHelperService } from '@auth0/angular-jwt'; import { environment } from '../../environments/environments'; import { StorageService } from './storage.service'; @Injectable({ providedIn: 'root', }) export class AuthService { tokensRevokeUrl = environment.apiUrl + '/tokens/revoke'; oauthTokenUrl = environment.apiUrl + '/oauth2/token'; oauthAuthorizeUrl = environment.apiUrl + '/oauth2/authorize'; jwtPayload: any; constructor( private http: HttpClient, private jwtHelper: JwtHelperService, private storage: StorageService ) { this.carregarToken(); } login(){ const state = this.gerarStringAleatoria(40); this.storage.setItem('state', state); const redirectURI = encodeURIComponent(environment.oauthCallbackUrl); const clientId = 'angular'; const scope = 'read write'; const responseType = 'code'; const params = [ 'response_type=' + responseType, 'client_id=' + clientId, 'scope=' + scope, 'state=' + state, 'redirect_uri=' + redirectURI ]; return this.oauthAuthorizeUrl + '?' + params.join('&'); } getAuthUrl(): string{ return this.login() } obterNovoAccessTokenComCode(code: string, state: string): Observable<any> { const stateSalvo = this.storage.getItem('state'); if (stateSalvo !== state) { return throwError(() => new Error('Estado inválido')); } const payload = new HttpParams() .append('grant_type', 'authorization_code') .append('code', code) .append('redirect_uri', environment.oauthCallbackUrl) const headers = new HttpHeaders() .append('Content-Type', 'application/x-www-form-urlencoded') .append('Authorization', 'Basic YW5ndWxhcjoxMjM0NTY='); return this.http.post<any>(this.oauthTokenUrl, payload, { headers }).pipe( map((response: any) => { this.armazenarToken(response['access_token']); this.armazenarRefreshToken(response['refresh_token']); console.log('Novo access token criado!'); this.storage.removeItem('state'); return response; }), catchError((error) => { console.error('Erro ao gerar o token com o code.', error); return throwError(() => new Error('Erro ao gerar o token com o code.')); }) ); } getLoginRedirectUrl(): Observable<string> { // Substitua esta lógica pela real, se necessário return this.http.get<string>(`${this.oauthAuthorizeUrl}`); } obterNovoAccessToken(): Observable<any> { const headers = new HttpHeaders() .append('Content-Type', 'application/x-www-form-urlencoded') .append('Authorization', 'Basic YW5ndWxhcjoxMjM0NTY='); const payload = new HttpParams() .append('grant_type', 'refresh_token') .append('refresh_token', this.storage.getItem('refreshToken')!); return this.http.post<any>(this.oauthTokenUrl, payload, { headers }).pipe( map((response: any) => { this.armazenarToken(response['access_token']); this.armazenarRefreshToken(response['refresh_token']); console.log('Novo access token criado!'); }), catchError((error) => { console.error('Erro ao renovar token.', error); return throwError(() => new Error('Erro ao renovar token.')); }) ); } isAccessTokenInvalido(): boolean { const token = this.storage.getItem('token'); return !token || this.jwtHelper.isTokenExpired(token); } temPermissao(permissao: string): boolean { return this.jwtPayload && this.jwtPayload.authorities.includes(permissao); } temQualquerPermissao(roles: any[]): boolean { for (const role of roles) { if (this.temPermissao(role)) { return true; } } return false; } public armazenarToken(token: string): void { this.jwtPayload = this.jwtHelper.decodeToken(token); this.storage.setItem('token', token); } public carregarToken(): void { const token = this.storage.getItem('token'); if (token) { this.armazenarToken(token); } } limparAccessToken(): void { this.storage.removeItem('token'); this.jwtPayload = null; } private armazenarRefreshToken(refreshToken: string): void { this.storage.setItem('refreshToken', refreshToken); } private gerarStringAleatoria(tamanho: number): string { let resultado = ''; // Chars que são URL safe const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; for (let i = 0; i < tamanho; i++) { resultado += chars.charAt(Math.floor(Math.random() * chars.length)); } return resultado; } logout(): Observable<void> { return this.http.delete(this.tokensRevokeUrl, { withCredentials: true }).pipe( map(() => { this.limparAccessToken(); }), catchError((error) => { console.error('Erro ao fazer logout.', error); return throwError(() => new Error('Erro ao fazer logout.')); }) ); } }
问题原因分析
- Angular 18默认启用SSR(服务器端渲染),服务器端执行代码时不存在
window对象,若StorageService直接依赖window.sessionStorage,会触发ReferenceError: window is not defined。 - 服务器端无法访问浏览器的sessionStorage,存储服务被迫使用内存存储,而内存存储在页面跳转(授权回调)后会丢失,导致验证state时找不到对应值,出现「State not found in storage」。
解决方案
1. 适配SSR的StorageService实现
修改StorageService,确保仅在浏览器环境使用sessionStorage,服务器端用内存Map存储:
import { Injectable, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class StorageService { private storage: Storage | Map<string, string>; constructor(@Inject(PLATFORM_ID) private platformId: Object) { this.storage = isPlatformBrowser(this.platformId) ? window.sessionStorage : new Map<string, string>(); } setItem(key: string, value: string): void { if (this.storage instanceof Storage) { this.storage.setItem(key, value); } else { (this.storage as Map<string, string>).set(key, value); } } getItem(key: string): string | null { if (this.storage instanceof Storage) { return this.storage.getItem(key); } else { return (this.storage as Map<string, string>).get(key) || null; } } removeItem(key: string): void { if (this.storage instanceof Storage) { this.storage.removeItem(key); } else { (this.storage as Map<string, string>).delete(key); } } }
2. 延迟AuthService的初始化逻辑
在AuthService构造函数中,仅在浏览器环境加载token:
import { Inject, PLATFORM_ID } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; // ... 其他代码保持不变 constructor( private http: HttpClient, private jwtHelper: JwtHelperService, private storage: StorageService, @Inject(PLATFORM_ID) private platformId: Object ) { if (isPlatformBrowser(this.platformId)) { this.carregarToken(); } }
3. 确保登录操作仅在浏览器执行
在触发登录跳转的组件中,添加平台判断:
import { Component, Inject, PLATFORM_ID } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; import { AuthService } from './auth.service'; @Component({ selector: 'app-login', template: '<button (click)="redirectToAuth()">登录</button>' }) export class LoginComponent { constructor( private authService: AuthService, @Inject(PLATFORM_ID) private platformId: Object ) {} redirectToAuth(): void { if (isPlatformBrowser(this.platformId)) { const authUrl = this.authService.getAuthUrl(); window.location.href = authUrl; } } }
4. 回调组件的state验证优化
在处理授权码的回调组件中,仅在浏览器环境读取并验证state:
import { Component, OnInit, Inject, PLATFORM_ID } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { AuthService } from './auth.service'; import { isPlatformBrowser } from '@angular/common'; @Component({ selector: 'app-oauth-callback', template: '<p>处理登录中...</p>' }) export class OAuthCallbackComponent implements OnInit { constructor( private route: ActivatedRoute, private authService: AuthService, @Inject(PLATFORM_ID) private platformId: Object ) {} ngOnInit(): void { if (isPlatformBrowser(this.platformId)) { this.route.queryParams.subscribe(params => { const code = params['code']; const state = params['state']; if (code && state) { this.authService.obterNovoAccessTokenComCode(code, state).subscribe({ next: () => window.location.href = '/', error: err => console.error('登录失败:', err) }); } }); } } }
关键注意事项
- 所有涉及浏览器API(window、sessionStorage)的操作必须用
isPlatformBrowser判断环境,避免服务器端执行报错。 - 授权码模式的state必须存储在浏览器的持久化存储(sessionStorage/localStorage)中,不能用内存存储,否则跳转后会丢失。
内容的提问来源于stack exchange,提问作者Lucas Moraes
相关产品推荐
相关产品推荐

