在自动化任务中,我们经常需要程序在后台无缝获取令牌,以便调用 API、发送邮件等操作。对于 Microsoft 个人账户(例如 Outlook.com 或 Hotmail),设备代码流是一种常见的交互式认证方式。但是每次都必须手动输入验证码显然不符合自动化需求。本文将详细介绍如何:

  1. 从零开始创建 Azure 应用注册并获取 Client ID;
  2. 更新身份验证设置,添加移动/桌面平台并启用公共客户端流,以支持设备代码流;
  3. 使用 Python 中的 MSAL 库实现设备代码认证,并利用持久化令牌缓存机制实现自动刷新。

注意:
由于微软的安全策略,对于个人账户的刷新令牌有效期通常固定在 90 天内,刷新令牌到期后仍需一次交互式验证。但通过合理的缓存机制,可以在大部分时间内实现自动化。


一、创建 Azure 应用注册

1. 登录 Azure Portal

  1. 打开浏览器,访问 Azure Portal 并使用你的 Microsoft 账户登录。

2. 进入“应用注册”

  1. 在左侧导航栏中点击 “Azure Active Directory”,或在顶部搜索栏中输入 “Azure Active Directory” 并进入该服务。
  2. 在 Azure Active Directory 页面中,点击左侧菜单的 “应用注册”

3. 创建新应用

  1. 点击 “新注册” 按钮。
  2. 在“注册应用”页面:
    • 输入一个有辨识度的应用名称(例如 “EmailAutomationApp”)。
    • “支持的账户类型” 中选择 “仅限个人 Microsoft 帐户”(个人账户适用于设备代码流认证,但注意后续必须使用 /consumers 端点)。
    • (可选)填写一个重定向 URI。如果你需要用到授权码流,可以使用常用的公共客户端重定向 URI:
      https://login.microsoftonline.com/common/oauth2/nativeclient
  3. 点击 “注册” 按钮提交。

4. 获取 Client ID

  1. 注册成功后,在应用的概览页面中可以找到 “应用程序(客户端)ID”
  2. 将该 Client ID 复制下来,后续在 Python 代码中会用到。

二、更新身份验证设置

为了支持无交互的自动化认证,我们需要确保应用注册中已正确配置身份验证设置。具体步骤如下:

1. 进入“身份验证”设置页面

  1. 在 Azure 应用注册详情页面的左侧菜单中,点击 “身份验证(Authentication)”

2. 添加移动/桌面平台

  1. 向下滚动到 “平台配置” 部分。
  2. 如果还没有添加移动/桌面平台,点击 “+ 添加平台” 按钮,然后选择 “移动和桌面应用”
  3. 系统会推荐一个重定向 URI,通常为:
    https://login.microsoftonline.com/common/oauth2/nativeclient
    你可以直接使用此 URI。

3. 启用公共客户端流(Public client flows)

  1. 在“身份验证”页面的底部,找到 “允许公共客户端流”(也可能显示为“Treat application as a public client”)选项。
  2. 确保该选项处于启用状态。启用后,应用将被标记为公共客户端,从而支持设备代码流等无需客户端密码的认证方式。

4. 保存修改

  1. 完成上述修改后,点击页面的 “保存” 按钮以应用设置更改。

三、实现设备代码认证与持久化令牌缓存

在 Python 中,我们主要使用 MSAL for Python 结合设备代码流获取 access token,再利用持久化缓存避免每次都需要交互认证。下面是实现步骤与详细代码。

1. 安装必要的库

请确保已安装 MSAL 库和其他依赖包:

pip install msal

2. 编写认证与缓存代码

下面的代码完整演示如何使用设备代码流获取令牌,并将令牌缓存存储到本地文件,从而在后续启动时自动静默获取 token:

import os
import msal
import base64
import smtplib
from email.message import EmailMessage

# ------------------------------
# 配置参数(请替换为你自己的数据)
# ------------------------------

client_id = "YOUR_CLIENT_ID"            # Azure 应用注册中获得的 Client ID
tenant = "consumers"                    # 对于个人账户,使用 "consumers" 端点
authority = f"https://login.microsoftonline.com/{tenant}"
# 对于 SMTP 发送邮件,此处的 scope 可设置为 "SMTP.Send"
scope = ["https://outlook.office.com/SMTP.Send"]

# 缓存文件路径(用于持久化令牌缓存)
cache_file = "token_cache.json"

