【Django】基于JWT的token认证

Json Web Token(JWT)这种结构化令牌的基础上实现了一套基于用户体系对用户的API进行授权访问的机制,满足用户个性化安全设置的需求。


一、基于token的认证

1.1 简介

很多对外开放的API需要识别请求者的身份,并据此判断所请求的资源是否可以返回给请求者。token就是一种用于身份验证的机制,基于这种机制,应用不需要在服务端保留用户的认证信息或者会话信息,可实现无状态、分布式的Web应用授权,为应用的扩展提供了便利。

1.2 流程描述

在这里插入图片描述
上图是API网关利用JWT实现认证的整个业务流程时序图,下面我们用文字来详细描述图中标注的步骤:

  1. 客户端向API网关发起认证请求,请求中一般会携带终端用户的用户名和密码;
  2. API网关将请求直接转发给后端服务;
  3. 后端服务读取请求中的验证信息(比如用户名、密码)进行验证,验证通过后使用私钥生成标准的token,返回给API网关;
  4. API网关将携带token的应答返回给客户端,客户端需要将这个token缓存到本地;
  5. 客户端向API网关发送业务请求,请求中携带token;
  6. API网关使用用户设定的公钥对请求中的token进行验证,验证通过后,将请求透传给后端服务;
  7. 后端服务进行业务处理后应答;
  8. API网关将业务应答返回给客户端。

在这个整个过程中,API网关利用token认证机制,实现了用户使用自己的用户体系对自己API进行授权的能力。下面我们就要介绍API网关实现token认证所使用的结构化令牌Json Web Toke(JWT)。

1.3 JWT

1.3.1 简介

Json Web Toke(JWT),是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准RFC7519。JWT一般可以用作独立的身份验证令牌,可以包含用户标识、用户角色和权限等信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,特别适用于分布式站点的登录场景。

1.3.2 JWT的构成

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

如上面的例子所示,JWT就是一个字符串,由三部分构成:

  1. Header(头部)
  2. Payload(数据)
  3. Signature(签名)

Header
JWT的头部承载两个信息:

  • 声明类型,这里是JWT
  • 声明加密的算法

完整的头部就像下面这样的JSON:

{
    
    
  'typ': 'JWT',
  'alg': 'HS256'
}

然后将头部进行Base64编码(该编码是可以对称解码的),构成了第一部分。

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

Payload
载荷就是存放有效信息的地方。定义细节如下:

iss:令牌颁发者。表示该令牌由谁创建,该声明是一个字符串
sub: Subject Identifier,iss提供的终端用户的标识,在iss范围内唯一,最长为255个ASCII个字符,区分大小写
aud:Audience(s),令牌的受众,分大小写的字符串数组
exp:Expiration time,令牌的过期时间戳。超过此时间的token会作废, 该声明是一个整数,是1970年1月1日以来的秒数
iat: 令牌的颁发时间,该声明是一个整数,是1970年1月1日以来的秒数
jti: 令牌的唯一标识,该声明的值在令牌颁发者创建的每一个令牌中都是唯一的,为了防止冲突,它通常是一个密码学随机值。这个值相当于向结构化令牌中加入了一个攻击者无法获得的随机熵组件,有利于防止令牌猜测攻击和重放攻击。

也可以新增用户系统需要使用的自定义字段,比如下面的例子添加了name 用户昵称:

{
    
    
  "sub": "1234567890",
  "name": "John Doe"
}

然后将其进行Base64编码,得到Jwt的第二部分:

JTdCJTBBJTIwJTIwJTIyc3ViJTIyJTNBJTIwJTIyMTIzNDU2Nzg5MCUyMiUyQyUwQSUyMCUyMCUyMm5hbWUlMjIlM0ElMjAlMjJKb2huJTIwRG9lJTIyJTBBJTdE

Signature
这个部分需要Base64编码后的Header和Base64编码后的Payload使用 . 连接组成的字符串,然后通过Header中声明的加密方式进行加密($secret 表示用户的私钥),然后就构成了jwt的第三部分。

// javascript
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
var signature = HMACSHA256(encodedString, '$secret');

将这三部分用 . 连接成一个完整的字符串,就构成了 1.3.2 节最开始的JWT示例。

1.3.3 授权范围与时效

