preface

The previous article introduced how to solve the pain point of writing documents with Swagger UI. This article will introduce how to use Redis to solve another pain point of JWT login authentication: the same account login crowding problem. (don’t update, the reader will send the blade -_ – | |)

GitHub project address, welcome everyone Star.

For the benefit of readers who have not yet learned Lesson 8, this tutorial has a separate branch called use-redis

preparation

What is a Redis

Redis is an open source (BSD-licensed), in-memory data structure storage system that can be used as a database, cache, and messaging middleware.

Redis is very efficient, the official data is 100000+ QPS, this is because:

  • Redis is completely memory based, and most requests are pure memory operations, which are highly efficient to execute.
  • Redis uses a single-process single-thread model (K, V) database to store data in memory, and access is not limited by disk IO, so its execution speed is extremely fast. In addition, a single thread can handle high concurrency requests, avoid frequent context switches and lock contention, and can start multiple instances if you want to run multi-core.
  • Redis does not use tables, does not force users to associate various relationships, and does not have complex relationship restrictions. Its storage structure is key-value pair, similar to HashMap. The biggest advantage of HashMap is that the time complexity of access is O(1).
  • Redis uses a multiplex I/O multiplexing model for non-blocking IO.

Note: the Redis USES multiplex I/O functions: epoll/kqueue/evport/select.

Install Redis

To use Redis, you must first install Redis. Since this article does not focus on Redis installation, here are the installation tutorials for Windows and MacOS environments.

MAC OS installation Redis – Jane book

Install Redis on Windows – Official

Interestingly, the official tutorial mentions:

Redis does not recommend using Redis on Windows, so there is no Windows version available for download. Fortunately, the Microsoft team maintains an open source version of Windows, although only version 3.2 is good enough for normal testing.

Redis visual client

1. Mac OS

The author use MacOS systems, so use AnotherRedisDesktopManager as Redis visualization client:

# clone code
git clone https://github.com/qishibo/AnotherRedisDesktopManager.git
cd AnotherRedisDesktopManager

# install dependencies
npm install  # if download electron failed during installing, use this command # ELECTRON_MIRROR="https://npm.taobao.org/mirrors/electron/" npm install  # serve with hot reload at localhost:9988 npm start  # after the previous step is completed, open another tab, build up a desktop client npm run electron Copy the code

2. Windows

On Windows, you can use Redis Desktop Manager

There is a fee for the official website, but the 0.8.8.384 version is available for testing.


Start Redis and connect to the client

Due to the use of MacOS systems, work directly with AnotherRedisDesktopManager here demonstrates, Windows is pretty much the same.

To start the Redis service, go to /usr/local/bin/(depending on your installation path) and type the following command:

$ redis-server
Copy the code

The following figure shows that the service is successfully started:


Then open a new terminal, go to the same directory, and start the Redis client:

$ redis-cli
Copy the code

To use the client connection, you may need to enter a password. Let’s set it up first. There are two instructions involved

View password:

$ config get requirepass
Copy the code

Set password:

$ config set requirepass [new passward]
Copy the code

The following is my command record, because I set the password root, so I need -a [password] after exiting and reentering. Another point is that the password set in this way will disappear after restarting the computer, so I need to reset it


Then start AnotherRedisDesktopManager, start method in the above mentioned, need to open a new terminal TAB to start the electron.

Click “New Connection” in the upper left corner and enter the configuration information:


Then you can see the overview:


All right, now we’re ready to get down to business.


Nest operation Redis

1. Redis connection configuration

First, write the Redis configuration file, which is incorporated directly into config/db.ts:

