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
- When creating a
User
Entity, using@PrimaryGeneratedColumn('uuid')
Create a primary columnid
, the value will be useduuid
Automatic generation.Uuid
Is a unique string; - implementationThe field name hump is underlinedName,
createTime
andupdateTime
The field is converted into an underscore named way to store in the database, just need in@Column
Specified in the decoratorname
Properties; - We used decorators
@BeforeInsert
To decorateencryptPwd
Method, which means that the method is called before data is inserted, ensuring that the passwords used to insert the database are encrypted. - There are three roles for the blog system
root
,autor
andvisitor
.root
With all the permissions,author
With permission to write articles,visitor
Can only read articles, registered users default isvisitor
.root
The 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 one
LocalStorage
Inheritance to@nestjs/passport
To provide thePassportStrategy
Class 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 above
passport-local
The default islocal
- Then call
super
Pass the policy parameter, in this case, if passedusername
andpassword
I 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 isemail
thatusernameField
The corresponding value isemail
. validate
isLocalStrategy
The 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, pass
addSelect
addpassword
Otherwise, password comparison cannot be performed.
- There’s one more thing here, pass
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 token
It 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 out
token
- Step 2: According to
token
Get 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 header
JWT
- FromBodyField: On request
Body
Find in fieldJWT
- FromAuthHeaderAsBearerToken: in the authorization header with
Bearer
Find 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:
- Obtain the authorization login QR code
- use
code
Exchange wechat interface call voucheraccess_token
- use
access_token
Obtaining User information - Login/registration is complete based on the user information
token
To 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 login
token
No expiration time is set, which is extremely risky; - Second, wechat scan code login
access_token
Is 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