After the user logs in, a user ID is returned, and the user can be identified by requesting another interface with this ID.


There are two options for identifying login status: session and jwt.


session is a cookie that returns an id associated with a session object stored in the server’s memory. When requesting, the server takes out the session object corresponding to the id in the cookie and gets the user’s information.


Instead of storing it on the server, jwt returns the user’s information directly in a token, so that every time a request is made with this token, the server can retrieve the user’s information from it.


This token is usually placed in a header called authorization.


The two options are one server-side storage, which carries the identifier via a cookie, and one client-side storage, which carries the identifier via a header.


The session scheme does not support distribution by default, because it is stored in memory on one server and not on another.


jwt’s solution naturally supports distribution because the information is stored in tokens and can simply be retrieved from them.

 So jwt’s program is still used a lot.


The server puts the user information into the token, sets an expiration time, the client carries the token in the header of the authorization when requesting, and the server verifies that the token passes, then it can get the user information from it.

 But there’s a problem with that:


The token has an expiration time, for example, 3 days, after which you will need to log in again to access it.

 This is not a good experience.


Imagine you’re using an app and suddenly you’re on the login page, telling you that you need to log in again.

 Was it a bad experience?


So add a renewal mechanism, i.e. extend the token expiration time.


The mainstream solution is to use two tokens, an access_token and a refresh_token.

 After a successful login, the two tokens are returned:


Access the interface with access_token access:


When the access_token expires, refresh it with refresh_token to get a new access_token and refresh_token.


The access_token here is the token we had before.


Why would an extra refresh_token simplify it?


Because if you log in again, don’t you need to fill in the username and password again? With refresh_token, you just need to bring the token to identify the user, and you don’t need to pass username and password to get a new token.


While access_token is usually set for a shorter expiration time, such as 30 minutes, refresh_token is set for a longer expiration time, such as 7 days.


This way, if you visit once in 7 days, you can refresh the token and renew it for another 7 days, never needing to log in.


But if you haven’t visited for more than 7 days, then the refresh_token has also expired and you need to log in again.

 Think about the APPs you use frequently, have you never logged back in again?


And if you don’t use the APP frequently, do you have to log in again when you open it again?

 This is usually done with double tokens.


Knowing what a double token is and the problem it solves, let’s implement it.

 Create a new nest project:

 npx nest new token-test

 Get into the program and run it up:

npm run start:dev


Visit http://localhost:3000 to see hello world, which means the service ran successfully:


Add a login post interface to the AppController:

@Post('login')
login(@Body() userDto: UserDto) {
    console.log(userDto);
    return 'success';
}


Here the content of the request body is fetched via @Body and set into the dto.


dto is data transfer object, data transfer object, used to hold parameters.

 We create src/user.dto.ts

export class UserDto {
    username: string;
    password: string;
}

 Access this interface in postman:


success is returned, and the server prints the received parameters:

 Then we implement the login logic:


We won’t connect to the database here, just a couple of built-in users to match the information.

const users = [
  { username: 'guang', password: '111111', email: '[email protected]'},
  { username: 'dong', password: '222222', email: '[email protected]'},
]

@Post('login')
login(@Body() userDto: UserDto) {
    const user = users.find(item => item.username === userDto.username);

    if(!user) {
      throw new BadRequestException('The user does not exist');
    }

    if(user.password !== userDto.password) {
      throw new BadRequestException("wrong password ");
    }

    return {
      userInfo: {
        username: user.username,
        email: user.email
      },
      accessToken: 'xxx',
      refreshToken: 'yyy'
    };
}

 If not found, it returns that the user does not exist.

 If you find it but the password is not correct, it returns a password error.

 Otherwise, return the user information and token.

 Test it:

 When username does not exist:

 When the password is incorrect:

 When login is successful:


We then introduce the jwt module to generate the token:

npm install @nestjs/jwt

 Register the module in AppModule:

JwtModule.register({
  secret: 'guang'
})


Then you can inject JwtService in the AppController to use it:

@Inject(JwtService)
private jwtService: JwtService

 This is nest’s dependency injection feature.


Then use this jwtService to generate access_token and refresh_token:

const accessToken = this.jwtService.sign({
  username: user.username,
  email: user.email
}, {
  expiresIn: '0.5h'
});

const refreshToken = this.jwtService.sign({
  username: user.username
}, {
  expiresIn: '7d'
})


The access_token expires in half an hour and the refresh_token expires in 7 days.

 Test it:


Once logged in, you can access other interfaces with this access_token.


