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

基于Django与NextJS 13的Passkeys登录认证系统解决方案求助

Django + Next.js 13 实现 Passkeys 登录方案

核心思路

Passkeys 基于 WebAuthn 规范,需前后端配合完成注册、登录全流程:前端调用浏览器原生 WebAuthn API 生成/验证凭证,后端负责凭证的存储、合法性校验。针对前后端分离架构,要确保跨域配置正确,且前后端的 WebAuthn 核心参数(如 RP ID、Origin)完全一致。

步骤一:后端(Django)实现 WebAuthn 核心逻辑

无需依赖第三方 Passkeys 包,直接基于标准 WebAuthn 库实现更灵活可控:

  • 安装依赖库:pip install webauthn
  • 编写四个核心视图处理流程:
    1. 注册初始化:生成注册挑战(challenge)返回前端,同时将挑战暂存到会话或数据库(绑定当前用户)
    2. 注册验证:接收前端传来的注册凭证,用webauthn库校验合法性,通过后将凭证信息(credential ID、公钥等)关联到用户存储
    3. 登录初始化:生成登录挑战返回前端,关联目标用户
    4. 登录验证:校验前端传来的登录凭证,通过后生成 JWT 或激活会话,返回给前端用于后续接口认证

示例代码片段(Django 视图):

from webauthn import generate_registration_options, verify_registration_response
from webauthn import generate_authentication_options, verify_authentication_response
from webauthn.helpers.structs import RegistrationCredential, AuthenticationCredential, UserVerificationRequirement
from django.http import JsonResponse
from django.conf import settings
from django.contrib.auth import get_user_model

User = get_user_model()

# 注册初始化
def start_registration(request):
    # 假设用户已通过邮箱等方式完成基础注册并登录
    user = request.user
    options = generate_registration_options(
        rp_name=settings.WEBAUTHN_RP_NAME,
        rp_id=settings.WEBAUTHN_RP_ID,
        user_id=str(user.id),
        user_name=user.email,
        user_display_name=user.username,
        user_verification=UserVerificationRequirement.PREFERRED,
    )
    request.session["webauthn_reg_challenge"] = options.challenge
    return JsonResponse(options.model_dump())

# 注册验证
def verify_registration(request):
    credential = RegistrationCredential(**request.POST)
    challenge = request.session.pop("webauthn_reg_challenge")
    
    try:
        verification = verify_registration_response(
            credential=credential,
            expected_challenge=challenge,
            expected_rp_id=settings.WEBAUTHN_RP_ID,
            expected_origin=settings.WEBAUTHN_ORIGIN,
            require_user_verification=True,
        )
    except Exception as e:
        return JsonResponse({"error": str(e)}, status=400)
    
    # 存储凭证到关联表(需提前定义WebAuthnCredential模型)
    WebAuthnCredential.objects.create(
        user=request.user,
        credential_id=verification.credential_id,
        public_key=verification.credential_public_key,
        sign_count=verification.sign_count,
    )
    return JsonResponse({"success": True})

# 登录初始化
def start_login(request):
    # 可通过前端传入的邮箱获取用户,提前加载其已注册的凭证ID
    user = User.objects.get(email=request.GET.get("email"))
    registered_creds = [
        {"id": cred.credential_id, "type": "public-key"}
        for cred in user.webauthn_credentials.all()
    ]
    
    options = generate_authentication_options(
        rp_id=settings.WEBAUTHN_RP_ID,
        allow_credentials=registered_creds,
        user_verification=UserVerificationRequirement.PREFERRED,
    )
    request.session["webauthn_login_challenge"] = options.challenge
    request.session["webauthn_login_user_id"] = str(user.id)
    return JsonResponse(options.model_dump())

# 登录验证
def verify_login(request):
    credential = AuthenticationCredential(**request.POST)
    challenge = request.session.pop("webauthn_login_challenge")
    user_id = request.session.pop("webauthn_login_user_id")
    user = User.objects.get(id=user_id)
    user_cred = user.webauthn_credentials.get(credential_id=credential.id)
    
    try:
        verification = verify_authentication_response(
            credential=credential,
            expected_challenge=challenge,
            expected_rp_id=settings.WEBAUTHN_RP_ID,
            expected_origin=settings.WEBAUTHN_ORIGIN,
            credential_public_key=user_cred.public_key,
            current_sign_count=user_cred.sign_count,
            require_user_verification=True,
        )
    except Exception as e:
        return JsonResponse({"error": str(e)}, status=400)
    
    # 更新签名计数,防止重放攻击
    user_cred.sign_count = verification.new_sign_count
    user_cred.save()
    
    # 生成JWT返回前端
    token = generate_jwt_token(user)  # 需自行实现JWT生成逻辑
    return JsonResponse({"access_token": token})

