preface

The previous article explained how to use Sequelize to connect to MySQL. Next, we will extend the original code to allow users to register and log in.

Here’s a quick mention of JWT:

JWT

JWT (JSON Web Token) is an open jSON-based standard (RFC 7519) implemented for the transfer of declarations between network application environments. The Token is designed to be compact and secure, especially suitable for single sign-on (SSO) scenarios in distributed sites. The JWT declaration is generally used to pass authenticated user identity information between the identity provider and the service provider to obtain resources from the resource server, and to add some additional declaration information necessary for other business logic. The Token can also be used directly for authentication or can be encrypted.

For details, please refer to “JSON Web Token Tutorial – Ruan Yifeng”

So the general process of JWT implementation [login] is as follows:

  1. Client user login request;
  2. The server gets the request and queries the user table according to the parameters.
  3. If a user is matched, the user information is issued with a visa and the Token is issued.
  4. The client takes the Token, stores it somewhere, and carries it with it in subsequent requests.
  5. After receiving the Token request, the server directly verifies the request based on the visa without querying user information.

Now, let’s start our actual combat:

GitHub project address, welcome everyone Star.

One, write encryption tool function

In the SRC directory, create a new folder utils, which will hold various utility functions, and create a new cryptogram.ts file:

import * as crypto from 'crypto';

/**
 * Make salt
 */
export function makeSalt(): string {
  return crypto.randomBytes(3).toString('base64'); } /** * Encrypt password * @param password Password * @param salt Password salt */export function encryptPassword(password: string, salt: string): string {
  if(! password || ! salt) {return ' ';
  }
  const tempSalt = Buffer.from(salt, 'base64');
  returnCrypto. Pbkdf2Sync (password, tempSalt, 10000, 16,'sha1').toString('base64')); }Copy the code

One is to create a random salt, and the other is to encrypt the password based on the salt.

These two functions will run through the functions of registration and login.

Second, user registration

Before writing the registration logic, we need to modify the findeOne() method in user.service.ts:

// src/logical/user/user.service.ts
import { Injectable } from '@nestjs/common';
import * as Sequelize from 'sequelize'; // Introduce the Sequelize library
import sequelize from '.. /.. /database/sequelize'; // Introduce the Sequelize instance

