10- vue django restful framework
Vue+Django REST framework实战
搭建一个前后端分离的生鲜超市网站
Django rtf 完成 手机注册和用户登录(中)
Json Web Token的原理
因为我们的drf 的token auth有它的缺点。所以最常用的还是JWT的方式
以下内容转载于: http://lion1ou.win/2017/01/18/
http是一种无状态的协议,前后两次请求它会不知道这是从同一个人还是不同的人发的。
传统方式: 采用session和cookie结合的方式
前后端分离的传统: 用户信息生成token token 和对于的用户id保存到数据库或session中。我们的drf 的 token auth 就是这种。
接着把token传给用户,存入浏览器cookie。之后的请求带上这个cookie,后端根据这个cookie值查询用户,验证过期的逻辑需要表里多一个字段,以及后端的逻辑验证。
问题: xss漏洞: cookie可以被js读取。作为后端识别用户的标识,cookie的泄露意味着用户信息不再安全。特别是drf我们的token auth没有过期时间
设置cookie时两个更安全的选项: httpOnly以及secure项.
- httponly的不能被js读取,浏览器会自动加在请求header中
- secure就只通过https
httponly 问题。很容易被xsrf攻击,因为cookie会默认发出去。
如果将验证信息保存数据库。每次都要查询。保存session,加大了服务器端存储压力。
那我们可以不要服务器去查询呢?
只要我们生成的token遵循一定的规律,比如使用对称加密算法来加密id 形成token。
服务端只需要解密token 就能知道id。
- 这样做的问题在于如果他知道你是怎么加密的。那么它可以通过这种加密方式伪造token
此时我们使用非对称加密算法。
对称加密,加密和解密使用的是同一个密钥。服务器把token传给用户,以及用户拿着token来服务器进行解密。加密解密都在服务器端。
JWT 是一个开放标准(RFC 7519),它定义了一种用于简洁,自包含的用于通信双方之间以 JSON 对象的形式安全传递信息的方法。JWT 可以使用 HMAC 算法或者是 RSA 的公钥密钥对进行签名。它具备两个特点
- 简洁(Compact)
可以通过URL, POST 参数或者在 HTTP header 发送,因为数据量小,传输速度快
- 自包含(Self-contained)
负载中包含了所有用户所需要的信息,避免了多次查询数据库
mark头部包含两部分,token和加密算法。会进行Base64加密。但是会很容易反解出来
- Header 头部
头部包含了两部分,token 类型和采用的加密算法
{
"alg": "HS256",
"typ": "JWT"
}
它会使用 Base64 编码组成 JWT 结构的第一部分。
- Payload 负载
这部分就是我们存放信息的地方了,你可以把用户 ID 等信息放在这里,JWT 规范里面对这部分有进行了比较详细的介绍,常用的由 iss(签发者),exp(过期时间),sub(面向的用户),aud(接收方),iat(签发时间)。
{
"iss": "lion1ou JWT",
"iat": 1441593502,
"exp": 1441594722,
"aud": "www.example.com",
"sub": "lion1ou@163.com"
}
同样的,它会使用 Base64 编码组成 JWT 结构的第二部分
- Signature 签名
前面两部分都是使用 Base64 进行编码的,即前端可以解开知道里面的信息。Signature 需要使用编码后的 header 和 payload 以及我们提供的一个密钥,然后使用 header 中指定的签名算法(HS256)进行签名。签名的作用是保证 JWT 没有被篡改过。
可以解开就是指前端可以通过token拿到用户的一部分非敏感信息。
JWT不会再保存数据了。a关注b,发邮件给b。b直接点击带回来串和action。就不需要登录了。
设计用户认证和授权系统(独立),以及单点登录。
mark单点登录,多个子域名通过一个统一的授权认证接口进行登录。
https://github.com/GetBlimp/django-rest-framework-jwt
首先要安装
pip install djangorestframework-jwt
使用:
需要将jsonWebAuth加入到drf 的default auth class中
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
对于用户post过来的token进行验证,将user取出来
- path中的配置
from rest_framework_jwt.views import obtain_jwt_token
#...
urlpatterns = [
'',
# ...
url(r'^api-token-auth/', obtain_jwt_token),
]
配置path中的jwt
# jwt的token认证
path('jwt-auth/', obtain_jwt_token )
mark
mark
curl -H "Authorization: JWT <your_token>" http://localhost:8000/protected-url/
mark
jwt的加密认证方式可以参照jwt的源码进行学习。
vue和jwt接口调试
- vue中登录的逻辑
//登录
export const login = params => {
return axios.post(`${host}/login/`, params)
}
vue中的登录post到login接口
要么改前端,要么改后端,毕竟我们现在在写后端。改后端url为login
# jwt的token认证
path('login/', obtain_jwt_token )
前往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()中
data中的值又通过v-model进行了与输入框中值的绑定(我猜的啊)
本地存储设置了cookie的名字和值,token和值。并设置了7天过期
mark可以看到我们的vue数据中已经有了name 和 token
setInfo会进行实时数据同步更新的操作
[types.SET_INFO] (state) {
state.userInfo = {
name:cookie.getCookie('name'),
token:cookie.getCookie('token')
}
console.log(state.userInfo);
},
我们的jwt 调用的是django自带的auth与userProfile中数据进行对比。而我们如果使用手机注册,就会导致验证失败。因为默认是用用户名和密码去查的。
自定义django用户认证函数
- 首先在setting中设置变量:
# 设置邮箱和用户名和手机号均可登录
AUTHENTICATION_BACKENDS = (
'users.views.CustomBackend',
)
- 在user/view中
class CustomBackend(ModelBackend):
"""
自定义用户验证规则
"""
def authenticate(self, username=None, password=None, **kwargs):
try:
# 不希望用户存在两个,get只能有一个。两个是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
mark
通过断点测试可以成功的进入了我们的这段逻辑。
JWT的过期时间设置
# 与drf的jwt相关的设置
JWT_AUTH = {
'JWT_EXPIRATION_DELTA': datetime.timedelta(seconds=20),
'JWT_AUTH_HEADER_PREFIX': 'JWT',
}
这里的header前缀我们要保持与前端的一致。
src/axios/index.js:
config.headers.Authorization = `Bearer ${store.state.userInfo.token}`;
云片网发送短信验证码
注册会用到一些高级的Serializer。
写一个接口:发送短信
form的验证。
- 云片网
可以有很多子账号,每个子账号都会有一个api key 这个api key就会很重要。
发送短信验证码这个key是必须要用到的。
文本短信 & 语音短信
发送国内短信申请签名。短信模板。
- 新建签名(审核), 新建模板(可包含变量,审核)
api文档中使用说明。
国内短信api文档: https://www.yunpian.com/doc/zh_CN/domestic/list.html
单条发送,批量发送(相同内容,不同内容)
https://www.yunpian.com/doc/zh_CN/domestic/single_send.html
参数。必填字段填过来。示例代码
utils下yunpian.py
线上部署时一定要将自己服务器的ip加入ip白名单中。测试时搜索本机ip地址。
# encoding: utf-8
__author__ = 'mtianyan'
__date__ = '2018/3/8 0008 09:28'
import json
import requests
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):
parmas = {
"apikey": self.api_key,
"mobile": mobile,
"text": "【慕学生鲜】您的验证码是{code}。如非本人操作,请忽略本短信".format(code=code)
}
response = requests.post(self.single_send_url, data=parmas)
re_dict = json.loads(response.text)
return re_dict
if __name__ == "__main__":
yun_pian = YunPian("apikey的值")
yun_pian.send_sms("2017", "手机号码")
注意text内容必须要与后台已申请过签名并审核通过的模板保持一致
drf实现发送短信验证码接口
# 发送验证码是创建model中一条记录的操作
from rest_framework.mixins import CreateModelMixin
用户传过来的手机号码我们要进行两次验证:
- 是否有效
- 有没有被注册过
Serializer和django里的form modelform 是一样的,所以这个验证我们把它放到我们的Serializer里面来做。
- 为什么不像goods中一样使用
serializers.ModelSerializer
因为我们model中的code也是必填项,而我们拥有的只有手机号,所以会导致验证失败
setting.py中
# 手机号码正则表达式
REGEX_MOBILE = "^1[358]\d{9}$|^147\d{8}$|^176\d{8}$"
users/serializers.py:
# encoding: utf-8
__author__ = 'mtianyan'
__date__ = '2018/3/8 0008 09:41'
import re
from datetime import datetime, timedelta
from VueDjangoFrameWorkShop.settings import REGEX_MOBILE
from users.models import VerifyCode
from rest_framework import serializers
from django.contrib.auth import get_user_model
User = get_user_model()
class SmsSerializer(serializers.Serializer):
mobile = serializers.CharField(max_length=11)
def validate_mobile(self, mobile):
"""
验证手机号码(函数名称必须为validate_ + 字段名)
"""
# 手机是否注册
if User.objects.filter(mobile=mobile).count():
raise serializers.ValidationError("用户已经存在")
# 验证手机号码是否合法
if not re.match(REGEX_MOBILE, mobile):
raise serializers.ValidationError("手机号码非法")
# 验证码发送频率
one_mintes_ago = datetime.now() - timedelta(hours=0, minutes=1, seconds=0)
# 添加时间大于一分钟以前。也就是距离现在还不足一分钟
if VerifyCode.objects.filter(add_time__gt=one_mintes_ago, mobile=mobile).count():
raise serializers.ValidationError("距离上一次发送未超过60s")
return mobile
然后views中class SmsCodeViewset(CreateModelMixin, viewsets.GenericViewSet):
重写CreateModelMixin中的create方法
原本的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)
serializer.is_valid(raise_exception=True)
有效性验证失败会直接抛异常。
被drf捕捉到返回400状态码。
其中的APIKEY需要我们添加到setting.py中
# 云片网设置
APIKEY = 'apikey值'
生成四位数的验证码值
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):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
mobile = serializer.validated_data["mobile"]
yun_pian = YunPian(APIKEY)
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)
mark
mark
将返回的json在yunpian中loads成dict
然后取出dict中的code和msg进行判断与返回。我们不需要向前端返回status。而是遵循restful api的规范。http状态码即可区分成功或失败。消息并不代表。
发送成功之后再保存验证码
调试是否正确
调试之前配置好对应的url
from users.views import SmsCodeViewset
# 配置codes的url
router.register(r'codes', SmsCodeViewset, base_name="codes")
mark
mark
返回中既设置了400的http code 又有和form类似的字段信息错误
字段名称 : 数组(告诉你该字段的错误)
发送成功(201)
mark