In the last article, I introduced you to nest.js. Next, I will continue to develop on the previous code. There are two main tasks: to achieve user registration and login.

Before realizing login and registration, we need to sort out the requirements first. We hope users can log in to the website to write articles in two ways, one is account password login, the other is wechat scanning code login. Article Content Outline

Pick up where the last chapter left off…

Create Contoller, Service, Module, and DTO files quickly:

nest g resouce user
Copy the code

We quickly created a REST API module with simple CRUD code, and found that half of what we learned in the previous chapter could be done with a single command

User registration

In the registration function, when the user is registered by user name and password, we can not directly save the password in the plaintext database, so we use BcryptJS to achieve encryption, and then stored in the database.

Before implementing the registration, first understand the encryption scheme bcryptjs, install the dependency package:

npm install bcryptjs
Copy the code

Bcryptjs is one of the best nodeJS packages with salt encryption. We deal with password encryption and verification using two methods:

/** * Encryption processing - synchronization method * bcryptjs.hashsync (data, salt) * -data The data to be encrypted * -slat the salt used to hash passwords. If specified as a number, salt is generated and used with the specified number of rounds. Recommended 10 * /
const hashPassword = bcryptjs.hashSync(password, 10)


/** * verify - Use synchronization method * bcryptjs.compareSync(data, encrypted) * -data Data to compare, use password passed at login * -encrypted data to compare, Use the encrypted password */ retrieved from the database
const isOk = bcryptjs.compareSync(password, encryptPassword)
Copy the code

Next, design the user entity:

// use/entities/user.entity.ts
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';

@Entity('user')
export class User {
  @PrimaryGeneratedColumn('uuid')
  id: number;

  @Column({ length: 100 })
  username: string; / / user name

  @Column({ length: 100 })
  nickname: string;  / / nickname

  @Column(a)password: string;  / / password

  @Column(a)avatar: string;   / / avatar

  @Column(a)email: string;

  @Column('simple-enum', { enum: ['root'.'author'.'visitor']})role: string;   // User role

  @Column({
    name: 'create_time'.type: 'timestamp'.default: () = > 'CURRENT_TIMESTAMP',})createTime: Date;

  @Column({
    name: 'update_time'.type: 'timestamp'.default: () = > 'CURRENT_TIMESTAMP',})updateTime: Date;
  
  @BeforeInsert(a)async encryptPwd() { 
    this.password = await bcrypt.hashSync(this.password); }}Copy the code
  1. When creating aUserEntity, using@PrimaryGeneratedColumn('uuid')Create a primary columnid, the value will be useduuidAutomatic generation.UuidIs a unique string;
  2. implementationThe field name hump is underlinedName,createTimeandupdateTimeThe field is converted into an underscore named way to store in the database, just need in@ColumnSpecified in the decoratornameProperties;
  3. We used decorators@BeforeInsertTo decorateencryptPwdMethod, which means that the method is called before data is inserted, ensuring that the passwords used to insert the database are encrypted.
  4. There are three roles for the blog systemroot,autorandvisitor.rootWith all the permissions,authorWith permission to write articles,visitorCan only read articles, registered users default isvisitor.rootThe user role can be changed for an account with permission.

Next, the business logic for registered users is implemented

Register Registered users

Implement the user.service.ts logic:

import { User } from './entities/user.entity';
import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { CreateUserDto } from './dto/create-user.dto';
import { Repository } from 'typeorm';

@Injectable(a)export class UserService {
  constructor(
    @InjectRepository(User)
    private userRepository: Repository<User>,
  ) {}
  async register(createUser: CreateUserDto) {
    const { username } = createUser;

    const existUser = await this.userRepository.findOne({
      where: { username },
    });
    if(existUser){
        throw new HttpException("Username already exists", HttpStatus.BAD_REQUEST)
    }

    const newUser = await this.userRepository.create(createUser)
    return await this.userRepository.save(newUser); }}Copy the code

I remember when I was inserting data into the database, I called CREATE directly without looking at the document, only to find that the data was not inserted into the database. Later, I found that the save method was the insertion method.

this.userRepository.create(createUser)
/ / equivalent to
new User(createUser)  // Just create a new user object
Copy the code

So far, the business logic of registered users has been realized. Controller is relatively simple, and the subsequent business implementation such as login will not present the Controller code one by one:

// user.controller.ts
 @ApiOperation({ summary: 'Registered User' })
 @ApiResponse({ status: 201.type: [User] })
 @Post('register')
 register(@Body() createUser: CreateUserDto) {
    return this.userService.register(createUser);
  }
