Json Web Token(JWT)这种结构化令牌的基础上实现了一套基于用户体系对用户的API进行授权访问的机制,满足用户个性化安全设置的需求。
很多对外开放的API需要识别请求者的身份,并据此判断所请求的资源是否可以返回给请求者。token就是一种用于身份验证的机制,基于这种机制,应用不需要在服务端保留用户的认证信息或者会话信息,可实现无状态、分布式的Web应用授权,为应用的扩展提供了便利。
上图是API网关利用JWT实现认证的整个业务流程时序图,下面我们用文字来详细描述图中标注的步骤:
在这个整个过程中,API网关利用token认证机制,实现了用户使用自己的用户体系对自己API进行授权的能力。下面我们就要介绍API网关实现token认证所使用的结构化令牌Json Web Toke(JWT)。
Json Web Toke(JWT),是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准RFC7519。JWT一般可以用作独立的身份验证令牌,可以包含用户标识、用户角色和权限等信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,特别适用于分布式站点的登录场景。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
如上面的例子所示,JWT就是一个字符串,由三部分构成:
Header
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示例。
API网关会认为用户颁发的token有权利访问整个分组下的所有绑定JWT插件的API。如果需要更细力度的权限管理,还需要后端服务自己解开token进行权限认证。API网关会验证token中的exp字段,一旦这个字段过期了,API网关会认为这个token无效而将请求直接打回。过期时间这个值必须设置,并且过期时间一定要小于7天。
方法一、在线生成:
用户可以在这个站点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)
我们在验证完用户的身份后(检验用户名和密码),需要向用户签发JWT,在需要用到用户身份信息的时候,还需核验用户的JWT。
关于签发和核验JWT,我们可以使用Django REST framework JWT扩展来完成。
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的有效期
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',
}
官方文档: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