# ------------------------------
# 初始化持久化令牌缓存
# ------------------------------

# 创建 MSAL 可序列化的令牌缓存对象
token_cache = msal.SerializableTokenCache()
if os.path.exists(cache_file):
    with open(cache_file, "r", encoding="utf-8") as f:
        token_cache.deserialize(f.read())

# 创建 PublicClientApplication,并传入令牌缓存
app = msal.PublicClientApplication(client_id, authority=authority, token_cache=token_cache)

# ------------------------------
# 尝试静默获取令牌(自动化流程)
# ------------------------------

accounts = app.get_accounts()
result = None
if accounts:
    result = app.acquire_token_silent(scope, account=accounts[0])

# 如果缓存中没有有效令牌,则进入设备代码流(需要交互认证一次)
if not result or "access_token" not in result:
    flow = app.initiate_device_flow(scopes=scope)
    if "user_code" not in flow:
        raise Exception("设备登录流程初始化失败:%s" % flow)
    # 输出提示信息:请在浏览器中打开页面并输入代码进行认证
    print(flow["message"])
    result = app.acquire_token_by_device_flow(flow)

# 检查获取 token 是否成功,并保存最新的令牌缓存
if "access_token" in result:
    access_token = result["access_token"]
    print("成功获取 access token,无需再次交互。")
    with open(cache_file, "w", encoding="utf-8") as f:
        f.write(token_cache.serialize())
else:
    raise Exception("获取 access token 失败:%s" % result.get("error_description"))

# ------------------------------
# 构造 XOAUTH2 认证字符串(用于 SMTP 认证)
# ------------------------------

def generate_oauth2_string(username, token):
    auth_string = f"user={username}\x01auth=Bearer {token}\x01\x01"
    return base64.b64encode(auth_string.encode("utf-8")).decode("utf-8")

username = "your_email@outlook.com"   # 替换为你的 Outlook 个人邮箱地址
oauth2_string = generate_oauth2_string(username, access_token)

# ------------------------------
# 发送邮件示例(使用 Outlook SMTP 服务)
# ------------------------------

msg = EmailMessage()
msg["Subject"] = "测试邮件 - 持久化令牌缓存示例"
msg["From"] = username
msg["To"] = "recipient_email@example.com"  # 替换为收件人邮箱
msg.set_content("这是一封使用设备代码流与持久化令牌缓存自动发送的测试邮件。")

smtp_server = "smtp-mail.outlook.com"
smtp_port = 587

try:
    with smtplib.SMTP(smtp_server, smtp_port) as server:
        server.ehlo()
        server.starttls()
        server.ehlo()
        code, response = server.docmd("AUTH", "XOAUTH2 " + oauth2_string)
        if code != 235:
            raise Exception(f"SMTP 认证失败, 返回码: {code}, 响应: {response}")
        server.send_message(msg)
    print("邮件发送成功!")
except Exception as e:
    print("邮件发送失败:", e)

3. 代码说明

  • 令牌缓存处理
    程序启动时会检查 token_cache.json 文件,如该文件存在,会加载之前存储的令牌缓存,并通过 acquire_token_silent() 尝试静默获取令牌。如果缓存中有有效令牌,就能直接使用,从而避免设备代码流的手动交互。
  • 设备代码流
    当静默获取失败时,MSAL 会调用 initiate_device_flow() 输出一条提示,要求用户在浏览器中打开指定链接并输入代码进行认证。认证成功后,程序会获取新的 access token,并将缓存保存下来供以后使用。
  • XOAUTH2 认证构造
    使用获取的 access token 构造 XOAUTH2 格式字符串,用于 SMTP 的身份验证,最终通过 Outlook SMTP 服务器发送邮件。

四、总结

本文详细介绍了如何从创建 Azure 应用注册、更新身份验证设置、到实现设备代码流认证与持久化令牌缓存的全过程。虽然对于 Microsoft 个人账户来说,微软的安全策略决定了刷新令牌的有效期大约为 90 天,因此仍需在过期后进行一次手动认证,但通过持久化令牌缓存机制,你可以大大降低频繁验证的次数。同时,如果业务允许,切换为支持客户端凭证流的企业账户则可完全实现无交互自动化认证。

希望这篇教程能帮助你顺利实现基于 Python 的自动身份认证与邮件发送。如果有任何问题或需要进一步讨论其他认证方式,欢迎在下方留言交流。