@Injectable(a)export class UserService {
  /** * check whether the user @param username username */ exists
  async findOne(username: string) :Promise<any | undefined> {
    const sql = `
      SELECT
        user_id userId, account_name username, real_name realName, passwd password,
        passwd_salt salt, mobile, role
      FROM
        admin_user
      WHERE
        account_name = '${username}'`; // A plain SQL query
    try {
      const user = (await sequelize.query(sql, {
        type: Sequelize.QueryTypes.SELECT, // Query mode
        raw: true.// Whether to display the result as an array assembly
        logging: true.// Whether to print SQL statements to the console}))0];
      // If no user is found, user === undefined
      return user;
    } catch (error) {
      console.error(error);
      return void 0; }}}Copy the code

FindOne () now works better with its method name, returning user information if found, undefined if not found.

Next, let’s start writing the registration function:

// src/logical/user/user.service.ts
import { Injectable } from '@nestjs/common';
import * as Sequelize from 'sequelize'; // Introduce the Sequelize library
import sequelize from '.. /.. /database/sequelize'; // Introduce the Sequelize instance

import { makeSalt, encryptPassword } from '.. /.. /utils/cryptogram'; // Introduce the encryption function

@Injectable(a)export class UserService {
  /** * check whether the user @param username username */ exists
  async findOne(username: string) :Promise<any | undefined> {... }/** * register * @param requestBody */
  async register(requestBody: any) :Promise<any> {
    const { accountName, realName, password, repassword, mobile } = requestBody;
    if(password ! == repassword) {return {
        code: 400,
        msg: 'Two different passwords entered'}; }const user = await this.findOne(accountName);
    if (user) {
      return {
        code: 400,
        msg: 'User already exists'}; }const salt = makeSalt(); // Make password salt
    const hashPwd = encryptPassword(password, salt);  // Encrypt the password
    const registerSQL = `
      INSERT INTO admin_user
        (account_name, real_name, passwd, passwd_salt, mobile, user_status, role, create_by)
      VALUES
        ('${accountName}', '${realName}', '${hashPwd}', '${salt}', '${mobile}', 1, 3, 0) ';
    try {
      await sequelize.query(registerSQL, { logging: false });
      return {
        code: 200,
        msg: 'Success'}; }catch (error) {
      return {
        code: 503,
        msg: `Service error: ${error}`}; }}}Copy the code

Once written, add routes in user.controller.ts

// src/logical/user/user.controller.ts
import { Controller, Post, Body } from '@nestjs/common';
import { UserService } from './user.service';

@Controller('user')
export class UserController {
  constructor(private readonly usersService: UserService) {}

  // @Post('find-one')
  // findOne(@Body() body: any) {
  // return this.usersService.findOne(body.username);
  // }

  @Post('register')
  async register(@Body() body: any) {
    return await this.usersService.register(body); }}Copy the code

Now, let’s use Postman to test this by deliberately entering a different password and an existing username:

As shown, the inconsistent password verification is triggered.

Then, we change the password to the same:

As shown, the verification of the existing user is triggered.

Then, we enter the correct parameters:

Let’s go to the database again:

The information has been inserted into the table, and the password is encrypted, so far, the registration function is basically complete.

Iii. Configuration and verification of JWT

In order to feel the processing order more intuitively, I added step printing to the code

1. Install dependency packages

$ yarn add passport passport-jwt passport-local @nestjs/passport @nestjs/jwt -S
Copy the code

2. Create the Auth module

$ nest g service auth logical
$ nest g module auth logical
Copy the code

3. Create a file to store constants

Add a constants.ts to the auth folder to store the various constants used:

// src/logical/auth/constats.ts
export const jwtConstants = {
  secret: 'shinobi7414' / / the secret key
};
Copy the code

4. Write JWT policies

Add a new jwt.strategy.ts to the auth folder to write JWT validation strategy:

// src/logical/auth/jwt.strategy.ts
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { jwtConstants } from './constants';

@Injectable(a)export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: jwtConstants.secret,
    });
  }
  
  // JWT validation -step 4: called by the guard
  async validate(payload: any) {
    console.log('JWT validation - Step 4: called by the guard');
    return{ userId: payload.sub, username: payload.username, realName: payload.realName, role: payload.role }; }}Copy the code

5. Write auth.service.ts validation logic

// 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';

@Injectable(a)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;
      // Use the password salt to encrypt the parameter, and then compare it with the database to determine whether it is equal
      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, realName: user.realName, role: user.role };
    console.log('JWT Validation - Step 3: Process JWT visa ');
    try {
      const token = this.jwtService.sign(payload);
      return {
        code: 200,
        data: {
          token,
        },
        msg: 'Login succeeded'}; }catch (error) {
      return {
        code: 600,
        msg: 'Wrong account or password'}; }}}Copy the code

When you save the file, the console displays an error:

We can leave it as it is not yet associated with JwtService and UserService in auth.module.ts.

5. Write local policies

This step is optional and depends on the needs of the project to determine whether a local policy is needed

// src/logical/auth/local.strategy.ts
import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service';

@Injectable(a)export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(private readonly authService: AuthService) {
    super(a); }async validate(username: string, password: string) :Promise<any> {
    const user = await this.authService.validateUser(username, password);
    if(! user) {throw new UnauthorizedException();
    }
    returnuser; }}Copy the code

6. The Module

// src/logical/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LocalStrategy } from './local.strategy';
import { JwtStrategy } from './jwt.strategy';
import { UserModule } from '.. /user/user.module';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { jwtConstants } from './constants';

@Module({
  imports: [
    PassportModule.register({ defaultStrategy: 'jwt' }),
    JwtModule.register({
      secret: jwtConstants.secret,
      signOptions: { expiresIn: '8h' }, // Token expires
    }),
    UserModule,
  ],
  providers: [AuthService, LocalStrategy, JwtStrategy],
  exports: [AuthService],
})
export class AuthModule {}
Copy the code

If save the file, and the above error at this time, you need to go to the app. The module. The ts, removes AuthService from will array, and add in the imports array AuthModule can:

// src/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UserModule } from './logical/user/user.module';
// import { AuthService } from './logical/auth/auth.service';
import { AuthModule } from './logical/auth/auth.module';

@Module({
  imports: [UserModule, AuthModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
Copy the code

7. Write a Login route

At this point, returning to user.controller.ts, we import the assembled JWT related files and determine the user status based on the verification code:

// src/logical/user/user.controller.ts
import { Controller, Post, Body } from '@nestjs/common';
import { AuthService } from '.. /auth/auth.service';
import { UserService } from './user.service';

@Controller('user')
export class UserController {
  constructor(private readonly authService: AuthService, private readonly usersService: UserService) {}

  // JWT authentication -step 1: user requests login
  @Post('login')
  async login(@Body() loginParmas: any) {
    console.log('JWT Authentication - Step 1: User requests login ');
    const authResult = await this.authService.validateUser(loginParmas.username, loginParmas.password);
    switch (authResult.code) {
      case 1:
        return this.authService.certificate(authResult.user);
      case 2:
        return {
          code: 600,
          msg: Incorrect account or password};default:
        return {
          code: 600,
          msg: 'No such person'}; }}@Post('register')
  async register(@Body() body: any) {
    return await this.usersService.register(body); }}Copy the code

The same error occurs when the file is saved:

This time we will go to user.module.ts and comment out the controllers:

We need to go to app.module.ts and add Controller back:

This is because if you add AuthService to user.module.ts, you will need to introduce other policies again. I feel very troublesome, so I simply use the app to unify management.

4. Login verification

Now that we have a list of code, it is time to check the effect, we will use the original registered information, the login request:

As you can see, a long list of tokens have been returned, and the console has printed the login steps and user information. With this token, the front end can request other guarded interfaces.

Let’s try typing in the wrong account or password:

Five, the guards

Now that you have issued the Token, you need to be able to verify the Token, so Guard is used.

Add @useGuards (AuthGuard(‘ JWT ‘)) to the route:

// src/logical/user/user.controller.ts
import { Controller, Post, Body, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from '.. /auth/auth.service';
import { UserService } from './user.service';

@Controller('user')
export class UserController {
  constructor(private readonly authService: AuthService, private readonly usersService: UserService) {}

  @Post('login')
  async login(@Body() loginParmas: any) {... }@UseGuards(AuthGuard('jwt')) // Use 'JWT' for validation
  @Post('register')
  async register(@Body() body: any) {
    return await this.usersService.register(body); }}Copy the code

Then, let’s try the request without token in the header:

As you can see, return the 401 status code for Unauthorized access.

Now, let’s try the Token case, copying the login tokens into the Postman authorship (select Bearer tokens) :

Then request the interface:

At this point, you are ready to access it. Look at the console print and the steps are as commented in the code:

At this point, the login function is basically complete.

conclusion

This article describes how to use JWT to issue tokens for user logins and how to verify user information when a Token request is received.

Of course, implementing login authentication is not limited to JWT, and there are many other methods that interested readers can explore for themselves.

The disadvantages of JWT are that it cannot be used to log in with the same account, and the later login can be used to edge out the earlier login, that is, to invalidate the previous Token, so as to ensure information security (at least I did not find the relevant solution, if there is a big god to solve this problem, please advise). Tokens can only be squeezed out using some other black tech (e.g. Redis).

Now that you have the registration and login functions, it’s time to refine the other common functions that a server should have.

The next article covers interceptors, exception handling, and log collection.

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

`