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

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.'));
      })
    );
  }
}
问题原因分析
  1. Angular 18默认启用SSR(服务器端渲染),服务器端执行代码时不存在window对象,若StorageService直接依赖window.sessionStorage,会触发ReferenceError: window is not defined。
  2. 服务器端无法访问浏览器的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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 20:47:08