DRF 商城项目-用户相关业务逻辑梳理
1.用户登录2.用户注册用户注册基于 手机注册发送验证码基于 榛子云网相关链接:榛子云网址示例接口文档验证码逻辑验证码APIfrom users.views import SmsViewSetfrom rest_framework.routers import DefaultRouter# Create a router and register our viewse...
1.用户登录
自定义登录验证逻辑
本项目中用户登录既可以是用户名,也可以是手机号,所以需要重写登录验证逻辑
class CustomBackend(ModelBackend):
"""
自定义用户验证
"""
def authenticate(self, username=None, password=None, **kwargs):
try:
user = User.objects.get(Q(username=username)|Q(mobile=username))
if user.check_password(password):
return user
except Exception as e:
return None
登录API
# 配置jwt路由
from rest_framework_jwt.views import obtain_jwt_token
urlpatterns = [
# jwt的认证接口
path('login/', obtain_jwt_token),
]
登录逻辑
利用jwt生成token实现登录,返回生成的token。
2.用户注册
用户注册基于 手机注册
发送验证码基于 榛子云网
相关链接:
验证码逻辑
验证码API
from users.views import SmsViewSet
from rest_framework.routers import DefaultRouter
# Create a router and register our viewsets with it.
router = DefaultRouter()
#配置手机短信验证码url
router.register(r'codes', SmsViewSet, base_name="codes")
验证码序列化组件
选取序列化方式的时候以为不是全部的字段都需要用上, 因此不需用到 ModelSerializer
mobile 字段相关验证:
- 是否注册
- 是否合法
- 频率限制
import re
import datetime
from django.contrib.auth import get_user_model
from rest_framework import serializers
from MxShop.settings import REGEX_MOBILE
from .models import VerifyCode
User = get_user_model()
# 为啥不用serializers.ModelSerializer,因为短信验证码模型有code和mobile两个字段
# 而注册的时候只有手机号,用ModelSerializer就会导致验证失败
class SmsSerializer(serializers.Serializer):
"""
短信验证码验证
"""
mobile = serializers.CharField(max_length=11)
def validated_mobile(self, mobile):
"""
验证手机号码,函数名规则为:validated_字段名()
:return:
"""
# 验证手机号是否已经注册
if User.objects.filter(mobile=mobile).count():
raise serializers.ValidationError("用户已经存在")
# 验证手机号是否合法
if not re.match(REGEX_MOBILE, mobile):
raise serializers.ValidationError("手机号码非法")
# 验证手机号是否为一分钟前验证过了(防止恶意注册)
one_min_ago = datetime.datetime.now() - datetime.timedelta(minutes=1)
if VerifyCode.objects.filter(add_time__gt=one_min_ago, mobile=mobile).count():
raise serializers.ValidationError("距离上一次发送未超过60s")
return mobile
验证码视图逻辑处理
视图主要处理 验证码生成发送相关逻辑
具体的榛子云网接口对接处理详情官网查阅
from random import choice
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
from django.db.models import Q
from django.shortcuts import render
from rest_framework.mixins import CreateModelMixin
from rest_framework import viewsets
from rest_framework.response import Response
from rest_framework import status
from .serializer import SmsSerializer
from MxShop.settings import APIKEY, AppId, AppSecret
from utils.zhenzi import ZhenziSmsClient
from .models import VerifyCode
class SmsViewSet(CreateModelMixin, viewsets.GenericViewSet):
"""
发送短信验证码
"""
serializer_class = SmsSerializer
def random_generate_code(self):
"""
随机生成四位数验证啊
:return:
"""
seeds = '1234567890'
random_str = []
for i in range(4):
random_str.append(choice(seeds))
code = ''.join(random_str)
return code
def create(self, request, *args, **kwargs):
"""
重写create方法
:param request:
:param args:
:param kwargs:
:return:
"""
serializer = self.get_serializer(data=request.data)
# raise_exception=True代表验证失败报异常
serializer.is_valid(raise_exception=True)
# 取出手机号(验证成功才会执行)
mobile = serializer.validated_data['mobile']
# # 随机生成四位数验证码
code = self.random_generate_code()
zhen_zi = ZhenziSmsClient("https://sms_developer.zhenzikj.com/", AppId, AppSecret)
send_status = zhen_zi.send(message="您的验证码是: {}。如非本人操作,请忽略本短信".format(code), number=mobile)
send_status = eval(send_status)
if send_status['code'] != 0:
# 发送失败
return Response({
'mobile': send_status['data']
}, status=status.HTTP_400_BAD_REQUEST )
else:
# 发送成功
# 只有在发送成功时才能保存数据
verify = VerifyCode(mobile=mobile, code=code)
verify.save()
return Response({
'mobile': send_status['data']
}, status=status.HTTP_201_CREATED)
榛子云验证码工具
# -*- coding: utf-8 -*-
__author__ = 'kevin'
__date__ = '2019/6/24 11:10'
import urllib.request
import urllib.parse
import ssl
class ZhenziSmsClient(object):
def __init__(self, apiUrl, appId, appSecret):
self.apiUrl = apiUrl
self.appId = appId
self.appSecret = appSecret
def send(self, number, message, messageId='', ):
data = {
'appId': self.appId,
'appSecret': self.appSecret,
'number': number,
'messageId': messageId,
'message': message
}
data = urllib.parse.urlencode(data).encode('utf-8')
ssl._create_default_https_context = ssl._create_unverified_context
req = urllib.request.Request(self.apiUrl + '/sms/send.do', data=data)
res_data = urllib.request.urlopen(req)
res = res_data.read()
res = res.decode('utf-8')
return res
榛子云相关配置
# 榛子云网配置
AppId = 我的应用的AppId
AppSecret = '我的应用的AppSecret '
注册逻辑
用户注册API
# 配置用户注册url
router.register(r'register', UserViewSet, base_name="users")
注册序列化组件
选取序列化方式的时候以为是全部的字段都需要用上, 所以要用到 ModelSerializer
前端传过来的所有的数据都在, initial_data 字典里面
验证个别字段方法名必须是:validate_ + 字段名
code字段相关验证:
- 是否存在
- 是否过期
- 是否和数据库中相同
为啥使用filter方法查询验证码,而不用get方法呢?
- 如果使用 get 方式需要处理两个异常, 分别是查找到多个信息的情况以及查询到0信息的情况的异常
- 但是使用 filter 方式查到多个就以列表方式返回, 如果查询不到数据就会返回空值, 各方面都很方便
字段属性说明:
- write_only - 只读字段包含在API输出中,但在创建或更新操作期间不应包含在输入中。 任何错误包含在序列化程序输入中的’read_only’字段都将被忽略。将此属性设置为True可确保在序列化表示时使用该字段,但在反序列化期间创建或更新实例时不使用该字段。
- required - 必填
- max_length - 最大长度
- min_length - 最小长度
- error_messages - 错误类型映射提示(blank-空字段提示 required-必填字段提示 max_length-超长度提示 min_length-过短提示)
- style - 更改输入标签显示类型
- validators - 可以指明一些默认的约束类
- help_text - 帮助提示信息
- label - 显示名字
- allow_blank - 将此参数设置为True将意味着序列化输出的默认值为null,但并不意味着输入反序列化的默认值
class UserRegisterSerializer(serializers.ModelSerializer):
"""
用户注册
"""
# write_only=True: code设置这个参数,防止返回时序列化报错->返回时该属性已经被删除
code = serializers.CharField(required=True, write_only=True, max_length=4, min_length=4, error_messages={
'max_length': '验证码格式错误',
'min_length': '验证码格式错误',
'required': '请输入验证码',
'blank': '请输入验证码'
}, help_text='手机验证码')
# validators 可以指明一些默认的约束类, 此处的 UniqueValidator 表示唯一约束限制不能重名
username = serializers.CharField(label="用户名", help_text="用户名", required=True, allow_blank=False,
validators=[UniqueValidator(queryset=User.objects.all(), message="用户已经存在")])
# style 可以设置为密文状态
password = serializers.CharField(
style={'input_type': 'password'}, help_text="密码", label="密码", write_only=True,
)
# 用户表中的 password 是需要加密后再保存的, 次数需要重写一次 create 方法
# 当然也可以不这样做, 这里的操作利用 django 的信号来处理, 详情见 signals.py
# def create(self, validated_data):
# user = super(UserRegSerializer, self).create(validated_data=validated_data)
# user.set_password(validated_data["password"])
# user.save()
# return user
# 对验证码的验证处理
# validate_ + 字段对个别字段进行单一处理
def validated_code(self, code):
# 如果使用 get 方式需要处理两个异常, 分别是查找到多个信息的情况以及查询到0信息的情况的异常
# 但是使用 filter 方式查到多个就以列表方式返回, 如果查询不到数据就会返回空值, 各方面都很方便
# try:
# verify_records = VerifyCode.objects.get(mobile=self.initial_data["username"], code=code)
# except VerifyCode.DoesNotExist as e:
# pass
# except VerifyCode.MultipleObjectsReturned as e:
# pass
# 前端传过来的所有的数据都在, initial_data 字典里面, 如果是验证通过的数据则保存在 validated_data 字典中
all_records = VerifyCode.objects.filter(mobile=self.initial_data['username']).order_by("-add_time")
if all_records:
# 时间倒叙排序后的的第一条就是最新的一条
last_record = all_records[0]
# 五分钟前的时间
five_mintes_ago = datetime.datetime.now() - datetime.timedelta(minutes=5)
# 验证码过期
if five_mintes_ago > last_record.add_time:
raise serializers.ValidationError('验证码过期')
# 验证码错误
if code != last_record.code:
raise serializers.ValidationError('验证码错误')
# return code # 没必要保存验证码记录, 仅仅是用作验证
else:
# 手机号没有
raise serializers.ValidationError('验证码错误')
# 对所有的字段进行限制
def validate(self, attrs):
# 重命名一下
attrs['mobile'] = attrs['username']
# 删除无用字段
del attrs['code']
return attrs
class Meta:
model = User
# username字段是系统中的字段
fields = ("username", "code", "mobile")
注册视图逻辑处理
视图主要处理 :
- 注册时用户的创建
- 重写 create 函数来完成注册后自动登录并跳转到主页面(原因:默认的create方法只能实现用户创建无法实现其他附加)
from rest_framework_jwt.serializers import jwt_payload_handler, jwt_encode_handler
class UserViewSet(CreateModelMixin, viewsets.GenericViewSet):
queryset = User.objects.all()
serializer_class = UserRegisterSerializer
默认的create方法
class CreateModelMixin(object):
"""
Create a model instance.
"""
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
def perform_create(self, serializer):
serializer.save()
def get_success_headers(self, data):
try:
return {'Location': str(data[api_settings.URL_FIELD_NAME])}
except (TypeError, KeyError):
return {}
如果想实现自动登录, 首先本质就是加入用户登录的状态, 即 token 的生成和保存
本次项目使用的是 JWT 作为 token 方案, 因此 需要考究在 JWT 的源码中 token 如何生成
密码加密
方法一 在Serializer类中重写create方法:
# apps/users/serializer.y
# 用户表中的 password 是需要加密后再保存的, 故需要重写一次 create 方法
# 当然也可以不这样做, 这里的操作利用 django 的信号来处理, 详情见 signals.py
def create(self, validated_data):
user = super(UserRegisterSerializer, self).create(validated_data=validated_data)
user.set_password(validated_data["password"])
user.save()
return user
方法二 利用django的信号量机制:
名称解释:
- created - 是否新建
- post_save - Django中的model对象保存后,自动触发
- sender - 指定User模型(指定要执行的模型)
- instance - 表示保存对象, 在这里是被保存的 user 对象
- set_password - 密码加密方法
# apps/users/signals.py
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth import get_user_model
User = get_user_model()
@receiver(post_save, sender=User)
def create_auth_token(sender, instance=None, created=False, **kwargs):
if created:
password = instance.password
instance.set_password(password)
instance.save()
# apps/users/apps.py
from django.apps import AppConfig
class UsersConfig(AppConfig):
name = 'users'
# 后台管理应用显示
verbose_name = '用户'
def ready(self):
import users.signals
JWT流程分析
JWT API
# 配置jwt路由
from rest_framework_jwt.views import obtain_jwt_token
urlpatterns = [
# jwt的认证接口
path('login/', obtain_jwt_token),
]
类视图
class RefreshJSONWebToken(JSONWebTokenAPIView):
"""
API View that returns a refreshed token (with new expiration) based on
existing token
If 'orig_iat' field (original issued-at-time) is found, will first check
if it's within expiration window, then copy it to the new token
"""
serializer_class = RefreshJSONWebTokenSerializer
obtain_jwt_token = ObtainJSONWebToken.as_view()
refresh_jwt_token = RefreshJSONWebToken.as_view()
verify_jwt_token = VerifyJSONWebToken.as_view()
class ObtainJSONWebToken(JSONWebTokenAPIView):
"""
API View that receives a POST with a user's username and password.
Returns a JSON Web Token that can be used for authenticated requests.
"""
serializer_class = JSONWebTokenSerializer
序列化组件实现token生成
class JSONWebTokenSerializer(Serializer):
"""
Serializer class used to validate a username and password.
'username' is identified by the custom UserModel.USERNAME_FIELD.
Returns a JSON Web Token that can be used to authenticate later calls.
"""
def __init__(self, *args, **kwargs):
"""
Dynamically add the USERNAME_FIELD to self.fields.
"""
super(JSONWebTokenSerializer, self).__init__(*args, **kwargs)
self.fields[self.username_field] = serializers.CharField()
self.fields['password'] = PasswordField(write_only=True)
@property
def username_field(self):
return get_username_field()
def validate(self, attrs):
credentials = {
self.username_field: attrs.get(self.username_field),
'password': attrs.get('password')
}
if all(credentials.values()):
user = authenticate(**credentials)
if user:
if not user.is_active:
msg = _('User account is disabled.')
raise serializers.ValidationError(msg)
payload = jwt_payload_handler(user)
return {
'token': jwt_encode_handler(payload),
'user': user
}
else:
msg = _('Unable to log in with provided credentials.')
raise serializers.ValidationError(msg)
else:
msg = _('Must include "{username_field}" and "password".')
msg = msg.format(username_field=self.username_field)
raise serializers.ValidationError(msg)
具体的源码逻辑,根据路由自行查找。
具体的token生成
if user:
if not user.is_active:
msg = _('User account is disabled.')
raise serializers.ValidationError(msg)
payload = jwt_payload_handler(user)
return {
'token': jwt_encode_handler(payload),
'user': user
}
生成token用到的方法:
- jwt_payload_handler
- jwt_encode_handler
具体实现
from rest_framework_jwt.serializers import jwt_payload_handler, jwt_encode_handler
class UserViewSet(CreateModelMixin, viewsets.GenericViewSet):
queryset = User.objects.all()
serializer_class = UserRegisterSerializer
# 重写 create 函数来完成注册后自动登录功能
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = self.perform_create(serializer)
re_dict = serializer.data
payload = jwt_payload_handler(user)
# 通过token 的添加只能用此方法
re_dict["token"] = jwt_encode_handler(payload)
# 自定义一个字段加入进去
re_dict["name"] = user.name if user.name else user.username
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
def perform_create(self, serializer):
return serializer.save()
3.用户退出
由于jwt不是保存在服务器端,而是保存在客服端,所以退出不需要再写一个 logout 接口 。只需要前端进行cookie的清空然后跳转到登录页面或主页面都可以,我这里是跳转到登录页面。
<script>
import { mapGetters } from 'vuex';
export default {
computed: {
...mapGetters({
userInfo:'userInfo'
})
},
methods: {
loginOut(){
cookie.delCookie('token');
cookie.delCookie('name');
//重新触发store
//更新store数据
this.$store.dispatch('setInfo');
//跳转到登录
this.$router.push({name: 'login'})
},
}
}
</script>
4.个人中心
个人资料的显示和修改
当我去获取用户详情信息的时候,我们就会发现我们不知道用户的id,这时候我们就可以通过重写get_object(控制retieve/delete)的方法返回当前用户。
def get_object(self):
"""
由于用户id未知,想要通过id获取用户详细信息不行
:return: 返回当前用户对象
"""
return self.request.user
当我们获取当前用户的时候,就出现另外一个问题,用户必须是登录的状态才可以,但是注册的时候用户又没有登录。所以我就可以通过动态的加载权限,具体如下:
- 用户注册时,无权限,所有人都可以
- 用户的获取、用户的修改时,IsAuthenticate权限
from rest_framework.permissions import IsAuthenticated
def get_permissions(self):
"""
动态加载权限
"""
# 只有是viewsets才有action
if self.action == "retrieve":
return [IsAuthenticated()]
elif self.action == "create":
return []
# 其它方法必须也要有权限
return []
加上登录验证
# JSONWebTokenAuthentication --> JWT登录验证
authentication_classes = (JSONWebTokenAuthentication, authentication.SessionAuthentication)
此时我们测试接口就会发现只显示两个字段的值,这是注册的序列化组件的值,所以我们还要实现动态加载序列化组件。
# users/serizalizer.py
class UserDetailSerializer(serializers.ModelSerializer):
class Meta:
model = User
# username字段是系统中的字段
fields = ("name", "gender", "birthday", "email", "mobile")
# users/views.py
def get_serializer_class(self):
"""
注册和用户同用一个视图,不同serializer
:return: 序列化组件
"""
if self.action == "retrieve":
return UserDetailSerializer
elif self.action == "create":
return UserRegisterSerializer
# 其它方法必须也要有权限
return UserDetailSerializer
修改个人信息,继承UpdateModelMixin即可
class UserViewSet(CreateModelMixin, mixins.UpdateModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
参考资料:
DRF 商城项目 - 用户( 登录, 注册,登出,个人中心 ) 逻辑梳理
相关链接:
Django中的信号及其用法
signals官方文档
内置信号文档
更多推荐
所有评论(0)