Copy the code

Execute the code above and return the following data:

{
  "data": {
    "username": "admin"."password": "$2a$10$vrgqi356K00XY6Q9wrSYyuBpOIVf2E.Vu6Eu.HQcUJP.hDTuclSEW"."nickname": null."avatar": null."email": null."id": "5c240dcc-a9b1-4262-8212-d5ceb2815ef8"."createTime": "The 2021-11-16 T03:00:16. 000 z"."updateTime": "The 2021-11-16 T03:00:16. 000 z"
  },
  "code": 0."msg": "Request successful"
}
Copy the code

It can be found that the password is also returned. The risk of this interface is self-evident. How to deal with it? Think about it

Consider from two aspects, one is the data level, from the database does not return the password field, the other way is to return data to the user, processing data, do not return to the front end. Let’s take a look at these two ways:

Method 1

TypeORM provides column property select, whether the column is hidden by default during query. But this can only be used for queries, such as when the data returned by the save method still contains the password.

// user.entity.ts
 @Column({ select: false})    // Hides the column
 password: string;  / / password
Copy the code

In this way, our code in user.service.ts can be modified as follows:

// user.service.ts
 async register(createUser: CreateUserDto){...await this.userRepository.save(newUser);
  return await this.userRepository.findOne({where:{username}})
 }
Copy the code

Method 2

Use the Exclude provided by class-Transformer to serialize the returned data to filter out the password field. Start with the @exclude decorator in user.entity.ts:

// user.entity.ts.import { Exclude } from 'class-transformer';

@Exclude(a)@Column(a)password: string;  / / password
Copy the code

Then use ClassSerializerInterceptor tags, where to request at this time, the POST/API/user/register the request returns data, this field will not contain the password.

  @UseInterceptors(ClassSerializerInterceptor)
  @Post('register')
  register(@Body() createUser: CreateUserDto){... }Copy the code

You don’t need to modify the logic in user.service.ts as you did in method 1. If you want to make the Controller of all the request does not contain the password field, then can be directly use ClassSerializerInterceptor tag class.

In fact, the combination of these two ways can be completely used.

The user login

In terms of user login, it has been mentioned before that we plan to use two ways, one is local authentication (user name & password), and the other is wechat scanning code login. Let’s take a look at how local authenticated login is implemented.

passport.js

First of all, there is a Nodejs middleware dedicated to identity authentication: Passport. Js, which can only do login authentication, but is very powerful. It supports local account authentication and third-party account login authentication (OAuth, OpenID, etc.), and supports most Web sites and services.

The most important concept in Passport is policy. The Passport module itself cannot be authenticated. All authentication methods are encapsulated in policy mode as plug-ins, which can be added to package.json if necessary. I’ve prepared a separate article to share some details about login authentication (Nodejs works with more than passport, but there are other nice packages out there).

Local Local authentication

If you want to install a dependency package, you need to install at least one of the passport policies. If you want to implement local authentication, you need to install the passport policy.

npm install @nestjs/passport passport passport-local
npm install @types/passport @types/passport-local
Copy the code

We’ve also installed a type hint, because Passport is a pure JS package, and it doesn’t affect the application if it’s not installed, it just doesn’t have a code hint when it’s written.

Create an Auth module that handles authentication related code, Controller, service, etc. We also need to create a local.strategy.ts file to write the local validation policy code:

// local.strategy.ts.import { compareSync } from 'bcryptjs';
import { PassportStrategy } from '@nestjs/passport';
import { IStrategyOptions, Strategy } from 'passport-local';
import { User } from 'src/user/entities/user.entity';

export class LocalStorage extends PassportStrategy(Strategy) {
  constructor(
    @InjectRepository(User)
    private readonly userRepository: Repository<User>,
  ) {
   
    super({
      usernameField: 'username'.passwordField: 'password',}as IStrategyOptions);
  }

  async validate(username: string, password: string) {
    const user = await this.userRepository
      .createQueryBuilder('user')
      .addSelect('user.password')
      .where('user.username=:username', { username })
      .getOne();

    if(! user) {throw new BadRequestException('Incorrect username! ');
    }

    if(! compareSync(password, user.password)) {throw new BadRequestException('Wrong password! ');
    }

    returnuser; }}Copy the code

