In the past, the authentication method of blog was session. Recently, I took some time to change it to jwt, and I learned about JWT by the way. The introduction of JWT can be seen Ruan Yifeng's Articles.
jwt implementation process
The figure above is the simplest jwt process. When token expires or fails, it will jump back to the login page. But my ideal state is not to log in for a week, and the expiration time will be delayed for a week after each login. Without modifying the database, the following two methods are proposed:
- Set the expiration time of the token to 7 days. Each time when the interface is accessed, the backend regenerates the token according to the current time, and then returns the newly generated token in each interface. After the front end receives the token, it stores it in a cookie and brings the new token in the next request.
- After login, a token with an expiration time of 3 hours and a refreshToken with an expiration time of 7 days are generated. When the front-end requests, it carries token information in the header. If the token expires, the front-end carries refreshToken to access the refresh token interface. If the refreshToken does not expire, the back-end returns the new token and refreshToken, otherwise the front-end jumps to the login page. The front end then re-accesses the new interface based on the newly returned token.
The obvious disadvantage of scheme 1 is that each call to the interface generates a token, which increases unnecessary overhead. And each of the back-end interfaces carries token information, and the back-end changes a lot=. = Scheme 2 seems to have no obvious shortcomings, so we choose Scheme 2 to practice.
The above figure shows the refresh token authentication process. You can see that there is one more step to refresh token and refresh token than the simplest process, which is also the key to refresh token.
Implementation code
Backend node
As agreed in the project, 401 indicates token timeout and 402 indicates refreshToken invalid
Initialize token and refreshToken at login time
// jwt.js const jwt = require('jsonwebtoken') const { SECRET_KEY, JWT_EXPIRES, REFRESH_JWT_EXPIRES } = require('../config/config') // Introduce SECRET_KEY, token expiration time, refreshToken expiration time from configuration file const { cacheUser } = require('../cache/user') // Caching User Names with Closures /** * Generating token and initToken * @param {string} username */ function initToken (username) { return { token: jwt.sign({ username: username }, SECRET_KEY, { expiresIn: JWT_EXPIRES }), refresh_token: jwt.sign({ username }, SECRET_KEY, { expiresIn: REFRESH_JWT_EXPIRES }) } } /** * Verify token/refreshToken * @param {string} token The format is `Beare ${token}` */ function validateToken (token, type) { try { token = token.replace(/^Beare /, '') const { username } = jwt.verify(token, SECRET_KEY) if (type !== 'refreshToken') { // If it is token and token takes effect, cache token cacheUser.setUserName(username) } } catch (e) { throw new Error(e.message) } } module.exports = { initToken, getTokenUser, validateToken }
Then call the validateToken method in the middleware
// Check whether to log in to the middleware method checkLogin (req, res, next) { const { headers: { authorization } } = req if (!authorization) { res.status(402).json({ code: 'ERROR', data: 'No login information detected' }) return false } try { validateToken(authorization) } catch (e) { if (e.message === 'jwt expired') { // token timeout res.status(401).json({ code: 'ERROR', data: 'login timeout' }) return false } else { res.status(402).json({ code: 'ERROR', data: 'No login information detected' }) return false } }
In some interfaces that require login authentication (such as getting user message interface), middleware is called to verify login first. The token is validated once in the middleware. Here I write a user cache with closures. After the middleware validation is successful, the user is stored in the closure. Then the getUserName method of the closure can be invoked to get the user information in the interface without parsing the token. This can avoid the trouble of the middleware validation token is successful but the validation token expires in the interface.
/** * Write a closure to cache user names */ const cacheUser = (() => { let username = '' return { setUserName: (user) => { if (username === user) { return false } username = user }, getUserName: () => { return username }, clearUserName: () => { username = '' } } })() module.exports = { cacheUser }
After successful login, get token and refreshToken and return to the front end.
Client js
js needs to re-encapsulate axios functions
import axios from 'axios' import qs from 'qs' ...... // Refresh token and refreshToken according to refreshToken const fetchRefreshToken = () => { const token = Cookies.get('refreshToken') return axios({ url: '/api/signin/refreshToken', headers: { Authorization: `Beare ${token}` }, method: 'get' }) } export const post = (url, formData, headers = {}) => { const token = Cookies.get('token') headers = Object.assign({}, headers, { Authorization: `Beare ${token}` }) return axios({ url, headers, method: 'post', data: qs.stringify(formData) }).then(res => { return res }).catch(e => { if (e.response.status === 401) { // Token timeout, access refresh token interface return fetchRefreshToken().then(res => { const { data } = res.data const { token, refresh_token: refreshToken } = data Cookies.set('token', token) Cookies.set('refreshToken', refreshToken) return post(url, formData, headers) // Re-invoke this interface }) } }) } ...
Modify axios interception function
// The axios interceptor jumps to the login page if it is not logged in axios.interceptors.response.use( res => { if (res.data.code === 'OK') { return res } else { // Tips for error reporting message.error(res.data.data, 10) return Promise.reject(res) } }, error => { if (error.response) { switch (error.response.status) { case 402: // Login timeout, jump to login page, self-realization // store.dispatch(actionCreators.logoutSuccess()) break // Jump to the login page default: break } } return Promise.reject(error) } )
Optimization point
So far, token automatic refresh has been implemented. But this method is still flawed. For example, when multiple interfaces are invoked at the same time, if the token expires, because the request is asynchronous, the refreshtoken interface will be invoked many times. This optimal solution should be the first interface to refresh token, and the remaining interface should wait until token refreshes before it starts calling.
sf has An article This method is explained in more detail.