基于Django与NextJS 13的Passkeys登录认证系统解决方案求助
Django + Next.js 13 实现 Passkeys 登录方案
核心思路
Passkeys 基于 WebAuthn 规范,需前后端配合完成注册、登录全流程:前端调用浏览器原生 WebAuthn API 生成/验证凭证,后端负责凭证的存储、合法性校验。针对前后端分离架构,要确保跨域配置正确,且前后端的 WebAuthn 核心参数(如 RP ID、Origin)完全一致。
步骤一:后端(Django)实现 WebAuthn 核心逻辑
无需依赖第三方 Passkeys 包,直接基于标准 WebAuthn 库实现更灵活可控:
- 安装依赖库:
pip install webauthn - 编写四个核心视图处理流程:
- 注册初始化:生成注册挑战(challenge)返回前端,同时将挑战暂存到会话或数据库(绑定当前用户)
- 注册验证:接收前端传来的注册凭证,用
webauthn库校验合法性,通过后将凭证信息(credential ID、公钥等)关联到用户存储 - 登录初始化:生成登录挑战返回前端,关联目标用户
- 登录验证:校验前端传来的登录凭证,通过后生成 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完成前端凭证的生成与提交:
- 注册流程:
- 调用后端注册初始化接口,获取配置选项
- 转换选项格式适配 WebAuthn API,生成注册凭证
- 将凭证编码为 Base64 格式发送到后端验证,完成绑定
- 登录流程:
- 传入用户标识(如邮箱)调用后端登录初始化接口,获取挑战
- 调用 API 获取登录凭证,编码后提交后端验证
- 验证通过后存储后端返回的 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
相关产品推荐
相关产品推荐

