login.vue 中查看登录的具体逻辑
login({
username:this.userName, //当前页码
password:this.parseWord
}).then((response)=> {
console.log(response);
//本地存储用户信息
cookie.setCookie('name',this.userName,7);
cookie.setCookie('token',response.data.token,7)
//存储在store
// 更新store数据
that.$store.dispatch('setInfo');
//跳转到首页页面
this.$router.push({ name: 'index'})
})
获取到当前的用户名和密码 这个用户名和密码来自当前的data()中
本地存储设置了cookie的名字和值,token和值。并设置了7天过期
我们的jwt 调用的是django自带的auth与userProfile中数据进行对比。而我们如果使用手机注册,就会导致验证失败。因为默认是用用户名和密码去查的。
1、首先在setting中设置变量:
# 设置邮箱和用户名和手机号均可登录
AUTHENTICATION_BACKENDS = (
'users.views.CustomBackend',
)
2、在 user/view 中添加 CustomBackend 类
class CustomBackend(ModelBackend):
"""
自定义用户验证规则
"""
def authenticate(self, username=None, password=None, **kwargs):
try:
# 不希望用户存在两个, get只能有一个
user = User.objects.get(Q(username=username)|Q(mobile=username))
# django的后台中密码加密的,所以不能password==password
# UserProfile继承的AbstractUser中有
# def check_password(self, raw_password)
if user.check_password(password):
return user
except Exception as e:
return None
# jwt相关的设置
import datetime
JWT_AUTH = {
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1),
'JWT_AUTH_HEADER_PREFIX': 'JWT',
}
注册会用到一些高级的 Serializer
写一个接口:发送短信
1、云片网
一个账号有多个子账号,每一个子账号都会有一个 api key
发送短信验证码前会验证这个 api key 是否正确
发送国内短信申请签名,短信模板
新建签名(需要审核),新建模板
国内短信api文档: https://www.yunpian.com/doc/zh_CN/domestic/list.html
utils/yunpian.py
线上部署时一定要将自己服务器的ip加入ip白名单中。测试时搜索本机ip地址。
import requests
import json
class YunPian(object):
def __init__(self, api_key):
self.api_key = api_key
self.single_send_url = "https://sms.yunpian.com/v2/sms/single_send.json"
def send_sms(self, code, mobile):
params = {
"apikey": self.api_key,
"mobile": mobile,
"text": "【云片网】您的验证码是{code}".format(code=code)
}
response = requests.post(self.single_send_url, data=params)
re_dict = json.loads(response.text)
# print(re_dict)
return re_dict
# if __name__ == "__main__":
# yun_pian = YunPian("****02cd03158****bed50****") #api key的值
# yun_pian.send_sms("2018", "157****4045") #手机号码
注意text内容必须要与后台已申请过签名并审核通过的模板保持一致,
我这里是测试阶段,没有审核,可以使用云片网的默认验证码格式进行测试。
# 发送验证码是创建model中一条记录的操作
from rest_framework.mixins import CreateModelMixin
用户传过来的手机号码我们要进行两次验证:
1、是否有效
2、是否被注册过
这个验证我们把它放到 Serializer 里面来做
为什么不像 goods 中一样使用 serializers.ModelSerializer
因为我们 model 中的 code 也是必填项,而我们拥有的只有手机号,所以会导致验证失败
setting.py中
# 手机号码正则表达式
REGEX_MOBILE = "^1[358]\d{9}$|^147\d{8}$|^176\d{8}$"
users/serializers.py
class SmsSerializer(serializers.Serializer):
mobile = serializers.CharField(max_length=11)
def validate_mobile(self, mobile):
"""
验证手机号码
"""
# 手机是否注册,存在一条即存在
if User.objects.filter(mobile=mobile).count():
raise serializers.ValidationError("用户已经存在")
# 验证手机是否合法
if not re.match(REGEX_MOBILE, mobile):
raise serializers.ValidationError("手机号非法")
# 验证码发送频率, 这里设置为60s
one_minutes_ago = datetime.now() - timedelta(hours=0, minutes=1, seconds=0)
# 验证码频率的时间判断, add_time的时间大于60s
if VerifyCode.objects.filter(add_time__gt=one_minutes_ago, mobile=mobile).count():
raise serializers.ValidationError("距离上一次发送未超过60s")
return mobile
然后 views 中重写 CreateModelMixin 中的 create 方法
class SmsCodeViewSet(CreateModelMixin, viewsets.GenericViewSet):
"""
发送短信验证码
"""
serializer_class = SmsSerializer
def generate_code(self):
"""
生成四位数字的验证码
"""
seeds = "1234567890"
random_str = []
for i in range(4):
random_str.append(choice(seeds))
return "".join(random_str)
def create(self, request, *args, **kwargs):
"""
重写CreateModelMixin的create方法
"""
# serializer这两个配置直接使用CreateModelMixin的create()的
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
mobile = serializer.validated_data["mobile"]
yun_pian = YunPian(API_KEY)
code = self.generate_code()
sms_status = yun_pian.send_sms(code=code, mobile=mobile)
if sms_status["code"] != 0:
return Response({
"mobile": sms_status["msg"]
}, status = status.HTTP_400_BAD_REQUEST)
else:
code_record = VerifyCode(code=code, mobile=mobile)
code_record.save()
return Response({
"mobile": mobile
}, status = status.HTTP_201_CREATED)
serializer.is_valid(raise_exception=True) 有效性验证失败会直接抛异常。
被 drf 捕捉到返回400状态码。
其中的APIKEY需要我们添加到setting.py中
# 云片网设置
APIKEY = 'apikey值'
调试之前配置好对应的 url
from users.views import SmsCodeViewset
# 配置验证码的路由
router.register(r'code', SmsCodeViewSet, base_name="code")
地址:http://127.0.0.1:8000/code/
注册页面需要我们输入手机号码,验证码,密码
Django 的form 和 model form 是用来验证用户提交的字段的合法性
restful API 中实际是对资源的操作,注册对应的资源就是用户
因为用户注册必定是会create model 操作的,所以继承 CreateModelMixin
先为 viewset 准备一个配套的 Serializers
users/serializers.py(继承modelSerializer,因为字段都是必填项,都有的,虽然相比较用户model多了一个code字段)
通过一些小技巧,既能享受model Serializer带来的好处,又能突破它的限制
class UserRegSerializer(serializers.ModelSerializer):
"""
用户注册的序列类
"""
code = serializers.CharField(required=True, max_length=4, min_length=4,
error_messages={
"blank": "请输入验证码",
"required": "请输入验证码:",
"max_length": "验证码格式错误",
"min_length": "验证码格式错误"
},
label="验证码",
write_only=True,
help_text="验证码")
验证 username 是否存在
username = serializers.CharField(required=True, allow_blank=False, label="手机号码",
validators=[UniqueValidator(queryset=User.objects.all(), message="用户已经存在")])
password = serializers.CharField(
style={'input_type': 'password'}, label="密码", write_only=True,
)
在Serializer中添加code字段,这个code是多余字段不会被保存到数据库中
def validate_code(self, code):
# 验证码在数据库中是否存在,用户从前端post过来的值都会放入initial_data里面,排序(最新一条)。
verify_records = VerifyCode.objects.filter(mobile=self.initial_data["username"]).order_by("-add_time")
if verify_records:
# 获取到最新一条
last_record = verify_records[0]
# 有效期为五分钟。
five_mintes_ago = datetime.now() - timedelta(hours=0, minutes=5, seconds=0)
if five_mintes_ago > last_record.add_time:
raise serializers.ValidationError("验证码过期")
if last_record.code != code:
raise serializers.ValidationError("验证码错误")
else:
raise serializers.ValidationError("验证码错误")
验证完之后将code这个字段删除掉
def validate(self, attrs):
# 不加字段名的验证器作用于所有字段之上。attrs是字段 validate之后返回的总的dict
attrs["mobile"] = attrs["username"]
del attrs["code"]
return attrs
views中的用户viewset中实例化Serializer
class UserViewset(CreateModelMixin, viewsets.GenericViewSet):
"""
用户
"""
serializer_class = UserRegSerializer
配置url
# 配置用户登录路由
router.register(r'users', UserViewSet, base_name="users")
用户注册逻辑编码。
后台添加一条用户短信验证码数据之后进行验证。
参考:官方文档
http://www.django-rest-framework.org/api-guide/fields/
我们 Serializer 配置的
fields = ("username", "code", "mobile", "password")
在 viewset 处理验证过程中已经删除了其中的code。
解决方案(重要参数):
code 字段添加 write only=true
。就不会将此字段进行序列化返回给前端。
如果是一个正常的Serializer会将我们post过去的数据序列化之后返回回来。
password也被返回了回来。这是不合理的,为password也添加write only =True参数
密码会被明文存储。
def create(self, validated_data):
user = super(UserRegSerializer, self).create(validated_data=validated_data)
user.set_password(validated_data["password"])
user.save()
return user
重载Serializer的create方法。可以实现
虽然重载代码量已经很少了,但是可能比较难理解,所以我们选择其他解决方案
我们的model对象进行操作的时候,会发出全局的信号。捕捉之后做出我们自己的操作。
参考文档
https://docs.djangoproject.com/en/2.0/ref/signals/
django的信号量如request_started和scrapy中
删除之前做一些事情,就可以接收pre_delete的信号
http://www.django-rest-framework.org/api-guide/authentication/
新建文件users/signals.py
# coding:utf-8
# author: Evan
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()
"""
参数一接收哪种方式的信号,参数二接受哪个model的信号
"""
@receiver(post_save, sender=User)
def create_user(sender, instance=None, created=False, **kwargs):
"""
django信号量的简单应用:遵循原来加密方式对密码加密
"""
""" 新建,update的时候也会进行post_save """
if created:
password = instance.password
instance.set_password(password)
instance.save()
做完刚才这些操作,还要重载一个配置。否则会导致虽然没有任何报错信息,但是密码并没有被加密
apps.py中
class UsersConfig(AppConfig):
name = 'users'
verbose_name = '用户管理'
def ready(self):
# 必须给django信号量配置就绪函数
import users.signals
我们修改的mobile的字段可为空,只在代码中修改是没有用的,还需要我们进行migrations
这样我们才可以在后台中不需要mobile字段直接添加用户。
如果是让用户自己去登录,那么就将cookie.setcookie这两行注释掉。直接让它跳转到首页。
但是如果是自动登录,那么我们此时就没有给前台返回jwt 的token。
重载createmodelmixin里面的create函数。
框架原本实现代码:
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()
这里的perform中的save是save了当前的model(user)。但是并没有返回该model。我们要想获取到user model 就必须重写让它返回model
然后在执行perform_create之后插入我们自己的逻辑。
分析 jwt 的源码实现,找到它哪部分才是生成token的。
obtain_jwt_token = ObtainJSONWebToken.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
此时我们就可以去查看继承的父类: JSONWebTokenAPIView
该类中用户在post数据过来之后。
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
if serializer.is_valid():
user = serializer.object.get('user') or request.user
token = serializer.object.get('token')
response_data = jwt_response_payload_handler(token, user, request)
response = Response(response_data)
if api_settings.JWT_AUTH_COOKIE:
expiration = (datetime.utcnow() +
api_settings.JWT_EXPIRATION_DELTA)
response.set_cookie(api_settings.JWT_AUTH_COOKIE,
token,
expires=expiration,
httponly=True)
return response
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
关键在于token是直接从Serializer中获取的,那么token的生成应该是在Serializer中实现的。
rest_framework_jwt/settings.py
DEFAULTS = {
'JWT_ENCODE_HANDLER':
'rest_framework_jwt.utils.jwt_encode_handler',
'JWT_PAYLOAD_HANDLER':
'rest_framework_jwt.utils.jwt_payload_handler',
}
所以我们已经找到了生成token的两个重要步骤,一payload,二encode
实现代码:
from rest_framework_jwt.serializers import jwt_payload_handler, jwt_encode_handler
re_dict = serializer.data
payload = jwt_payload_handler(user)
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(re_dict, status=status.HTTP_201_CREATED, headers=headers)
其中的token要和前端保持一致。注意将原本返回的Serializer.data进行加工之后返回。
退出功能就变得更好做了,因为jwt的token并不是保存在服务器端的。
src/views/head/shophead.vue
<a @click="loginOut">退出a>
退出按钮调用的是loginout函数
loginOut(){
cookie.delCookie('token');
cookie.delCookie('name');
//重新触发store
//更新store数据
this.$store.dispatch('setInfo');
//跳转到登录
this.$router.push({name: 'login'})
},
清空token给axios发一个通知。跳转到登录页面。
注册页面测试,将code与前端保持一致,以及修改localhost
UserViewSet类如下:
class UserViewSet(CreateModelMixin, mixins.UpdateModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
"""
用户
"""
serializer_class = UserRegSerializer
queryset = User.objects.all()
# 登录验证
authentication_classes = (JSONWebTokenAuthentication, SessionAuthentication)
# 设置动态序列化
def get_serializer_class(self):
if self.action == "retrieve":
return UserDetailSerializer
elif self.action == "create":
return UserRegSerializer
return UserDetailSerializer
# 设置动态权限配置, 用户注册无需判断登录状态
def get_permissions(self):
if self.action == "retrieve":
return [permissions.IsAuthenticated()]
elif self.action == "create":
return []
return []
def create(self, request, *args, **kwargs):
# 重写CreateModelMixin的create()方法
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)
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(re_dict, status=status.HTTP_201_CREATED, headers=headers)
# 用于返回当前用户
def get_object(self):
return self.request.user
def perform_create(self, serializer):
serializer.save()