如何使用msal-angular存储Android端Azure AD B2C令牌实现免登录?
同一Azure AD B2C租户下Android与Angular应用单点登录方案
前提配置
- 确保Android应用(公共客户端)与Angular SPA应用注册在同一Azure AD B2C租户下。
- 为Android应用添加Angular SPA的API权限(例如将Angular应用的客户端ID作为范围,或自定义API的权限范围),保证Android获取的令牌受众(
aud)或权限范围与Angular应用匹配。 - 确认Angular应用的MSAL配置(
MsalModule)中,auth.clientId、auth.authority等参数正确指向你的B2C租户和应用ID。
步骤1:Android端传递令牌到Web应用
方式1:HTTPS深层链接传递(适用于打开外部浏览器或WebView)
当Android应用通过Azure AD B2C获取到id_token和access_token后,构造带参数的HTTPS链接打开Angular应用:
// Android代码示例 val idToken = "获取到的ID Token" val accessToken = "获取到的Access Token" val expiresIn = "令牌有效期(秒)" val deepLinkUrl = "https://your-angular-app.com/auth/external-login?id_token=$idToken&access_token=$accessToken&expires_in=$expiresIn" // 打开链接(外部浏览器) val intent = Intent(Intent.ACTION_VIEW, Uri.parse(deepLinkUrl)) startActivity(intent) // 或在WebView中加载 webView.loadUrl(deepLinkUrl)
方式2:WebView JavaScript注入(更安全,避免URL暴露令牌)
配置Android WebView启用JavaScript和DOM存储,直接将令牌注入到Angular应用的localStorage:
// Android WebView配置 webView.settings.javaScriptEnabled = true webView.settings.domStorageEnabled = true webView.webViewClient = object : WebViewClient() { override fun onPageFinished(view: WebView?, url: String?) { super.onPageFinished(view, url) // 注入令牌到localStorage val injectTokenScript = """ window.localStorage.setItem('external-id-token', '$idToken'); window.localStorage.setItem('external-access-token', '$accessToken'); window.localStorage.setItem('external-expires-in', '$expiresIn'); """.trimIndent() view?.evaluateJavascript(injectTokenScript, null) } } webView.loadUrl("https://your-angular-app.com/auth/external-login")
步骤2:Angular端接收并集成令牌到msal-angular
1. 创建回调路由
在Angular路由中添加处理外部登录的路由:
// app-routing.module.ts const routes: Routes = [ // 其他路由... { path: 'auth/external-login', component: ExternalLoginComponent } ];
2. 实现令牌处理逻辑
在ExternalLoginComponent中,接收令牌并将其存入MSAL缓存,触发登录状态:
// external-login.component.ts import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { MsalService } from '@azure/msal-angular'; import { AccountInfo, IdTokenClaims } from '@azure/msal-browser'; @Component({ selector: 'app-external-login', templateUrl: './external-login.component.html' }) export class ExternalLoginComponent implements OnInit { // 替换为你的B2C租户ID和Angular应用客户端ID private readonly tenantId = "your-b2c-tenant-id"; private readonly clientId = "your-angular-client-id"; // 替换为Android请求的权限范围 private readonly scopes = ["openid", "profile", "your-api-scope"]; constructor( private msalService: MsalService, private router: Router ) {} ngOnInit(): void { this.handleExternalToken(); } private handleExternalToken(): void { let idToken: string | null; let accessToken: string | null; let expiresIn: string | null; // 优先从localStorage读取(WebView注入方式) idToken = localStorage.getItem('external-id-token'); accessToken = localStorage.getItem('external-access-token'); expiresIn = localStorage.getItem('external-expires-in'); // 如果没有则从URL参数读取(深层链接方式) if (!idToken) { const urlParams = new URLSearchParams(window.location.search); idToken = urlParams.get('id_token'); accessToken = urlParams.get('access_token'); expiresIn = urlParams.get('expires_in'); } if (!idToken) { this.router.navigate(['/login']); return; } // 验证ID Token有效性(必须步骤,防止伪造) try { this.msalService.instance.validateIdToken(idToken, this.clientId, this.tenantId); } catch (err) { console.error("无效的ID Token:", err); this.router.navigate(['/login']); return; } // 解析ID Token获取用户信息 const idTokenClaims = this.msalService.instance.getTokenClaims(idToken) as IdTokenClaims; const account: AccountInfo = { homeAccountId: `${idTokenClaims.oid}.${this.tenantId}`, environment: "login.microsoftonline.com", tenantId: this.tenantId, username: idTokenClaims.email || idTokenClaims.preferred_username || "", localAccountId: idTokenClaims.oid || "", name: idTokenClaims.name || "", idToken: idToken, idTokenClaims: idTokenClaims }; // 将账户存入MSAL缓存并设置为活跃账户 this.msalService.instance.setActiveAccount(account); this.msalService.instance.saveAccount(account); // 存入Access Token到缓存(如果有) if (accessToken && expiresIn) { const expiresOn = new Date(); expiresOn.setSeconds(expiresOn.getSeconds() + parseInt(expiresIn)); const tokenCache = this.msalService.instance.getTokenCache(); tokenCache.addAccessToken({ homeAccountId: account.homeAccountId, environment: account.environment, clientId: this.clientId, secret: accessToken, expiresOn: expiresOn, scopes: this.scopes, tenantId: this.tenantId }); } // 清理临时存储的令牌 localStorage.removeItem('external-id-token'); localStorage.removeItem('external-access-token'); localStorage.removeItem('external-expires-in'); // 跳转到应用主页 this.router.navigate(['/']); } }
安全注意事项
- 强制使用HTTPS:无论是深层链接还是WebView加载,必须使用HTTPS传输令牌,防止中间人攻击。
- 令牌验证:Angular端必须验证ID Token的签名、受众、签发者和过期时间,绝对不能直接信任外部传入的令牌。
- WebView安全配置:限制WebView仅能访问你的Angular应用域名,禁用不必要的权限(如文件访问),避免XSS风险。
- 令牌过期处理:集成后,msal-angular会自动处理令牌刷新,无需额外操作。
内容的提问来源于stack exchange,提问作者Robbie Matthews
相关产品推荐
相关产品推荐

