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

Angular+NodeJS栈如何通过OBO流让后端获取MS Graph访问权限

解决方案

1. 完成Azure AD应用注册前置配置

  • 找到你当前使用的Azure AD应用注册,进入「证书和密码」页面,新建一个客户端密钥,保存好密钥值,后续后端配置会用到
  • 进入「API权限」页面,添加Microsoft Graph的委托权限User.Read,点击「授予管理员同意」(企业租户需要租户管理员操作,个人开发者账号自行同意即可)
  • 确认你之前配置的自定义API scope(对应代码中的environment.aad_scope_api)状态正常

2. 前端代码无需调整

你当前app.module.ts的配置已经符合要求:已经在授权请求中同时申请了user.read和自定义API scope,msal拦截器也会自动给发往后端的请求携带正确的访问令牌,不需要修改。

3. 后端Node.js实现OBO令牌兑换逻辑

你现在遇到的核心问题是前端传给后端的令牌受众是你的后端API,不是MS Graph,直接用来调用Graph接口必然返回401,需要通过OBO流兑换成Graph可用的令牌。
首先安装依赖:

npm install @azure/msal-node axios

然后修改login.js代码如下:

const { ConfidentialClientApplication } = require('@azure/msal-node');
const axios = require('axios');

// 初始化MSAL机密客户端,用于OBO令牌兑换
const msalClient = new ConfidentialClientApplication({
  auth: {
    clientId: process.env.AAD_CLIENT_ID, // 和前端使用同一个客户端ID
    clientSecret: process.env.AAD_CLIENT_SECRET, // 之前生成的客户端密钥
    authority: `https://login.microsoftonline.com/${process.env.AAD_TENANT_ID}/`
  }
});

// 重写fetchUser方法
async function fetchUser(req) {
  // 提取前端传入的访问令牌,去掉Bearer前缀
  const incomingToken = req.headers.authorization?.split(' ')[1];
  if (!incomingToken) throw new Error('缺少授权令牌');

  try {
    // 第一步:通过OBO流兑换MS Graph访问令牌
    const oboResult = await msalClient.acquireTokenOnBehalfOf({
      oboAssertion: incomingToken,
      scopes: ['https://graph.microsoft.com/User.Read'], // 要申请的Graph权限
    });

    // 第二步:用兑换到的令牌调用MS Graph接口
    const graphRes = await axios.get('https://graph.microsoft.com/v1.0/me', {
      headers: {
        Authorization: `Bearer ${oboResult.accessToken}`
      }
    });

    // 第三步:处理返回的用户信息
    const userObj = {
      id: graphRes.data.id,
      displayName: graphRes.data.displayName,
      mail: graphRes.data.mail || graphRes.data.userPrincipalName
      // 补充你需要的其他字段
    };
    return userObj;
  } catch (err) {
    console.error('身份验证失败:', err);
    throw new Error('用户身份验证失败');
  }
}

// 路由部分调整
router.post('/login', (req, res) => {
  if (req.session.user) {
      return res.json(req.session.user);
  }
  fetchUser(req)
    .then(result => {
      req.session.user = result;
      res.json(result);
    }).catch(err => {
      res.status(401).json({ error: err.message });
    })
});

注意事项

  • 不要硬编码客户端ID、客户端密钥、租户ID这类敏感信息,统一放到环境变量中管理
  • 生产环境可以把兑换到的Graph令牌和用户会话绑定,缓存起来避免每次请求都重复兑换,过期后再重新兑换即可
  • 你原代码中的fetchUser写法存在逻辑问题:回调式的request方法没有封装为Promise,async函数不会等待回调执行完成就会返回,换成axios或者把request包装为Promise更稳妥。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.26 03:57:03