Let’s look at the code implementation from top to bottom:

  • First of all, I defined oneLocalStorageInheritance to@nestjs/passportTo provide thePassportStrategyClass that takes two arguments
    • The first parameter: Strategy, the Strategy you want to use, in this case passport-local
    • The second argument: is the policy alias, as abovepassport-localThe default islocal
  • Then callsuperPass the policy parameter, in this case, if passedusernameandpasswordI don’t have to write it, but the default argument is, let’s say we’re authenticating with a mailbox, and the parameter that we pass in isemailthatusernameFieldThe corresponding value isemail.
  • validateisLocalStrategyThe built-in method, mainly is the reality of the user query and password comparison, because the password is encrypted, there is no way to directly compare the user name and password, only according to the user name to find the user, and then compare the password.
    • There’s one more thing here, passaddSelectaddpasswordOtherwise, password comparison cannot be performed.

With this strategy, we can now implement a simple/Auth /login route and apply the nest.js built-in Guard AuthGuard for validation. Open the app.Controller.ts file and replace its contents with the following:

.import { AuthGuard } from '@nestjs/passport';

@ApiTags('validation')
@Controller('auth')
export class AuthController {
  @UseGuards(AuthGuard('local'))
  @UseInterceptors(ClassSerializerInterceptor)
  @Post('login')
  async login(@Body() user: LoginDto, @Req() req) {
    returnreq.user; }}Copy the code

Also don’t forget to import PassportModule and entity User in auth.module.ts and inject LocalStorage for shared use within its modules.

// auth.module.ts.import { PassportModule } from '@nestjs/passport';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from 'src/user/entities/user.entity';
import { LocalStorage } from './local.strategy';

@Module({
  imports: [TypeOrmModule.forFeature([User]), PassportModule],
  controllers: [AuthController],
  providers: [AuthService, LocalStorage],
})
Copy the code

The interface returns the following data, is this what we need?

After login in development, should not return a token that can identify the user?

Yes, the client uses the username and password for authentication, and the server should issue an identity token to the client after successful authentication, so that the client can later use this token to prove its identity. There are many ways to identify a user’s identity, and here we use JWT (see this article on the 5 ways to know about front-end authentication: Cookie, Session, Token, JWT and single sign-on).

JWT generated token

The next step is to generate a token string and return it. JWT is a mature scheme for generating token strings, which generate token content of this form:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImQyZTZkNjRlLWU1YTAtNDhhYi05ZjU2LWMyMjY3ZjRkZGMyNyIsInVzZXJuYW1lIjoiYWRtaW4 xIiwicm9sZSI6InZpc2l0b3IiLCJpYXQiOjE2Mzc1NjMzNjUsImV4cCI6MTYzNzU3Nzc2NX0.NZl4qLA2B4C9qsjMjaXmZoFUyNjt2FH4C-zGSlviiXACopy the code

How do you make something like this?

You can see that in the figure aboveJWT tokenIt consists of three parts: header, payload, and signature. Practice a

npm install @nestjs/jwt
Copy the code

First register JwtModule in auth.module.ts:

.import { JwtModule } from '@nestjs/jwt';

const jwtModule = JwtModule.register({
    secret:"test123456".signOptions: { expiresIn: '4h'}})@Module({
  imports: [
    ...
    jwtModule,
  ],
  exports: [jwtModule],
})
Copy the code

In the above code, secret is achieved by writing secret into the code, which is not recommended in the actual development. The secret configuration should be obtained from the environment variable just like the database configuration, otherwise secret will be leaked and other people can generate the corresponding token. Feel free to get your data, we use the following asynchronous acquisition method:

.const jwtModule = JwtModule.registerAsync({
  inject: [ConfigService],
  useFactory: async (configService: ConfigService) => {
    return {
      secret: configService.get('SECRET'.'test123456'),
      signOptions: { expiresIn: '4h'}}; }}); .Copy the code

Don’t forget to set the SECRET configuration in the. Env file.

Finally, we implement the business logic in auth.service.ts:

//auth.service.ts.import { JwtService } from '@nestjs/jwt';

@Injectable(a)export class AuthService {
  constructor(
    private jwtService: JwtService,
  ) {}

 / / token is generated
  createToken(user: Partial<User>) {
    return this.jwtService.sign(user);
  }

  async login(user: Partial<User>) {
    const token = this.createToken({
      id: user.id,
      username: user.username,
      role: user.role,
    });

    return{ token }; }}Copy the code

So far, we have achieved the return of a token to the user through Passport – Local and JWT. Then, when the user carries the token and requests data, we need to verify whether the token carried is correct, such as the interface to obtain user information.

Get user information interface implementation

To implement token authentication, Passport also provides the corresponding Passport – JWT strategy, which is also very convenient to implement.

First install:

npm install passport-jwt @types/passport-jwt
Copy the code

