1.JWT的介绍:
- JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在两个组织之间传递安全可靠的信息。
- 官方文档
2.JWT && JWS && JWE
- JWT包括JWS和JWE,如下图

- JWS简单介绍:
- JSON Web Signature是一个有着简单的统一表达形式的字符串:
- JWS包括Heard,Payload,Signature三部分
- JWE简单介绍:
- 相对于JWS,JWE则同时保证了安全性与数据完整性。JWE由五部分组成:
- Heard、JWE Encrypted Key、Initialization Vector、Ciphertext、Authentication Tag
- 具体生成步骤为:
1.JOSE含义与JWS头部相同。
2.生成一个随机的Content Encryption Key (CEK)。
3.使用RSAES-OAEP 加密算法,用公钥加密CEK,生成JWE Encrypted Key。
4.生成JWE初始化向量。
5.使用AES GCM加密算法对明文部分进行加密生成密文Ciphertext,算法会随之生成一个128位的认证标记Authentication Tag。
6.对五个部分分别进行base64编码。
可见,JWE的计算过程相对繁琐,不够轻量级,因此适合与数据传输而非token认证,但该协议也足够安全可靠,用简短字符串描述了传输内容,兼顾数据的安全性与完整性。
3.Python的JWT
- itsdangerous
- JSONWebSignatureSerializer
- TimedJSONWebSignatureSerializer (可设置有效期)
- pyjwt
- pyjwt使用
- 1.安装
- 2.例子(官方)
>>> import jwt
>>> encoded_jwt = jwt.encode({'some': 'payload'}, 'secret', algorithm='HS256')
>>> encoded_jwt
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzb21lIjoicGF5bG9hZCJ9.4twFt5NiznN84AWoo1d7KO1T_yoc0Z6XOpOVswacPZg'
>>> jwt.decode(encoded_jwt, 'secret', algorithms=['HS256'])
{'some': 'payload'}
- 3.命令行
pyjwt --key=secret decode TOKEN
pyjwt decode --no-verify TOKEN
4.举个栗子(运用在网站上)
- ①.需求:实现简单的网站业务逻辑,有首页,个人中心,登录。进行分析
- ②.初级分析:
- 1.不需要登录的页面,如:首页。
- 2.需要登陆的网页,如:个人中心。
- 3.登录页面登陆时,服务端返回给客户端携带用户信息以及有效期的token。
- 4.当用户想访问个人中心时,客户端将token发送给服务端,若token为空以及没有token字段,都跳转到登录界面。
- 5.如果token存在,需要检测token的有效期,如果token失效以及token错误,都跳转用户登录页面,重新登陆,重新获取新的token。
- ③.中级分析
- 1.当客户端传递给服务端token时,如果token错误,应该和空token归为一类,都需要返回到登录页面,登录以获取token。
- 2.若token过期(token需要频繁更换,否则有可能会被窃取破解),此时如果让用户去登陆,会显得非常麻烦,毕竟在逐渐智能化的时代,方便快捷的网页应用才会深得人心。借鉴wechat类APP应用几乎是登录一次,一年半载都不需要用户手动输入密码频繁登录。
- 3.此时,可以想到的是在用户进行首次登陆时,返回给客户端两个token。一个token用于平常验证,过期时间短。另一个未过期(此token过期时间较长)refresh_token用于当token过期时,换取新的token值,以及一个新的refresh_token。
- 为什么要返回一个新的refresh_token呢?
- 答:保证token和refresh_token的状态时同生型,即重新刷新refresh_token的过期时间,能够循环往返,达到登陆状态持久化。
- 四.深入分析(注:此时只把refresh_token的业务逻辑完善。)
- 0.(承接初级分析1,2,3,4,其中3处服务端需要返回客户端token,refresh_token。4处需要加入token错误判断)
- 1.如果token存在且正确,直接进入个人中心页面。
- 2.如果token存在却过期,就需要给客户端发送相应以及携带专属响应状态码。
- 3.客户端接收到响应状态后,紧接着将refresh_token发送给服务端。服务端接收到refresh_token,判断是否存在,若不存在,跳转登陆页面。
- 4.若存在,判断refresh_token是否过期(将要过期【可有可无】)以及是否正确。
- 5.若过期或者不正确(因为jwt.decode(refresh_token)会判断过期时间以及正确与否的判断),则跳转到登录界面,进行登录。
- 5.若refresh_token未过期并且正确,则换取新的token值以及新的refresh_token值。
5.上代码!!!
import datetime
import json
from time import mktime
from rediscluster import RedisCluster
from flask_sqlalchemy import SQLAlchemy
import jwt
from flask import Flask, request, g, jsonify, redirect, url_for
from flask_restful import Api, Resource
import random
"""
1.此处采用了redis集群以及flask,mysql
"""
class Config(object):
key = 'woobrain'
algorithm = 'HS256'
exp_time = {
'days': 12,
'hours': 2,
'minutes': 3,
'seconds': 4
}
DATE_HOURS = datetime.datetime.now() + datetime.timedelta(hours=2)
DATE_DAYS = datetime.datetime.now() + datetime.timedelta(days=14)
TIME_CHUO = mktime(datetime.datetime.now().timetuple())
SQLALCHEMY_DATABASE_URI = 'mysql://root:mysql@127.0.0.1:3306/user'
app = Flask(__name__)
app.config.from_object(Config)
db = SQLAlchemy(app=app)
api = Api(app)
REDIS_CLUSTER = [
{'host': '127.0.0.1', 'port': '7000'},
{'host': '127.0.0.1', 'port': '7001'},
{'host': '127.0.0.1', 'port': '7002'},
]
class UserInfo(db.Model):
__tablename__ = 'user_info'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(20))
password = db.Column(db.String(20))
def __repr__(self):
return self.username
def login_token(username):
token = jwt.encode({'username': username,
'count':random.random(),
'exp': Config.DATE_HOURS},
key=Config.key, algorithm=Config.algorithm)
refrash_token = jwt.encode({'username': username,
'exp': Config.DATE_DAYS,
'count':random.random(),
'token': True},
key=Config.key, algorithm=Config.algorithm)
return token.decode(), refrash_token.decode()
def check_login(token):
try:
user_info = jwt.decode(token, key=Config.key)
except Exception:
return None
else:
return user_info
def check_token():
breare_token = request.headers.get('Authorization')
username = json.loads(request.data.decode()).get('username')
key = 'user:{}:token'.format(username)
if breare_token:
token_list = breare_token.split(' ')
if len(token_list) == 2:
token = token_list[1]
redis_con = RedisCluster(startup_nodes=REDIS_CLUSTER)
res = redis_con.get(key)
if token == res.decode():
user_info = check_login(token)
return user_info
return None
@app.before_request
def UserInfoToken():
user_info = check_token()
if user_info:
if user_info.get('token') is None:
g.username = user_info['username']
else:
return redirect('/login')
else:
g.username = None
def loginverify(func):
def wrapper(*args, **kwargs):
if g.username:
return func(*args, **kwargs)
else:
print(g.username)
return {'msg': 'token error'}, 401
return wrapper
class IndexView(Resource):
def get(self):
return {'msg': 'ok', 'index': 'index ....'}
class LoginView(Resource):
def get(self):
return {'msg': 'ok', 'data': '这是登录'}
def post(self):
data = json.loads(request.data.decode())
username = data.get('username')
password = data.get('password')
user = UserInfo.query.get(1)
redis_con = RedisCluster(startup_nodes=REDIS_CLUSTER)
if username == user.username and password == user.password:
key = 'user:{}:token'.format(username)
token, refrash_token = login_token(username)
redis_con.setex(key, token, 6000)
return {'msg': 'ok', 'data': {'token': token, 'refrash_token': refrash_token, 'exp': Config.TIME_CHUO}}
class CenterVerView(Resource):
method_decorators = [
loginverify
]
def get(self):
return {'msg': 'ok', 'username': g.username}
class RefreshTokenView(Resource):
def get(self):
user_info = check_token()
if user_info:
if user_info.get('token') is not None:
print(user_info.get('token'))
new_token, new_refresh_token = login_token(g.username)
return {'msg': 'ok',
'data': {'token': new_token, 'refresh_token': new_refresh_token, 'exp': Config.TIME_CHUO}}
return redirect('/login')
class PasswdVerify(Resource):
def put(self):
data = json.loads(request.data.decode())
username = data.get('username')
o_password = data.get('o_password')
n_password = data.get('n_password')
user = UserInfo.query.get(1)
redis_con = RedisCluster(startup_nodes=REDIS_CLUSTER)
if username == user.username and o_password == user.password:
key = 'user:{}:token'.format(username)
user.password = n_password
db.session.commit()
token, refrash_token = login_token(username)
redis_con.setex(key, token, 6000)
return {'msg': 'ok', 'data': {'token': token, 'refrash_token': refrash_token, 'exp': Config.TIME_CHUO}}
api.add_resource(IndexView, '/')
api.add_resource(LoginView, '/login')
api.add_resource(CenterVerView, '/token')
api.add_resource(RefreshTokenView, '/refresh_token')
api.add_resource(PasswdVerify, '/verifypasswd')
print(app.url_map)
if __name__ == '__main__':
app.run(debug=True, host='192.168.179.131')
6.解惑
- 1.上述代码加入了JWT禁用,当用户修改密码的时候,需要让之前的token和refresh_token过期,生成新的。这就是为什么用到了redis,使用集合set,key为user:{username}:token,value为token。
- 2.由于在进行登录时,我让token去查询redis数据库,所以可进行判断。这时候问题出现了,token和refresh_token几乎没什么不同,所以当进行密码修改时,字段几乎没有什么变动,所以,可能出现了token值一直不变的情况,此时想到使用一个随机值充当一个字段。这时候就加入了’count’:random.random()。产生随机浮点数。这样就解决了更改密码时token值可能不变的问题,真正的实现了JWT禁用。
7.初来乍到,如有不足之处,还请指出,共同进步。