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

MSAL登录设置cacheLocation为none不跳转,如何用NGRX存令牌?

问题分析与解决方案

当你把cacheLocation设为'none'时,MSAL失去了存储认证流程临时状态(比如跳转前的请求上下文)的介质,导致OAuth2跳转后的回调无法恢复流程,最终引发跳转失效。MSAL要求必须有缓存来完成完整的认证链路,我们可以用'memoryStorage'(内存缓存,页面刷新即清空,不会持久化到本地存储)替代localStorage,同时通过NGRX来持久化管理令牌。


步骤1:调整MSAL基础配置,改用内存缓存

修改AppModule中的MSAL配置,将cacheLocation改为'memoryStorage',既规避localStorage,又能让MSAL完成跳转流程:

// AppModule.ts
@NgModule({
  imports: [
    MsalModule.forRoot(new PublicClientApplication({
      auth: {
        clientId: environment.uiClientId,
        redirectUri: environment.redirectUri,
        authority: `https://login.microsoftonline.com/${environment.tenantId}`
      },
      cache: {
        cacheLocation: 'memoryStorage', // 改用内存缓存
        storeAuthStateInCookie: isIE
      }
    }),
    // 其余配置保持不变
    )
  ],
  // 其余配置保持不变
})
export class AppModule { }

步骤2:实现NGRX令牌状态管理

2.1 定义Auth状态结构

创建src/app/store/auth/auth.state.ts:

export interface AuthState {
  accessToken: string | null;
  idToken: string | null;
  expiresOn: number | null;
}

export const initialAuthState: AuthState = {
  accessToken: null,
  idToken: null,
  expiresOn: null
};

2.2 创建Auth动作

创建src/app/store/auth/auth.actions.ts:

import { createAction, props } from '@ngrx/store';
import { AuthState } from './auth.state';

export const setTokens = createAction(
  '[Auth] Set Tokens',
  props<AuthState>()
);

export const clearTokens = createAction('[Auth] Clear Tokens');

2.3 实现Auth Reducer

创建src/app/store/auth/auth.reducer.ts:

import { createReducer, on } from '@ngrx/store';
import { initialAuthState } from './auth.state';
import { setTokens, clearTokens } from './auth.actions';

export const authReducer = createReducer(
  initialAuthState,
  on(setTokens, (state, { accessToken, idToken, expiresOn }) => ({
    ...state,
    accessToken,
    idToken,
    expiresOn
  })),
  on(clearTokens, () => initialAuthState)
);

2.4 配置根Store

创建src/app/store/app.state.ts:

import { AuthState } from './auth/auth.state';

export interface AppState {
  auth: AuthState;
}

在AppModule中导入Store模块:

// AppModule.ts
import { StoreModule } from '@ngrx/store';
import { authReducer } from './store/auth/auth.reducer';

@NgModule({
  imports: [
    // 其余导入保持不变
    StoreModule.forRoot({ auth: authReducer })
  ],
  // 其余配置保持不变
})
export class AppModule { }

步骤3:监听MSAL事件,同步令牌到NGRX

创建AuthService监听MSAL登录/登出事件,将令牌同步到NGRX:

// src/app/services/auth.service.ts
import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
import { MsalService, BroadcastService } from '@azure/msal-angular';
import { setTokens, clearTokens } from '../store/auth/auth.actions';
import { AppState } from '../store/app.state';

@Injectable({ providedIn: 'root' })
export class AuthService {
  constructor(
    private msalService: MsalService,
    private broadcastService: BroadcastService,
    private store: Store<AppState>
  ) {
    // 监听登录成功事件
    this.broadcastService.subscribe('msal:loginSuccess', () => {
      const account = this.msalService.instance.getActiveAccount();
      if (account) {
        this.msalService.instance.getAccessTokenSilent({
          account,
          scopes: ['user.read', environment.apiScope]
        }).then(tokenResponse => {
          this.store.dispatch(setTokens({
            accessToken: tokenResponse.accessToken,
            idToken: account.idToken,
            expiresOn: tokenResponse.expiresOn?.getTime() || null
          }));
        });
      }
    });

    // 监听登出成功事件
    this.broadcastService.subscribe('msal:logoutSuccess', () => {
      this.store.dispatch(clearTokens());
    });
  }

  login() {
    this.msalService.loginRedirect();
  }

  logout() {
    this.msalService.logoutRedirect();
  }
}

步骤4:自定义拦截器,从NGRX读取令牌

创建自定义拦截器,替代默认MsalInterceptor,从NGRX获取令牌并添加到请求头:

// src/app/interceptors/ngrx-msal.interceptor.ts
import { Injectable } from '@angular/core';
import {
  HttpRequest,
  HttpHandler,
  HttpEvent,
  HttpInterceptor
} from '@angular/common/http';
import { Observable, switchMap, take } from 'rxjs';
import { Store } from '@ngrx/store';
import { AppState } from '../store/app.state';
import { selectAccessToken } from '../store/auth/auth.selectors';

@Injectable()
export class NgrxMsalInterceptor implements HttpInterceptor {

  constructor(private store: Store<AppState>) {}

  intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
    return this.store.select(selectAccessToken).pipe(
      take(1),
      switchMap(accessToken => {
        if (accessToken && this.isProtectedResource(request.url)) {
          request = request.clone({
            setHeaders: {
              Authorization: `Bearer ${accessToken}`
            }
          });
        }
        return next.handle(request);
      })
    );
  }

  // 判断是否为受保护资源,复用已定义的protectedResourceMap
  private isProtectedResource(url: string): boolean {
    return protectedResourceMap.some(([resource]) => url.startsWith(resource));
  }
}

创建对应的Selector:

// src/app/store/auth/auth.selectors.ts
import { createFeatureSelector, createSelector } from '@ngrx/store';
import { AuthState } from './auth.state';

const selectAuthState = createFeatureSelector<AuthState>('auth');

export const selectAccessToken = createSelector(
  selectAuthState,
  (state) => state.accessToken
);

在AppModule中替换默认拦截器:

// AppModule.ts
import { NgrxMsalInterceptor } from './interceptors/ngrx-msal.interceptor';

@NgModule({
  providers: [
    // 移除默认MsalInterceptor,替换为自定义实现
    { provide: HTTP_INTERCEPTORS, useClass: NgrxMsalInterceptor, multi: true },
  ],
  // 其余配置保持不变
})
export class AppModule { }

步骤5:页面刷新后恢复登录状态(可选)

在AppComponent中添加初始化逻辑,从NGRX恢复MSAL账户状态:

// src/app/app.component.ts
import { Component, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { MsalService } from '@azure/msal-angular';
import { selectAuthState } from './store/auth/auth.selectors';
import { AppState } from './store/app.state';
import { take } from 'rxjs';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html'
})
export class AppComponent implements OnInit {
  constructor(
    private msalService: MsalService,
    private store: Store<AppState>
  ) {}

  ngOnInit(): void {
    this.store.select(selectAuthState).pipe(take(1)).subscribe(authState => {
      if (authState.idToken) {
        const idTokenPayload = JSON.parse(atob(authState.idToken.split('.')[1]));
        const account = this.msalService.instance.getAccountByUsername(idTokenPayload.upn);
        if (account) {
          this.msalService.instance.setActiveAccount(account);
        }
      }
    });
  }
}

内容的提问来源于stack exchange,提问作者Samarth

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.17 14:14:53