1. Foreword
Imagine that you are browsing pages normally on the web. Suddenly a window pops up, telling you that your login is invalid and jumping back to the login page, asking you to log in again. Are you annoyed. At this time, the role of senseless refresh is reflected.
2. Plan
2.1 redis set expiration time
In the latest technology, tokens are generally stored in the Redis server and have an expiration time set. As long as the request is reissued within the valid time, the expiration time in Redis will be updated, so that only one token is needed. This solution is usually done on the back end.
2.2 dual token mode
2.21 Principle
- The user logs in and sends the account password to the server. If the login fails, the user returns to the client to log in again. After successful login, the server generates accessToken and refreshToken, and returns the generated token to the client.
- In the request interceptor, the request header carries the accessToken request data, and the server verifies whether the accessToken has expired. If the token is valid, continue to request data. If the token is invalid, invalidation information will be returned to the client.
- The client receives the request information sent by the server and determines whether there is accessToken invalidation information in the response interceptor of the secondary encapsulated axios, but no response data is returned. If there is invalid information, bring refreshToken to request a new accessToken.
- The server verifies whether refreshToken is valid. If valid, the token will be regenerated and the new token and prompt information will be returned to the client. If invalid, invalid information will be returned to the client.
- The client response interceptor determines whether the response information has a refreshToken that is valid or invalid. Invalid, log out of current login. Valid, re-store the new token and continue to request the data of the last request.
2.22 up code
Backend: node.js, koa2 server, jwt, koa-cors, etc. (You can use koa scaffolding to create projects. This project is based on koa scaffolding. The complete code can be found at the github address at the end of the article)
- Create new utils/token.js (double token)
const jwt=require('jsonwebtoken') const secret='2023F_Ycb/wp_sd' //Key /* expiresIn:5 Expiration time, time unit is seconds You can also write expiresIn:1d to represent one day 1h represents one hour */ // This time is for testing, so set the time to 5 seconds for short token and 15 seconds for long token const accessTokenTime=5 const refreshTokenTime=15 // Generate accessToken const accessToken=(payload={})=>{ // payload carries user information return jwt.sign(payload,secret,{expiresIn:accessTokenTime}) } //Generate refreshToken const refreshToken=(payload={})=>{ return jwt.sign(payload,secret,{expiresIn:refreshTokenTime}) } module.exports={ secret, accessToken, refreshToken }
- router/index.js creates routing interface
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' } }) /*Login interface*/ router.get('/login',(ctx)=>{ let code,msg,data=null code=2000 msg='Login successful, token obtained' data={ accessToken:accessToken(), refreshToken:refreshToken() } ctx.body={ code, msg, data } }) /*Data acquisition interface for testing*/ router.get('/getTestData',(ctx)=>{ let code,msg,data=null code=2000 msg='Get data successfully' ctx.body={ code, msg, data } }) /*Verify whether the long token is valid and refresh the short token It should be noted here that when refreshing the short token, the new long token will also be returned and the long token will be continued. This way active users will not be forced to log out during ongoing operations. Non-activity that has been inactive for a long time The user's long token has expired and he needs to log in again. */ router.get('/refresh',(ctx)=>{ let code,msg,data=null //Get the long token carried in the request header let r_tk=ctx.request.headers['pass'] //Parse token parameters token key callback function return information jwt.verify(r_tk,secret,(error)=>{ if(error){ code=4006, msg='The long token is invalid, please log in again' } else{ code = 2000, msg = 'Long token is valid, return new token' data = { accessToken: accessToken(), refreshToken: refreshToken() } } ctx.body={ code, msg:msg?msg:null, data } }) }) module.exports = router
3. Create new utils/auth.js (middleware)
const { secret } = require('./token') const jwt = require('jsonwebtoken') /*Whitelist, login and refresh short tokens are not restricted, so there is no need for token verification*/ const whiteList=['/login','/refresh'] const isWhiteList=(url,whiteList)=>{ return whiteList.find(item => item === url) ? true : false } /*middleware Verify whether the short token is valid */ const auth = async (ctx,next)=>{ let code, msg, data = null let url = ctx.path if(isWhiteList(url,whiteList)){ //Execute next step return await next() } else { // Get the short token carried in the request header const a_tk=ctx.request.headers['authorization'] if(!a_tk){ code=4003 msg='accessToken is invalid, no permission' ctx.body={ code, msg, data } } else{ // parse token await jwt.verify(a_tk,secret,async (error)=>{ if(error){ code=4003 msg='accessToken is invalid, no permission' ctx.body={ code, msg, data } } else { // token is valid return await next() } }) } } } module.exports=auth
- app.js
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
Front-end: vite, vue3, axios, etc. (The complete code can be found at the github address at the end of the article)
- Create new config/constants.js
export const ACCESS_TOKEN = 'a_tk' // short token field export const REFRESH_TOKEN = 'r_tk' // short token field export const AUTH = 'Authorization' //Header carries short token export const PASS = 'pass' //Header carries long token
- Create new config/storage.js
import * as constants from "./constants" //Storage short token export const setAccessToken = (token) => localStorage.setItem(constants.ACCESS_TOKEN, token) //Storage long token export const setRefreshToken = (token) => localStorage.setItem(constants.REFRESH_TOKEN, token) // Get short token export const getAccessToken = () => localStorage.getItem(constants.ACCESS_TOKEN) // Get long token export const getRefreshToken = () => localStorage.getItem(constants.REFRESH_TOKEN) // Delete short token export const removeAccessToken = () => localStorage.removeItem(constants.ACCESS_TOKEN) // Delete long token export const removeRefreshToken = () => localStorage.removeItem(constants.REFRESH_TOKEN)
3. Create new 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 //Set the switch to ensure that the short token can only be requested once at a time to prevent customers from doing this and making multiple requests. /*Add expired requests to the array*/ export const addRequest = (request) => { subscribes.push(request) } /*Call expired request*/ export const retryRequest = () => { console.log('Request the last interrupted data'); subscribes.forEach(request => request()) subscribes = [] } /*The short token expires, carry the token to request the token again*/ export const refreshToken=()=>{ console.log('flag--',flag) if(!flag){ flag = true; let r_tk = getRefreshToken() // Get long token if(r_tk){ server.get('/refresh',Object.assign({},{ headers:{PASS : r_tk} })).then((res)=>{ //The long token is invalid, log out if(res.code===4006){ flag = false removeRefreshToken(REFRESH_TOKEN) } else if(res.code===2000){ // Store new token setAccessToken(res.data.accessToken) setRefreshToken(res.data.refreshToken) flag = false // Request data retryRequest() } }) } } }
4. Create new 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', // your server timeout: 1000 * 10, headers: { "Content-type": "application/json" } }) /*Request interceptor*/ server.interceptors.request.use(config => { // Get the short token, carry it to the request header, and verify it on the server side let aToken = storage.getAccessToken(constants.ACCESS_TOKEN) config.headers[constants.AUTH] = aToken return config }) /*Response interceptor*/ server.interceptors.response.use( async response => { // Obtain configuration and backend response data let {config, data} = response console.log('Response prompt information:', data.msg); return new Promise((resolve, reject) => { // Short token is invalid if (data.code === 4003) { //Remove expired short token storage.removeAccessToken(constants.ACCESS_TOKEN) // Store the expired request to request a new short token and request again to achieve a senseless refresh. addRequest(() => resolve(server(config))) // Carry long token to request new token refreshToken() } else { // Effectively return the corresponding data resolve(data) } }) }, error => { return Promise.reject(error) } ) export default server
5. Create a new apis/index.js
import server from "../utils/server.js"; /*Log in*/ export const login = () => { return server({ url: '/login', method: 'get' }) } /*Request data*/ export const getList = () => { return server({ url: '/getTestData', method: 'get' }) }
6. Modify 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">Login</button> <button @click="getData">Request data</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>
2.23 Rendering
3. Complete project code
3.1 address
GitHub – heyu3913/doubleToken: No sense refresh
3.2 run
rear end:
cd server i pnpm start
front end
cd my-vue-app i pnpmdev
4 PS: Here you will get a free gpt address. You can set it up by yourself without any charge. Register and use:
Industry wizard
There will be times when strong winds and waves break, and the clouds and sails will be hung directly to help the sea