jwt私钥和公钥怎么获取_一文讲解JWT用户认证全流程

一、什么是JWT(what)JWT(JSON Web Token)是一个开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,以JSON对象的形式在各方之间安全地传输信息。

JWT是一个数字签名,生成的信息是可以验证并被信任的。

使用密钥(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对JWT进行签名。

JWT是目前最流行的跨域认证解决方案

1.1 JWT令牌结构

SON Web令牌以紧凑的形式由三部分组成,这些部分由点(.)分隔,分别是:Header

Payload

Signature

即为: xxxx.yyyy.zzzz

1.1.1 Header

Header通常由两部分组成:令牌的类型(即JWT)和所使用的签名算法(例如HMAC SHA256或RSA)。 例如:

{

"alg": "HS256",

"typ": "JWT"

}

Header会被Base64Url编码为JWT的第一部分。即为:

$ echo -n '{"alg":"HS256","typ":"JWT"}'|base64

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

1.1.2 Payload

Payload是有关实体(通常是用户)和其他数据的声明,它包含三部分:

(1)注册声明

这些是一组预定义的权利要求,不是强制性的,而是建议使用的,以提供一组有用的可互操作的权利要求。其中一些是: iss(JWT的签发者), exp(expires,到期时间), sub(主题), aud(JWT接收者),iat(issued at,签发时间)等。

注意:声明名称都是三个字符

(2)公开声明

公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密。

(3)私有声明

私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。

例子:

{ "iat": 1593955943,

"exp": 1593955973,

"uid": 10,

"username": "test",

"scopes": [ "admin", "user" ]

}

Payload会被Base64Url编码为JWT的第二部分。即为:

$ echo -n '{"iat":1593955943,"exp":1593955973,"uid":10,"username":"test","scopes":["admin","user"]}'|base64

eyJ1c2VybmFtZSI6InRlc3QiLCJpYXQiOjE1OTM5NTU5NDMsInVpZCI6MTAsImV4cCI6MTU5Mzk1NTk3Mywic2NvcGVzIjpbImFkbWluIiwidXNlciJdfQ注意:对于已签名的令牌,此信息尽管可以防止篡改,但任何人都可以读取。除非将其加密,否则请勿将机密信息放入JWT的有效负载或报头元素中。

1.1.3 Signature

Signature部分的生成需要base64编码之后的Header,base64编码之后的Payload,密钥(secret),Header需要指定签字的算法。

例如,如果要使用HMAC SHA256算法,则将通过以下方式创建签名:

HMACSHA256(

base64UrlEncode(header) + "." +

base64UrlEncode(payload),

secret)

整合在一起

输出是三个由点分隔的Base64-URL字符串,可以在HTML和HTTP环境中轻松传递这些字符串,与基于XML的标准(例如SAML)相比,它更紧凑。

"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InRlc3QiLCJpYXQiOjE1OTM5NTU5NDMsInVpZCI6MTAsImV4cCI6MTU5Mzk1NTk3Mywic2NvcGVzIjpbImFkbWluIiwidXNlciJdfQ.VHpxmxKVKpsn2Iytqc_6Z1U1NtiX3EgVki4PmA-J3Pg"

JWT是无状态授权机制,服务器的受保护路由将Header中检查有效的token,如果存在,则将允许用户访问受保护的资源。如果JWT包含必要的数据,则可以减少查询数据库中某些操作的需求。

二、什么时候使用JWT(when)授权:一旦用户登录,每个后续请求将包括JWT,从而允许用户访问该令牌允许的路由,服务和资源。单一登录是当今广泛使用JWT的一项功能,因为它的开销很小并且可以在不同的域中轻松使用。这也是JWT最常见的方案。

信息交换:JSON Web令牌是各方之间安全地传输信息的好办法。对JWT进行签名,所以您可以确保发件人是他们所说的人。由于,签名可以设置有效时长,可以验证内容是否遭到篡改。

三、如何使用JWT(how)

3.1 JWT工作流程

根据下面的这张流程图来分析一下JWT的工作过程JWT 认证流程用户登录:提供用户名和密码;

JWT生成token和refresh_token,返回客户端;(注意:refresh_token的过期时间长于token的过期时间)

客户端保存token和refresh_token,并携带token,请求服务端资源;

服务端判断token是否过期,若没有过期,则解析token获取认证相关信息,认证通过后,将服务器资源返回给客户端;

服务端判断token是否过期,若token已过期,返回token过期提示;

客户端获取token过期提示后,用refresh_token接着继续上一次请求;

服务端判断refresh_token是否过期,若没有过期,则生成新的token和refresh_token,并返回给客户端,客户端丢弃旧的token,保存新的token;

服务端判断refresh_token是否过期,若refresh_token已过期,则返回给客户端token过期,需要重新登录的提示。

3.2 python+flask+JWT实战

import time

from functools import wraps

from flask import Flask, request, jsonify

import jwt

from jwt import ExpiredSignatureError

app = Flask(__name__)

max_time = 60

refresh_max_time = 120

token_secret = "This is a secret"

def verify_token(func):

@wraps(func)

def decorator(*args, **kwargs):

try:

token = request.headers["token"]

print(token)

data = jwt.decode(token, token_secret, algorithms=['HS256'])

now = int(time.time())

time_interval = now - data['time']

if time_interval >= max_time:

# create new token

token, refresh_token = creat_token()

return jsonify({"token": token, "refresh_token": refresh_token})

except ExpiredSignatureError:

return "Token expired"

except Exception as ex:

print(ex)

return "Log in again"

return func(*args, **kwargs)

return decorator

def creat_token(uid):

now = int(time.time())

payload = {'uid': uid, 'time': now, 'exp': now + max_time}

refresh_payload = {'uid': uid, 'time': now, 'exp': now + refresh_max_time}

token = jwt.encode(payload, token_secret, algorithm='HS256')

refresh_token = jwt.encode(refresh_payload, token_secret, algorithm='HS256')

return token, refresh_token

@app.route('/login', methods=["POST"])

def login():

user_name = request.values.get('user_name')

password = request.values.get('password')

# @TODO 根据user_name和password 获取唯一的uid

uid = 10

token, refresh_token = creat_token(uid=uid)

return jsonify({"token": token, "refresh_token": refresh_token})

@app.route('/test', methods=['GET'])

@verify_token

def test():

return 'hello world'

if __name__ == "__main__":

app.run(host="0.0.0.0", port=5000)

3.3 第三方库-itsdangerous

3.3.1 isdangerous简介

itsdangerous支持JSON Web 签名 (JWS),内部默认使用了HMAC和SHA1来签名,其中类JSONWebSignatureSerializer内部与JWT一致,也分成三部分(header,payload,signature),查看源码可知:

def dumps(self, obj, salt=None, header_fields=None):

"""Like :meth:`.Serializer.dumps` but creates a JSON Web

Signature. It also allows for specifying additional fields to be

included in the JWS header.

"""

header = self.make_header(header_fields)

signer = self.make_signer(salt, self.algorithm)

return signer.sign(self.dump_payload(header, obj))

def dump_payload(self, header, obj):

base64d_header = base64_encode(

self.serializer.dumps(header, **self.serializer_kwargs)

)

base64d_payload = base64_encode(

self.serializer.dumps(obj, **self.serializer_kwargs)

)

return base64d_header + b"." + base64d_payloadobj保存用户相关信息,类似JWT中的payload

base64url对obj和header进行编码之后,使用.拼接

将拼接之后的数据,作为signer的输入以及初始化__init__中用户定义的secret来生成新的token

感兴趣的朋友可以直接参看github源码,这里不再展开赘述。

3.3.2 python+flask+isdangerous实战

import time

from functools import wraps

from flask import Flask, request, jsonify

from itsdangerous import TimedJSONWebSignatureSerializer as Serializer, SignatureExpired

app = Flask(__name__)

max_time = 60

refresh_max_time = 120

token_secret = "This is a secret"

def verify_token(func):

@wraps(func)

def decorator(*args, **kwargs):

try:

token = request.headers["token"]

print(token)

s = Serializer(token_secret)

data = s.loads(token)

now = int(time.time())

time_interval = now - data['time']

if time_interval >= max_time:

# create new token

token, refresh_token = creat_token()

return jsonify({"token": token, "refresh_token": refresh_token})

except SignatureExpired:

return "Token expired"

except Exception as ex:

print(ex)

return "Log in again"

return func(*args, **kwargs)

return decorator

def creat_token(uid):

now = int(time.time())

s = Serializer(token_secret, expires_in=max_time)

token = s.dumps({"uid": uid, "time": now}).decode("ascii")

refresh_s = Serializer(token_secret, expires_in=refresh_max_time)

refresh_token = refresh_s.dumps({"uid": uid, "time": now}).decode("ascii")

return token, refresh_token

@app.route('/token', methods=["POST"])

def token():

user_name = request.values.get('user_name')

password = request.values.get('password')

# @TODO 根据user_name和password 获取唯一的uid

uid = 10

token, refresh_token = creat_token(uid=uid)

return jsonify({"token": token, "refresh_token": refresh_token})

@app.route('/test', methods=['GET'])

@verify_token

def test():

return 'hello world'

if __name__ == "__main__":

app.run(host="0.0.0.0")

TimedJSONWebSignatureSerializer相比JSONWebSignatureSerializer在header中赠加了过期时间,如果过期会抛出SignatureExpired异常。

四、问题

4.1 用户登出,如何设置token无效?

JWT是无状态的,用户登出设置token无效就已经违背了JWT的设计原则,但是在实际应用场景中,这种功能是需要的,那该如何实现呢?提供几种思路:用户登出,浏览器端丢弃token

使用redis数据库,用户登出,从redis中删除对应的token,请求访问时,需要从redis库中取出对应的token,若没有,则表明已经登出为了保持数据的一致性,每一次认证都需要从redis中取出对应的token,每一次都以redis中的token为准。

4.2 使用redis,两个不同的设备,一个设备登出,另外一个设备如何处理?

请思考这样一种场景:同一个用户从两个设备登陆到服务端(设备1,设备2);

设备1登出,删除redis中的对应的token - 设备2再次请求数据时,redis中的数据为空,需要重新登录。

很明显,这种情况是不应该出现的,说一下自己的想法:每一个设备与用户生成唯一的key,保存在redis中,即设备1的用户登出,只删除对应的token,设备2的token仍然存在

服务器端维护一个版本号,相同用户不同设备登入,版本号加1,这样保持key的唯一性(和上面差不多)

欢迎大家【关注】我,我们一起学习进步。

另外,有问题大家评论区讨论,积极沟通。

如果本文对你有帮助,不要忘记【点赞】、【收藏】,拒绝伸手党!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值