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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.08 14:03:11