As mentioned earlier, jwt carries the token through the authorization header, in the format Bearer xxxx

 And that’s about it:

 Let’s define another interface that requires login access:

@Get('aaa')
aaa(@Req() req: Request) {
    const authorization = req.headers['authorization'];

    if(!authorization) {
      throw new UnauthorizedException('User not logged in');
    }
    try{
      const token = authorization.split(' ')[1];
      const data = this.jwtService.verify(token);

      console.log(data);
    } catch(e) {
      throw new UnauthorizedException('token failed, please log in again');
    }
}


The header for authorization is retrieved from the interface, if it is not there, you are not logged in.


Then take the token from it and verify it with jwtService.verify.


Returns an error that the token is invalid if the checksum fails, otherwise prints the information in it.

 Try it:

 Access the interface with a token:


The server prints the information in the token, which is what we put in it when we logged in:

 Try the wrong token:

 Then we implement the interface for refreshing the token:

@Get('refresh')
refresh(@Query('token') token: string) {
    try{
      const data = this.jwtService.verify(token);

      const user = users.find(item => item.username === data.username);

      const accessToken = this.jwtService.sign({
        username: user.username,
        email: user.email
      }, {
        expiresIn: '0.5h'
      });

      const refreshToken = this.jwtService.sign({
        username: user.username
      }, {
        expiresIn: '7d'
      })

      return {
        accessToken,
        refreshToken
      };

    } catch(e) {
      throw new UnauthorizedException('token failed, please log in again');
    }
}


Defines a get interface with a refresh_token argument.


Take the username out of the token, query the corresponding user information, and regenerate a double token to return.

 Test it:

 Get the refreshToken after logging in:

 Then take this token and access the refresh interface:


A new token is returned, which is also called a senseless refresh.

 How does that work in a front-end project?

 Let’s create a new react project and try it out:

npx create-react-app --template=typescript token-test-frontend

 Run it up:

npm run start


Because port 3000 is occupied, it runs on port 3001.

 Successful run up.

 Let’s change App.tsx

import { useCallback, useState } from "react";

interface User {
  username: string;
  email?: string;
}

function App() {
  const [user, setUser] = useState<User>();

  const login = useCallback(() => {
    setUser({username: 'guang', email: '[email protected]'});
  }, []);

  return (
    <div className="App">
      {
        user?.username
          ? `user: ${ user?.username }`
          : <button onClick={login}>login</button>
      }
    </div>
  );
}

export default App;


Displays user information if already logged in, otherwise displays the Login button.

 Clicking on the Login button will set up the user information.


Here the login method is parameterized, so it is wrapped in a useCallback to avoid unnecessary rendering.

 Then we access the login interface in the login method.

 The first step is to enable cross-domain support in the nest service:


Call enbalbeCors in main.ts to enable cross-domain.

 Then access the interface in the front-end code:

 Install axios first

npm install --save axios


Then create an interface.ts to manage all the interfaces:

import axios from "axios";

const axiosInstance = axios.create({
    baseURL: 'http://localhost:3000/',
    timeout: 3000
});

export async function userLogin(username: string, password: string) {
    return await axiosInstance.post('/login', {
        username,
        password
    });
}

async function refreshToken() {
    
}
async function aaa() {

}

 Call down in the App component:

const login = useCallback(async () => {
    const res = await userLogin('guang', '111111');

    console.log(res.data);
}, []);


The interface call was successful, we got userInfo, access_token, refresh_token


Then we store the token in localStorage, because we’ll need it later.

const login = useCallback(async () => {
    const res = await userLogin('guang', '111111');

    const { userInfo, accessToken, refreshToken } = res.data;

    setUser(userInfo);

    localStorage.setItem('access_token', accessToken);
    localStorage.setItem('refresh_token', refreshToken);
}, []);


Add the aaa interface to interface.ts:

export async function aaa() {
    return await axiosInstance.get('/aaa');
}

 Accessed from within the component:

const xxx = useCallback(async () => {
    const res = await aaa();

    console.log(res);
}, []);


Clicking the aaa button reported an error because the interface returned a 401.


Since the interface is accessed without the token, we can do this in the interceptor.


The interceptor is a mechanism provided by axios to add some generic processing logic before a request and after a response:


The logic for adding the token fits nicely in the interceptor:

axiosInstance.interceptors.request.use(function (config) {
    const accessToken = localStorage.getItem('access_token');

    if(accessToken) {
        config.headers.authorization = 'Bearer ' + accessToken;
    }
    return config;
})

 Now click the aaa button again and the interface responds normally:


Because axios has tokenized it in its interceptor:


