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

如何使用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.05 13:45:23