Azure 身份验证与 SSO 配置
概述
Azure Active Directory (Azure AD) 是 Microsoft 的云身份和访问管理服务,提供单点登录 (SSO)、多重身份验证 (MFA) 和条件访问等安全功能。本文介绍如何在 Azure 中配置用户身份验证、SSO 以及相关的身份管理逻辑。
Azure AD 基础概念
核心组件
| 组件 | 描述 | 用途 |
|---|---|---|
| Azure AD | 云身份和访问管理服务 | 管理用户、组和应用程序访 问 |
| SSO (单点登录) | 一次登录访问多个应用 | 提升用户体验,减少密码管理负担 |
| 应用注册 | 在 Azure AD 中注册应用程序 | 启用 OAuth 2.0 和 OpenID Connect |
| 服务主体 | 应用程序的身份标识 | 用于应用程序访问 Azure 资源 |
| 条件访问 | 基于策略的访问控制 | 根据条件(位置、设备等)控制访问 |
应用注册与配置
在 Azure 门户中注册应用
- 登录 Azure 门户
- 导航到 Azure Active Directory > 应用注册
- 点击 新注册
- 填写应用信息:
- 名称: 您的应用名称
- 支持的帐户类型: 选择适当的选项
- 重定向 URI: 应用的回调地址
使用 Azure CLI 注册应用
# 登录 Azure
az login
# 创建应用注册
az ad app create \
--display-name "My Application" \
--web-redirect-uris "https://localhost:3000/callback" \
--enable-id-token-issuance true
# 获取应用 ID(客户端 ID)
az ad app list --display-name "My Application" --query "[].appId" -o tsv
# 创建服务主体
az ad sp create --id <app-id>
# 创建客户端密钥
az ad app credential reset --id <app-id> --append
SSO 单点登录实现
OAuth 2.0 授权码流程
- Python (MSAL)
- JavaScript (MSAL.js)
- C# (.NET)
from msal import ConfidentialClientApplication
import requests
# 应用配置
CLIENT_ID = "your-client-id"
CLIENT_SECRET = "your-client-secret"
AUTHORITY = "https://login.microsoftonline.com/your-tenant-id"
REDIRECT_URI = "https://localhost:3000/callback"
SCOPE = ["User.Read"]
# 创建 MSAL 应用实例
app = ConfidentialClientApplication(
client_id=CLIENT_ID,
client_credential=CLIENT_SECRET,
authority=AUTHORITY
)
# 步骤 1: 获取授权 URL
def get_authorization_url():
auth_url = app.get_authorization_request_url(
scopes=SCOPE,
redirect_uri=REDIRECT_URI
)
return auth_url
# 步骤 2: 处理回调并获取令牌
def acquire_token_by_authorization_code(authorization_code):
result = app.acquire_token_by_authorization_code(
code=authorization_code,
scopes=SCOPE,
redirect_uri=REDIRECT_URI
)
if "access_token" in result:
return result["access_token"]
else:
raise Exception(f"获取令牌失败: {result.get('error_description')}")
# 步骤 3: 使用访问令牌调用 Microsoft Graph API
def get_user_info(access_token):
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
}
response = requests.get(
"https://graph.microsoft.com/v1.0/me",
headers=headers
)
if response.status_code == 200:
return response.json()
else:
raise Exception(f"获取用户信息失败: {response.text}")
# 使用示例
if __name__ == "__main__":
# 1. 获取授权 URL(在浏览器中打开)
auth_url = get_authorization_url()
print(f"请在浏览器中访问: {auth_url}")
# 2. 用户授权后,从回调 URL 中获取授权码
# authorization_code = input("请输入授权码: ")
# 3. 使用授权码获取访问令牌
# access_token = acquire_token_by_authorization_code(authorization_code)
# 4. 使用令牌获取用户信息
# user_info = get_user_info(access_token)
# print(f"用户信息: {user_info}")
import { ConfidentialClientApplication } from "@azure/msal-node";
// 应用配置
const msalConfig = {
auth: {
clientId: "your-client-id",
clientSecret: "your-client-secret",
authority: "https://login.microsoftonline.com/your-tenant-id",
},
};
const clientApp = new ConfidentialClientApplication(msalConfig);
// 获取授权 URL
async function getAuthorizationUrl() {
const authCodeUrlParameters = {
scopes: ["User.Read"],
redirectUri: "https://localhost:3000/callback",
};
const authUrl = await clientApp.getAuthCodeUrl(authCodeUrlParameters);
return authUrl;
}
// 使用授权码获取令牌
async function acquireTokenByCode(authorizationCode) {
const tokenRequest = {
code: authorizationCode,
scopes: ["User.Read"],
redirectUri: "https://localhost:3000/callback",
};
try {
const response = await clientApp.acquireTokenByCode(tokenRequest);
return response.accessToken;
} catch (error) {
console.error("获取令牌失败:", error);
throw error;
}
}
// 使用访问令牌调用 Microsoft Graph API
async function getUserInfo(accessToken) {
const response = await fetch("https://graph.microsoft.com/v1.0/me", {
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
});
if (response.ok) {
return await response.json();
} else {
throw new Error(`获取用户信息失败: ${await response.text()}`);
}
}
// Express.js 示例路由
const express = require("express");
const app = express();
app.get("/login", async (req, res) => {
const authUrl = await getAuthorizationUrl();
res.redirect(authUrl);
});
app.get("/callback", async (req, res) => {
const { code } = req.query;
try {
const accessToken = await acquireTokenByCode(code);
const userInfo = await getUserInfo(accessToken);
res.json({ user: userInfo, token: accessToken });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
using Microsoft.Identity.Client;
using System;
using System.Threading.Tasks;
public class AzureADAuth
{
private readonly string _clientId = "your-client-id";
private readonly string _clientSecret = "your-client-secret";
private readonly string _tenantId = "your-tenant-id";
private readonly string _redirectUri = "https://localhost:3000/callback";
private readonly string[] _scopes = { "User.Read" };
private IConfidentialClientApplication _app;
public AzureADAuth()
{
_app = ConfidentialClientApplicationBuilder
.Create(_clientId)
.WithClientSecret(_clientSecret)
.WithAuthority($"https://login.microsoftonline.com/{_tenantId}")
.WithRedirectUri(_redirectUri)
.Build();
}
// 获取授权 URL
public async Task<string> GetAuthorizationUrlAsync()
{
var authUrlBuilder = _app.GetAuthorizationRequestUrl(_scopes);
var authUrl = await authUrlBuilder.ExecuteAsync();
return authUrl.ToString();
}
// 使用授权码获取令牌
public async Task<string> AcquireTokenByCodeAsync(string authorizationCode)
{
try
{
var result = await _app.AcquireTokenByAuthorizationCode(_scopes, authorizationCode)
.ExecuteAsync();
return result.AccessToken;
}
catch (MsalException ex)
{
throw new Exception($"获取令牌失败: {ex.Message}");
}
}
// 使用访问令牌调用 Microsoft Graph API
public async Task<string> GetUserInfoAsync(string accessToken)
{
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);
var response = await client.GetAsync("https://graph.microsoft.com/v1.0/me");
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
}
}
用户身份验证流程
完整的 Web 应用 SSO 实现
from flask import Flask, redirect, request, session, url_for
from msal import ConfidentialClientApplication
import requests
app = Flask(__name__)
app.secret_key = "your-secret-key"
# Azure AD 配置
CLIENT_ID = "your-client-id"
CLIENT_SECRET = "your-client-secret"
TENANT_ID = "your-tenant-id"
AUTHORITY = f"https://login.microsoftonline.com/{TENANT_ID}"
REDIRECT_URI = "http://localhost:5000/callback"
SCOPE = ["User.Read", "openid", "profile"]
msal_app = ConfidentialClientApplication(
client_id=CLIENT_ID,
client_credential=CLIENT_SECRET,
authority=AUTHORITY
)
@app.route("/")
def index():
"""主页 - 检查用户是否已登录"""
if "user" in session:
return f"""
<h1>欢迎, {session['user']['displayName']}!</h1>
<p>邮箱: {session['user']['mail']}</p>
<a href="/logout">退出登录</a>
"""
else:
return """
<h1>Azure AD SSO 示例</h1>
<a href="/login">使用 Azure AD 登录</a>
"""
@app.route("/login")
def login():
"""发起登录流程"""
auth_url = msal_app.get_authorization_request_url(
scopes=SCOPE,
redirect_uri=REDIRECT_URI
)
return redirect(auth_url)
@app.route("/callback")
def callback():
"""处理 Azure AD 回调"""
# 获取授权码
code = request.args.get("code")
if not code:
return "登录失败: 未收到授权码", 400
try:
# 使用授权码获取令牌
result = msal_app.acquire_token_by_authorization_code(
code=code,
scopes=SCOPE,
redirect_uri=REDIRECT_URI
)
if "access_token" in result:
# 获取用户信息
headers = {
"Authorization": f"Bearer {result['access_token']}",
"Content-Type": "application/json"
}
user_response = requests.get(
"https://graph.microsoft.com/v1.0/me",
headers=headers
)
if user_response.status_code == 200:
user_info = user_response.json()
session["user"] = user_info
session["access_token"] = result["access_token"]
return redirect(url_for("index"))
else:
return f"获取用户信息失败: {user_response.text}", 500
else:
return f"获取令牌失败: {result.get('error_description')}", 500
except Exception as e:
return f"处理回调时出错: {str(e)}", 500
@app.route("/logout")
def logout():
"""退出登录"""
session.clear()
# 重定向到 Azure AD 登出页面
logout_url = f"{AUTHORITY}/oauth2/v2.0/logout?post_logout_redirect_uri={REDIRECT_URI}"
return redirect(logout_url)
if __name__ == "__main__":
app.run(debug=True, port=5000)
服务主体认证(应用程序身份)
使用服务主体访问 Azure 资源
from azure.identity import ClientSecretCredential
from azure.mgmt.resource import ResourceManagementClient
from azure.mgmt.storage import StorageManagementClient
# 服务主体配置
TENANT_ID = "your-tenant-id"
CLIENT_ID = "your-client-id" # 服务主体的应用 ID
CLIENT_SECRET = "your-client-secret"
SUBSCRIPTION_ID = "your-subscription-id"
# 创建凭据
credential = ClientSecretCredential(
tenant_id=TENANT_ID,
client_id=CLIENT_ID,
client_secret=CLIENT_SECRET
)
# 创建资源管理客户端
resource_client = ResourceManagementClient(credential, SUBSCRIPTION_ID)
# 列出资源组
def list_resource_groups():
resource_groups = resource_client.resource_groups.list()
print("资源组列表:")
for rg in resource_groups:
print(f" - {rg.name} (位置: {rg.location})")
# 创建存储账户客户端
storage_client = StorageManagementClient(credential, SUBSCRIPTION_ID)
# 列出存储账户
def list_storage_accounts():
storage_accounts = storage_client.storage_accounts.list()
print("\n存储账户列表:")
for account in storage_accounts:
print(f" - {account.name} (位置: {account.location})")
# 使用示例
if __name__ == "__main__":
list_resource_groups()
list_storage_accounts()
条件访问策略
使用 Azure CLI 配置条件访问
# 创建条件访问策略
az rest --method POST \
--uri "https://graph.microsoft.com/v1.0/policies/conditionalAccessPolicies" \
--headers "Content-Type=application/json" \
--body '{
"displayName": "要求 MFA 访问敏感应用",
"state": "enabled",
"conditions": {
"applications": {
"includeApplications": ["your-app-id"]
},
"users": {
"includeUsers": ["All"]
}
},
"grantControls": {
"operator": "AND",
"builtInControls": ["mfa"]
}
}'
# 列出所有条件访问策略
az rest --method GET \
--uri "https://graph.microsoft.com/v1.0/policies/conditionalAccessPolicies"
用户和组管理
使用 Microsoft Graph API 管理用户
import requests
def get_access_token():
"""获取服务主体的访问令牌"""
token_url = f"https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/token"
data = {
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"scope": "https://graph.microsoft.com/.default",
"grant_type": "client_credentials"
}
response = requests.post(token_url, data=data)
token_data = response.json()
return token_data["access_token"]
def list_users(access_token):
"""列出所有用户"""
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
}
response = requests.get(
"https://graph.microsoft.com/v1.0/users",
headers=headers
)
if response.status_code == 200:
users = response.json().get("value", [])
print("用户列表:")
for user in users:
print(f" - {user.get('displayName')} ({user.get('userPrincipalName')})")
return users
else:
raise Exception(f"获取用户列表失败: {response.text}")
def get_user_groups(access_token, user_id):
"""获取用户所属的组"""
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
}
response = requests.get(
f"https://graph.microsoft.com/v1.0/users/{user_id}/memberOf",
headers=headers
)
if response.status_code == 200:
groups = response.json().get("value", [])
print(f"\n用户组列表:")
for group in groups:
print(f" - {group.get('displayName')}")
return groups
else:
raise Exception(f"获取用户组失败: {response.text}")
def create_user(access_token, user_data):
"""创建新用户"""
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
}
response = requests.post(
"https://graph.microsoft.com/v1.0/users",
headers=headers,
json=user_data
)
if response.status_code == 201:
user = response.json()
print(f"用户创建成功: {user.get('userPrincipalName')}")
return user
else:
raise Exception(f"创建用户失败: {response.text}")
# 使用示例
if __name__ == "__main__":
token = get_access_token()
# 列出用户
users = list_users(token)
# 获取第一个用户的组
if users:
user_id = users[0].get("id")
get_user_groups(token, user_id)
# 创建新用户示例
# new_user = {
# "accountEnabled": True,
# "displayName": "测试用户",
# "mailNickname": "testuser",
# "userPrincipalName": "testuser@yourdomain.onmicrosoft.com",
# "passwordProfile": {
# "forceChangePasswordNextSignIn": True,
# "password": "TempPassword123!"
# }
# }
# create_user(token, new_user)
多租户应用配置
配置多租户应用
from msal import ConfidentialClientApplication
# 多租户应用配置
CLIENT_ID = "your-multi-tenant-app-id"
CLIENT_SECRET = "your-client-secret"
# 使用 "common" 或 "organizations" 作为租户 ID
AUTHORITY = "https://login.microsoftonline.com/common"
app = ConfidentialClientApplication(
client_id=CLIENT_ID,
client_credential=CLIENT_SECRET,
authority=AUTHORITY
)
def get_authorization_url(redirect_uri):
"""获取多租户授权 URL"""
auth_url = app.get_authorization_request_url(
scopes=["User.Read"],
redirect_uri=redirect_uri
)
return auth_url
# 多租户应用允许任何 Azure AD 租户的用户登录
# 在应用注册中,将"支持的帐户类型"设置为:
# - "任何组织目录中的帐户"(多租户)
# - "任何组织目录中的帐户和个人 Microsoft 帐户"
最佳实践
1. 安全配置
# 使用环境变量存储敏感信息
import os
CLIENT_ID = os.getenv("AZURE_CLIENT_ID")
CLIENT_SECRET = os.getenv("AZURE_CLIENT_SECRET")
TENANT_ID = os.getenv("AZURE_TENANT_ID")
# 使用密钥保管库存储密钥(推荐)
from azure.keyvault.secrets import SecretClient
from azure.identity import DefaultAzureCredential
credential = DefaultAzureCredential()
secret_client = SecretClient(
vault_url="https://your-keyvault.vault.azure.net/",
credential=credential
)
CLIENT_SECRET = secret_client.get_secret("azure-client-secret").value
2. 令牌缓存
from msal import ConfidentialClientApplication
import json
# 使用文件缓存令牌
CACHE_FILE = "token_cache.json"
def load_cache():
try:
with open(CACHE_FILE, "r") as f:
return json.load(f)
except FileNotFoundError:
return {}
def save_cache(cache):
with open(CACHE_FILE, "w") as f:
json.dump(cache, f)
app = ConfidentialClientApplication(
client_id=CLIENT_ID,
client_credential=CLIENT_SECRET,
authority=AUTHORITY,
token_cache=load_cache()
)
# 使用后保存缓存
save_cache(app.token_cache)
3. 错误处理
from msal import ConfidentialClientApplication
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def acquire_token_safely(app, scopes):
"""安全地获取令牌,包含错误处理"""
try:
# 首先尝试从缓存获取
accounts = app.get_accounts()
if accounts:
result = app.acquire_token_silent(scopes, account=accounts[0])
if result and "access_token" in result:
return result["access_token"]
# 如果缓存中没有,需要重新授权
logger.warning("缓存中没有有效令牌,需要重新授权")
return None
except Exception as e:
logger.error(f"获取令牌时出错: {str(e)}")
return None
故障排查
常见问题
问题:获取令牌时返回 "AADSTS70011: Invalid scope"
# 解决方案:检查应用注册中的 API 权限
# 1. 在 Azure 门户中,导航到应用注册 > API 权限
# 2. 确保添加了所需的权限(如 Microsoft Graph > User.Read)
# 3. 确保授予了管理员同意(如果需要)
问题:重定向 URI 不匹配
# 解决方案:确保重定向 URI 完全匹配
# 在应用注册中,检查重定向 URI 是否包括:
# - 协议(http/https)
# - 完整路径
# - 端口号(如果使用非标准端口)
问题:多租户应用无法访问其他租户
# 解决方案:确保应用注册配置正确
# 1. 支持的帐户类型设置为"任何组织目录中的帐户"
# 2. 在目标租户中授予管理员同意
# 3. 检查应用权限是否正确配置
总结
Azure AD 提供了强大的身份验证和授权功能,通过 SSO 可以显著提升用户体验。正确配置应用注册、实现 OAuth 2.0 流程、管理用户和组,以及使用条件访问策略,可以构建安全可靠的应用程序。