上篇讲到 Nest
的入门,这篇讲下在 Nest
中守卫,也叫授权。
我们现在就以常见的身份认证作为示例,通过登录获取token,在特定的请求头需要带上token,并校验token是否过期
身份认证
Passport
是最流行的 node.js
身份验证库。将这个库与使用 @nestjs/passport
模块的 Nest
应用程序集成起来非常简单。这是官方推荐的一个库,接下来我们就使用该库来实现认证
安装
$ npm install --save @nestjs/passport passport passport-local
$ npm install --save-dev @types/passport-local
Passport
提供了一种名为 Passport-local
的策略,它实现了一种用户名/密码身份验证机制。关于其策略,官网都有详细的说明,我们接下来就看具体实现。
实现
我们新增一个 auth
模块,用于权限验证;在实现这块功能时,你就会发现我们之前写的 user
模块是很有用的。
nest g controller auth
nest g service auth
nest g module auth
生成 auth.controller.ts
、auth.service.ts
和 auth.module.ts
文件,现在我们来完善它们
- 新增登录接口
auth.controller.ts
import { Controller, Post, Body } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiForbiddenResponse, ApiNotFoundResponse } from '@nestjs/swagger';
import { AuthService } from './auth.service';
import { LoginDto } from './dto';
@ApiTags('auth')
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@ApiOperation({ summary: 'user login' })
@ApiForbiddenResponse({ description: 'Forbidden' })
@ApiNotFoundResponse({ description: 'Not Found' })
@Post('/login')
async login(@Body() _: LoginDto) {
return this.authService.login(_);
}
}
auth.service.ts
提供登录服务,并返回 token
import { Injectable } from '@nestjs/common';
@Injectable()
export class AuthService {
async login(user) {
return {
token: `Bearer token`,
};
}
}
auth.module.ts
提供 auth.service
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
@Module({
imports: [],
providers: [AuthService],
controllers: [AuthController],
})
export class AuthModule {}
更新 app.module.ts
import { Module } from '@nestjs/common';
import { SequelizeModule } from '@nestjs/sequelize';
import { UserModule } from './user/user.module';
import { User } from './user/user.model';
import { AuthModule } from './auth/auth.module';
@Module({
imports: [
UserModule,
AuthModule,
SequelizeModule.forRoot({
dialect: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'root',
database: 'mysql_demo',
models: [User],
}),
],
controllers: [],
providers: [],
})
export class AppModule {}
这时刷新浏览器,就能看见新增了一个接口,我们执行一下,就能看见接口返回的模拟 token
那么接下来我们就是需要完善登录这个服务了,我们的需求是用户输入账号密码,服务端去数据库查找,验证是否能够匹配的上,所以 service
的任务是检索用户并验证密码。我们提供一个 validateUser
方法,调用 user
模块的服务查找该用户的账号,并对比输入的密码与返回的密码是否一致
auth.service.ts
import { Injectable } from '@nestjs/common';
import { UserService } from '../user/user.service';
@Injectable()
export class AuthService {
constructor(
private readonly userService: UserService,
) {}
async validateUser(username: string, pass: string): Promise {
const user = await this.userService.findOneByName(username);
if (user && user.password === pass) {
const { password, ...result } = user;
return result;
}
return null;
}
async login(user) {
return {
token: `Bearer token`,
};
}
}
user.service.ts
需要新增一个通过用户名查询用户的服务,并返回用户的密码
async findOneByName(username: string): Promise {
return await this.userModel.findOne({
where: { username },
attributes: ['id', 'username', 'password'],
});
}
好了,现在查询用户的服务有了,校验用户身份的方法也有了,那么我们就可以实现 Passport
本地身份验证策略了。
-
Passport
本地策略
在 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()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private readonly authService: AuthService) {
super();
}
async validate(username: string, password: string): Promise {
const user = await this.authService.validateUser(username, password);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
passport-local
用例中,没有配置选项,因此我们的构造函数只是调用 super()
,没有 options
对象。
对于每个策略,Passport
将使用适当的特定于策略的一组参数调用 verify
函数(使用 @nestjs/Passport
中的 validate()
方法实现)。对于本地策略,Passport
需要一个具有以下签名的 validate()
方法: validate(username: string, password: string): any
。任何 Passport
策略的 validate()
方法都将遵循类似的模式,只是表示凭证的细节方面有所不同。如果找到了用户并且凭据有效,则返回该用户,以便 Passport
能够完成其任务,并且请求处理管道可以继续。如果没有找到,抛出一个异常,让异常层处理它。
更新一下 auth.module
import { Module } from '@nestjs/common';
import { UserModule } from '../user/user.module';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { PassportModule } from '@nestjs/passport';
import { LocalStrategy } from './local.strategy';
@Module({
imports: [
UserModule,
PassportModule,
],
providers: [AuthService, LocalStrategy],
controllers: [AuthController],
})
export class AuthModule {}
内置守卫
守卫的主要功能:确定请求是否由路由处理程序。简单来说,我们针对不同的路由可能会存在不同的权限管控,如:auth/login
我们就需要用户名/密码凭证来启动身份验证;而其他接口(获取用户信息)我们就需要启用令牌 token
(也就是 JWT
机制)来检验。
当然 @nestjs/passport
模块为我们提供了一个内置的守卫,我们可以应用内置的守卫来启动护照本地流。
auth.controller.ts
为其添加本地守卫
import { Controller, Post, Body, UseGuards } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiForbiddenResponse, ApiNotFoundResponse } from '@nestjs/swagger';
import { AuthService } from './auth.service';
import { LoginDto } from './dto';
import { LocalAuthGuard } from './local-auth.guard';
@ApiTags('auth')
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@ApiOperation({ summary: 'user login' })
@ApiForbiddenResponse({ description: 'Forbidden' })
@ApiNotFoundResponse({ description: 'Not Found' })
@UseGuards(LocalAuthGuard)
@Post('/login')
async login(@Body() _: LoginDto) {
return this.authService.login(_);
}
}
这里 @UseGuards(LocalAuthGuard)
就是一个守卫,也有这样写的 @UseGuards(AuthGuard('local')
;对于前者,我们需新建一个 guard
文件来负责,其实现很简单
local-auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}
这样的好处是,我们能清楚的知道每个守卫自己的职责,随着业务的扩展,守卫也会随之增加,也方便对其守卫进行一些扩展。
Passport
会根据从 validate
方法返回的值自动创建一个 user
对象,并将其作为 req.user
分配给请求对象。因此我们需要修改 auth.controller.ts
来接受这个返回值
import { Controller, Post, Body, UseGuards, Request } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiForbiddenResponse, ApiNotFoundResponse } from '@nestjs/swagger';
import { AuthService } from './auth.service';
import { LoginDto } from './dto';
import { LocalAuthGuard } from './local-auth.guard';
@ApiTags('auth')
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@ApiOperation({ summary: 'user login' })
@ApiForbiddenResponse({ description: 'Forbidden' })
@ApiNotFoundResponse({ description: 'Not Found' })
@UseGuards(LocalAuthGuard)
@Post('/login')
async login(@Body() _: LoginDto, @Request() req) {
return req.user.dataValues;
}
}
此时,我们调用登录接口,当我们输入错误的账号密码时,就会发现接口返回401的状态码,告诉我们无权限,当我们输入正取的账号密码时,就能看见返回该用户
说明我们的校验生效了,回到最开始,我们是希望登录返回一个 token
(JWT
),而 token
就是包含了用户信息,过期时间等;接下来我们就实现 token
JWT 功能
JWT
全称是 JSON Web Token
,是目前最流行的跨域认证解决方案;其原理就是服务器认证以后,生成一个 JSON
对象,包含了用户基本信息以及过期时间,然后返回给调用者,而后有权限的 API
都需要带上该信息
安装
$ npm install @nestjs/jwt passport-jwt
$ npm install @types/passport-jwt --save-dev
实现
之前我们说到 LocalAuthGuard
守卫会返回 user
并以 req.user
给到请求者,现在我们就来完善生成 token
auth.service.ts
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UserService } from '../user/user.service';
@Injectable()
export class AuthService {
constructor(
private readonly userService: UserService,
private readonly jwtService: JwtService,
) {}
async validateUser(username: string, pass: string): Promise {
const user = await this.userService.findOneByName(username);
if (user && user.password === pass) {
const { password, ...result } = user;
return result;
}
return null;
}
async login(user: {username: string, id: string}) {
const payload = { username: user.username, sub: user.id };
return {
access_token: `Bearer ${this.jwtService.sign(payload)}`,
};
}
}
@nestjs/jwt
库提供了一个 sign
函数,用于从用户对象属性的子集生成 jwt
,然后返回 access_token
接下来需要更新 auth.module.ts
来导入新的依赖项并配置 JwtModule
。需要创建一个共享密钥用在 JWT
签名和验证步骤之间,为了方便,我们新建 constants.ts
来存放该密钥,但在正式的生产环境中不推荐这样做,因为密钥需要一定的保护措施,你可以放在环境变量中或者其他的方式也可以,总之要有一定的安全保护
constants.ts
export const jwtConstants = {
secret: 'secretKey',
};
auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { UserModule } from '../user/user.module';
import { AuthService } from './auth.service';
import { PassportModule } from '@nestjs/passport';
import { LocalStrategy } from './local.strategy';
import { jwtConstants } from './constants';
import { AuthController } from './auth.controller';
@Module({
imports: [
UserModule,
PassportModule,
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.register({
secret: jwtConstants.secret,
signOptions: { expiresIn: '12h' },
}),
],
providers: [AuthService, LocalStrategy],
controllers: [AuthController],
})
export class AuthModule {}
这里我们使用 register
配置 JwtModule
,并传入一个配置对象,更多的配置项可以查看官网
接下来修改 auth.controller.ts
的 login
接口,使其调用 service
的 login
方法
@ApiTags('auth')
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@ApiOperation({ summary: 'user login' })
@ApiForbiddenResponse({ description: 'Forbidden' })
@ApiNotFoundResponse({ description: 'Not Found' })
@UseGuards(LocalAuthGuard)
@Post('/login')
async login(@Body() _: LoginDto, @Request() req) {
return this.authService.login(req.user.dataValues);
}
}
我们重新请求下登录接口,就能看到返回的数据就是 access_token
拦截
接下来我们就可以使用 token
来对 API
路由进行校验;passport-jwt
策略可以用于用 JSON Web
标记保护 RESTful
端点。
新建 jwt.strategy.ts
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { jwtConstants } from './constants';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: jwtConstants.secret,
});
}
async validate(payload: any) {
return { id: payload.sub, username: payload.username };
}
}
-
jwtFromRequest
:提供从请求中提取JWT
的方法。我们将使用在API
请求的授权头中提供token
的标准方法 -
ignoreExpiration
:选择默认设置false
,它将确保JWT
没有过期的责任委托给Passport
模块。这意味着,如果我们的路由提供了一个过期的JWT
,请求将被拒绝,并发送401
未经授权的响应 -
secretOrkey
:使用权宜的选项来提供对称的密钥来签署令牌 -
validate
:对于JWT
策略,Passport
首先验证JWT
的签名并解码JSON
。然后调用validate
方法,该方法将解码后的JSON
作为其单个参数传递
auth.module.ts
中添加新的 JwtStrategy
作为提供者
@Module({
imports: [
UserModule,
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.register({
secret: jwtConstants.secret,
signOptions: { expiresIn: '12h' },
}),
],
providers: [AuthService, LocalStrategy, JwtStrategy],
controllers: [AuthController],
})
export class AuthModule {}
新建 jwt-auth.guard.ts
import {
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
canActivate(context: ExecutionContext) {
return super.canActivate(context);
}
handleRequest(err, user) {
if (err || !user) {
throw err || new UnauthorizedException();
}
return user;
}
}
上面我们看到 jwt.strategy.ts
的 validate
方法会返回 user
;在 handleRequest
我们可以看到如果找不到 user
就会抛出没有权限的错,我们在获取用户的接口加上该拦截
user.controller.ts
import { Controller, Get, Param, UseGuards, Post, Body } from '@nestjs/common';
import {
ApiTags,
ApiOperation,
ApiUnauthorizedResponse,
ApiForbiddenResponse,
ApiNotFoundResponse,
} from '@nestjs/swagger';
import { CreateUserDto } from './dto';
import { UserService } from './user.service';
import { User } from './user.model';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
@ApiTags('user')
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}
@ApiOperation({
summary: 'find a user by id',
})
@ApiUnauthorizedResponse({ description: 'Unauthorized' })
@ApiForbiddenResponse({ description: 'Forbidden' })
@ApiNotFoundResponse({ description: 'Not Found' })
@UseGuards(JwtAuthGuard)
@Get(':id')
findOne(@Param('id') id: string): Promise {
return this.userService.findOneById(id);
}
@ApiOperation({
summary: 'find user list',
})
@ApiUnauthorizedResponse({ description: 'Unauthorized' })
@ApiForbiddenResponse({ description: 'Forbidden' })
@ApiNotFoundResponse({ description: 'Not Found' })
@UseGuards(JwtAuthGuard)
@Get()
findAll(): Promise {
return this.userService.findAll();
}
@ApiOperation({
summary: 'create a user',
})
@ApiUnauthorizedResponse({ description: 'Unauthorized' })
@ApiForbiddenResponse({ description: 'Forbidden' })
@ApiNotFoundResponse({ description: 'Not Found' })
@UseGuards(JwtAuthGuard)
@Post()
create(
@Body() createUserDto: CreateUserDto,
): Promise {
return this.userService.create(createUserDto);
}
}
这时我们之前去访问接口,就能看见接口报 401
给文档加权限
swagger
提供了装饰器,可以在请求头带上 token
修改 main.ts
import { NestFactory } from '@nestjs/core';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const options = new DocumentBuilder()
.setTitle('nest demo example')
.setDescription('The nest demo API description')
.setVersion('1.0')
.addBearerAuth()
.build();
const document = SwaggerModule.createDocument(app, options);
SwaggerModule.setup('api-docs', app, document);
await app.listen(3000);
}
bootstrap();
然后在 user.controller.ts
文件里的接口加上 @ApiBearerAuth()
装饰器,这时我们看见接口问题出现了 Authorizate
图标
我们点击那个锁,就会弹窗一个弹窗让我们填写 token
,我们调用登录接口,把返回回来的 access_token
值粘贴上,就能正常访问需要权限的接口了,注意由于之前返回带上了 Bearer
前缀,所以在粘贴的时候需要去掉
到此守卫就入门了,接下来期待自己深入的研究
代码传送门:nest-demo
参考资料:Nest 文档