So where is the logic for refreshing the token when it fails?


Obviously, it can also be placed in an interceptor.


Let’s say we change the access_token in localStorage to manually disable it.


Clicking on the aaa button again will result in a token failure error:


We determine this in the interceptor and refresh the token if it fails:

axiosInstance.interceptors.response.use(
    (response) => {
        return response;
    },
    async (error) => {
        let { data, config } = error.response;

        if (data.statusCode === 401 && !config.url.includes('/refresh')) {
            
            const res = await refreshToken();

            if(res.status === 200) {
                return axiosInstance(config);
            } else {
                alert(data || 'login out');
            }
        } else {
            return error.response;
        }
    }
)

async function refreshToken() {
    const res = await axiosInstance.get('/refresh', {
        params: {
          token: localStorage.getItem('refresh_token')
        }
    });
    localStorage.setItem('access_token', res.data.accessToken);
    localStorage.setItem('refresh_token', res.data.refreshToken);
    return res;
}


The interceptor of the response has two parameters. When it returns 200, it takes the first handler and returns the response directly.


When the returned value is not 200, the second handler is used to determine if the returned value is 401, and then the interface for refreshing the token is called.


The /refresh interface should also be excluded here, i.e., a failed refresh does not continue the refresh.


If the token refresh succeeds, the previous request is retransmitted; otherwise, you are prompted to log in again.

 Other errors are returned directly.


In the token refresh interface, we update the local token after getting the new access_token and refresh_token.

 Test it:


After I manually changed the access_token to disable it, I clicked on the aaa button and found that three requests were sent:


The first visit to the aaa interface returned 401, the refresh interface was automatically called to refresh, and then the aaa interface was revisited.


This completes the axios interceptor-based senseless token refresh.


But it’s not perfect yet, for example when clicking the button, I call the aaa interface 3 times at the same time:


How many times will the token for all three interfaces be refreshed?

 Yes 3 times.

 Refreshing it a few more times doesn’t matter, it doesn’t affect the functionality.

 But doing it a little more perfectly could be handled:


Add a refreshing flag, and if it’s refreshing, return a promise and add its resolve method and config to the queue.


When refresh succeeds, resend the request in the queue and return the result via resolve.

interface PendingTask {
    config: AxiosRequestConfig
    resolve: Function
}
let refreshing = false;
const queue: PendingTask[] = [];

axiosInstance.interceptors.response.use(
    (response) => {
        return response;
    },
    async (error) => {
        let { data, config } = error.response;

        if(refreshing) {
            return new Promise((resolve) => {
                queue.push({
                    config,
                    resolve
                });
            });
        }

        if (data.statusCode === 401 && !config.url.includes('/refresh')) {
            refreshing = true;
            
            const res = await refreshToken();

            refreshing = false;

            if(res.status === 200) {

                queue.forEach(({config, resolve}) => {
                    resolve(axiosInstance(config))
                })

                return axiosInstance(config);
            } else {
                alert(data || 'login out');
            }
        } else {
            return error.response;
        }
    }
)

axiosInstance.interceptors.request.use(function (config) {
    const accessToken = localStorage.getItem('access_token');

    if(accessToken) {
        config.headers.authorization = 'Bearer ' + accessToken;
    }
    return config;
})

 Test it:

 Now it’s a concurrent request to refresh only once.


In this way, we have implemented a perfect two-token sensorless refresh mechanism based on the axios interceptor.


There are two options for identifying the login status: session and jwt.


session is a session associated with the server through a cookie carrying a sid, and the user information is stored on the server.


jwt is a token that holds user information, which is carried in the authorization header by means of Bearer xxx, and the user information is stored on the client side.

 The jwt approach is used more often because of its natural support for distribution.


However, only one token will have the problem of needing to re-login after the expiration date, so for a better experience, we generally use double tokens to do senseless refreshing.


That is, the user is identified by an access_token, and when it expires, it is refreshed by refresh_token to get a new token.


We implemented this double token mechanism with nest and tested it in postman.


Accessing these interfaces in a react project also requires a dual token mechanism. We’ve encapsulated it in an axios interceptor.


In axios.request.interceptor, read the access_token from localStorage and put it in the header.


In axios.response.interceptor, if the return is 401, call the refresh interface to refresh the token, and then resend the request.


We also support that if a token expires during a concurrent request, the request will be put into a queue, refreshed only once, and the request will be resent in bulk after the refresh.


This makes for a perfect two-token sensorless refresh based on Axios.

By lzz

Leave a Reply

Your email address will not be published. Required fields are marked *