相信各位在使用各大网站提供的功能时,都会需要注册帐号来获得更多的使用体验,比如:google、facebook 等,这种帐户机制可以说是非常重要的一环,在现今的应用上已经可以视为标配。
而一个应用程序可能会有非常多种的注册方式,比如:本地帐号注册方式、使用 facebook 注册、使用 google 注册等,每一种帐号注册方式都有一套自己的 策略(Strategy),那要怎麽管理各种 帐户验证(Authentication) 的策略也是非常重要的,我们会希望各种策略都能采用同一套标准来进行开发,这时候就可以透过一些工具来辅助我们处理这件事,在 node.js 圈子中,最热门的帐户验证管理工具即 Passport.js (简称:passport
),而 Nest 也有将其包装成模组,让开发人员轻松在 Nest 中使用 passport
,模组名称为 PassportModule
。
passport
采用了 策略模式 来管理各种验证方式,它主要由两个部分构成整个帐户验证程序,分别为:passport
与 passport strategy
。passport
本身是用来处理 验证流程 的,而 passport strategy
则是 验证机制,两者缺一不可,整个 passport
生态系有上百种的验证机制让开发人员使用,如:facebook 验证策略、google 验证策略、本地验证策略等,完美解决各种验证机制的处理。
在 Nest 中,passport strategy
会与 Guard 进行搭配,透过 AuthGuard
将 strategy
包装起来,就可以透过 Nest 的 Guard 机制来与 passport
做完美的搭配!
透过 npm
来安装 passport
,需要安装 Nest 包装的模组以及 passport
本身:
$ npm install @nestjs/passport passport
注意:目前仅安装了
passport
,前面有提到还需要passport strategy
来满足完整的验证程序,这部分後面会再额外进行安装。
在开始实作帐户验证之前,需要先设计一个帐户注册的 API,好让使用者可以顺利注册成为会员,这里我们以 MongoDB 作为资料库,并使用上一篇的技巧来完成资料库的操作。
注意:这里会略过
MongooseModule
在AppModule
注册的部分,详情可以参考上一篇的「连线 MongoDB」。
既然是帐户注册,那就跟「使用者」的资料息息相关,故我们要建立一个名为 User
的 schema
来定义使用者的资料结构。我们在 src/common/models
下新增一个名为 user.model.ts
的档案,并将使用者的 schema
、Document
、schema
实体 与 ModelDefinition
在这里做定义:
import { ModelDefinition, Prop, raw, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';
export type UserDocument = User & Document;
@Schema()
export class User {
@Prop({
required: true,
minlength: 6,
maxlength: 16
})
username: string;
@Prop({
required: true
})
email: string;
@Prop({
type: raw({
hash: String,
salt: String
}),
required: true
})
password: Record<string, any>;
}
export const UserSchema = SchemaFactory.createForClass(User);
export const USER_MODEL_TOKEN = User.name;
export const UserDefinition: ModelDefinition = {
name: USER_MODEL_TOKEN,
schema: UserSchema,
};
可以看到共设计了三个栏位,分别为:username
、email
与 password
,其中,password
为巢状结构,原因是我们不希望密码直接储存在资料库里面,而是透过密码学中的加盐来替密码进行加密。
盐加密经常用在密码管理,它的概念很简单,就是将 输入值(input) 与 某个特定的值(salt) 进行加密,最後会得出一个 结果(hash),只要将 salt
与 hash
存入资料库就可以避免把原始密码直接储存的问题,不过为什麽是储存这两个值呢?这就要解释一下解密的原理了,使用者在登入的时候,会提供我们 username
与 password
这两个值,这时候我们就要用使用者提供的 username
去找出对应的使用者资料,如果有找到的话就要来验证 password
的值是否正确,我们只要用 password
与 salt
再进行一次加密,并用计算出来的值跟 hash
做比对,如果完全相同就表示这个使用者提供的密码与当初在注册时提供的密码是相同的。
我们来实作一个共用的方法来处理盐加密,在 src/core/utils
下新增一个 common.utility.ts
档案,并设计一个静态方法 encryptBySalt
,它有两个参数:input
与 salt
,其中,salt
的预设值为 randomBytes
计算出来的值,而 input
与 salt
透过 pbkdf2Sync
进行 SHA-256 加密并迭代 1000
次,最终返回 hash
与 salt
:
import { randomBytes, pbkdf2Sync } from 'crypto';
export class CommonUtility {
public static encryptBySalt(
input: string,
salt = randomBytes(16).toString('hex'),
) {
const hash = pbkdf2Sync(input, salt, 1000, 64, 'sha256').toString('hex');
return { hash, salt };
}
}
在完成 schema
与加密演算之後,就可以来设计注册的 API 了,我们会需要建立两个模组:UserModule
与 AuthModule
,UserModule
是用来处理与使用者相关的操作,而 AuthModule
则是处理与身分验证有关的操作,基本上 AuthModule
必定与 UserModule
产生依赖,因为要有使用者才有办法做身分验证!
透过 CLI 在 src/features
下产生 UserModule
与 UserService
:
$ nest generate module features/user
$ nest generate service features/user
UserModule
因为要对使用者资料进行操作,需要使用 MongooseModule
来建立 model
,又因为 AuthModule
会依赖於 UserModule
去操作使用者资料,故我们要将 UserService
汇出让 AuthModule
可以透过 UserService
去操作使用者资料:
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { UserDefinition } from '../../common/models/user.model';
import { UserService } from './user.service';
@Module({
imports: [MongooseModule.forFeature([UserDefinition])],
providers: [UserService],
exports: [UserService],
})
export class UserModule {}
根据我们设计的使用者资料结构,我们可以设计一个 DTO 来给定参数型别与进行简单的资料验证,在 src/features/user/dto
下新增 create-user.dto.ts
:
import { IsNotEmpty, MaxLength, MinLength } from 'class-validator';
export class CreateUserDto {
@MinLength(6)
@MaxLength(16)
public readonly username: string;
@MinLength(8)
@MaxLength(20)
public readonly password: string;
@IsNotEmpty()
public readonly email: string;
}
我们直接在 AppModule
透过依赖注入的方式来启用 ValidationPipe
:
import { APP_PIPE } from '@nestjs/core';
import { Module, ValidationPipe } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { MongooseModule } from '@nestjs/mongoose';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UserModule } from './features/user/user.module';
import { AuthModule } from './features/auth/auth.module';
import MongoConfigFactory from './config/mongo.config';
@Module({
imports: [
ConfigModule.forRoot({
load: [MongoConfigFactory],
isGlobal: true
}),
MongooseModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
uri: config.get<string>('mongo.uri'),
}),
}),
UserModule,
AuthModule,
],
controllers: [AppController],
providers: [
AppService,
{ // 注入全域 Pipe
provide: APP_PIPE,
useClass: ValidationPipe,
},
],
})
export class AppModule {}
最後,我们要在 UserService
注入 model
并设计 createUser(user: CreateUserDto)
方法来建立使用者,其中,password
需要透过盐加密来处理:
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { CommonUtility } from '../../core/utils/common.utility';
import { UserDocument, USER_MODEL_TOKEN } from '../../common/models/user.model';
import { CreateUserDto } from './dto/create-user.dto';
@Injectable()
export class UserService {
constructor(
@InjectModel(USER_MODEL_TOKEN)
private readonly userModel: Model<UserDocument>,
) {}
createUser(user: CreateUserDto) {
const { username, email } = user;
const password = CommonUtility.encryptBySalt(user.password);
return this.userModel.create({
username,
email,
password,
});
}
}
如果觉得 import
时的路径太繁琐,可以做一个 index.ts
来将对外的部分做统一的汇出:
export { UserModule } from './user.module';
export { UserService } from './user.service';
export { CreateUserDto } from './dto/create-user.dto';
透过 CLI 在 src/features
下产生 AuthModule
与 AuthController
:
$ nest generate module features/auth
$ nest generate controller features/auth
在 AuthModule
中汇入 UserModule
:
import { Module } from '@nestjs/common';
import { AuthController } from './auth.controller';
import { UserModule } from '../user';
@Module({
imports: [UserModule],
controllers: [AuthController],
})
export class AuthModule {}
接着,在 AuthController
设计一个 [POST] /auth/signup
的 API,并调用 UserService
的 createUser(user: CreateUserDto)
方法来建立使用者:
import { Body, Controller, Post } from '@nestjs/common';
import { CreateUserDto, UserService } from '../user';
@Controller('auth')
export class AuthController {
constructor(private readonly userService: UserService) {}
@Post('/signup')
signup(@Body() user: CreateUserDto) {
return this.userService.createUser(user);
}
}
透过 Postman 进行测试:
帐户验证与登入息息相关,在登入的过程中,会进行一些帐号密码的检测,检测通过之後便完成登入程序。本地帐户登入可以使用 passport-local
这个 strategy
与 passport
进行搭配,透过 npm
进行安装即可:
$ npm install passport-local
$ npm install @types/passport-local -D
首先,我们需要先在 UserService
添加一个 findUser
方法来取得使用者资料,用途是让使用者输入 username
与 password
後,可以去资料库中寻找对应的使用者:
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { FilterQuery, Model } from 'mongoose';
import { CommonUtility } from '../../core/utils/common.utility';
import { UserDocument, USER_MODEL_TOKEN } from '../../common/models/user.model';
import { CreateUserDto } from './dto/create-user.dto';
@Injectable()
export class UserService {
constructor(
@InjectModel(USER_MODEL_TOKEN)
private readonly userModel: Model<UserDocument>,
) {}
createUser(user: CreateUserDto) {
const { username, email } = user;
const password = CommonUtility.encryptBySalt(user.password);
return this.userModel.create({
username,
email,
password,
});
}
findUser(filter: FilterQuery<UserDocument>) {
return this.userModel.findOne(filter).exec();
}
}
透过 CLI 产生 AuthService
来处理检测帐户的工作:
$ nest generate service features/auth
在 AuthService
设计一个 validateUser(username: string, password: string)
的方法,先透过 username
寻找对应的使用者资料,再针对使用者输入的密码与 salt
进行盐加密,如果结果与资料库中的 hash
相同,就回传使用者资料,否则回传 null
:
import { Injectable } from '@nestjs/common';
import { CommonUtility } from '../../core/utils/common.utility';
import { UserService } from '../user';
@Injectable()
export class AuthService {
constructor(private readonly userService: UserService) {}
async validateUser(username: string, password: string) {
const user = await this.userService.findUser({ username });
const { hash } = CommonUtility.encryptBySalt(password, user?.password?.salt);
if (!user || hash !== user?.password?.hash) {
return null;
}
return user;
}
}
完成了使用者验证的方法後,就要来将它与 passport
的机制接上,我们需要建立一个 Provider 来作为 strategy
,透过该 strategy
即可与 passport
进行介接。
在 src/features/auth
底下建立一个 stratgies
资料夹并建立 local.strategy.ts
,在这个档案中实作一个 LocalStrategy
的 class
,需特别注意的是该 class
要继承 passport-local
的 strategy
,但需要透过 Nest 制作的 function
来与它做串接,并实作 validate(username: string, password: string)
方法,该方法即为 passport
流程的 进入点,在这里我们就用呼叫刚刚在 AuthService
实作的方法来进行帐号验证:
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-local';
import { AuthService } from '../auth.service';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private readonly authService: AuthService) {
super();
}
async validate(username: string, password: string) {
const user = await this.authService.validateUser(username, password);
if (!user) {
throw new UnauthorizedException();
}
return { username: user.username, email: user.email };
}
}
别忘了在 AuthModule
汇入 PassportModule
与在 providers
里面添加 LocalStrategy
:
import { Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import { AuthController } from './auth.controller';
import { UserModule } from '../user';
import { AuthService } from './auth.service';
import { LocalStrategy } from './strategies/local.strategy';
@Module({
imports: [
PassportModule,
UserModule
],
controllers: [
AuthController
],
providers: [
AuthService,
LocalStrategy
],
})
export class AuthModule {}
实作完 stragegy
以後,就要实作一个 API 来处理登入验证,我们在 AuthController
添加一个 signin
方法并套用 AuthGuard
,因为我们是使用 passport-local
这个 strategy
,所以要在 AuthGuard
带入 local
这个字串,passport
会自动与 LocalStrategy
进行串接,然後 passport
会将 LocalStrategy
中 validate
方法回传的值写入 请求物件 的 user
属性中,这样就可以在 Controller 中使用该使用者的资讯:
import { Body, Controller, Post, Req, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Request } from 'express';
import { CreateUserDto, UserService } from '../user';
@Controller('auth')
export class AuthController {
constructor(private readonly userService: UserService) {}
@Post('/signup')
signup(@Body() user: CreateUserDto) {
return this.userService.createUser(user);
}
@UseGuards(AuthGuard('local'))
@Post('/signin')
signin(@Req() request: Request) {
return request.user;
}
}
透过 Postman 进行测试,会顺利得到对应的使用者资料:
今天我们实作了注册与登入的功能并且初步了解到 passport
在 Nest 中如何使用,不过这些都只算是 登入前 的处理,还有 登入後 要怎麽保持授权状态等步骤要处理,这部分就留到下篇跟各位详细说明吧!这里附上今天的懒人包:
passport
采用策略模式来管理各种帐户验证的方式。passport
需要搭配 strategy
来完成完整的帐户验证程序。AuthGuard
作为 Guard 并指定要使用哪个 strategy
进行验证。passport-local
来处理本地帐户登入的验证。
>>: Flexbox-30天学会HTML+CSS,制作精美网站
复制字串 i.strcpy() 宣告时宣告另一空字元字串,当strcpy()执行完毕时,就会将此字...
本文开始 回顾一下过去四天提到的,使用OpenCV & Dlib做人脸侦测的方法: Open...
写完测试当然要加入到 CI 里做自动化测试拉! 但本人也是第一次串所以见谅见谅 这次我们使用 Tra...
上篇文章中,我提出了一个「规画系统」,其系统的起始点,是由 PO 与 Designer 组成的子系统...
12 - rescue exception 异常处理在开发过程中时常可见,举例来说 Rails 中找...