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

基于MSAL通过Access Token实现后端Refresh Token获取与邮件访问

实现方案

一、前端调整:确保获取刷新令牌

要实现长期后台访问邮箱,必须请求offline_access权限——仅靠有效期1小时左右的访问令牌(accessToken)无法满足定时任务需求,只有刷新令牌(refreshToken)能用来持续获取新的访问令牌。

修改MSAL配置,补充权限并调整向后端传递的数据:

// 示例MSAL配置,补充offline_access权限
const msalConfig = {
  auth: {
    clientId: "你的客户端ID",
    authority: "https://login.microsoftonline.com/common",
    redirectUri: "你的前端回调地址",
  },
  tokenRequest: {
    scopes: ["Mail.Read", "offline_access"] // 必须添加offline_access
  }
};

const msalInstance = new PublicClientApplication(msalConfig);

const loginPopup = async () => {
  try {
    const loginResponse = await msalInstance.loginPopup(
      msalConfig.tokenRequest
    );
    // 向后端传递刷新令牌、租户ID、用户名等关键信息
    const authDataToSend = {
      refreshToken: loginResponse.refreshToken,
      tenantId: loginResponse.tenantId,
      username: loginResponse.account.username
    };
    props.onSuccess(authDataToSend); // 发送至后端
    console.log("loginResponse", loginResponse);
  } catch (err) {
    console.error(err);
    props.onFailure(err);
  }
};

二、后端:存储令牌并实现刷新逻辑

1. 安全存储令牌

刷新令牌属于敏感数据,必须加密存储(如数据库加密字段、加密文件)。以下是数据库存储的示例(用SQLAlchemy定义模型):

from sqlalchemy import Column, String, DateTime
from sqlalchemy.ext.declarative import declarative_base
import datetime

Base = declarative_base()

class UserMailToken(Base):
    __tablename__ = 'user_mail_tokens'
    username = Column(String, primary_key=True)
    refresh_token = Column(String, nullable=False) # 需加密存储
    tenant_id = Column(String, nullable=False)
    created_at = Column(DateTime, default=datetime.datetime.utcnow)

2. 接收前端令牌并存储

处理前端POST请求,完成令牌的新增/更新:

from flask import request, jsonify
import json
from your_models import UserMailToken, db
from msal import ConfidentialClientApplication
import consts

@app.route('/save-mail-token', methods=['POST'])
def save_mail_token():
    auth_data = request.get_json()
    # 检查用户是否已存在,存在则更新令牌,不存在则创建新记录
    existing_token = UserMailToken.query.filter_by(username=auth_data['username']).first()
    if existing_token:
        existing_token.refresh_token = auth_data['refreshToken']
        existing_token.tenant_id = auth_data['tenantId']
    else:
        new_token = UserMailToken(
            username=auth_data['username'],
            refresh_token=auth_data['refreshToken'],
            tenant_id=auth_data['tenantId']
        )
        db.session.add(new_token)
    db.session.commit()
    return jsonify({"status": "success"})

3. 令牌刷新与访问令牌获取

编写工具函数,用刷新令牌获取有效的访问令牌:

def get_access_token(username):
    # 从数据库读取用户的刷新令牌和租户ID
    user_token = UserMailToken.query.filter_by(username=username).first()
    if not user_token:
        raise ValueError(f"用户{username}未完成邮箱授权")
    
    # 初始化MSAL客户端
    with open(consts.O365_OAUTH_CREDS_PATH) as f:
        creds_data = json.load(f)
    
    app = ConfidentialClientApplication(
        client_id=creds_data["auth"]["client_id"],
        client_credential=creds_data["auth"]["client_secret"],
        authority=f'{creds_data["auth"]["authority_base_uri"]}{user_token.tenant_id}',
    )
    
    # 用刷新令牌换取新的访问令牌
    result = app.acquire_token_by_refresh_token(
        refresh_token=user_token.refresh_token,
        scopes=["Mail.Read"]
    )
    
    if "access_token" in result:
        # 部分场景下会返回新的刷新令牌,需更新存储
        if "refresh_token" in result:
            user_token.refresh_token = result["refresh_token"]
            db.session.commit()
        return result["access_token"]
    else:
        # 刷新失败(令牌过期/用户撤销授权),需提示用户重新登录
        raise Exception(f"令牌刷新失败: {result.get('error_description')}")

三、邮件读取与定时任务

1. 调用Graph API读取邮件主题

用获取到的访问令牌调用Microsoft Graph API筛选邮件:

import requests

def fetch_target_mails(username):
    access_token = get_access_token(username)
    # 调用Graph API获取邮件主题,可自定义筛选条件
    graph_url = "https://graph.microsoft.com/v1.0/me/messages?$select=subject&$top=100"
    
    headers = {
        "Authorization": f"Bearer {access_token}"
    }
    
    response = requests.get(graph_url, headers=headers)
    if response.status_code == 200:
        messages = response.json().get('value', [])
        # 筛选符合特定条件的邮件主题
        target_subjects = [msg['subject'] for msg in messages if "目标关键词" in msg['subject']]
        return target_subjects
    else:
        raise Exception(f"读取邮件失败: {response.text}")

2. 定时任务实现

用APScheduler实现定时邮件检查:

from apscheduler.schedulers.background import BackgroundScheduler

def scheduled_mail_check():
    # 遍历所有已授权用户,执行邮件检查
    all_users = UserMailToken.query.all()
    for user in all_users:
        try:
            target_subjects = fetch_target_mails(user.username)
            # 处理筛选结果,如存入用户通知表、推送至前端等
            print(f"用户{user.username}符合条件的邮件主题: {target_subjects}")
        except Exception as e:
            print(f"处理用户{user.username}邮件失败: {str(e)}")

# 初始化定时任务,示例为每小时执行一次
scheduler = BackgroundScheduler()
scheduler.add_job(scheduled_mail_check, 'interval', hours=1)
scheduler.start()

关键注意事项

  • 令牌安全:刷新令牌长期有效,必须加密存储,禁止明文暴露。
  • 权限最小化:仅请求Mail.Read和offline_access必要权限,避免过度授权。
  • 错误处理:令牌刷新失败时,需引导用户重新登录授权;调用Graph API时要处理401、403等权限错误。
  • 租户适配:多租户应用需正确存储每个用户的tenantId,确保授权地址匹配。

内容的提问来源于stack exchange,提问作者Bar Ezra

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.18 17:53:13