API网关会认为用户颁发的token有权利访问整个分组下的所有绑定JWT插件的API。如果需要更细力度的权限管理,还需要后端服务自己解开token进行权限认证。API网关会验证token中的exp字段,一旦这个字段过期了,API网关会认为这个token无效而将请求直接打回。过期时间这个值必须设置,并且过期时间一定要小于7天。

1.3.4 JWT的几个特点

  1. JWT 默认是不加密,不能将秘密数据写入 JWT。
  2. JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。
  3. JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
  4. 为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用HTTPS 协议传输。

二、用户系统如何应用JWT插件保护API

2.1 生成一对JWK(JSON Web 密钥)

方法一、在线生成:
用户可以在这个站点https://mkjwk.org 生成用于token生成与验证的私钥与公钥, 私钥用于授权服务签发JWT,公钥配置到JWT插件中用于API网关对请求验签,目前API网关支持的密钥对的加密算法为RSA SHA256,密钥对的加密的位数为2048。

在这里插入图片描述
方法二、本地生成:

在jwt.io 生成jwt 时会发现签名中使用了一种base64UrlEncode 的方法,这个方法的基本功能如下:

输入一个utf-8编码的字符串s1
将字符串s1使用base64编码得到字符串s2
如果s2末尾有等号,去除末尾的所有等号
如果s2中含有加号(+),将所有加号替换为减号(-)
如果s2中含有斜杠(/),将所有斜杠替换为下划线(_)

实现一个HS256 加密的 JWT 生成

# -*- coding: utf-8 -*-

import hmac
import base64
from hashlib import sha256
from urllib import parse as urlp

def b64url(str1):
    if type(str1) == str:
        return str(base64.b64encode(str1.encode('utf-8')), encoding="utf-8").strip('=').replace('+','-').replace('/','_')
    elif type(str1) == bytes:
        return str(base64.b64encode(str1), encoding="utf-8").strip('=').replace('+','-').replace('/','_')
    else:
        raise TypeError("The type of given argument must be string or bytes")

# Enter Your Infomation Here ...
header = b64url('{"alg":"HS256","typ":"JWT"}')
payload = b64url('{"sub":"1234567890","name":"John Doe","iat":1516239022}')
secret = 'happynewyear'.encode('utf-8')
# ###

sig = b64url(hmac.new(
    secret, (header+'.'+payload).encode('utf-8'), digestmod=sha256
).digest())

jwt = header+'.'+payload+'.'+sig
print(jwt)

三、Django项目使用JWT(Django REST framework JWT)

我们在验证完用户的身份后(检验用户名和密码),需要向用户签发JWT,在需要用到用户身份信息的时候,还需核验用户的JWT。

关于签发和核验JWT,我们可以使用Django REST framework JWT扩展来完成。

3.1 安装配置

pip install djangorestframework-jwt

settings.py

REST_FRAMEWORK = {
    
    
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_jwt.authentication.JSONWebTokenAuthentication', # 先进行token认证
        'rest_framework.authentication.SessionAuthentication', # 次要进行session认证
        'rest_framework.authentication.BasicAuthentication', # 最后进行基本认证
    ),
}
import datetime
JWT_AUTH = {
    
    
  # JWT_EXPIRATION_DELTA 指明token的有效期
    'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1),
}

JWT_EXPIRATION_DELTA 指明token的有效期

3.2 后端实现

Django REST framework JWT提供了登录签发JWT的视图,可以直接使用

from django.conf.urls import re_path
from rest_framework_jwt.views import obtain_jwt_token

urlpatterns = [
    re_path(r'^authorizations/$', obtain_jwt_token),
]

但是默认的返回值仅有token,我们还需在返回值中增加username和user_id。

通过修改该视图的返回值可以完成我们的需求。

def jwt_response_payload_handler(token, user=None, request=None):
    """
    自定义jwt认证成功返回数据
    """
    return {
    
    
        'token': token,
        'id': user.id,
        'username': user.username
    }

修改配置文件

# JWT配置
JWT_AUTH = {
    
    
# token有效期
    'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1),
    # 指明自定义返回数据在哪个函数中
    'JWT_RESPONSE_PAYLOAD_HANDLER': 'xxx.utils.jwt_response_payload_handler',
}

四、Django认证插件:rest_framework_simplejwt

官方文档:https://django-rest-framework-simplejwt.readthedocs.io/en/latest/getting_started.html
简单的JWT可以用pip安装:

