在Angular和Node.js中基于Microsoft Graph API实现授权码授予流程
实现Angular + Node.js Express集成Microsoft Graph API的授权码流程
前置准备
在Azure AD注册Web应用:
- 配置重定向URI为前端回调地址(如
http://localhost:4200/auth-callback) - 获取
客户端ID和客户端密钥 - 添加
Calendars.Read和offline_access权限(offline_access用于获取刷新令牌,实现令牌续期) - 确保应用启用了授权码授予流程
1. Angular前端:触发授权并获取授权码
生成授权跳转URL
创建auth.service.ts封装授权逻辑:
import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class AuthService { private readonly clientId = 'YOUR_AZURE_CLIENT_ID'; private readonly redirectUri = 'http://localhost:4200/auth-callback'; private readonly scopes = 'Calendars.Read offline_access'; getAuthUrl(): string { const authEndpoint = 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize'; const params = new URLSearchParams({ client_id: this.clientId, response_type: 'code', redirect_uri: this.redirectUri, scope: this.scopes, response_mode: 'query', prompt: 'select_account' }); return `${authEndpoint}?${params.toString()}`; } }
登录组件触发授权
在自定义登录组件中添加跳转逻辑:
import { Component } from '@angular/core'; import { AuthService } from './auth.service'; @Component({ selector: 'app-login', template: `<button (click)="initAuthFlow()">登录并授权日历访问</button>` }) export class LoginComponent { constructor(private authService: AuthService) {} initAuthFlow(): void { window.location.href = this.authService.getAuthUrl(); } }
回调组件处理授权码
创建auth-callback组件,配置路由path: 'auth-callback',接收授权码并发送到后端:
import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { HttpClient } from '@angular/common/http'; @Component({ selector: 'app-auth-callback', template: `<p>正在处理授权...</p>` }) export class AuthCallbackComponent implements OnInit { constructor( private route: ActivatedRoute, private router: Router, private http: HttpClient ) {} ngOnInit(): void { this.route.queryParams.subscribe(params => { const code = params['code']; if (!code) { this.router.navigate(['/login']); return; } this.http.post('/api/auth/exchange-code', { code }).subscribe({ next: () => this.router.navigate(['/calendar']), error: () => this.router.navigate(['/login']) }); }); } }
2. Node.js Express后端:交换令牌并存储
依赖与配置
安装依赖:
npm install axios dotenv mongoose # 根据你的数据库替换mongoose
创建.env文件存储敏感信息:
AZURE_CLIENT_ID=你的客户端ID AZURE_CLIENT_SECRET=你的客户端密钥 AZURE_REDIRECT_URI=http://localhost:4200/auth-callback
令牌交换路由
创建routes/auth.js:
require('dotenv').config(); const express = require('express'); const axios = require('axios'); const router = express.Router(); const UserToken = require('../models/userToken'); // 数据库模型,需自行定义 // 交换授权码为访问令牌 router.post('/exchange-code', async (req, res) => { const { code } = req.body; const tokenEndpoint = 'https://login.microsoftonline.com/common/oauth2/v2.0/token'; try { const response = await axios.post(tokenEndpoint, new URLSearchParams({ client_id: process.env.AZURE_CLIENT_ID, client_secret: process.env.AZURE_CLIENT_SECRET, code, redirect_uri: process.env.AZURE_REDIRECT_URI, grant_type: 'authorization_code' }), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }); const { access_token, refresh_token, expires_in, user_id } = response.data; // 存储令牌到数据库,关联微软user_id或你的应用用户ID await UserToken.findOneAndUpdate( { microsoftUserId: user_id }, { accessToken: access_token, refreshToken: refresh_token, expiresAt: Date.now() + expires_in * 1000 }, { upsert: true, new: true } ); res.sendStatus(200); } catch (err) { console.error('令牌交换失败:', err.response?.data || err.message); res.sendStatus(500); } }); module.exports = router;
数据库模型示例(MongoDB)
创建models/userToken.js:
const mongoose = require('mongoose'); const userTokenSchema = new mongoose.Schema({ microsoftUserId: { type: String, unique: true, required: true }, accessToken: { type: String, required: true }, refreshToken: { type: String, required: true }, expiresAt: { type: Date, required: true } }); module.exports = mongoose.model('UserToken', userTokenSchema);
3. 调用Microsoft Graph API获取日历数据
在后端添加日历数据接口,自动处理令牌续期:
// 在routes/auth.js或单独的routes/calendar.js中添加 router.get('/calendar/events', async (req, res) => { // 从请求中获取用户标识(如JWT解析出的ID或微软user_id) const microsoftUserId = req.user.microsoftId; // 需根据你的认证方式调整 try { let userToken = await UserToken.findOne({ microsoftUserId }); if (!userToken) return res.sendStatus(401); // 令牌过期则用刷新令牌续期 if (Date.now() > userToken.expiresAt.getTime()) { const tokenEndpoint = 'https://login.microsoftonline.com/common/oauth2/v2.0/token'; const refreshRes = await axios.post(tokenEndpoint, new URLSearchParams({ client_id: process.env.AZURE_CLIENT_ID, client_secret: process.env.AZURE_CLIENT_SECRET, refresh_token: userToken.refreshToken, grant_type: 'refresh_token' }), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }); const { access_token, refresh_token, expires_in } = refreshRes.data; userToken = await UserToken.findOneAndUpdate( { microsoftUserId }, { accessToken: access_token, refreshToken: refresh_token, expiresAt: Date.now() + expires_in * 1000 }, { new: true } ); } // 调用Graph API获取日历事件 const graphRes = await axios.get('https://graph.microsoft.com/v1.0/me/events', { headers: { Authorization: `Bearer ${userToken.accessToken}` } }); res.json(graphRes.data); } catch (err) { console.error('获取日历数据失败:', err.response?.data || err.message); res.sendStatus(500); } });
4. 安全最佳实践
- 令牌加密存储:数据库中使用AES等算法加密令牌,避免明文存储
- 后端接口认证:前端调用后端接口时需携带应用自身的认证凭证(如JWT),防止未授权访问
- 权限最小化:仅申请
Calendars.Read等必要权限,避免过度授权 - 重定向URI限制:在Azure AD中严格限定重定向URI,仅允许可信域名
内容的提问来源于stack exchange,提问作者Jay Patel
相关产品推荐
相关产品推荐

