Django+React集成Microsoft Graph API的OAuth授权流程实现方法
Django+React集成Microsoft Graph API 邮件能力授权方案
你需要采用 OAuth 2.0 授权码流(机密客户端模式) 实现凭证托管,这个模式下refresh_token全程不暴露给前端,后端可以长期持有凭证代表用户调用接口,是符合你需求的标准实现方案,具体落地步骤如下:
前置配置(Azure AD侧)
- 注册Azure AD应用,添加两个平台配置:
- 单页应用平台:配置React前端的路由地址(比如本地调试用
http://localhost:3000,生产环境替换为业务域名) - Web平台:配置Django后端的授权回调地址(比如本地调试用
http://localhost:8000/api/auth/ms-callback),同时生成客户端密钥(Client Secret),这个密钥只存在后端环境变量里,绝对不能发到前端
- 单页应用平台:配置React前端的路由地址(比如本地调试用
- 配置委托权限(不要选应用程序权限,应用权限是无用户上下文的全局权限,不适合代表单个用户操作),添加以下权限:
Mail.ReadWrite、Mail.Send、offline_access、openid、profile,内部租户使用可以直接点管理员同意,多租户场景等用户首次授权时手动同意即可 - 记录三个核心参数:客户端ID(Client ID)、租户ID(Tenant ID,多租户场景填
common)、客户端密钥(Client Secret)
Django后端实现
- 先建存储用户微软凭证的关联表,不要明文存refresh_token,建议用字段级加密:
from django.db import models from django.conf import settings class MSUserCredential(models.Model): user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) ms_account_id = models.CharField(max_length=200, unique=True) # 微软侧用户唯一标识 access_token = models.TextField() refresh_token = models.TextField() # *生产环境务必加密存储* access_token_expire_at = models.DateTimeField() create_time = models.DateTimeField(auto_now_add=True) update_time = models.DateTimeField(auto_now=True)
- 写两个核心授权接口:
- 授权地址生成接口:前端点击「绑定微软邮箱」时请求该接口,后端生成随机state字符串存在当前用户session中(用于回调时防CSRF校验),拼接微软授权地址返回给前端,授权地址参数固定包含:client_id、redirect_uri(后端回调地址)、response_type=code、scope(前面加的5个权限,空格分隔)、state
- 授权回调接口:微软完成用户授权后,会带着code和state参数重定向到这个接口,首先校验请求里的state和session中存储的state是否一致,不一致直接返回403;校验通过后,后端携带code、client_id、client_secret、redirect_uri、grant_type=authorization_code参数,请求微软令牌端点兑换令牌,拿到access_token、refresh_token、expires_in后,计算access_token过期时间,和当前登录用户绑定存入
MSUserCredential表,最后重定向回前端的绑定成功页即可
- 封装通用的Graph调用工具类,自动处理access_token过期刷新逻辑,避免每次调用都手动判断过期:
import requests from django.utils import timezone from datetime import timedelta from django.conf import settings def get_valid_access_token(credential): # 提前5分钟刷新token,避免临界时间请求失败 if credential.access_token_expire_at - timedelta(minutes=5) > timezone.now(): return credential.access_token # 用refresh_token换发新token refresh_payload = { "grant_type": "refresh_token", "refresh_token": credential.refresh_token, "client_id": settings.MS_CLIENT_ID, "client_secret": settings.MS_CLIENT_SECRET, "scope": "Mail.ReadWrite Mail.Send offline_access openid profile" } resp = requests.post( f"https://login.microsoftonline.com/{settings.MS_TENANT_ID}/oauth2/v2.0/token", data=refresh_payload ) resp.raise_for_status() token_data = resp.json() # 更新数据库中存储的凭证 credential.access_token = token_data["access_token"] credential.refresh_token = token_data["refresh_token"] credential.access_token_expire_at = timezone.now() + timedelta(seconds=token_data["expires_in"]) credential.save() return credential.access_token # 示例:调用发送邮件接口 def send_user_mail(user, subject, content, to_addr_list): cred = MSUserCredential.objects.get(user=user) token = get_valid_access_token(cred) req_headers = { "Authorization": f"Bearer {token}", "Content-Type": "application/json" } mail_body = { "message": { "subject": subject, "body": {"contentType": "Text", "content": content}, "toRecipients": [{"emailAddress": {"address": addr}} for addr in to_addr_list] } } resp = requests.post( "https://graph.microsoft.com/v1.0/me/sendMail", headers=req_headers, json=mail_body ) resp.raise_for_status() return resp.status_code == 202
后续要实现读邮件、管理邮件文件夹等操作,直接用拿到的有效access_token调用对应Graph接口即可。
React前端实现
前端逻辑非常简单,不需要引入复杂的SDK处理令牌:
- 绑定微软账号按钮点击后,请求后端的授权地址生成接口,拿到地址后直接
window.location.href跳转到该地址即可 - 提前写好绑定成功/失败的路由页,等后端回调完成重定向回来后,给用户展示对应提示
- 后续需要触发邮件相关操作时,直接请求你自己封装的Django业务接口即可,不需要在前端持有任何微软相关的令牌
常见踩坑点
- 不要用已经被微软弃用的隐式授权流,安全性差且无法拿到长期有效的refresh_token
- 必须加
offline_access权限,否则拿不到refresh_token,access_token1小时过期后必须让用户重新授权,体验极差 - state校验不能省略,否则存在CSRF漏洞,可能导致攻击者诱导用户绑定恶意账号
- 多租户场景下,token兑换成功后要校验返回的租户ID是否在你的允许范围内,避免任意微软账号都能接入你的应用
- 生产环境必须加密存储refresh_token,避免数据库泄露导致用户邮箱被未授权访问
- 所有携带client_secret的请求必须在后端发起,绝对不能把client_secret暴露给前端代码
内容的提问来源于stack exchange,提问作者esieboma jeremiah
相关产品推荐
相关产品推荐