pip install djangorestframework-simplejwt

项目配置
settings.py

# 在setting中配置认证插件
REST_FRAMEWORK = {
    
    
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    )
}

# 注册应用
INSTALLED_APPS=[
'rest_framework_simplejwt',
]


#在 setting 配置认证插件的参数
SIMPLE_JWT = {
    
    
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5),
    'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
    'USER_ID_FIELD': 'id',
    'USER_ID_CLAIM': 'user_code',
    'ALGORITHM': 'HS256',
    'SIGNING_KEY': SECRET_KEY,
    'AUTH_HEADER_TYPES': ('Token',),
}


此外,在您的根文件(或任何其他 url 配置)中,包括路由 对于简单的 JWT 和视图:urls.py

from rest_framework_simplejwt.views import (
    TokenObtainPairView,
    TokenRefreshView,
)

urlpatterns = [
    ...
    path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
    ...
]

如果您愿意,您还可以包括简单 JWT 的路线 允许 API 用户验证 HMAC 签名的令牌,而无需访问您的 签名密钥:TokenVerifyView

from rest_framework_simplejwt.views import TokenVerifyView

urlpatterns = [
    ...
    path('api/token/verify/', TokenVerifyView.as_view(), name='token_verify'),
    ...
]

用法
要验证简单 JWT 是否正常工作,您可以使用 curl 发出几个 测试请求:

curl \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"username": "davidattenborough", "password": "boatymcboatface"}' \
  http://localhost:8000/api/token/

...
{
    
    
  "access":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX3BrIjoxLCJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiY29sZF9zdHVmZiI6IuKYgyIsImV4cCI6MTIzNDU2LCJqdGkiOiJmZDJmOWQ1ZTFhN2M0MmU4OTQ5MzVlMzYyYmNhOGJjYSJ9.NHlztMGER7UADHZJlxNG0WSi22a2KaYSfd1S-AuT7lU",
  "refresh":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX3BrIjoxLCJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImNvbGRfc3R1ZmYiOiLimIMiLCJleHAiOjIzNDU2NywianRpIjoiZGUxMmY0ZTY3MDY4NDI3ODg5ZjE1YWMyNzcwZGEwNTEifQ.aEoAYkSJjoWH1boshQAaTkf8G3yn0kapko6HFRt7Rh4"
}

可以使用返回的访问令牌来证明受保护的身份验证 视图:

curl \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX3BrIjoxLCJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiY29sZF9zdHVmZiI6IuKYgyIsImV4cCI6MTIzNDU2LCJqdGkiOiJmZDJmOWQ1ZTFhN2M0MmU4OTQ5MzVlMzYyYmNhOGJjYSJ9.NHlztMGER7UADHZJlxNG0WSi22a2KaYSfd1S-AuT7lU" \
  http://localhost:8000/api/some-protected-view/

当此短期访问令牌过期时,可以使用生存期较长的 刷新令牌以获取另一个访问令牌:

curl \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"refresh":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX3BrIjoxLCJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImNvbGRfc3R1ZmYiOiLimIMiLCJleHAiOjIzNDU2NywianRpIjoiZGUxMmY0ZTY3MDY4NDI3ODg5ZjE1YWMyNzcwZGEwNTEifQ.aEoAYkSJjoWH1boshQAaTkf8G3yn0kapko6HFRt7Rh4"}' \
  http://localhost:8000/api/token/refresh/

...
{
    
    "access":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX3BrIjoxLCJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiY29sZF9zdHVmZiI6IuKYgyIsImV4cCI6MTIzNTY3LCJqdGkiOiJjNzE4ZTVkNjgzZWQ0NTQyYTU0NWJkM2VmMGI0ZGQ0ZSJ9.ekxRxgb9OKmHkfy-zs1Ro_xs1eMLXiR17dIDBVxeT-w"}

五、参考引用

【1】基于JWT的token认证——https://help.aliyun.com/document_detail/177489.html
【2】Python 实现 JWT 生成——https://www.cnblogs.com/soowin/p/14095180.html
【3】Django项目使用JWT(Django REST framework JWT)——https://blog.csdn.net/li944254211/article/details/109509974
【4】Django认证插件:rest_framework_simplejwt——https://blog.csdn.net/qq_44715689/article/details/120783553

猜你喜欢

转载自blog.csdn.net/m0_58598240/article/details/129808146