In fact, the JWT strategy is mainly implemented in two steps

  • Step 1: How to take it outtoken
  • Step 2: According totokenGet user information

Let’s look at the implementation:

//jwt.strategy.ts.import { ConfigService } from '@nestjs/config';
import { UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { StrategyOptions, Strategy, ExtractJwt } from 'passport-jwt';

export class JwtStorage extends PassportStrategy(Strategy) {
  constructor(
    @InjectRepository(User)
    private readonly userRepository: Repository<User>,
    private readonly configService: ConfigService,
    private readonly authService: AuthService,
  ) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKey: configService.get('SECRET'),}as StrategyOptions);
  }

  async validate(user: User) {
    const existUser = await this.authService.getUser(user);
    if(! existUser) {throw new UnauthorizedException('Token incorrect');
    }
    returnexistUser; }}Copy the code

The ExtractJwt in the above policy provides several ways to ExtractJwt from the request. The common ways are as follows:

  • FromHeader: Lookup in the Http request headerJWT
  • FromBodyField: On requestBodyFind in fieldJWT
  • FromAuthHeaderAsBearerToken: in the authorization header withBearerFind in the schemaJWT

We adopt fromAuthHeaderAsBearerToken, requested action demonstration behind as you can see, send a request header needs to take, this scheme is also now a lot of back-end favour:

'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImQyZTZkNjRlLWU1YTAtNDhhYi05ZjU2LWMyMjY3ZjRkZGMyNyIsInVzZXJuYW1lIjoiYWRtaW4 xIiwicm9sZSI6InZpc2l0b3IiLCJpYXQiOjE2Mzc1NzUxMzMsImV4cCI6MTYzNzU4OTUzM30._-v8V2YG8hZWpL1Jq3puxBlETeSuWg8DBEPCL2X-h5c'Copy the code

Don’t forget to inject JwtStorage in auth.module.ts:

.import { JwtStorage } from './jwt.strategy';

@Module({...providers: [AuthService, LocalStorage, JwtStorage],
  ...
})
Copy the code

Finally, just use the binding JWT authorization guard in the Controller:

// user.controller.ts

@ApiOperation({ summary: 'Get user information' })
@ApiBearerAuth(a)// Swagger document sets token
@UseGuards(AuthGuard('jwt'))
@Get(a)getUserInfo(@Req() req) {
    return req.user;
}
Copy the code

To obtain user information, the interface ends. Finally, in order to smoothly use Swagger to test bearer token interfaces, addBearerAuth needs to be added:

// main.ts.const config = new DocumentBuilder()
    .setTitle('Admin background')
    .setDescription('Manage Backend Interface Document')
    .addBearerAuth()
    .build();
  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('docs', app, document);
  await app.listen(9080); .Copy the code

Take a look at the interface demo:

Wechat scan code login

To the local verification login is completed, through the above learning, I believe that we have mastered the process of login, next I will share how TO achieve wechat scan code login in the development process.

Note: This section requires an account on the wechat open platform. If not, you can also apply for the account system through the public platform. The specific process is not mentioned here.

What to prepare

First you need to apply for an app and get your AppID and AppSecret

Next, you need to configure the domain name of the back authorization, that is, the domain name of the website to which the scan succeeds.

If you set www.baidu.com, http://www.baidu.com/aaa?code=xxx can be successful, but if you want to skip http://lms.baidu.com/aaa?code=xxx, it will not be successful. The redirect_URI parameter is incorrect.

After the account is ready, we will see what the requirements are.

What does scan login look like?

Wechat scanning code login is a very common requirement for users to use wechat to log in to third-party applications or websites, which are generally presented in two ways:

  • The first: redirect to the scan code page specified by wechat
  • Second: the wechat login QR code embedded in our website page

Here is the first method, direct redirection, redirection after the page display like this:

Use a picture to illustrate the process:

As can be seen from the figure, wechat login requires the participation of website page, wechat client, website server and wechat open platform service. The above processes are also available in wechat official documents, which I will explain in detail here. The following will be implemented in code, the back end is divided into the following four steps:

  1. Obtain the authorization login QR code
  2. usecodeExchange wechat interface call voucheraccess_token
  3. useaccess_tokenObtaining User information
  4. Login/registration is complete based on the user informationtokenTo the front

Code implementation

The first step is to redirect to the wechat scan code login page, which can be done at the front end or at the back end. It is also easy to use the AppId and redirectUri callback addresses to concatenate the redirection. The code is as follows:

