前端刷新token,判断token是否过期(jwt鉴权)

4.1 什么是 JWT

JWT 是 Auth0 提出的通过 对 JSON 进行加密签名来实现授权验证的方案;

就是登录成功后将相关用户信息组成 JSON 对象,然后对这个对象进行某种方式的加密,返回给客户端;

客户端在下次请求时带上这个 Token;

服务端再收到请求时校验 token 合法性,其实也就是在校验请求的合法性。

4.2 JWT 的组成

JWT 由三部分组成: Header 头部、 Payload 负载 和 Signature 签名

它是一个很长的字符串,中间用点(.)分隔成三个部分。列如 :

javascript 复制代码
`eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c`

Header 头部:

在 Header 中通常包含了两部分:

typ:代表 Token 的类型,这里使用的是 JWT 类型;

alg:使用的 Hash 算法,例如 HMAC SHA256 或 RSA.

javascript 复制代码
 {
   "alg": "HS256",
   "typ": "JWT"
 }

Payload 负载:

它包含一些声明 Claim (实体的描述,通常是一个 User 信息,还包括一些其他的元数据) ,用来存放实际需要传递的数据,JWT 规定了7个官方字段:

iss (issuer):签发人

exp (expiration time):过期时间

sub (subject):主题

aud (audience):受众

nbf (Not Before):生效时间

iat (Issued At):签发时间

jti (JWT ID):编号

除了官方字段,你还可以在这个部分定义私有字段,下面就是一个例子。

javascript 复制代码
 {
   "sub": "1234567890",
   "name": "John Doe",
   "admin": true
 }

Signature 签名

Signature 部分是对前两部分的签名,防止数据篡改。

首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。

javascript 复制代码
 HMACSHA256(
   base64UrlEncode(header) + "." +
   base64UrlEncode(payload),
   secret)

4.3 JWT 的使用方式

客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。

此后,客户端每次与服务器通信,都要带上这个 JWT。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求的头信息Authorization字段里面。

javascript 复制代码
 Authorization: Bearer <token>

4.4 JWT 的认证流程图

其实 JWT 的认证流程与 Token 的认证流程差不多,只是不需要再单独去查询数据库查找用户用户;简要概括如下:

4.5 JWT 的优点

不需要在服务端保存会话信息(RESTful API 的原则之一就是无状态),所以易于应用的扩展,即信息不保存在服务端,不会存在 Session 扩展不方便的情况;

JWT 中的 Payload 负载可以存储常用信息,用于信息交换,有效地使用 JWT,可以降低服务端查询数据库的次数

4.6 JWT 的缺点

加密问题: JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。

到期问题: 由于服务器不保存 Session 状态,因此无法在使用过程中废止某个 Token,或者更改 Token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。

5.判断token是否过期

1、刷新令牌(Refresh Token)

复制代码
    在用户登录时,除了发放一个访问令牌(Access Token)以外,再发放一个刷新令牌(Refrsh Token)。

    访问令牌的有效期比较短,刷新令牌的有效期比较长。

    当访问令牌过期时,使用刷新令牌向服务器请求新的访问令牌。如果刷新令牌也过期,则跳转回登录界面。

    这种方式的优点是可以避免用户频繁登录,但需要妥善保管刷新令牌,因为它的安全性比访问令牌更高。

一般会根据时间戳判断时间是否超过有效期时间(可借鉴下面的例子进行操作)

login.js中,登录后设置即将过期时间:

javascript 复制代码
import {setTokenOverdueTime} from '../src/api/refreshToken'
localStorage.setItem("tokenOverdueTime", setTokenOverdueTime());
1
2
新建一个refreshToken.js文件,文件内容如下:

import Vue from 'vue'

//设置token快过期时间,当前时间+20分钟
export function setTokenOverdueTime() {
    let t = new Date().getTime() + 1200000;
    let d = new Date(t);
    let theMonth = d.getMonth() + 1;
    let theDate = d.getDate();
    let theHours = d.getHours();
    let theMinutes = d.getMinutes();
    if (theMonth < 10) {
        theMonth = '0' + theMonth
    }
    if (theDate < 10) {
        theDate = '0' + theDate
    }
    if (theHours < 10) {
        theHours = '0' + theHours
    }
    if (theMinutes < 10) {
        theMinutes = '0' + theMinutes
    }
    let date = d.getFullYear() + '-' + theMonth + '-' + theDate
    let time = theHours + ':' + theMinutes
    let Spare = date + ' ' + time
    return Spare;
}

