JWT---Python使用JWT感悟(网站基本业务实现)

本文介绍了JWT、JWS和JWE的基本概念,并重点讲解了Python中使用JWT进行网站登录认证的实践,包括JWT的生成、验证、有效期管理以及refresh_token的使用,确保用户登录状态的持久化。同时,文中还讨论了JWT禁用策略,防止密码修改后旧令牌仍能使用的问题。

1.JWT的介绍:

  • JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在两个组织之间传递安全可靠的信息。
  • 官方文档

2.JWT && JWS && JWE

  • JWT包括JWS和JWE,如下图
    三者关系图
  • JWS简单介绍:
    • JSON Web Signature是一个有着简单的统一表达形式的字符串
    • JWS包括HeardPayloadSignature三部分
  • JWE简单介绍:
    • 相对于JWS,JWE则同时保证了安全性与数据完整性。JWE由五部分组成:
      • HeardJWE Encrypted KeyInitialization VectorCiphertextAuthentication 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.安装
      • pip install pyjwt
    • 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 [options] INPUT
      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())
	#链接mysql数据库
    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集群
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

# 登录生成token函数
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()

# 检查客户端发来的token
def check_login(token):
    try:
        user_info = jwt.decode(token, key=Config.key)
    except Exception:
        return None
    else:
        return user_info


# 抽取
# 获取token
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():
    # 前端给后端传入token时,形式是
    # Authorization:Bearer token
    user_info = check_token()

    if user_info:
        # 不会抛出错误的get
        if user_info.get('token') is None:
            g.username = user_info['username']
        else:
            return redirect('/login')
    else:
        g.username = None

# 装饰器进行token视图判断
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}


# refresh_token视图
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__':
    # db.create_all()
    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.初来乍到,如有不足之处,还请指出,共同进步。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值