django 自定义登录与认证
2022年7月11日大约 4 分钟约 1154 字
JSON Web Token 认证
jwt的字符串有两块:前面是base64编码的用户信息,后面是签名。签名只有后端能解码,用于识别伪造的token。
session认证方式一般用于前后端不分离的场景,session值是保存在内存中 drf内置的token认证功能过于简单,token值保存到数据库的表中,没有过期时间设置 jwt认证方式是普通token升级版,用户信息+后端签名作为token发给前端保存。签名解密密钥只有后端有,也不存在token被篡改。无须保存用户状态,由前端保存
文档:点击查看
- 安装包pip install djangorestframework-simplejwt
- 配置
settings.py
from datetime import timedelta
INSTALLED_APPS = [
    # ...
    'rest_framework_simplejwt',  # 使用jwt做用户认证
    # ...
]
REST_FRAMEWORK = {
    # ...
    'DEFAULT_AUTHENTICATION_CLASSES': ( # 类似于中间件,按顺序执行authenticate(),主要是通过不同的认证方式来查找用户并设置request.user
        # ...
        'rest_framework.authentication.BasicAuthentication',
        'rest_framework.authentication.SessionAuthentication',  # 依赖django的SessionMiddleware、AuthenticationMiddleware,一般浏览器常见,前后端分离一般不用,不过内置文档功能要用
        # 'rest_framework_simplejwt.authentication.JWTAuthentication', # 这里是全局做 jwt接收到request.user的转换,有些api是不需要登录的,所以一般也不在这配,仅作演示
    )
    # ...
} 
SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(days=7),urls.py
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView, TokenVerifyView
urlpatterns = [
    ...
    path('token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
    path('token/verify/', TokenVerifyView.as_view(), name='token_verify'),
    ...
]persons/views.py
# 全局配置取消,ViewSet单独配置需要登录才能访问该视图集,如果不配置就无须认证
from rest_framework_simplejwt.authentication import JWTAuthentication
class PersonListViewSet(...):
    authentication_classes = (JWTAuthentication, )- 前端获取token测试
# ------登录-------
发送post请求:http://localhost:8000/token/
post数据:{"username": "xiongda", "password": "123456"}
request头:"Content-Type: application/json"
# ------使用-------
在使用时,在request头部添加 Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI...自定义用户验证
使用手机短信验证
apps/utils/send_sms.py
这里是发送短信的api
import json
class SendSMS():
    def __init__(self, api_key):
        self.api_key = api_key
        self.send_url = "https://sms.aliyun.com/...."
    
    def send_verification_code(self, code, phone):
        data = {
            "apikey": self.api_key,
            "mobile": phone,
            "text": "[签名]您的验证码是{code},请不要告诉他人".format(code=code)
        }
        
        response = requests.post(self.send_url, data=data)
        r_dict = json.loads(response.text)
        print(r_dict)
        return r_dict
if __name__ == "__main__":
    send_sms = SendSMS("dhw2nbfh35j43rbd35irdfj")
    send_sms.send_verification_code("1234", "13928374837")users/models.py
保存短信验证码的数据表
class VerifyCode(models.Model):
    """
    短信验证码表
    """
    code = models.CharField(max_length=10, verbose_name="验证码")
    phone = models.CharField(max_length=11, verbose_name="电话")
    add_time = models.DateTimeField(default=datetime.now, verbose_name="添加时间")users/serializers.py
验证码表的序列化器
import re
from datetime import datetime, timedelta
from django.contrib.auth import get_user_model
from .models import VerifyCode
import random
UserModel = get_user_model()
# 短信验证码表的序列化器
class SmsSerializer(serializers.Serializer):
    phone = serializers.CharField(max_length=11)
    
    # 验证手机号是否可发送验证码
    def validate_phone(self, phone):
        # 手机是否注册
        if UserModel.objects.filter(phone=phone).count():
            raise serializers.ValidationError("用户已经存在")
        # 验证手机号是否合法
        if not re.match("^1[358]\{9}$ | ^147\d{8}$ | ^176\d{8}$", phone):
            raise serializers.ValidationError("非法手机号")
        # 验证发送频率
        before_datetime = datetime.now() - timedelta(hours=0, minutes=1, seconds=0)
        if VerifyCode.objects.filter(add_time__gt = before_datetime, phone=phone):
            raise serializers.ValidationError("请间隔1分钟再次发送")
            
        return phone
    
    # 生成随机验证码
    def generate_code(self):
        seeds = "1234567890"
        random_str = []
        for i in range(4):
            random_str.append(choice(seeds))
        return "".join(random_str)
# 用户表的序列化器
class UserSerializer(serializers.ModelSerizlier):
    code = serializers.CharField(max_length=4, min_length=4, required=True)
    
    def validate_code(self, code):
        verify_records = VerifyCode.object.filter(phone=self.inital_data['username']).order_by('-add_time')
        before_datetime = datetime.now() - timedelta(hours=0, minutes=1, seconds=0)
        # ...
        # 这里是识别验证码是否在数据库中存在和有效,代码没写完,太多了
        
    class Meta:
        model = UserModel
        fields = ('username', 'code', 'phone')users/views.py
发送短信的视图集
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth import get_user_model
from django.db.models import Q
from rest_framework.mixins import CreateModelMixin
from rest_framework import viewsets
from .serializers import SmsSerializer 
from utils.send_sms import SendSMS
UserModel = get_user_model()
class CustomBackend(ModelBackend):
    def authenticate(self, username=None, password=None, **kwargs):
        try:
            user = UserModel.objects.get(Q(username=username)|Q(phone=username))
            if user.check_password(password):
                return user
        except Exception as e:
            return None
# 发送短信验证码
class SmsCodeViewSet(viewsets.GenericViewSet ,CreateModelMixin):
    serializer_class = SmsSerializer
    
    # 拷贝CreateModelMixin方法,重写
    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)  # 有异常时返回400
        phone = serializer.validated_data['phone']
        
        send_sms = SendSMS("sdgso3inas030sa")
        sms_status = send_sms.send_sms(code=self.generate_code(), phone=phone)
        if sms_status['code'] != 0:
            return Response({ "phone": sms_status['msg'] }, status=status.HTTP_400_BAD_REQUEST)
        else:
            code_record = VerifyCode(code=code, phone=phone)
            code_record.save()  # 发送成功再保存验证码状态
            return Response({ "phone": phone }, status=status.HTTP_201_CREATED)settings.py
配置自定义验证
# 在settings中配置
AUTHENTICATION_BACKENDS = (
    'users.views.CustomBackend',
)urls.py
添加url路由
router.register(r'codes', SmsCodeViewSet, basename='codes')对象级别的权限
persons/permissions.py
from rest_framework import permissions
class IsOwnerOrReadOnly(permissions.BasePermission):
    """
    自定义权限,只允许对象的所有者编辑它,确保只有创建该数据的用户才能更新或删除它
    """
    def has_object_permission(self, request, view, obj):
        # 读取权限允许任何请求,所以我们总是允许GET,HEAD或OPTIONS请求。
        if request.method in permissions.SAFE_METHODS:
            return True
        # 只有该person的所有者才允许写权限。
        return obj.owner == request.user