Python JWT
session认证
我们知道,http协议本身是一种无状态的协议,而这就意味着如果用户向应用提供了用户名和密码来进行用户认证,那么下一次请求时,用户还要再一次进行用户认证才行,因为根据http协议,并不能确定是哪个用户发出的请求,所以为了能识别是哪个用户发出的请求,只能在服务器存储一份用户登录的信息,这份登录信息会在响应时传递给浏览器,告诉其保存为cookie,以便下次请求时发送给服务器,这样我们服务器就能识别请求来自哪个用户了,这就是传统的基于session认证。
但是这种基于session的认证使应用本身很难得到扩展,随着不同客户端用户的增加,独立的服务器已无法承载更多的用户,而这时候基于session认证应用的问题就会暴露出来.
session认证问题
Session: 每个用户经过我们的应用认证之后,我们的应用都要在服务端做一次记录,以方便用户下次请求的鉴别,通常而言session都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大。
扩展性: 用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力。
CSRF: 因为是基于cookie来进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。
token鉴权机制
基于token的鉴权机制类似于http协议也是无状态的,它不需要在服务端去保留用户的认证信息或者会话信息。这就意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利。
流程上是这样的:
- 用户使用用户名密码来请求服务器
- 服务器进行验证用户的信息
- 服务器通过验证发送给用户一个token
- 客户端存储token,并在每次请求时附送上这个token值
- 服务端验证token值,并返回数据
这个token必须要在每次请求时传递给服务端,它应该保存在请求头里, 另外,服务端要支持CORS(跨来源资源共享)
策略,一般我们在服务端端要做跨域支持。
JWT的构成
第一部分我们称它为头部(header),第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),第三部分是签证(signature).
header
jwt的头部承载两部分信息:
- 声明类型,这里是jwt
- 声明加密的算法 通常直接使用 HMAC SHA256
完整的头部就像下面这样的JSON:
{
'typ': 'JWT',
'alg': 'HS256'
}
然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分.
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
playload
载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分
- 标准中注册的声明
- 公共的声明
- 私有的声明
标准中注册的声明 (建议但不强制使用) :
- iss: jwt签发者
- sub: jwt所面向的用户
- aud: 接收jwt的一方
- exp: jwt的过期时间,这个过期时间必须要大于签发时间
- nbf: 定义在什么时间之前,该jwt都是不可用的.
- iat: jwt的签发时间
- jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
公共的声明 :
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
私有的声明 :
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
定义一个payload:
{
"phone": "13331153360",
"name": "admin",
}
然后将其进行base64加密,得到Jwt的第二部分。
dy63zcIiOiIxMjMdTY3ODkwIiwiaFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
signature
jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
- header (base64后的)
- payload (base64后的)
- secret
这个部分需要base64加密后的header和base64加密后的payload使用.
连接组成的字符串,然后通过header中声明的加密方式进行加盐secret
组合加密,然后就构成了jwt的第三部分。
// javascript
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
var signature = HMACSHA256(encodedString, 'secret'); // TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
将这三部分用.
连接成一个完整的字符串,构成了最终的jwt:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
Python JWT实现
python 本身有携带JWT模块,我们可以采用jwt模块自己进行基于django的JWT登陆:
抛开登陆和数据部分,这两个部分是视图部分,我们自己需要编写的以下功能:
用户名密码校验
token生成
token校验
搭建自己的用户表
class PUser(models.Model):
username = models.CharField(max_length = 32)
password = models.CharField(max_length = 32)
user_group = models.ManyToManyField(UserGroup)
用户名密码校验
def check_user(username,password):
"""
校验用户是否存在
:param username: 用户名
:param password: 密码
:return:
"""
result = {
"state": 0,
"message": "",
"bool": False,
"data": {
}
}
user = PUser.objects.filter(username = username,password = password)
if user:
result["state"] = 1
result["message"] = "当前用户存在!"
result["bool"] = True
result["data"].update(
{
"username": user.username,
"user_id": user.id
}
)
else:
result["state"] = 0
result["message"] = "当前用户不存在!"
result["bool"] = False
return result
原生JWT_Token
按照jwt模块
(d_37) C:Usersdell>pip install jwt
Collecting jwt
Downloading jwt-1.1.0-py3-none-any.whl (14 kB)
Requirement already satisfied: cryptography<=3.2.1,>2.9.2 in d:anaconda3envsd_37libsite-packages (from jwt) (3.2.1)
Requirement already satisfied: cffi!=1.11.3,>=1.8 in d:anaconda3envsd_37libsite-packages (from cryptography<=3.2.1,>2.9.2->jwt) (1.14.3)
Requirement already satisfied: six>=1.4.1 in d:anaconda3envsd_37libsite-packages (from cryptography<=3.2.1,>2.9.2->jwt) (1.15.0)
Requirement already satisfied: pycparser in d:anaconda3envsd_37libsite-packages (from cffi!=1.11.3,>=1.8->cryptography<=3.2.1,>2.9.2->jwt) (2.20)
Installing collected packages: jwt
Successfully installed jwt-1.1.0
(d_37) C:Usersdell>
jwt的生成token格式如下,即:由点(.)连接的三段字符组成
HEADER部分
第一段HEADER部分,固定包含算法和token类型,对此json进行base64url加密,这就是token的第一段
{
"alg": "HS256",
"typ": "JWT"
}
使用python的内置模块进行简单的实现
import json
import base64
header = {
"alg": "HS256",
"typ": "JWT",
}
json_header = json.dumps(header).encode()
string_headers = base64.b64encode(json_header)
print(string_headers)
print(base64.b64decode(string_headers)) #这里是解码
PAYLOAD部分
第二段PAYLOAD部分,包含一些数据,对此json进行base64url加密,这就是token的第二段,这个部分是可以被解密出来的,所以建议大家放数据,但是不建议大家放敏感数据,比如密码。当然除了校验之外,我们也可以使用这里的数据进行数据查询或者处理
依然,我们采用使用python的内置模块进行简单的实现
import json
import base64
payload = {
"username": "admin",
"userid": 1,
}
json_payload = json.dumps(payload).encode()
string_payload = base64.b64encode(json_payload)
print(string_payload)
print(base64.b64decode(string_payload)) #这里是解码
SIGNATURE部分
第三段SIGNATURE部分,把前两段的base秘闻通过.拼接起来,然后对其进行HS256加密,再然后对hs256秘闻进行base64url加密,最终得到token的第三段。
注意整个流程当中,需要在hash256加密的过程当中添加盐值,来保证加密不会被很轻易的暴力破解出来。
import hashlib
that_string = string_headers+b"."+string_payload
hash256 = hashlib.sha256(b"helloworld")
hash256.update(that_string)
hash_string = hash256.hexdigest()
string_signature = base64.b64encode(hash_string.encode())
print(string_signature)
然后按照格式,使用.进行拼接,代码如下:
import json
import base64
import hashlib
header = {
"alg": "HS256",
"typ": "JWT",
}
json_header = json.dumps(header).encode()
string_headers = base64.b64encode(json_header)
print(string_headers)
payload = {
"username": "admin",
"userid": 1,
}
json_payload = json.dumps(payload).encode()
string_payload = base64.b64encode(json_payload)
print(string_payload)
that_string = string_headers+b"."+string_payload
hash256 = hashlib.sha256(b"helloworld")
hash256.update(that_string)
hash_string = hash256.hexdigest()
string_signature = base64.b64encode(hash_string.encode())
print(string_signature)
print(string_headers+b"."+string_payload+b"."+hash_string.encode())
然后编写解析token的代码
import json
import base64
import hashlib
header,payload,signature = result.split(b".")
header_json = base64.b64decode(header) #这里是解码
payload_json = base64.b64decode(payload) #这里是解码
print(json.loads(header_json))
print(json.loads(payload_json))
print(signature)
sha256加密是没有办法进行逆向解密的,所以signature没有办法解密查看内容,可以对获取的前两段再次进行加密新成新的第三段然后和当前的第三段进行比对来判断这个第三段是否正常,这里有个关键的点就是盐值,所以还是要强调盐值问题,如果盐值泄露,用户就可以很轻松的模仿出token来了。
上面可以实现jwt的生成和校验功能的简单功能了,但是有点复杂,并且很难保证代码的一致性(当然通过代码封装可以解决这一点),python也提供了jwt模块:
Python Jwt模块
安装
(D_37) C:Usersdell>pip install pyjwt
使用
有用户签名部分:
import jwt
import datetime
dic = {
'exp': datetime.datetime.now() + datetime.timedelta(days=1), # 过期时间
'iat': datetime.datetime.now(), # 开始时间
'iss': 'lianzong', # 签名
'data': { # 内容,一般存放该用户id和开始时间
'a': 1,
'b': 2,
},
}
s = jwt.encode(dic, 'secret', algorithm='HS256') # 加密生成字符串
print(s)
s = jwt.decode(s, 'secret', issuer='lianzong', algorithms=['HS256']) # 解密,校验签名
print(s)
print(type(s))
restframework-jwt
restframework-jwt是基于django 的restframework的登陆验证部分,可以快速的实现登陆和校验部分,当然也提供了开发者自定义的方法
基本配置
安装
pip install djangorestframework-jwt
settings配置
....
# Application definition
INSTALLED_APPS = [
....
'rest_framework_jwt'
]
......
#全局配置
REST_FRAMEWORK = {
#permission_classes
"DEFAULT_PERMISSION_CLASSES": (
"rest_framework.permissions.IsAuthenticated",
),
"DEFAULT_AUTHENTICATION_CLASSES":(
"rest_framework_jwt.authentication.JSONWebTokenAuthentication",
) # 全局设置的方法,也可在单个视图中设置
}
其他配置
import datetime
JWT_AUTH = {
# token过期时间
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7),
# 允许刷新token
'JWT_ALLOW_REFRESH': True,
# 刷新的过期时间
'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=7),
#这个很重要,是设置token开头的一种
'JWT_AUTH_HEADER_PREFIX': 'JWT'
}
局部使用
from rest_framework.permissions import IsAuthenticated
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
class CompanyModelViewSet(ModelViewSet):
authentication_classes = [JSONWebTokenAuthentication]
queryset = Company.objects.all()
serializer_class = CompanySerializers
permission_classes = [OurPermission,IsAuthenticated]
访问测试
首先提交用户名和密码
这里要注意的是restframewrok_jwt默认采用系统的库作为用户识别的库,所以需要提前使用createsuper来注册用户,然后进行测试。
发送数据的时候,需要把数据携带到请求的头部,这里携带请求的字段叫做: Authorization,如果你采用的是postman的话,可以补全出来的。
然后是注意发送token的格式,是需要遵循在settings当中的JWT_AUTH_HEADER_PREFIX配置的格式的,也就是说需要是
JWT eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNjA5MjEwNjQ0LCJlbWFpbCI6ImFkbWluQHFxLmNvbSJ9.5OadYhBVuAQg-vveQA7o-SyF25HjeosOW_rIFGyfesY
这个格式。
postman 进行如下设置
axios 案例
<script>
vue = new Vue({
el: "#container",
data: {
per_data: []
},
created: function(){
axios.defaults.headers.common['Authorization'] = "JWT eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNjA5Mjk0NTczLCJlbWFpbCI6ImFkbWluQHFxLmNvbSJ9.6sBQexTiv54wGezNGOYevMbr50tMJM3qQ8p8w6aGz30";
axios.get("http://127.0.0.1/Permission/").then(
function(data){
vue.per_data = data["data"]
console.log(data)
},
function(error){
console.log(error)
}
)
}
})
</script>
自定义开发
rest_framework本身会加载自己的这个配置文件:
$python_path/lib/site-package/restframework-jwt/settings.py
import datetime
from django.conf import settings
from rest_framework.settings import APISettings
USER_SETTINGS = getattr(settings, 'JWT_AUTH', None)
DEFAULTS = {
'JWT_ENCODE_HANDLER':
'rest_framework_jwt.utils.jwt_encode_handler',
'JWT_DECODE_HANDLER':
'rest_framework_jwt.utils.jwt_decode_handler',
'JWT_PAYLOAD_HANDLER':
'rest_framework_jwt.utils.jwt_payload_handler',
'JWT_PAYLOAD_GET_USER_ID_HANDLER':
'rest_framework_jwt.utils.jwt_get_user_id_from_payload_handler',
'JWT_PRIVATE_KEY':
None,
'JWT_PUBLIC_KEY':
None,
'JWT_PAYLOAD_GET_USERNAME_HANDLER':
'rest_framework_jwt.utils.jwt_get_username_from_payload_handler',
'JWT_RESPONSE_PAYLOAD_HANDLER':
'rest_framework_jwt.utils.jwt_response_payload_handler',
'JWT_SECRET_KEY': settings.SECRET_KEY,
'JWT_GET_USER_SECRET_KEY': None,
'JWT_ALGORITHM': 'HS256',
'JWT_VERIFY': True,
'JWT_VERIFY_EXPIRATION': True,
'JWT_LEEWAY': 0,
'JWT_EXPIRATION_DELTA': datetime.timedelta(seconds=300),
'JWT_AUDIENCE': None,
'JWT_ISSUER': None,
'JWT_ALLOW_REFRESH': False,
'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=7),
'JWT_AUTH_HEADER_PREFIX': 'JWT',
'JWT_AUTH_COOKIE': None,
}
# List of settings that may be in string import notation.
IMPORT_STRINGS = (
'JWT_ENCODE_HANDLER',
'JWT_DECODE_HANDLER',
'JWT_PAYLOAD_HANDLER',
'JWT_PAYLOAD_GET_USER_ID_HANDLER',
'JWT_PAYLOAD_GET_USERNAME_HANDLER',
'JWT_RESPONSE_PAYLOAD_HANDLER',
'JWT_GET_USER_SECRET_KEY',
)
api_settings = APISettings(USER_SETTINGS, DEFAULTS, IMPORT_STRINGS)
从这个方法里面我们可以抽取出很多有用的方法:
from django.http import JsonResponse
def login(request):
user = PUser.objects.get(id = 1)
payload = jwt_payload_handler(user) #从用户表当中抽取出数据,然后编译成一个字典结构的数据
jwt_token = jwt_encode_handler(payload) #生成一个JWT token
decode_token = jwt_decode_handler(jwt_token) #解码一个JWT token
print(payload)
print(jwt_token)
print(decode_token)
return JsonResponse(payload)
那么基于上述的方法和Django restful我们完成下面的接口视图
class RegisterModelViewSet(ViewSet):
"""
注册接口
只有保存接口
"""
authentication_classes = [] #登录部分
permission_classes = [] #权限部分
def __init__(self, **kwargs):
super(RegisterModelViewSet,self).__init__(**kwargs)
self.result = {
"code": 200,
"detail": [
],
"token": ""
}
def serializers(self,obj):
is_one = isinstance(obj,PUser)
if is_one:
result = [AuthSerializers(obj).data]
else:
result = [AuthSerializers(o).data for o in obj]
self.result["detail"] = result
def set_password(self,password):
md5 = hashlib.md5()
md5.update(password.encode())
result = md5.hexdigest()
return result
def send_token(self,user):
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
payload = jwt_payload_handler(user) # 从用户表当中抽取出数据,然后编译成一个字典结构的数据
jwt_token = jwt_encode_handler(payload) # 生成一个JWT token
return jwt_token
def get(self,request,id = 0):
if id:
user = PUser.objects.filter(id = int(id)).first()
else:
username = request.GET.get("username")
if username:
user = PUser.objects.filter(username = username).first() #检索数据
else:
user = PUser.objects.all() #返回所有数据
self.serializers(user)
return Response(self.result)
def post(self,request):
username = request.POST.get("username")
password = request.POST.get("password")
request_type = request.POST.get("request_type")
if username and password:
if request_type == "register":
self.regsiter(username,password)
else:
self.login(username,password)
else:
self.result["code"] = 404
self.result["detail"].append("用户名和密码不可以为空!")
return Response(self.result)
def regsiter(self,username,password):
"""
注册
"""
user = PUser()
user.username = username
user.password = self.set_password(password)
user.save()
self.serializers(user)
def login(self,username,password):
"""
登录
"""
pwd = self.set_password(password)
user = PUser.objects.filter(username = username,password = pwd).first()
if user:
token = self.send_token(user)
self.serializers(user)
self.result["token"] = token
else:
self.result["code"] = 500
self.result["detail"].append("用户和密码有误")
使用如下: