想象下,你正常在網頁上瀏覽頁面。突然彈出一個視窗,告訴你登入失效,跳回了登入頁面,讓你重新登入。你是不是很惱火。這時候無感重新整理的作用就體現出來了。
在最新的技術當中,token一般都是在Redis伺服器存著,設定過期時間。只要在有效時間內,重新發出請求,Redis中的過期時間會去更新,這樣前只需要一個token。這個方案一般是後端做。
後端:node.js、koa2伺服器、jwt、koa-cors等(可使用koa腳手架建立專案,本專案基於koa腳手架建立。完整程式碼可見文章末尾github地址)
const jwt=require('jsonwebtoken')
const secret='2023F_Ycb/wp_sd' // 金鑰
/*
expiresIn:5 過期時間,時間單位是秒
也可以這麼寫 expiresIn:1d 代表一天
1h 代表一小時
*/
// 本次是為了測試,所以設定時間 短token5秒 長token15秒
const accessTokenTime=5
const refreshTokenTime=15
// 生成accessToken
const accessToken=(payload={})=>{ // payload 攜帶使用者資訊
return jwt.sign(payload,secret,{expiresIn:accessTokenTime})
}
//生成refreshToken
const refreshToken=(payload={})=>{
return jwt.sign(payload,secret,{expiresIn:refreshTokenTime})
}
module.exports={
secret,
accessToken,
refreshToken
}
const router = require('koa-router')()
const jwt = require('jsonwebtoken')
const { accessToken, refreshToken, secret }=require('../utils/token')
router.get('/', async (ctx, next) => {
await ctx.render('index', {
title: 'Hello Koa 2!'
})
})
router.get('/string', async (ctx, next) => {
ctx.body = 'koa2 string'
})
router.get('/json', async (ctx, next) => {
ctx.body = {
title: 'koa2 json'
}
})
/*登入介面*/
router.get('/login',(ctx)=>{
let code,msg,data=null
code=2000
msg='登入成功,獲取到token'
data={
accessToken:accessToken(),
refreshToken:refreshToken()
}
ctx.body={
code,
msg,
data
}
})
/*用於測試的獲取資料介面*/
router.get('/getTestData',(ctx)=>{
let code,msg,data=null
code=2000
msg='獲取資料成功'
ctx.body={
code,
msg,
data
}
})
/*驗證長token是否有效,重新整理短token
這裡要注意,在重新整理短token的時候回也返回新的長token,延續長token,
這樣活躍使用者在持續操作過程中不會被迫退出登入。長時間無操作的非活
躍使用者長token過期重新登入
*/
router.get('/refresh',(ctx)=>{
let code,msg,data=null
//獲取請求頭中攜帶的長token
let r_tk=ctx.request.headers['pass']
//解析token 引數 token 金鑰 回撥函數返回資訊
jwt.verify(r_tk,secret,(error)=>{
if(error){
code=4006,
msg='長token無效,請重新登入'
}
else{
code = 2000,
msg = '長token有效,返回新的token'
data = {
accessToken: accessToken(),
refreshToken: refreshToken()
}
}
ctx.body={
code,
msg:msg?msg:null,
data
}
})
})
module.exports = router
3.新建utils/auth.js (中介軟體)
const { secret } = require('./token')
const jwt = require('jsonwebtoken')
/*白名單,登入、重新整理短token不受限制,也就不用token驗證*/
const whiteList=['/login','/refresh']
const isWhiteList=(url,whiteList)=>{
return whiteList.find(item => item === url) ? true : false
}
/*中介軟體
驗證短token是否有效
*/
const auth = async (ctx,next)=>{
let code, msg, data = null
let url = ctx.path
if(isWhiteList(url,whiteList)){
// 執行下一步
return await next()
} else {
// 獲取請求頭攜帶的短token
const a_tk=ctx.request.headers['authorization']
if(!a_tk){
code=4003
msg='accessToken無效,無許可權'
ctx.body={
code,
msg,
data
}
} else{
// 解析token
await jwt.verify(a_tk,secret,async (error)=>{
if(error){
code=4003
msg='accessToken無效,無許可權'
ctx.body={
code,
msg,
data
}
} else {
// token有效
return await next()
}
})
}
}
}
module.exports=auth
const Koa = require('koa')
const app = new Koa()
const views = require('koa-views')
const json = require('koa-json')
const onerror = require('koa-onerror')
const bodyparser = require('koa-bodyparser')
const logger = require('koa-logger')
const cors=require('koa-cors')
const index = require('./routes/index')
const users = require('./routes/users')
const auth=require('./utils/auth')
// error handler
onerror(app)
// middlewares
app.use(bodyparser({
enableTypes:['json', 'form', 'text']
}))
app.use(json())
app.use(logger())
app.use(require('koa-static')(__dirname + '/public'))
app.use(cors())
app.use(auth)
app.use(views(__dirname + '/views', {
extension: 'pug'
}))
// logger
app.use(async (ctx, next) => {
const start = new Date()
await next()
const ms = new Date() - start
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`)
})
// routes
app.use(index.routes(), index.allowedMethods())
app.use(users.routes(), users.allowedMethods())
// error-handling
app.on('error', (err, ctx) => {
console.error('server error', err, ctx)
});
module.exports = app
前端:vite、vue3、axios等 (完整程式碼可見文章末尾github地址)
export const ACCESS_TOKEN = 'a_tk' // 短token欄位
export const REFRESH_TOKEN = 'r_tk' // 短token欄位
export const AUTH = 'Authorization' // header頭部 攜帶短token
export const PASS = 'pass' // header頭部 攜帶長token
import * as constants from "./constants"
// 儲存短token
export const setAccessToken = (token) => localStorage.setItem(constants.ACCESS_TOKEN, token)
// 儲存長token
export const setRefreshToken = (token) => localStorage.setItem(constants.REFRESH_TOKEN, token)
// 獲取短token
export const getAccessToken = () => localStorage.getItem(constants.ACCESS_TOKEN)
// 獲取長token
export const getRefreshToken = () => localStorage.getItem(constants.REFRESH_TOKEN)
// 刪除短token
export const removeAccessToken = () => localStorage.removeItem(constants.ACCESS_TOKEN)
// 刪除長token
export const removeRefreshToken = () => localStorage.removeItem(constants.REFRESH_TOKEN)
3.新建utils/refresh.js
export {REFRESH_TOKEN,PASS} from '../config/constants.js'
import { getRefreshToken, removeRefreshToken, setAccessToken, setRefreshToken} from '../config/storage'
import server from "./server";
let subscribes=[]
let flag=false // 設定開關,保證一次只能請求一次短token,防止客戶多此操作,多次請求
/*把過期請求新增在陣列中*/
export const addRequest = (request) => {
subscribes.push(request)
}
/*呼叫過期請求*/
export const retryRequest = () => {
console.log('重新請求上次中斷的資料');
subscribes.forEach(request => request())
subscribes = []
}
/*短token過期,攜帶token去重新請求token*/
export const refreshToken=()=>{
console.log('flag--',flag)
if(!flag){
flag = true;
let r_tk = getRefreshToken() // 獲取長token
if(r_tk){
server.get('/refresh',Object.assign({},{
headers:{PASS : r_tk}
})).then((res)=>{
//長token失效,退出登入
if(res.code===4006){
flag = false
removeRefreshToken(REFRESH_TOKEN)
} else if(res.code===2000){
// 儲存新的token
setAccessToken(res.data.accessToken)
setRefreshToken(res.data.refreshToken)
flag = false
// 重新請求資料
retryRequest()
}
})
}
}
}
4.新建utils/server.js
import axios from "axios";
import * as storage from "../config/storage"
import * as constants from '../config/constants'
import { addRequest, refreshToken } from "./refresh";
const server = axios.create({
baseURL: 'http://localhost:3000', // 你的伺服器
timeout: 1000 * 10,
headers: {
"Content-type": "application/json"
}
})
/*請求攔截器*/
server.interceptors.request.use(config => {
// 獲取短token,攜帶到請求頭,伺服器端校驗
let aToken = storage.getAccessToken(constants.ACCESS_TOKEN)
config.headers[constants.AUTH] = aToken
return config
})
/*響應攔截器*/
server.interceptors.response.use(
async response => {
// 獲取到設定和後端響應的資料
let { config, data } = response
console.log('響應提示資訊:', data.msg);
return new Promise((resolve, reject) => {
// 短token失效
if (data.code === 4003) {
// 移除失效的短token
storage.removeAccessToken(constants.ACCESS_TOKEN)
// 把過期請求儲存起來,用於請求到新的短token,再次請求,達到無感重新整理
addRequest(() => resolve(server(config)))
// 攜帶長token去請求新的token
refreshToken()
} else {
// 有效返回相應的資料
resolve(data)
}
})
},
error => {
return Promise.reject(error)
}
)
export default server
5.新建apis/index.js
import server from "../utils/server.js";
/*登入*/
export const login = () => {
return server({
url: '/login',
method: 'get'
})
}
/*請求資料*/
export const getList = () => {
return server({
url: '/getTestData',
method: 'get'
})
}
6.修改App.vue
<script setup>
import {login,getList} from "./apis";
import {setAccessToken,setRefreshToken} from "./config/storage";
const getToken=()=>{
login().then(res=>{
setAccessToken(res.data.accessToken)
setRefreshToken(res.data.refreshToken)
})
}
const getData = ()=>{
getList()
}
</script>
<template>
<button @click="getToken">登入</button>
<button @click="getData">請求資料</button>
</template>
<style scoped>
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vue:hover {
filter: drop-shadow(0 0 2em #42b883aa);
}
</style>
https://github.com/heyu3913/doubleToken
後端:
cd server
pnpm i
pnpm start
前端
cd my-vue-app
pnpm i
pnpm dev