// auth.controller.ts
  @ApiOperation({ summary: 'Wechat Login Jump' })
  @Get('wechatLogin')
  async wechatLogin(@Headers() header, @Res() res) {
    const APPID = process.env.APPID;
    const redirectUri = urlencode('http://lms.siyuanren.com/web/login_front.html');
    res.redirect(
      `https://open.weixin.qq.com/connect/qrconnect?appid=${APPID}&redirect_uri=${header.refere}&response_type=code&scope=snsapi_login&state=STATE#wechat_redirect`,); }Copy the code

After logging in through the wechat client, the address transmitted by redirect_URI will be redirected and the code parameter will be added. At this time, the front-end will pass the code to the back-end, and the back-end can complete the following steps 2,3, and 4.

Continue to write wechat login interface in auth.controller.ts:

//auth.controller.ts
 @ApiOperation({ summary: 'wechat Login' })
 @ApiBody({ type: WechatLoginDto, required: true })
 @Post('wechat')
 async loginWithWechat(@Body('code') code: string) {
    return this.authService.loginWithWechat(code);
 }
Copy the code

Access_token access_token access_token access_token access_token access_token

// auth.service.ts.import {AccessTokenInfo, AccessConfig, WechatError, WechatUserInfo} from './auth.interface';
import { lastValueFrom } from 'rxjs';
import { AxiosResponse } from 'axios';

  constructor(.private userService: UserService,
    private httpService: HttpService,
  ) {}
    
  / / get access_token
   async getAccessToken(code) {
    const { APPID, APPSECRET } = process.env;
    if(! APPSECRET) {throw new BadRequestException('[getAccessToken] must have appSecret');
    }
    if(!this.accessTokenInfo ||
      (this.accessTokenInfo && this.isExpires(this.accessTokenInfo))
    ) {
      // Use httpService to request accessToken data
      const res: AxiosResponse<WechatError & AccessConfig, any> =
        await lastValueFrom(
          this.httpService.get(
            `The ${this.apiServer}/sns/oauth2/access_token? appid=${APPID}&secret=${APPSECRET}&code=${code}&grant_type=authorization_code`,),);if (res.data.errcode) {
        throw new BadRequestException(
          `[getAccessToken] errcode:${res.data.errcode}, errmsg:${res.data.errmsg}`,); }this.accessTokenInfo = {
        accessToken: res.data.access_token,
        expiresIn: res.data.expires_in,
        getTime: Date.now(),
        openid: res.data.openid,
      };
    }

    return this.accessTokenInfo.accessToken;
  }
Copy the code

Access_token = access_token = access_token = access_token = access_token

parameter version
access_token Interface call credentials
expires_in Timeout time for access_token interface to call credentials, in seconds
refresh_token The access_token was refreshed. Procedure
openid Unique identifier of an authorized user
scope Scope of user authorization, separated by commas (,)

Openid is the unique identifier of the user registered with wechat. At this time, we can check whether the user exists in the database. If not, we can register a new user:

// auth.service.ts
async loginWithWechat(code) {
    if(! code) {throw new BadRequestException('Please enter wechat authorization code');
    }
    await this.getAccessToken(code);

    // Check whether the user exists
    const user = await this.getUserByOpenid();
    if(! user) {// Obtain wechat user information and register new users
      const userInfo: WechatUserInfo = await this.getUserInfo();
      return this.userService.registerByWechat(userInfo);
    }
    return this.login(user);
}

async getUserByOpenid() {
    return await this.userService.findByOpenid(this.accessTokenInfo.openid);
}
Copy the code

Here the implementation of the code is relatively long, will not show all, request wechat open platform interface are similar, it omitted the use of access_token to obtain user information, source code can be obtained by themselves.

If you are interested, you can package the wechat login into a module, so that the requests of the wechat public platform do not have to be mixed in the Auth module.

Finally, I will show you the results:

Wechat scan code login is relatively simple to achieve, login registration this article is more detailed, the content is longer, a separate chapter, will improve the article module and upload file function in the next article, I hope to provide a little help to everyone’s learning.

conclusion

The article has realized the registration, as well as JWT local authentication login and wechat scan code login, the overall looks ok, but actually buried two pits.

  • First, local authentication logintokenNo expiration time is set, which is extremely risky;
  • Second, wechat scan code loginaccess_tokenIs time-sensitive, how to achieve multiple use within the validity period, rather than every scan code to obtainaccess_token

These two problems can be solved with Redis. In the later explanation of Redis, solutions to these two problems will be given, so you can think about it.

Nest. Js takes you hand in hand – project creation & database operation

Reference article:

  • Passport. Js Study Notes