无感知更新token详解

文章目录

  • 前言
  • 实现思路
  • flask实现
  • 前端实现
  • 总结


前言

在状态保持中,如果设置token的时间过长,会导致token长时间暴露,带来较高的风险。

所以token的过期时间一般都设置为两小时

为了不降低用户体验,所以要实现无感知的更新token。


实现思路

一般我们在用户登陆时,都会生成两个token。

token(2小时):用于用户正常验证。

refresh_token(15天):更新(生成新)两小时token时,需要验证refresh_token是否过期或被篡改。

具体思路:

无感知更新token详解_第1张图片

当验证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实现

使用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的具体内容了

你可能感兴趣的:(状态保持,前端,java)