// 判断token是否即将过期
function isTokenExpired() {
    let curTime = new Date();
    //获取即将过期时间
    let tokenOverdueTime = localStorage.getItem("tokenOverdueTime");
    // 判断当前时间是否大于即将过期时间
    if (curTime > new Date(tokenOverdueTime)) {
        return true
    }
    return false;
}
// 将所有的请求都push到数组中,其实数组是[function(token){}, function(token){},...]
function cacheRequestArrHandle(cb) {
    cacheRequestArr.push(cb);
}
// 数组中的请求得到新的token之后自执行,用新的token去重新发起请求
function afreshRequest(token) {
    cacheRequestArr.map(cb => cb(token));
    cacheRequestArr = [];
}
//定义一个空数组,用来缓存请求
let cacheRequestArr = [];
// 是否正在刷新的标志
window.isRefreshing = false;
export function isRefreshToken(instance, config, store) {
	// 判断token是否即将过期,且不是请求刷新token的接口
    if(isTokenExpired() && config.url !== '/refreshToken'){
    	// 所有的请求来了,先判断是否正在刷新token,
        // 如果不是,将刷新token标志置为true并请求刷新token.
        // 如果是,则先将请求缓存到数组中
        // 等到刷新完token后再次重新请求之前缓存的请求接口即可
        if (!window.isRefreshing) {
            window.isRefreshing = true;
            instance.get('/refreshToken').then(res => {
                if(res.data.status === 0){
                	// 更新 store和缓存里的值
                    localStorage.setItem("userToken", JSON.stringify(res.data.data));
                    store.dispatch("setUserToken", res.data.data);
                    // 更新即将过期时间
                    localStorage.setItem("tokenOverdueTime", setTokenOverdueTime());
                    // 将刷新的token替代老的token
                    config.headers.token = res.data.data;
                    // 刷新token完成后重新请求之前的请求
                    afreshRequest(res.data.data);
                }
            }).finally(() => {
                window.isRefreshing = false;
            })
            // 下面这段代码一定要写,不然第一个请求的接口带过去的token还是原来的,要将第一个请求也缓存起来
            let retry = new Promise((resolve) => {
                cacheRequestArrHandle((token) => {
                    config.headers.token = token; // token为刷新完成后传入的token
                    // 将请求挂起
                    resolve(config)
                })
            })
            return retry;
        }
        else{
            let retry = new Promise((resolve) => {
                cacheRequestArrHandle((token) => {
                    config.headers.token = token; // token为刷新完成后传入的token
                    // 将请求挂起
                    resolve(config)
                })
            })
            return retry;
        }
    }
    else{
        return config
    }
}

设置拦截器:

javascript 复制代码
import store from '../../store';
import {isRefreshToken} from '../src/api/refreshToken'
/**
 * 请求拦截
 * interceptors
 * @param instance
 * */
export const interceptors = (instance) => {
    //请求拦截
    instance.interceptors.request.use(config => {
        //  请求头携带token 和env
        let token = localStorage.getItem("userToken") ? JSON.parse(localStorage.getItem("userToken")) : null;
        if(token){
            config.headers.token = token;
        }
        let flag = isRefreshToken(instance, config, store);
        return flag ? flag : config;
    }, (error) => {
        return Promise.reject(error);
    })
    //响应拦截
    instance.interceptors.response.use(res => {
        //返回数据
        if(res.data.dev) return res;
        if (res.data.code !== 0) {
            return Promise.reject(res);
        }else {
            return res;
        }
    }, (error) => {
        return Promise.reject(error);
    })

2、滑动窗口

复制代码
    用户每次使用使用访问令牌时,服务器都会更新访问令牌的过期时间。

    这种方式的优点是用户只要频繁访问,就不需要登录,但可能会增加服务器负担。

3、重新登录

复制代码
    当访问令牌过期时,跳转回登录界面,让用户重新登录。这是最简单的一种方式,但可能会影响用户体验。
相关推荐
腾讯TNTWeb前端团队7 小时前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
范文杰10 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪10 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪10 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy11 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom12 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom12 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom12 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom12 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试
uhakadotcom12 小时前
Node.js 包管理器:npm vs pnpm
前端·javascript·面试