前言
- 前后端分离的项目不需要做csrf认证,app和网站服务端已经跨站
- drf有三种验证方式
- BasicAuthentication 通过账号密码进行基本认证,密码经过base64编码和sha256并加盐存入数据库
- TokenAuthentication 通过cookie设置的相关代码键为token或设置值,
- SessionAuthentication sessionId存在cookie,通过sessionId获取存在缓存或数据库的session值再通过相应算法解析出用户
DRFToken
1 | # settings |
- 当用户注册时我们调用接口为用户生成token并存入数据库,前端通过用户登录时调用obtain_auth_token的接口获取token放到header,后端再通过request.auth获取token进行验证。
1
2
3
4
5
6
7
8
9
10
11
12def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = self.perform_create(serializer)
# token和payload的生成
re_dict = serializer.data
payload = jwt_payload_handler(user)
print(request.auth)
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)
1 | from rest_framework.authtoken.models import Token |
1 | Authorization: Token xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx |
- 如果想使用不同的关键字如bearer即继承TokenAuthentication为子类设置keyword类变量
- 身份验证成功后,request.user为django user的实例,request.auth是一个rest_framework.authtoken.models.Token的实例
- drf token缺陷
- 保存在数据库中,分布式不友好
- token未设置过期时间,用户可以一直使用,如果被盗很糟糕
- 如果在rest_framework中配置token是全局性的,也可在单一view中配置
1
2
3
4
5
6
7
8
9from rest_framework.authentication import TokenAuthentication # 导入token验证相关模块
class GoodsListViewSet(mixins.ListModelMixin,viewsets.GenericViewSet):
"""
list:
商品列表页数据
"""
…
authentication_classes = (TokenAuthentication,) # 新增,部分页面token验证6.2 JSON WEB TOKEN
6.2.1 简介
- 由于drf tokenauth有局限性所以还是用JWT验证用户登录的方式
- JWT 是一个开放标准(RFC 7519),它定义了一种用于简洁,自包含的用于通信双方之间以 JSON 对象的形式安全传递信息的方法。JWT 可以使用 HMAC 算法或者是 RSA 的公钥密钥对进行签名。
- 两个特点:
- 简洁Compact:可以通过url、post参数或者请求头中发送,因为数据量小,传输速度快
- 自包含(self-contained):负载了用户所有需要的信息,避免多次查询数据库
drf JWT验证流程
- 在最底层的ApiView中,通过属性和函数搭配封装使得这种解耦设计得以方便实现
1
2
3
4
5
6
7
8
9
10
11
12
13class APIView(View):
# The following policies may be set at either globally, or per-view.
renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES
parser_classes = api_settings.DEFAULT_PARSER_CLASSES
authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES
throttle_classes = api_settings.DEFAULT_THROTTLE_CLASSES
permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES
content_negotiation_class = api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS
metadata_class = api_settings.DEFAULT_METADATA_CLASS
versioning_class = api_settings.DEFAULT_VERSIONING_CLASS
# Allow dependency injection of other settings to make testing easier.
settings = api_settings
schema = DefaultSchema() - 当调用到apiview的子类时,触发apiview的dispatch,这里先initialize_request把request变为restframework的request,然后执行initial把封装的一系列class全部执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63def dispatch(self, request, *args, **kwargs):
"""
`.dispatch()` is pretty much the same as Django's regular dispatch,
but with extra hooks for startup, finalize, and exception handling.
"""
self.args = args
self.kwargs = kwargs
request = self.initialize_request(request, *args, **kwargs)
self.request = request
self.headers = self.default_response_headers # deprecate?
try:
self.initial(request, *args, **kwargs)
# Get the appropriate handler method
if request.method.lower() in self.http_method_names:
handler = getattr(self, request.method.lower(),
self.http_method_not_allowed)
else:
handler = self.http_method_not_allowed
response = handler(request, *args, **kwargs)
except Exception as exc:
response = self.handle_exception(exc)
self.response = self.finalize_response(request, response, *args, **kwargs)
return self.response
# 转变request为restframework的reuqest
def initialize_request(self, request, *args, **kwargs):
"""
Returns the initial request object.
"""
parser_context = self.get_parser_context(request)
return Request(
request,
parsers=self.get_parsers(),
authenticators=self.get_authenticators(),
negotiator=self.get_content_negotiator(),
parser_context=parser_context
)
def initial(self, request, *args, **kwargs):
"""
Runs anything that needs to occur prior to calling the method handler.
"""
self.format_kwarg = self.get_format_suffix(**kwargs)
# Perform content negotiation and store the accepted info on the request
neg = self.perform_content_negotiation(request)
request.accepted_renderer, request.accepted_media_type = neg
# Determine the API version, if versioning is in use.
version, scheme = self.determine_version(request, *args, **kwargs)
request.version, request.versioning_scheme = version, scheme
# Ensure that the incoming request is permitted
self.perform_authentication(request)
self.check_permissions(request)
self.check_throttles(request) - 之后调用restframework的request.user->_authenticate->authenticatior.authenticate->jsontoken.authenticatie
- 所以其实这种方式在普通view上亦可通过重写dispatch进行实现,也算是一种经典的工厂设计模式
基本使用
1 | # pip install djangorestframework-jwt |
- 可通过restframework_jwt.settings查看配置
6.2.4 JWT详细介绍
- 由三部分构成(以.分割)
- 1.header头文件
- 2.payload信息主体
- 3.signature签名和盐
1
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxNiwidXNlcm5hbWUiOiJyb290IiwiZXhwIjoxNjIxMzI3MjAwLCJlbWFpbCI6IjUwNTEzMzExNUBxcS5jb20ifQ.se-Y8Dxs-VLBL2jzvHmat2lxcRx7Oz1MF40jFfrlPP
- header
- 包含{“alg”:”HS256”, “typ”:”JWT”}
- 被base64编码为第一部分
- payload(三个部分)
- 注册声明 iss(JWT签发者)、exp过期时间、sub主题、audJWT接收者、iat(签发时间等)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36def jwt_payload_handler(user):
username_field = get_username_field()
username = get_username(user)
warnings.warn(
'The following fields will be removed in the future: '
'`email` and `user_id`. ',
DeprecationWarning
)
payload = {
'user_id': user.pk,
'username': username,
'exp': datetime.utcnow() + api_settings.JWT_EXPIRATION_DELTA
}
if hasattr(user, 'email'):
payload['email'] = user.email
if isinstance(user.pk, uuid.UUID):
payload['user_id'] = str(user.pk)
payload[username_field] = username
# Include original issued at time for a brand new token,
# to allow token refresh
if api_settings.JWT_ALLOW_REFRESH:
payload['orig_iat'] = timegm(
datetime.utcnow().utctimetuple()
)
if api_settings.JWT_AUDIENCE is not None:
payload['aud'] = api_settings.JWT_AUDIENCE
if api_settings.JWT_ISSUER is not None:
payload['iss'] = api_settings.JWT_ISSUER
return payload - 公开声明 可以添加任何信息,一般添加用户相关的信息或其他必要信息,不建议添加敏感信息
- 私有声明 base64对称加密相当于明文,不存敏感信息
- 注册声明 iss(JWT签发者)、exp过期时间、sub主题、audJWT接收者、iat(签发时间等)
- Signature
- Signature部分的生成需要base64编码之后的Header,base64编码之后的Payload,密钥(secret),Header需要指定签字的算法。
1
2
3
4HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
- Signature部分的生成需要base64编码之后的Header,base64编码之后的Payload,密钥(secret),Header需要指定签字的算法。
1 | def encode(self, |
验证码发送 第三方(这里使用云片)
- 1.进入服务网站设置模板签名
- 2.查看相应第三方短信商的api文档
- 3.写开发接口
- 4.设置代理和获取apikey
- 5.把服务器ip加入到云片白名单
- 6.可以使用redis存储
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33# _*_ coding: utf-8 _*_
__author__ = '春江花月夜oo'
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)
}
proxies = {
"http": "http://xxxx:xxxxx@ip:port",
"https": "https://xxxx:xxxxx@ip:port"
}
response = requests.post(self.single_send_url, proxies=proxies, data=params)
re_dict = json.loads(response.text)
print(re_dict)
return re_dict
if __name__ == "__main__":
yun_pian = YunPian("43553608181588f4d559a70abdada087")
yun_pian.send_sms("2020", "18296163425")
6.4 drf自定义权限校验
- 权限和authentication流程基本一致,都是apiView中调用接口时,dispatch中进行处理
- check_permissions和check_object_permissions,check_permissions是在所有接口中统一检查的,而dispatch是当有不安全请求方法时,在get_object()时检查(即增删改时查看此权限)
1 | # 1.全局drf权限 |