如何在Django REST Framework中实现无密码Token认证
核心结论
- 重写
is_authenticated()实现自定义认证逻辑完全走不通。这个方法只是Django用户模型的状态标记方法,作用仅仅是返回布尔值,标识当前绑定到请求的用户实例是不是已认证状态,根本不参与凭证校验、身份匹配的认证全流程,改它碰不到DRF的认证执行链路,不会有任何效果。 - 用PIN数字码替代传统密码做身份校验完全可行。PIN本质是和用户绑定的专属校验凭证,只要遵循DRF的认证组件扩展规范实现逻辑,就能顺畅替代密码完成校验,和传统密码认证没有本质区别。
无密码Token+PIN认证落地方案
1. 模型层改造
首先扩展原有用户模型,新增PIN哈希存储字段,注意绝对不能明文存储PIN,直接复用Django自带的哈希校验工具即可:
from django.contrib.auth.models import AbstractUser from django.db import models from django.contrib.auth.hashers import check_password class CustomUser(AbstractUser): pin_hash = models.CharField(max_length=128, verbose_name="PIN码哈希值") # 不需要保留传统密码字段可以直接置空,也可以保留做兼容 password = models.CharField(max_length=128, blank=True, null=True) def verify_pin(self, raw_pin: str) -> bool: return check_password(raw_pin, self.pin_hash)
新增无密码认证Token模型,绑定用户、控制有效期、支持作废逻辑:
from django.db import models from django.conf import settings from django.utils import timezone class PasswordlessToken(models.Model): user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) token = models.CharField(max_length=64, unique=True, db_index=True) created_at = models.DateTimeField(auto_now_add=True) expires_at = models.DateTimeField() is_revoked = models.BooleanField(default=False) @property def is_valid(self) -> bool: return not self.is_revoked and timezone.now() < self.expires_at
2. 实现DRF自定义认证类
这才是DRF认证逻辑的正确扩展点,继承BaseAuthentication重写authenticate方法即可,不需要动用户模型的is_authenticated:
from rest_framework.authentication import BaseAuthentication from rest_framework.exceptions import AuthenticationFailed from .models import PasswordlessToken class PinTokenAuthentication(BaseAuthentication): keyword = "Bearer" def authenticate(self, request): auth_header = request.META.get("HTTP_AUTHORIZATION", "") if not auth_header.startswith(self.keyword): return None try: token_value = auth_header.split(" ")[1].strip() except IndexError: raise AuthenticationFailed("认证凭证格式错误") try: token = PasswordlessToken.objects.select_related("user").get(token=token_value) except PasswordlessToken.DoesNotExist: raise AuthenticationFailed("无效的认证凭证") if not token.is_valid: raise AuthenticationFailed("认证凭证已过期或已作废") # 认证通过后返回(用户实例, 凭证实例),DRF会自动将request.user.is_authenticated置为True return (token.user, token)
3. 实现PIN换Token接口
写一个不需要认证的公开接口,校验用户提交的身份标识和PIN,通过后下发有效Token:
import secrets from datetime import timedelta from django.utils import timezone from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.exceptions import AuthenticationFailed from rest_framework import status from .models import CustomUser, PasswordlessToken class GetTokenByPinView(APIView): authentication_classes = [] permission_classes = [] def post(self, request): user_account = request.data.get("account") # 可以是用户名、手机号、邮箱等任意唯一标识 input_pin = request.data.get("pin") if not all([user_account, input_pin]): return Response({"detail": "请提交账号和PIN码"}, status=status.HTTP_400_BAD_REQUEST) try: user = CustomUser.objects.get(username=user_account) except CustomUser.DoesNotExist: # 模糊报错,避免接口被用来扫号 raise AuthenticationFailed("账号或PIN码错误") if not user.verify_pin(input_pin): raise AuthenticationFailed("账号或PIN码错误") # 作废该用户名下所有未失效的旧Token,避免多端凭证泄露 PasswordlessToken.objects.filter(user=user, is_revoked=False).update(is_revoked=True) # 生成新Token,有效期按需设置,比如2小时 new_token = PasswordlessToken.objects.create( user=user, token=secrets.token_urlsafe(32), expires_at=timezone.now() + timedelta(hours=2) ) return Response({ "token": new_token.token, "expires_at": new_token.expires_at })
4. 全局配置生效
在项目配置文件中注册自定义认证类为默认认证组件:
REST_FRAMEWORK = { "DEFAULT_AUTHENTICATION_CLASSES": [ "your_app_path.authentication.PinTokenAuthentication", ] }
落地注意事项
- PIN码为纯数字,熵值远低于普通密码,必须给PIN校验接口加上严格的限频策略,防止暴力破解
- PIN码必须加盐哈希存储,绝对不能明文落库
- Token过期时间不要设置过长,用户登出时直接将对应Token的
is_revoked字段置为True即可实现主动失效 - 不要尝试修改
is_authenticated()方法实现逻辑,认证类返回合法用户实例后,框架会自动处理这个属性的返回值,和原生逻辑完全兼容,额外改造反而会引入权限判断异常。
内容的提问来源于stack exchange,提问作者Nisha Manu
相关产品推荐
相关产品推荐

