Double token mechanism
About * * "double token mechanism" * * refers to a security mechanism that keeps the user's login status valid during the active period.
Generally, in order to verify the user's identity or login status, when the user logs in, the server will generate a token that stores or points to the user's information and return it to the client for storage. This token is called access_token.
Access when requesting other interfaces_ The token is sent to the server (via Header or Cookie).
Server access_ The token obtains the user information as the basis for authentication.
In order to prevent the token from being maliciously used by people other than the user, the maximum active time of a user is generally estimated as access_ Once the validity period of the token expires, the authentication request fails, and the client is obliged to notify the user (such as forced logout, prompt to log in again, etc.).
But when access_ When the token expires, the user is still active and suddenly prompted to log in. This experience is not very friendly.
Cancel access without_ On the premise of token validity, developers came up with the method of double token.
That is, the server regenerates into a token, which is specially used to refresh (or update) access_token. The refresh method can be to extend access_ The validity period of the token can also be to generate and return a new access_token. The token used for refresh is called refresh_token.
What the client needs to improve is to actively request the server to refresh access when necessary_ Token, which realizes the effect of automatic re login when the user's login expires during the active period, and greatly improves the user experience.
Through refresh_ The contents that can be obtained by token can include:
- Client source
- access_token
- User information
- other
The server can refresh_token and access_ The token is returned to the client as a parameter for the client to send a refresh request. It can implement more security mechanisms, or it can not be provided to the client. Its ultimate purpose is to refresh access_token.
Now the login status is maintained when the user is active, but refresh is still required for security_ Set a valid period for token when refresh_ When the token also expires, access cannot be refreshed_ Token, so that if the login expires, the client will notify the user.
Set refresh_ The purpose of token validity is to reset the login status of the user after the user is no longer active for a certain time, so as to realize the same security mechanism for the validity of a single token.
As for refresh access_ Whether to synchronously refresh refresh when token_ The token is based on the actual needs.
refresh_ The validity period of token must be greater than access_ The validity period of a token is generally twice that of the latter.
axios package
Front end request encapsulation processing based on axios dual token mechanism:
import axios from 'axios' // API status code const SUCCESS = '200' // success const FORBIDDEN = '403' // token expiration // axios instance const service = axios.create() // Request queue const requestQueue = { // The request list stores the requests initiated during the refresh token list: [], // Add request add(config, resolve) { // failed = true indicates that the token acquisition fails. It is used to determine the Promise of the request when clearing the queue to avoid memory accumulation this.list.push((failed) => { if (failed) { resolve() } else { // Update token config.headers.access_token = storage.getItem('access_token') // Re request resolve(service(config)) } }) }, // Execution queue // failed = true indicates that the token acquisition fails. It is used to determine the Promise of the request when clearing the queue to avoid memory accumulation execute(failed) { let fn // eslint-disable-next-line while (fn = this.list.shift()) { fn(failed) } }, // Empty queue clear() { this.execute(true) }, } // Request interception service.interceptors.request.use( config => { const accessToken = storage.getItem('access_token') // If you are not logged in, request directly if (!accessToken) { return config } // Add token in the header config.headers.access_token = accessToken // Judge whether it is a refresh token operation if (config.url.includes('refreshToken')) { return config } return handleByRefreshStatus(config, 'request') }, error => { return Promise.reject(error) }, ) // Response interception service.interceptors.response.use( response => { // Judge whether it is a refresh token operation if (response.config.url.includes(refreshToken)) { return response } const res = response.data // token expiration if (res.code === FORBIDDEN) { return handleByRefreshStatus(response.config) } // Request failed (server response level) if (res.code !== SUCCESS) { return Promise.reject(res) } else { return res } }, error => { // Request failed (exception level) return Promise.reject(error) }, ) // Processing requests based on refresh status function handleByRefreshStatus(config, type = 'response') { return new Promise((resolve) => { const status = localStorage.getItem('refresh_token_status') if (type === 'request') { // Determine whether the token is being refreshed if (!status) { resolve(config) } else { requestQueue.add(config, resolve) } } else { // Add the request to the queue and execute the queue after the refresh is completed requestQueue.add(config, resolve) // Judge the token refresh status if (!status) { // Set refresh status localStorage.setItem('refresh_token_status', 1) // Get new token refreshTokenFetch(localStorage.getItem('access_token')) .then(({ data: res }) => { if (!(res && res.data && res.data.access_token)) { throw res } localStorage.setItem('access_token', res.data.access_token) localStorage.removeItem('refresh_token_status') // Execution queue requestQueue.execute() }) .catch(err => { console.error('refresh token failed', err) localStorage.removeItem('refresh_token_status') // Empty queue requestQueue.clear() // Login expiration processing loginExpired() }) } } }) } // Login expiration processing function loginExpired() { // The client prompts the user to log in and expire } // Refresh token request function refreshTokenFetch(refresh_token) { return service.post('/refreshToken', { refresh_token, }) } export default service
Description supplement
Main logic
- When the request returns 403 (token expired), create a new request according to the current request and add it to the queue, send a refresh token request, and execute the request in the queue if the acquisition is successful.
- When the client sends a request, if the token is being refreshed, the request is appended to the queue and waiting for execution.
localStorage storage
Storing tokens and other information in localStorage is to share tokens and refresh status when multiple tabs open applications. Avoid conflicts caused by triggering refresh requests respectively.
Specific requirements shall prevail.
Empty queue
The purpose of emptying the queue is to clean up memory, not just empty the queue array.
The request initiated by axios actually creates a Promise. When the refresh token is triggered, the processing of the current request is stored in the queue for execution, and the processing of the request is the Promise's resolution.
If the processing of user login expiration does not reset the page state (e.g. location jump), and the request in the queue is not completed, Promise will always be in the pending state, so the closure generated by the request process will always be stored in memory.
Therefore, each request in the queue must be processed (Promise).