Django应用集成Azure AD:如何实现单点登录及免重复登录?
实现Azure AD与Django集成的自动/快速登录方案
针对你遇到的每次登录都需要输入Azure AD凭据的问题,有几个可行的解决方案,结合你的现有代码,我会给出具体的实现建议:
一、核心思路:利用Refresh Token实现无感知续期
Azure AD的OAuth2.0流程支持获取refresh_token,这个令牌可以用来在无需用户重新输入凭据的情况下,获取新的access_token和id_token。这是实现“一次登录,后续自动认证”最直接的方案。
步骤1:修改授权请求,获取Refresh Token
首先,你需要在发起Azure AD授权请求时,加入offline_access权限范围,这样Azure AD才会返回refresh_token。修改你的授权URL生成逻辑:
def get_auth_url(company_azure): auth_endpoint = f"https://login.microsoftonline.com/{company_azure.tenant_id}/oauth2/v2.0/authorize" params = { 'client_id': company_azure.client_id, 'response_type': 'code', 'redirect_uri': '你的回调URI', 'scope': 'openid email profile offline_access', # 必须包含offline_access 'state': generate_state(), 'prompt': 'select_account' # 首次登录让用户选择账户,后续可改为none尝试静默登录 } return f"{auth_endpoint}?{urllib.parse.urlencode(params)}"
步骤2:持久化存储Refresh Token
创建一个模型来关联你的内部用户和Azure AD的refresh_token,方便后续调用:
from django.db import models from django.contrib.auth.models import User from django.utils import timezone class UserAzureToken(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) refresh_token = models.TextField() expires_at = models.DateTimeField() # 记录token过期时间,提前刷新 updated_at = models.DateTimeField(auto_now=True)
然后在你的回调视图中,将refresh_token保存到数据库:
def callback(request): # ... 原有代码 ... token = get_token_from_code(url, expected_state, company_azure) user = get_user(token) # 保存/更新refresh_token到数据库 user_token, created = UserAzureToken.objects.get_or_create(user=user_internal) user_token.refresh_token = token.get('refresh_token') # 计算过期时间:当前时间 + expires_in秒(Azure返回的expires_in通常是3600秒) expires_in = token.get('expires_in', 3600) user_token.expires_at = timezone.now() + timezone.timedelta(seconds=expires_in) user_token.save() # ... 原有代码 ...
步骤3:登录时优先使用Refresh Token
修改你的邮箱登录视图,检查当前用户是否有有效的refresh_token,如果有则直接用它换取新的令牌,无需跳转到Azure AD:
def login(request): if request.method == 'POST': email = request.POST.get('email') user_internal = User.objects.filter(email=email).first() if user_internal: # 检查是否存在未过期的refresh_token try: user_token = UserAzureToken.objects.get(user=user_internal) if user_token.expires_at > timezone.now(): # 用refresh_token换取新的access_token new_token = get_token_from_refresh_token(user_token.refresh_token, company_azure) if new_token: # 更新数据库中的token信息 user_token.refresh_token = new_token.get('refresh_token') user_token.expires_at = timezone.now() + timezone.timedelta(seconds=new_token.get('expires_in', 3600)) user_token.save() # 完成认证,重定向到首页 store_token(request, new_token) store_user(request, get_user(new_token)) return redirect('home') except UserAzureToken.DoesNotExist: pass # 没有有效refresh_token,走原有重定向流程 request.session['user_id'] = user_internal.id auth_url = get_auth_url(company_azure) return redirect(auth_url) # GET请求,返回邮箱输入页面 return render(request, 'login.html')
步骤4:实现Refresh Token换Token的函数
新增一个函数调用Azure AD的令牌端点,用refresh_token换取新的令牌:
import requests def get_token_from_refresh_token(refresh_token, company_azure): token_endpoint = f"https://login.microsoftonline.com/{company_azure.tenant_id}/oauth2/v2.0/token" payload = { 'client_id': company_azure.client_id, 'client_secret': company_azure.client_secret, 'refresh_token': refresh_token, 'grant_type': 'refresh_token', 'scope': 'openid email profile offline_access' } response = requests.post(token_endpoint, data=payload) if response.status_code == 200: return response.json() return None
二、优化Azure AD会话配置,利用SSO会话
Azure AD默认提供SSO会话保持功能,如果用户在同一浏览器中已经登录过Azure AD,再次访问应该会自动完成认证。你可以检查以下配置:
- 登录Azure门户,进入你的企业应用,找到单一登录 -> 会话控制,延长会话超时时间和令牌生命周期;
- 确保用户的浏览器没有禁用第三方Cookie(Azure AD的SSO会话依赖
login.microsoftonline.com的Cookie)。
三、前端静默登录提升体验(可选)
如果你的应用有前端页面,可以结合MSAL.js实现静默登录:在用户打开页面时,前端自动检查是否有有效的Azure AD会话,若有则直接获取令牌并传给后端完成认证,无需跳转。示例前端代码:
const msalInstance = new msal.PublicClientApplication({ auth: { clientId: '你的Azure AD Client ID', authority: 'https://login.microsoftonline.com/你的租户ID' } }); async function silentSignIn() { try { const account = msalInstance.getAllAccounts()[0]; if (account) { const response = await msalInstance.acquireTokenSilent({ account: account, scopes: ['openid', 'email', 'profile', 'offline_access'] }); // 将令牌传给Django后端完成认证 fetch('/azure-silent-auth', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': document.cookie.split('; ').find(row => row.startsWith('csrftoken=')).split('=')[1] }, body: JSON.stringify({access_token: response.accessToken}) }).then(res => { if (res.ok) window.location.href = '/home'; }); } else { window.location.href = '/login'; } } catch (error) { // 静默登录失败,跳转到Azure AD登录页 msalInstance.loginRedirect({ scopes: ['openid', 'email', 'profile', 'offline_access'], redirectUri: '你的回调URI' }); } } // 页面加载时触发静默登录 window.onload = silentSignIn;
后端对应视图:
from django.http import JsonResponse from django.contrib.auth import login def azure_silent_auth(request): if request.method == 'POST': access_token = request.POST.get('access_token') user_profile = get_user({'access_token': access_token}) user_internal = User.objects.filter(email=user_profile['email']).first() if user_internal: login(request, user_internal) return JsonResponse({'status': 'success'}) return JsonResponse({'status': 'error', 'msg': '用户不存在'}, status=404) return JsonResponse({'status': 'error'}, status=400)
内容的提问来源于stack exchange,提问作者GustavoNogueira
相关产品推荐
相关产品推荐

