在当今的 Web 应用开发中,确保系统的安全性和对用户角色的精确权限控制是至关重要的。本文将详细介绍如何使用后端的 NestJS 结合 MySQL 和 Redis,以及前端的 Vue3 结合 TypeScript 来实现角色的权限控制。
源码地址
预览地址 cszh1/1q2w3e4r%T
一、后端技术实现
-
NestJS 框架
- NestJS 是一个基于 Node.js 的渐进式后端框架,它提供了模块化、可扩展和易于维护的架构。
- 通过使用 NestJS 的依赖注入和模块系统,我们能够清晰地组织和管理后端的业务逻辑。
-
MySQL 数据库
-
MySQL 用于存储角色、权限和用户相关的信息。
-
设计合理的数据库表结构,例如
roles表(包含角色 ID、角色名称等字段)、permissions表(包含权限 ID、权限名称等字段)和user_roles表(关联用户和角色)。 -
以下是数据库中的相关表:
-
-
Redis 缓存
- Redis 用于缓存频繁访问的权限数据,提高系统的性能。
- 例如,将用户的角色权限信息缓存起来,减少对数据库的查询次数。
-
权限拦截器
权限拦截器是用于在请求处理过程中进行权限检查和控制的关键组件。它在整个系统的权限管理体系中发挥着重要作用。 其主要目的是确保只有具有适当权限的用户能够继续执行请求的处理逻辑。通过与 Redis 缓存和反射机制的结合,它能够高效地获取和验证用户的权限信息。
在具体实现中,首先从反射器获取与当前处理函数相关的权限信息。如果未获取到,直接让请求继续处理。若获取到权限信息,会从请求中提取必要的数据来构建 Redis 缓存的键,然后从 Redis 中获取相应的按钮权限信息。
对于获取到的权限信息,会进行类型转换和权限匹配的检查。如果权限匹配成功,允许请求继续处理;否则,根据不同情况抛出相应的异常,提示未授权或会话过期。
这种设计确保了系统的安全性和权限的精确控制,防止未经授权用户越权操作,保障了系统数据和功能的安全性和完整性。
import { CallHandler, ExecutionContext, HttpStatus, Injectable, NestInterceptor } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { RedisService } from 'src/modules/redis/redis.service';
import { PERMISSION } from '../decorator';
import { Observable } from 'rxjs';
import { Request } from 'express';
import { ApiCode, RedisCache } from '../enums';
import { SysMenu } from 'src/modules/system/menu/entities/menu';
import { ApiException } from '../common';
@Injectable()
export class PermissionInterceptor implements NestInterceptor {
constructor(private redisService: RedisService, private readonly reflector: Reflector) {}
async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>> {
const handler = this.reflector.getAllAndOverride(PERMISSION, [context.getHandler()]);
if (!handler) {
return next.handle();
}
const request = context.switchToHttp().getRequest<Request>();
const buttonCacheKey = `${RedisCache.ROLE_BUTTON_PERMISSION}${request.user.id}`;
// 方法设置的按钮code
let values = handler[0];
const buttonPermission = await this.redisService.get<SysMenu[]>(buttonCacheKey);
if (buttonPermission && buttonPermission.length > 0) {
// 如果是字符串 转换为数组
if (typeof values === 'string') {
values = [values];
}
const flag = buttonPermission.map((item) => item.code).some((item) => values.includes(item));
if (flag) {
return next.handle();
} else {
throw new ApiException('未授权', ApiCode.FORBIDDEN, HttpStatus.FORBIDDEN);
}
} else {
throw new ApiException('会话已过期,请重新登录', ApiCode.SESSION_EXPIRED, HttpStatus.GATEWAY_TIMEOUT);
}
}
}
- JWT 授权
在授权机制中,采用了 Strategy 来实现用户登录体系。
其主要作用是对用户输入的登录信息(包括账号、密码、验证码等)进行严格的验证和处理。
import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { AuthService } from '../auth.service';
import { HttpStatus, Injectable } from '@nestjs/common';
import { UserLoginDto } from 'src/modules/system/user/dto/user-login.dto';
import { ApiCode, RedisCache } from 'src/utility/enums';
import { RedisService } from 'src/modules/redis/redis.service';
import { ApiException } from 'src/utility/common';
import { Request } from 'express';
import { LoginLogService } from 'src/modules/log/login-log/login-log.service';
import { LoginStatus } from 'src/modules/log/login-log/enums/login.status.enum';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private readonly redisService: RedisService, private loginLogService: LoginLogService, private readonly authService: AuthService) {
super({
usernameField: 'account',
passReqToCallback: true
});
}
async validate(request: Request, account: string, password: string) {
const body = request.body as UserLoginDto;
const { uuid, code } = body;
const redisKey = `${RedisCache.CAPTCHA_CODE}${uuid}`;
const redisCode = await this.redisService.get(redisKey);
if (!code) throw new ApiException('验证码不能为空', ApiCode.BAD, HttpStatus.BAD_REQUEST);
if (!uuid) throw new ApiException('uuid不能为空', ApiCode.BAD, HttpStatus.BAD_REQUEST);
if (!account) throw new ApiException('账号不能为空', ApiCode.BAD, HttpStatus.BAD_REQUEST);
if (!password) throw new ApiException('密码不能为空', ApiCode.BAD, HttpStatus.BAD_REQUEST);
if (redisCode === null) {
this.loginLogService.create({
code: LoginStatus.FAIL,
msg: '验证码已过期',
request
});
throw new ApiException('验证码过期', ApiCode.CODE_EXPIRED, HttpStatus.OK);
}
if (redisCode.toLowerCase() !== code.toLowerCase()) {
this.loginLogService.create({
code: LoginStatus.FAIL,
msg: '验证码错误',
request
});
this.redisService.delete(uuid);
throw new ApiException('验证码错误', ApiCode.CODE_INVALID, HttpStatus.OK);
}
// 验证账号密码
const user = await this.authService.validateUser(request, account, password);
return user;
}
}
二、前端技术实现
- Vue3 框架
- Vue3 提供了更高效和灵活的组件化开发方式。
- 使用组合式 API 来管理组件的状态和逻辑。
- TypeScript
- TypeScript 为前端代码提供了类型安全和更好的可维护性。
- 定义接口和类型来约束数据的结构和类型。
在前端,根据用户的登录状态获取其角色权限信息,并在页面组件中根据权限来控制页面元素的显示和隐藏。例如,如果用户没有某个操作的权限,对应的按钮将被移出dom
三、权限控制流程
- 用户登录时,后端验证用户身份,并从数据库获取其角色信息。
- 根据角色信息,获取对应的权限列表,并将其存储在 Redis 缓存中。
- 前端在每次页面加载时,向后端请求用户的权限信息。
- 后端从 Redis 缓存中获取权限信息并返回给前端。
- 前端根据权限信息控制页面的显示和操作。
通过以上后端和前端技术的结合,我们成功实现了一个安全可靠的角色权限控制系统。
源码地址
预览地址 cszh1/1q2w3e4r%T
最附上几张系统截图:
这是我的第一个真正意义上的 Node 后台项目,由于经验不足,可能在某些地方做得不够完善,比如在代码结构的优化、数据库设计的合理性或者接口的规范性方面,考虑也不是很周全。但我一直在努力学习和改进,希望大家能够不吝指出,给予我宝贵的意见和建议