前言
权限管理是软件开发绕不过去的一个业务,之前我曾介绍过前端如何做权限管理:
本篇文章将介绍后端如何做权限管理,对Nest不熟悉的,可先查看我之前的文章:
1. 基本概念
在Nest的执行顺序中,守卫处于中间件middleware之后,拦截器interceptor之前。
守卫的作用:根据运行时出现的某些条件(例如权限,角色,访问控制列表等)来确定给定的请求是否由路由处理程序处理,还是直接返回请求失败信息。
守卫其实同拦截器,异常过滤器一样,都是中间件的底层逻辑,不过Nest对其进行了细分,守卫专用作权限校验。
1. 创建守卫
nest g guard guards/roles --no-spec --flat
//简写为
nest g gu guards/roles --no-spec --flat
--no-spec:不生成测试文件。
--flat:不要将生成的文件放入目录中,而是直接放在当前目录下
执行这段命令后,将会在src目录下,新建guards文件夹,并在其下新建roles.guard.ts
通过命令,生成的守卫模块为:
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class RolesGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
return true;
}
}
@Injectable():告诉NestJS依赖注入系统这个类可以作为一个服务被注入到其他地方;
canActivate():函数接收单个参数context,即当前请求的上下文。并返回一个布尔值,表示是否阻止请求到达控制器。
2. 绑定守卫
共有三种级别的守卫,分别为:1. 方法范围的守卫;2. 控制器范围的守卫;3. 全局守卫
1. 方法范围的守卫
使用 @UseGuards() 装饰器注册方法范围级别的守卫
@UseGuards(RolesGuard)
@Get('profile')
getProfile() {}
2. 控制器范围的守卫
使用 @UseGuards() 装饰器注册控制器范围级别的守卫
@Controller('cats')
@UseGuards(RolesGuard) //控制器级别的守卫
export class CatsController {}
3. 全局守卫
一般在app.module.ts,providers中,使用useClass进行注册。
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { RolesGuard } from './guards/roles.guard';
//...
@Module({
//...
providers: [
//...
{
provide: APP_GUARD,
useClass: RolesGuard,
},
],
})
export class AppModule {}
2. 配置准备
1. 配置环境变量
目的:项目启动时,根据启动参数,读取不同的配置文件。
- 安装
npm i cross-env -D
- 修改package.json
"scripts": {
"start:dev": "cross-env NODE_ENV=development nest start --watch",
"start:prod": "cross-env NODE_ENV=production node dist/main",
},
- 在项目中使用
process.env.NODE_ENV读取环境变量
2. 配置全局白名单
目的:使用JWT接口验证,肯定是要配置白名单的,例如登录接口需配置在白名单内。虽然Nest文档上使用SetMetadata设置元数据来区分不需要权限的接口。但个人而言,更喜欢使用@nestjs/config将配置注入到全局,白名单在配置文件中统一管理。
1. 安装
npm i @nestjs/config
npm i js-yaml
npm i -D @types/js-yaml
@nestjs/config:内部基于dotenv实现,在Nest中,用于提供全局配置。
js-yaml:将 YML 格式的字符串解析成 JavaScript 对象。
2. 自定义配置文件
新建src/config/development.yml
# jwt 配置
jwt:
secretkey: 'you_secretkey'
expiresin: '60s'
# 权限 白名单配置
router:
whitelist:
[
{ path: '/user/register', method: 'POST' },
{ path: '/user/login', method: 'POST' },
]
3. 读取配置文件
新建src/config/index.ts
import { readFileSync } from 'fs';
import * as yaml from 'js-yaml';
import { join } from 'path';
//读取环境变量
const env = process.env.NODE_ENV;
export default () => {
return yaml.load(
readFileSync(join(__dirname, '../../', `src/config/${env}.yml`), 'utf8'),
) as Record<string, any>;
};
4. 全局注册
修改app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import configuration from './config';
//...
@Module({
imports: [
ConfigModule.forRoot({
cache: true, //缓存
load: [configuration], //加载配置文件
isGlobal: true, //设置为全局
})
],
//...
})
export class AppModule {}
使用ConfigModule.forRoot()静态方法,配置和初始化配置模块。
3. 登录认证 JWT
1. 安装
npm i @nestjs/passport passport
npm i @nestjs/jwt passport-jwt
npm i @types/passport-jwt -D
@nestjs/passport:是一个用于在 NestJS 应用程序中集成 passport 的模块。Passport.js 是一个流行的 Node.js 身份验证中间件,提供了多种身份验证策略,如本地身份验证、OAuth、JWT 等。
@nestjs/jwt:是一个用于在 NestJS 应用程序中实现JWT身份验证的模块。
passport-jwt:是一个 passport 策略,与@nestjs/passport结合使用,用于通过JWT身份验证。
四个包之间的关系:passport 是Node.js身份验证中间件;@nestjs/passport用于将 Passport 集成到 Nest 应用程序中;passport-jwt是passport的一种策略,即验证JWT的token是否有效;@nestjs/jwt是Nest的JWT验证模块,用于JWT模块的注册与token的生成。
2. Auth模块
Auth模块负责注册passport与passport-jwt
1. 使用命令行生成 Auth Module
nest g mo auth
2. 新建auth/auth.strategy.ts
编写身份验证策略,这里验证JWT策略
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private readonly config: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: config.get('jwt.secretkey'),
});
}
async validate(payload) {
return payload;
}
}
jwtFromRequest:ExtractJwt.fromAuthHeaderAsBearerToken():从请求头中使用 Authorization 字段提取 JWT,并且期望格式为 Bearer 。
ignoreExpiration:false 不忽略 JWT 的过期时间,即如果令牌过期,将被视为无效。
secretOrKey:验证 JWT 的密钥,这里从配置服务中读取 jwt.secretkey,即上文yml文件的secretkey字段的值you_secretkey
validate:可在validate函数中,做额外的自定义权限校验,例如检查用户状态。这里直接返回参数。
3. 修改auth.module.ts
导入PassportModule,注册JwtStrategy
import { Module } from '@nestjs/common';
import { JwtStrategy } from './auth.strategy';
import { PassportModule } from '@nestjs/passport';
@Module({
imports: [PassportModule],
providers: [JwtStrategy],
exports: [],
})
export class AuthModule {}
3. User模块
User模块负责注册@nestjs/jwt,以及在登录接口中返回生成的token值
1. 使用命令行新建一个User模块
nest g mo user
nest g co user --no-spec
nest g s user --no-spec
2. 修改user.module.ts,注册JWT
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { UserController } from './user.controller';
import { UserService } from './user.service';
@Module({
imports: [
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: async (config: ConfigService) => ({
secret: config.get('jwt.secretkey'),
signOptions: { expiresIn: config.get('jwt.expiresin') },
}),
inject: [ConfigService],
}),
],
controllers: [UserController],
providers: [UserService],
})
export class UserModule {}
JwtModule.registerAsync:动态注册 JWT 模块,允许在运行时从外部配置中获取 JWT 的相关配置。
imports:导入ConfigModule模块,以便使用ConfigService服务
inject:接受ConfigService服务,在实例化过程中,Nest 将解析该数组并将其作为参数传递给工厂函数。
useFactory:允许动态创建提供程序。这里根据ConfigService服务读取配置文件,获取创建的参数
secret:从配置服务中获取 JWT 的秘钥,即yml配置文件中secretkey的值you_secretkey
signOptions:设置 JWT 的签名选项,这里设置过期时间,即yml配置文件中expiresin的值60s
3. 修改user.service.ts
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class UserService {
constructor(private readonly jwtService: JwtService) {}
//生成令牌
createToken(payload: { username: string; userId: string }): string {
//jwt会使用secret中配置的密钥,对payload进行加密,从而生成token
//这里根据用户名与用户ID进行加密,生成token
const accessToken = this.jwtService.sign(payload);
return accessToken;
}
}
UserService中包含一个createToken方法,用于根据用户名与用户id生成token。
4. 修改user.controller.ts
import { Controller, Body, Post, Get, Request } from '@nestjs/common';
import { UserService } from './user.service';
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}
@Post('/login')
async login(@Body() params) {
//可先从数据库中查询用户,将用户信息作为载荷,生成token
//这里直接模拟一条用户数据
const user = { userId: '1', username: '张三', role: 1 };
const accessToken = this.userService.createToken(user);
return {
accessToken,
};
}
@Get()
getUser(@Request() req) {
/* auth.strategy.ts中,validate方法返回的值会自动创建一个user对象,
并将其作为 req.user 分配给请求对象。
之后的在别的模块,直接使用req.user便可获取用户信息,这里当调用到该接口时,直接返回用户信息 */
return req.user;
}
}
UserController中包含2个方法:
login方法用于在请求/user/login时,将参数传递给userService.createToken以生成token,并将token返回给客户端。
getUser方法用于在请求/user时,将用户信息返回给客户端
4. 守卫
守卫的作用:在控制器处理请求前,先读取配置中的白名单,如果请求路由在白名单中,直接不拦截,否则继续走passport 策略,验证JWT是否有效。
1. 安装
npm i path-to-regexp@^7.1.0
path-to-regexp 是一个用于将 URL 路径字符串转换为正则表达式的 JavaScript 库,主要用于路由匹配。在这里用于校验路径是否匹配
2. 新建src/guards/auth.guard.ts
import { ConfigService } from '@nestjs/config';
import { AuthGuard } from '@nestjs/passport';
import { pathToRegexp } from 'path-to-regexp';
import { ExecutionContext, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
private globalWhiteList = [];
constructor(private readonly config: ConfigService) {
super();
this.globalWhiteList = [].concat(this.config.get('router.whitelist') || []);
}
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const isInWhiteList = this.checkWhiteList(context);
//如果在白名单内,直接返回true
//否则调用super.canActivate,走PassPort策略
if (isInWhiteList) {
return true;
}
return super.canActivate(context);
}
/**
* 检查接口是否在白名单内
* @param context
* @returns
*/
checkWhiteList(context: ExecutionContext): boolean {
const req = context.switchToHttp().getRequest();
const i = this.globalWhiteList.findIndex((route) => {
// 请求方法类型相同
if (req.method.toUpperCase() === route.method.toUpperCase()) {
// 对比 url
return !!pathToRegexp(route.path).exec(req.url);
}
return false;
});
return i > -1;
}
}
canActivate():接收单个参数context,即当前请求的上下文。并返回一个布尔值,表示是否阻止请求到达控制器。如果请求路由在白名单中,直接返回true。否则调用JWT验证。
3. 修改app.module.ts,全局注册
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AuthModule } from './auth/auth.module';
import configuration from './config';
import { JwtAuthGuard } from 'src/guards/auth.guard';
import { APP_GUARD } from '@nestjs/core';
import { UserModule } from './user/user.module';
@Module({
imports: [
ConfigModule.forRoot({
cache: true, //缓存
load: [configuration], //加载配置文件
isGlobal: true, //设置为全局
}),
AuthModule,
UserModule,
],
controllers: [],
providers: [
{
provide: APP_GUARD,
useClass: JwtAuthGuard, //全局注册守卫
},
],
})
export class AppModule {}
在providers中,使用useClass注册全局守卫。
运行 npm run start:dev 启动项目,使用POST访问localhost:3000/user/login,获取token。
将token使用Authorization字段加到请求头中,值为Bearer token(中间有空格),使用GET访问localhost:3000/user,获取用户信息
对于无权限,或者token过期的请求,接口会返回401状态码报错
{
"message": "Unauthorized",
"statusCode": 401
}
4. 权限划分
JWT仅负责登录验证,确保用户需先登录才能访问接口。而项目中大部分情况,还需做权限的进一步细分,比如管理员账户,非管理员账户或者是VIP账户,非VIP账户。
思路分为三步:
-
登录时,会查询数据库,获取用户权限信息。而passport会通过validate函数将用户信息存放在request.user上。
-
通过在路由上使用SetMetadata设置元数据,将路由需要的权限附加在路径处理程序上。
-
新建一个守卫,守卫通过reflector获取路由上的元数据,即权限内容。通过req.user获取用户信息,根据用户信息上的权限等级与元数据上的所需权限进行比较,符合返回true,否则false。
第1步在JWT中已配置好,用户的信息为{ userId: '1', username: '张三', role: 1 },通过req.user获取。
1. 设置元数据
- 新建src/decorators/require-role.decorator.ts
import { SetMetadata } from '@nestjs/common';
export enum Role {
SUPER_ADMIN = 1, // 超级管理员
ADMIN = 2, // 管理员
DEVELOPER = 3, // 开发者
HUMAN = 4, // 普通用户
}
export const ROLES_KEY = 'roles';
export const RequireRole = (role: Role) => SetMetadata(ROLES_KEY, role);
自定义@Roles()装饰器,传递参数role,并调用SetMetadata设置元数据
- 修改user.controller.ts
//...
import { RequireRole, Role } from 'src/decorators/require-role.decorator';
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}
//...
@RequireRole(Role.SUPER_ADMIN) //使用自定义装饰器
@Get()
getUser(@Request() req) {
return req.user;
}
}
2. 守卫
1. 新建守卫
修改src/guards/roles.guard.ts
import {
CanActivate,
ExecutionContext,
Injectable,
ForbiddenException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from 'src/decorators/require-role.decorator';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
async canActivate(ctx: ExecutionContext): Promise<boolean> {
// 全局配置,
const req = ctx.switchToHttp().getRequest();
const role = this.reflector.getAllAndOverride(ROLES_KEY, [
ctx.getClass(),
ctx.getHandler(),
]);
if (role && req.user.role > role) {
throw new ForbiddenException('对不起,您无权操作');
}
return true;
}
}
使用this.reflector.getAllAndOverride获取元数据。传递常量ROLES_KEY,获取接口对应的权限等级
使用ctx.switchToHttp().getRequest(),获取上下文中的请求信息,从请求信息中获取用户信息
如果用户等级数字大于接口元数据上定义的,说明无权限(数字越小,权限越高)
2. 全局注册守卫
修改app.module.ts
//...
import { JwtAuthGuard } from 'src/guards/auth.guard';
import { RolesGuard } from 'src/guards/roles.guard';
@Module({
//...
providers: [
{
provide: APP_GUARD,
useClass: JwtAuthGuard, //全局注册守卫
},
{
provide: APP_GUARD,
useClass: RolesGuard,
},
],
})
export class AppModule {}
提示:注意注册顺序,RolesGuard在JwtAuthGuard之后。
因为在user.controller.ts的login中,用户等级为1,代表超级管理员。因此访问localhost:3000/user,接口正常返回。如果将等级降低,改为2
@Post('/login')
async login(@Body() params) {
//可先从数据库中查询用户,将用户信息作为载荷,生成token
const user = { userId: '1', username: '张三', role: 2 };
//...
}
使用POST访问localhost:3000/user/login,重新获取token。再将token添加到请求头Authorization字段中,重新访问localhost:3000/user,接口将提示无权限。