// config/db.ts
const productConfig = {
  mysql: {
Port: 'database port ',Host: 'database address ',User: 'username ',Password: 'password ',Database: 'nest_zero_to_one', // Library nameConnectionLimit: 10, // connectionLimit }, + redis: { + port: 'online Redis port ', + host: 'Redis ', + db: 'library name' + password: 'Redis ', +} };  const localConfig = {  mysql: { Port: 'database port ',Host: 'database address ',User: 'username ',Password: 'password ',Database: 'nest_zero_to_one', // Library nameConnectionLimit: 10, // connectionLimit }, + redis: { + port: 6379, Host: + '127.0.0.1, + db: 0, + password: 'root', +} };  // There is no process.env.node_env on the local run to distinguish between development environment and production environment.const config = process.env.NODE_ENV ? productConfig : localConfig;  export default config; Copy the code

2. Build Redis factories

Will be needed here with ioredis use:

$ yarn add ioredis -S
Copy the code

Once added, we need to write a file that generates a list of Redis instances:

// src/database/redis.ts
import * as Redis from 'ioredis';
import { Logger } from '.. /utils/log4js';
import config from '.. /.. /config/db';

let n: number = 0; const redisIndex = []; // Used to record the redis instance index const redisList = []; // Used to store redis instances  export class RedisInstance {  static async initRedis(method: string, db: number = 0) {  const isExist = redisIndex.some(x= > x === db);  if(! isExist) { Logger.debug(`[Redis ${db}] from${method}Method call, Redis instantiated${++n}Time `);  redisList[db] = newRedis({ ... config.redis, db }); redisIndex.push(db);  } else {  Logger.debug(`[Redis ${db}] from${method}Method call ');  }  return redisList[db];  } } Copy the code

Because Redis can have multiple libraries at the same time (255 for the company and 15 for the local library), we need to pass in db to distinguish between them. Of course, we can also write to death, but after using each library, we need to write a new class. From the perspective of code reuse, this design is bad, so we made an integration here.

Function inside the print, is to facilitate the later log redisk, positioning call location.

3. Adjust the token issuing process

When the user logs in successfully, the user information and token are stored in Redis, and the expiration time (unit: second) is set. In normal cases, the expiration time should be consistent with JWT. For debugging convenience, only 300 seconds is written here:

// src/logical/auth/auth.service.ts
import { Injectable } from '@nestjs/common';
import { UserService } from '.. /user/user.service';import { JwtService } from '@nestjs/jwt';
import { encryptPassword } from '.. /.. /utils/cryptogram';+ import { RedisInstance } from '.. /.. /database/redis';  @Injectable() export class AuthService {  constructor(private readonly usersService: UserService, private readonly jwtService: JwtService) {}  // JWT authentication-step 2: verifies user information async validateUser(username: string, password: string): Promise<any> { // console.log('JWT validation - Step 2: verify user information '); const user = await this.usersService.findOne(username);  if (user) {  const hashedPassword = user.password;  const salt = user.salt;  const hashPassword = encryptPassword(password, salt);  if (hashedPassword === hashPassword) { // Correct password return {  code: 1,  user,  };  } else { // The password is incorrect return {  code: 2,  user: null,  };  }  } // There is no such person return {  code: 3,  user: null,  };  }  // JWT validation - Step 3: Process JWT visa async certificate(user: any) {  const payload = {  username: user.username, -sub: user.userId, // + sub: user.id,  realName: user.realName,  role: user.role,  }; // console.log('JWT validation - Step 3: handle JWT visa ', 'payload: ${json.stringify (payload)}'); try {  const token = this.jwtService.sign(payload); // instantiate redis + const redis = await RedisInstance.initRedis('auth.certificate', 0); + // Store user information and token in redis and set expiration time. Syntax: [key, seconds, value] + await redis.setex(`${user.id}-${user.username}`, 300, `${token}`);  return {  code: 200,  data: {  token,  }, MSG: 'Login succeeded', };  } catch (error) {  return {  code: 600, MSG: 'Wrong account or password', };  }  } } Copy the code

As for the use of Redis, some popular science tutorials are attached at the end of the article. If you need to check instructions in the learning process, you can go to here: Redis command Reference

4. Adjust guard strategy

Token.guard. Ts = token.guard. Ts = token.guard. Ts = token.guard.

// src/guards/rbac.guard.ts
- import { CanActivate, ExecutionContext, Injectable, ForbiddenException } from '@nestjs/common';
+ import { CanActivate, ExecutionContext, Injectable, ForbiddenException, UnauthorizedException } from '@nestjs/common';
+ import { RedisInstance } from '.. /database/redis';

@Injectable() export class RbacGuard implements CanActivate { / / role/user role: super administrator | 0-1 - administrator | 2 - development & testing & operation | 3 - ordinary users (can see) constructor(private readonly role: number) {}  async canActivate(context: ExecutionContext): Promise<boolean> {  const request = context.switchToHttp().getRequest();  const user = request.user;  + // Get the token in the request header + const authorization = request['headers'].authorization || void 0; + const token = authorization.split(' ')[1]; // authorization: Bearer xxx  + // Get the token cached in redis + const redis = await RedisInstance.initRedis('TokenGuard.canActivate', 0); + const key = `${user.userId}-${user.username}`; + const cache = await redis.get(key);  + if (token ! == cache) { + // If the token does not match, access is prohibited + throw new UnauthorizedException(' Your account is logged in elsewhere, please log in again '); +}   if (user.role > this.role) { // If the permissions do not match, access is forbiddenThrow new ForbiddenException(' Sorry, you are not allowed to do this '); }  return true;  } } Copy the code

5. Verify

Let’s try logging in:


First look at the log, Redis is not called:


Take a look at the records in the Redis client:


It was discovered that the token had been saved and 42 seconds had passed by the time the screenshot was taken.

We then copy the token to the interface that requests the list of goods, asking:


Here is what a normal request looks like, and then we log in again without changing the token of this interface:


Attached is the relevant log:


As you can see in the figure above, the strategy has worked.

To see if records in Redis will expire, click the green refresh button next to TTL to view the remaining time:


A TTL of -2 means that the key has expired and the record does not exist. We can refresh it by clicking the magnifying glass on the left:


Note: TTL -1 indicates that the expiration time is not set (that is, it always exists). A value of -2 indicates that the key does not exist.

As you can see, the record has disappeared and is no longer taking up any space.

At this point, we’re done.

conclusion

This article shows you how to use Redis in Nest and implement a login extrusion feature that slightly offsets the JWT strategy. Here is just a “extrusion” idea, not limited to the guard, if you have a better idea, feel free to leave a comment below.

You can do a lot of things with Redis, such as handling high concurrency, keeping track of some user status, etc. I have used [queue] to process red envelope rain activity and the pressure record is 300+ requests/second.

It can also be used to handle “login timeout” requirements, such as setting JWT to a period of 10 days and half months, and then giving Redis only 1-2 hours of validity. However, each request will reset the expiration time, and finally determine whether the key exists to confirm whether the login timeout, the specific implementation is not here. Interested readers can complete their own.

This article is included in the NestJS practical tutorial, more articles please pay attention to.

References:

Redis from shallow to deep deep Anatomy

“Redis is enough.”

This article is formatted using MDNICE