技术栈:前端是 antd + react + cra,后端是 nest + typeorm,数据库是 mysql + redis,API 文档用 swagger 生成,部署用 docker compose + pm2,网关使用 nginx。
项目连接: GitHub - zhaojiudong/cms_tem: CMS系统模板
全局注册:
拦截器(useGlobalInterceptors),通道(useGlobalPipes),过滤器(useGlobalFilters)都是在 main.ts里注册使用。
路由(Guard)是在app.ts 里注册使用 ,也可以在main里使用api全局使用
providers: [
AppService,
{
provide: APP_GUARD,
useClass: LoginGuard
},
{
provide: APP_GUARD,
useClass: PermissionGuard
}
]
一、 CMS系统模板 必带功能
注册登录功能
角色鉴权功能
二、 要实现的特性
confjg配置文件
// app.moudle.ts
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({
imports: [
// 这里给项目注册了 ConfigMoudle 实例后, 就可以通过 ConfigService 去读取.env的变量
ConfigModule.forRoot({
isGlobal: true,
// envFilePath: 'src/.env'
envFilePath: path.join(__dirname, '.env')
}),
//使用 ConfigService 去读取.env的变量例子
TypeOrmModule.forRootAsync({
useFactory(configService: ConfigService) {
return {
type: "mysql",
host: configService.get('mysql_server_host'),
port: configService.get('mysql_server_port'),
username: configService.get('mysql_server_username'),
password: configService.get('mysql_server_password'),
database: configService.get('mysql_server_database'),
synchronize: true,
logging: true,
entities: [
User, Role, Permission, MeetingRoom, Booking
],
poolSize: 10,
connectorPackage: 'mysql2',
extra: {
authPlugin: 'sha256_password',
}
}
},
inject: [ConfigService] // 重点在这里,这个ConfigService就是已经被初始化过的对象
}),
]
controllers: [AppController],
providers: [
AppService,
{
provide: APP_GUARD,
useClass: LoginGuard
},
{
provide: APP_GUARD,
useClass: PermissionGuard
}
]
// .env
# redis 相关配置
redis_server_host=redis-container
redis_server_port=6379
redis_server_db=1
# nodemailer 相关配置
nodemailer_host=smtp.qq.com
nodemailer_port=587
nodemailer_auth_user=102xxxx375@qq.com
nodemailer_auth_pass=nkmdmgzkhjkkbfab
# mysql 相关配置
mysql_server_host=mysql-container
mysql_server_port=3306
mysql_server_username=root
mysql_server_password=guang
mysql_server_database=meeting_room_booking_system
# nest 服务配置
nest_server_port=3005
# jwt 配置
jwt_secret=guang
jwt_access_token_expires_time=30m
jwt_refresh_token_expres_time=7d
基于 RBAC 实现权限控制 ( Role Based Access Control,基于角色的权限控制 )
JWT 登录,权限问题 (tip:两个功能一起由下面代码实现)
// app.ts
import { JwtModule } from '@nestjs/jwt';
// 引入 JWT模块实例
// 使用方法 this.jwtService.sign { params: {} , config: {expiresIn : 30ms} }
// 解密: this.jwtService.verify(token)
@Module({
imports: [
JwtModule.registerAsync({
global: true,
useFactory(configService: ConfigService) {
return {
secret: configService.get('jwt_secret'),
signOptions: {
expiresIn: '30m' // 默认 30 分钟
}
}
},
inject: [ConfigService]
}),
providers: [
AppService,
{
provide: APP_GUARD,
useClass: LoginGuard
},
{
provide: APP_GUARD,
useClass: PermissionGuard
}
]
]
使用LoginGuard、PermissionGuard 来做登录和权限的鉴权,根据 handler 上的 metadata 来确定要不要做鉴权、需要什么权限。
// LoginGuard.ts
import { Reflector } from '@nestjs/core';
import { JwtService } from '@nestjs/jwt';
import { Request } from 'express';
const requireLogin = this.reflector.getAllAndOverride('require-login', [
context.getClass(),
context.getHandler()
]);
if(!requireLogin) {
return true;
}
const authorization = request.headers.authorization;
if(!authorization) {
throw new UnauthorizedException('用户未登录');
}
try{
const token = authorization.split(' ')[1];
const data = this.jwtService.verify<JwtUserData>(token);
request.user = {
userId: data.userId,
username: data.username,
roles: data.roles,
permissions: data.permissions
}
return true;
} catch(e) {
throw new UnauthorizedException('token 失效,请重新登录');
}
// PermissionGuard.ts
import { Reflector } from '@nestjs/core';
import { Request } from 'express';
const requiredPermissions = this.reflector.getAllAndOverride<string[]>('require-permission', [
context.getClass(),
context.getHandler()
])
if(!requiredPermissions) {
return true;
}
for(let i = 0; i < requiredPermissions.length; i++) {
const curPermission = requiredPermissions[i];
const found = permissions.find(item => item.code === curPermission);
if(!found) {
throw new UnauthorizedException('您没有访问该接口的权限');
}
}
return true;
// controller层的文件 通过SetMetadata给这个路由增加权限
@Get('aaa')
@SetMetadata('require-login', true)
@SetMetadata('require-permission', ['ddd'])
aaaa() {
return 'aaa';
}
输入守卫 , 输入输出拦截器, 封装context对象,把cookie提取到ctx对象上,输出时,把data封装到一个统一的输出对象上。
加一个修改响应内容的拦截器。把响应的格式改成 {code、message、data}
输入:
由上面实现 , 定义全局的路由守卫,然后给路由增加权限标识
@SetMetadata('require-login', true)
路由就可以跟 全局路由守卫 产生联系
输出:
// main.ts
const app = await NestFactory.create<NestExpressApplication>(AppModule);
app.useGlobalInterceptors(new FormatResponseInterceptor());
// format-response.interceptor.ts
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Response } from 'express';
import { map, Observable } from 'rxjs';
@Injectable()
export class FormatResponseInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const response = context.switchToHttp().getResponse<Response>();
return next.handle().pipe(map((data) => {
return {
code: response.statusCode,
message: 'success',
data
}
}));
}
}
三、要创建的表,要实现的modal层
实现 RBAC ,登陆功能需要三个Entity(实体) , 5个表 :
用户表 User
角色表 Role
权限表 Premissions
用户-角色表 user_roles
角色-权限表 role_permissions
-----------------------------------
RBAC ( Role Based Access Control,基于角色的权限控制 ) :