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
相关产品推荐
相关产品推荐

