在状态保持中,如果设置token的时间过长,会导致token长时间暴露,带来较高的风险。
所以token的过期时间一般都设置为两小时。
为了不降低用户体验,所以要实现无感知的更新token。
一般我们在用户登陆时,都会生成两个token。
token(2小时):用于用户正常验证。
refresh_token(15天):更新(生成新)两小时token时,需要验证refresh_token是否过期或被篡改。
具体思路:
当验证token时,发现token过期时返回自定义状态码(例:409、408)。
前端使用响应拦截器,根据自定义的状态码进行拦截。(此时两小时token过期需要更新token)
在发起更新token请求时,取出refresh_token,添加到请求头,发起更新token请求。
注意:默认所有请求会被请求拦截器,拦截携带token(2小时)。
更新token请求时,需携带refresh_token(15天)。
后端验证15天token,如果过期则返回token过期,需用户重新登录。
未过期,则解码token从payload中获取用户信息,重新生成token(2小时)、refresh_token(15天)。
最后返回前端,前端接收更新token、refresh_token。
这里更新token时,作者默认为用户登录,更新了用户的登陆时间。
也就是说当用户浏览页面时,两小时更新一次token、refresh_token、用户登录时间 。
refresh_token更新:相当于每次更新token时,延长了用户状态保持的时间(15天)。
关于refresh_token过期
过期条件:当用户距离上次访问时间超过15天,也就时十五天以内未访问页面,refresh_token就会过期,用户再次访问时需重新登录账号。
总结
2小时以内:用户访问不做任何操作。
2小时至15天以内:用户访问,则更新token、refresh_token、用户登录时间。
超过15天:用户访问需重新登录账号
使用flask钩子函数(before_request),在路由执行前验证token
验证token函数
import jwt
from flask import current_app,request,g,jsonify
from common.utils.jwt_utils import check_token,gen_token
from jwt.exceptions import ExpiredSignatureError,DecodeError
import re
def jwt_authentication():
# 获取访问路由
path = request.url_rule
if re.findall(r"register|send_sms_code|login", str(path), re.I): # re.I 忽略大小写
# 继续往后走
return None
# 获取token
token = request.headers.get("Authorization")
g.user_id = None
# 判单token是否存在
if token:
try:
key = current_app.config.get("SECRET_KEY")
payload = jwt.decode(token, key=key, algorithms=['HS256'])
# token 校验成功, 把用户id 写入g 对象
g.user_id = payload.get('user_id')
# 判断是否是刷新生成的token
return None
except ExpiredSignatureError:
# print("token过期")
return jsonify(msg="刷新token", code=401) # 返回自定义状态码
except DecodeError:
# print("token认证失败,token被篡改")
return jsonify(msg="请重新登陆!", code=408) # 返回自定义状态码
return jsonify(msg="请重新登陆!", code=401) # 返回自定义状态码
更新token视图
# 刷新token
class refresh_token(Resource):
def get(self):
try:
id = g.user_id
user = UserModel.query.filter_by(id=id)
user.update({"last_login": datetime.datetime.now()}) # 更新用户登录时间
db.session.commit()
user = user.first()
payload = {
"user_id": user.id,
"username": user.username,
"account": user.account,
}
token,refresh_token = _gen_token(payload) # 调用生成token函数,生成token(2小时)、refresh_token(15天)
return jsonify(
message="更新token!",
code=200,
data={
"user_id" : user.id,
"token":token,
"refresh_token":refresh_token
}
)
except Exception as e:
print("错误",e)
return jsonify(message="服务器错误!", code=500)
main文件
// main文件
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import installElementPlus from './plugins/element'
import store from './store/index'
const app = createApp(App)
installElementPlus(app)
app.use(router).use(store).mount('#app')
import axios from "axios";
// 是否正在更新token的标记
let isRefreshing = false
// 重试请求队列,每一项将是一个待执行的函数形式
let requests = []
// 创建axios实例
const service = axios.create({
// axios中请求配置有baseURL选项,表示请求URL公共部分
// baseURL: process.env.VUE_APP_BASE_API,
baseURL: 'http://127.0.0.1:8000',
// 超时
timeout: 10000,
})
import instance from 'axios'
// 请求拦截器
instance.interceptors.request.use(function (config) {
let news_token= localStorage.getItem('news_token')
if(news_token && !isRefreshing){ // 更新token时不携带token(2小时)
news_token= JSON.parse(news_token)
let token = news_token.token;
config.headers.Authorization = token;
}
return config
},
function (err){
return Promise.reject(err)
}
)
// 响应拦截
// 服务器返回数据之后都会先执行次方法
instance.interceptors.response.use(function (response){
// console.log("拦截",response)
let code = response.data.code
if (code === 401) { // token过期
const config = response.config
// console.log("重试请求",config)
if (!isRefreshing) {
console.log("刷新token")
refresh_token() // 调用更新token请求函数
}
// 将所有错误的请求存入重试队列中,等待token更新完毕重试请求。
return new Promise((resolve) => { // 返回一个未执行resolve的promise
// 将resolve放进队列,用一个函数形式来保存,等token刷新后直接执行
requests.push((token) => {
config.headers.Authorization = token // 使用更新后的token,重试请求
resolve(service(config))
})
})
}
return response
},
function (err){
return Promise.reject(err)
}
)
// 更新token函数
function refresh_token() {
isRefreshing = true
let news_token= localStorage.getItem('news_token')
news_token= JSON.parse(news_token)
let refresh_token = blogs_token.refresh_token
return axios.get("/refresh_token/", {
headers: {
"Authorization": refresh_token
}
}
).then(resp => {
console.log("更新token请求", resp)
if (resp.data.code != 200) { //refresh_token 过期重新登录
localStorage.clear()
router.push('/login')
}
let news_token= {
user_id: resp.data.user_id,
token: resp.data.token,
refresh_token: resp.data.refresh_token,
}
localStorage.setItem("news_token", JSON.stringify(news_token));
console.log("请求队列", requests)
isRefreshing = false // 更新token完毕
let token = resp.data.token;
// 更新token完毕token,将队列中的所有请求进行重试
requests.forEach(cb => cb(token)) // 传入更新后的token,重试请求
// 重试完了清空这个队列
requests = []
}).catch(err => {
console.log(err)
}).finally(() => {
console.log("重试之前的请求")
})
}
以上就是无感知更新token的具体内容了