登录认证

前言

  • 前后端分离的项目不需要做csrf认证,app和网站服务端已经跨站
  • drf有三种验证方式
    • BasicAuthentication 通过账号密码进行基本认证,密码经过base64编码和sha256并加盐存入数据库
    • TokenAuthentication 通过cookie设置的相关代码键为token或设置值,
    • SessionAuthentication sessionId存在cookie,通过sessionId获取存在缓存或数据库的session值再通过相应算法解析出用户

DRFToken

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# settings
INSTALLED_APPS = (
...
'rest_framework.authtoken' # 新添加 drf自带用户验证
)
# 注意需要makemigrations、migrate,即生成token表

# urls
from rest_framework.authtoken import views
re_path(r'^api-token-auth/', views.obtain_auth_token),

# settings
REST_FRAMEWORK = {
...
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.SessionAuthentication',
# 尽量不要全局配置 在需要使用token的view里面配置
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
# 'rest_framework.authentication.TokenAuthentication',
],
...
}
  • 当用户注册时我们调用接口为用户生成token并存入数据库,前端通过用户登录时调用obtain_auth_token的接口获取token放到header,后端再通过request.auth获取token进行验证。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    def 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
2
3
from rest_framework.authtoken.models import Token
token = Token.object.create(user=...)
print(token.key)
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
    9
    from 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
    13
    class 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
    63
     def 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
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
# pip install djangorestframework-jwt
REST_FRAMEWORK = {
...
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.SessionAuthentication',
# 尽量不要全局配置 在需要使用token的view里面配置
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
],
...
}

form rest_framework_jwt.views import obtain_jwt_token
path('login/', obtain_jwt_token)
# JWT验证默认使用用户名+密码验证,如需自定义验证(jwt验证调用的是django自带的authenticate验证方法):这个验证是中间件的基本authentication
settings.py
AUTHENTICATION_BACKENDS = (
'users.views.CustomBackend',
)

import datetime
# JWT设置
JWT_AUTH = {
'JWT_SECRET_KEY': SECRET_KEY,
# 一天后Token过期
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7),
}
users.views.py
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth import get_user_model
from django.db.models import Q

User = get_user_model() # 当前用户
class CustomBackend(ModelBackend):
"""
自定义用户验证
"""
def authenticate(self, request, 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
  • 可通过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
      36
      def 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对称加密相当于明文,不存敏感信息
  • Signature
    • Signature部分的生成需要base64编码之后的Header,base64编码之后的Payload,密钥(secret),Header需要指定签字的算法。
      1
      2
      3
      4
      HMACSHA256(
      base64UrlEncode(header) + "." +
      base64UrlEncode(payload),
      secret)
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
def encode(self,
payload, # type: Union[Dict, bytes]
key, # type: str
algorithm='HS256', # type: str
headers=None, # type: Optional[Dict]
json_encoder=None # type: Optional[Callable]
):
segments = []

if algorithm is None:
algorithm = 'none'

if algorithm not in self._valid_algs:
pass

# Header
header = {'typ': self.header_typ, 'alg': algorithm}

if headers:
self._validate_headers(headers)
header.update(headers)

json_header = force_bytes(
json.dumps(
header,
separators=(',', ':'),
cls=json_encoder
)
)

segments.append(base64url_encode(json_header))
segments.append(base64url_encode(payload))

# Segments
signing_input = b'.'.join(segments)
try:
alg_obj = self._algorithms[algorithm]
key = alg_obj.prepare_key(key)
signature = alg_obj.sign(signing_input, key)

except KeyError:
if not has_crypto and algorithm in requires_cryptography:
raise NotImplementedError(
"Algorithm '%s' could not be found. Do you have cryptography "
"installed?" % algorithm
)
else:
raise NotImplementedError('Algorithm not supported')

segments.append(base64url_encode(signature))

return b'.'.join(segments)

验证码发送 第三方(这里使用云片)

  • 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
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
63
64
65
66
67
68
69
70
# 1.全局drf权限
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
),
#..............
}
# 2.自定义权限(可以创建utils在其中建permissions.py进行统一管理)
from rest_framework import permissions


# permission

# 这里通过不同的场景实时传参进去构建permission_classes
class StudentViewSet(viewsets.ModelViewSet):
queryset = models.Student.objects.all()
# ...........

def get_permissions(self):
if self.action == 'list':
# 如果是对根目录进行 GET 则需要列表权限
self.permission_classes = (
perms.PermsRequired('student.list_student'),
)
if self.action == 'create':
# 如果对根目录进行 POST 则需要创建权限
self.permission_classes = (
perms.PermsRequired('student.create_student'),
)

return super(StudentViewSet, self).get_permissions()

class PermsRequired(permissions.BasePermission):
"""需要有多个权限之一,也就是说有其中一个权限即可"""

def __init__(self, *perms):
self.perms = perms # 接受多个参数,全部都是权限

def __call__(self):
return self # 被 call(前期可以理解成被当成函数调用) 的时候返回自身

def has_permission(self, request, view):
user = request.user # 既然传进来了 request,那就可以拿到 user 了

if user.is_superuser: # superuser 拥有一切权限
return True

user_perms = user.get_all_permissions() # 取得用户所拥有的的所有权限

# 取本次需要的权限和用户所有的权限的交集,如果有交集则校验通过(也就是 “或” 校验
return True if user_perms & set(self.perms) else False

# 如果想要“与”校验,可以改成下面这种
# return all([perm in user_perms for perm in set(self.perms)])


# object_permission
class IsOwnerOrReadOnly(permissions.BasePermission):
"""
对象级权限仅允许对象的所有者对其进行编辑
假设模型实例具有`owner`属性。
"""
def has_object_permission(self, request, view, obj):
# 任何请求都允许读取权限,
# 所以我们总是允许GET,HEAD或OPTIONS 请求.
if request.method in permissions.SAFE_METHODS:
return True

# 示例必须要有一个名为`owner`的属性
return obj.user == request.user
分享到