步骤二:前端(Next.js 13)实现 WebAuthn 交互

用浏览器原生navigator.credentialsAPI完成前端凭证的生成与提交:

  • 注册流程:
    1. 调用后端注册初始化接口,获取配置选项
    2. 转换选项格式适配 WebAuthn API,生成注册凭证
    3. 将凭证编码为 Base64 格式发送到后端验证,完成绑定
  • 登录流程:
    1. 传入用户标识(如邮箱)调用后端登录初始化接口,获取挑战
    2. 调用 API 获取登录凭证,编码后提交后端验证
    3. 验证通过后存储后端返回的 JWT,用于后续接口请求

示例代码片段(Next.js 客户端组件):

'use client';
import { useRouter } from 'next/navigation';

const router = useRouter();

// 注册Passkeys
async function registerPasskey() {
  try {
    const initRes = await fetch('/api/django/auth/start-registration', { 
      method: 'GET', 
      credentials: 'include' 
    });
    const options = await initRes.json();
    
    // 转换二进制数据格式
    options.user.id = Uint8Array.from(atob(options.user.id), c => c.charCodeAt(0));
    options.challenge = Uint8Array.from(atob(options.challenge), c => c.charCodeAt(0));
    
    // 创建凭证
    const credential = await navigator.credentials.create({ publicKey: options });
    
    // 编码为可传输格式
    const submitData = {
      id: credential.id,
      rawId: btoa(String.fromCharCode(...new Uint8Array(credential.rawId))),
      type: credential.type,
      response: {
        attestationObject: btoa(String.fromCharCode(...new Uint8Array(credential.response.attestationObject))),
        clientDataJSON: btoa(String.fromCharCode(...new Uint8Array(credential.response.clientDataJSON))),
      },
    };
    
    const verifyRes = await fetch('/api/django/auth/verify-registration', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      credentials: 'include',
      body: JSON.stringify(submitData),
    });
    
    if (verifyRes.ok) alert('Passkeys注册成功');
  } catch (err) {
    console.error('注册失败:', err);
  }
}

// 用Passkeys登录
async function loginWithPasskey(email) {
  try {
    const initRes = await fetch(`/api/django/auth/start-login?email=${email}`, { 
      method: 'GET', 
      credentials: 'include' 
    });
    const options = await initRes.json();
    
    options.challenge = Uint8Array.from(atob(options.challenge), c => c.charCodeAt(0));
    options.allowCredentials = options.allowCredentials?.map(cred => ({
      ...cred,
      id: Uint8Array.from(atob(cred.id), c => c.charCodeAt(0)),
    }));
    
    const credential = await navigator.credentials.get({ publicKey: options });
    
    const submitData = {
      id: credential.id,
      rawId: btoa(String.fromCharCode(...new Uint8Array(credential.rawId))),
      type: credential.type,
      response: {
        authenticatorData: btoa(String.fromCharCode(...new Uint8Array(credential.response.authenticatorData))),
        clientDataJSON: btoa(String.fromCharCode(...new Uint8Array(credential.response.clientDataJSON))),
        signature: btoa(String.fromCharCode(...new Uint8Array(credential.response.signature))),
        userHandle: credential.response.userHandle 
          ? btoa(String.fromCharCode(...new Uint8Array(credential.response.userHandle))) 
          : null,
      },
    };
    
    const verifyRes = await fetch('/api/django/auth/verify-login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      credentials: 'include',
      body: JSON.stringify(submitData),
    });
    
    if (verifyRes.ok) {
      const { access_token } = await verifyRes.json();
      localStorage.setItem('authToken', access_token);
      router.push('/dashboard');
    }
  } catch (err) {
    console.error('登录失败:', err);
  }
}

// 组件渲染部分
export default function AuthPage() {
  return (
    <div>
      <button onClick={registerPasskey}>绑定Passkeys</button>
      <button onClick={() => loginWithPasskey('user@example.com')}>用Passkeys登录</button>
    </div>
  );
}

关键注意事项

  • 跨域配置:Django 中需在settings.py设置CORS_ALLOWED_ORIGINS,允许 Next.js 域名;Cookie 需设置SameSite=Lax(跨域场景设为None并开启Secure)
  • 参数一致性:RP ID 需为根域名(如example.com),Origin 为前端完整地址(如https://app.example.com),两者必须符合 WebAuthn 规则
  • 安全防护:后端需校验签名计数,防止凭证重放攻击;生产环境需开启 HTTPS,WebAuthn API 仅在安全上下文可用
  • 用户体验:登录前可让用户输入邮箱,后端返回其已注册的凭证 ID,引导浏览器快速匹配对应 Passkeys

内容的提问来源于stack exchange,提问作者Federico Raponi

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 23:57:15