通用权限控制系统
一、功能图
按照正常的情况下来设计,应该是5张表,分别是用户表,角色表,用户角色表,权限表,角色权限表,用户授予角色,角色再包含权限,这里我们简化一下,直接由用户到权限,然后再根据用户权限表来完成操作
同时这里的权限,我们直接使用以下的等级与分类
- token权限,用户登录与未登录的区别
- 页面权限,登录以后的用户还要细分是否可以进入某一个页面,也就是根据权限来生成动态路由
- API请求权限,这一个权限其实与前台的权限基本保持一致,但仍然细分
- 无需权限:即这个API接口不需要任何权限就可以直接 使用,如登录,注册,上传头像等(一般称之为游客权限)
- 泛权限:这一个权限即代表用户登录以后具备一定的身份,这个身份可以访问一些需要经过token认证的API
- 具体权限:这一个权限是强验证权限,即只有授权了这个权限的用户才可以访问这个API接口
二、数据库设计
-
用户表
列名 默认值 是否为空 类型 索引 其他信息 注释 id NO int PRI auto_increment account NO varchar(50) 账号 password NO varchar(50) 密码 avatar varchar(255) 头像 phone_number varchar(30) 手机号 status 0 NO int 0代表正常,1代表禁用 login_time datetime 最后一次登录时间 remark text 备注说明 is_del 0 NO tinyint 软删除 create_time datetime 创建时间 update_time datetime 修改时间 -
权限表
列名 默认值 是否为空 类型 索引 其他信息 注释 id NO int PRI auto_increment 主键 permission_name NO varchar(50) 权限名称 icon varchar(50) 图标 parent_id NO int 上级菜单 order_num 0 NO int 排序 font_path varchar(255) 前端路径 component varchar(255) 组件名称 permission_type NO int 权限类型 0代表目录 1代表页面 2代表按钮 permission_key NO varchar(255) 权限标识,不能重复 create_time datetime 创建时间 update_time datetime 修改时间 remark 备注 is_del 0 NO tinyint 软删除 -
用户权限表
列名 默认值 是否为空 类型 索引 其他信息 注释 id NO int PRI auto_increment 主键 user_id NO int 用户id permission_id NO int 权限id create_time datetime 创建时间 update_time datetime 修改时间 is_del NO int 软删除
三、安装项目所需要的依赖包
本次项目使用express+typeorm的方式来创建,使用typescript语法来完成
-
先准备一个文件夹,取名为premission_express_20230523
-
打开控制台,执行
npm init --yes
快速进行初始化 -
安装项目所需要使用的依赖,本次项目的依赖主要有以下几个
nodemon
用于项目热启动的ts-node
使用node编译typescript的typescript
这是TS的编译核心包express
这是express项目的核心包body-parser
进行post数据提交的时候接收参数的mysql2
这是连接mysql数据库需要的typeorm
操作数据库的ORM框架
-
创建tsconfig.json的配置文件
{ "compilerOptions": { "emitDecoratorMetadata": true, "experimentalDecorators": true, "target": "ES2015", "module": "CommonJS", "outDir": "./dist", "lib": [ "DOM", "ES2015", "ESNext" ], }, "include": [ "./src/**/*" ], "exclude": [ "node_modules" ] }
tsconfig.json 文件是 TypeScript 项目的配置文件,它包含了编译器的配置选项。常用的配置属性如下:
compilerOptions
: 编译器的选项,如语言版本、目标 JavaScript 版本、生成的 sourcemap 等。include
: 指定需要编译的文件路径或文件夹路径。exclude
: 指定不需要编译的文件路径或文件夹路径。files
: 指定需要编译的文件列表。extends
: 指定继承自另一个 tsconfig.json 文件。compileOnSave
: 指定是否在保存时编译文件。buildOnSave
: 指定是否在保存时编译文件。target
:编译目标 JavaScript 版本,可以是 “ES3”,“ES5” 或 “ES2015” 等。module
:指定模块系统,可以是 “CommonJS”,“AMD” 或 “System” 等。sourceMap
:是否生成 sourcemap 文件。outDir
:编译输出目录。rootDir
:设置项目的根目录。strict
:是否开启严格类型检查。noImplicitAny
:是否禁止隐式 any 类型。lib
:指定要包含在编译中的库文件,如 “es2015”。paths
: 指定模块路径别名。baseUrl
: 指定基础目录。jsx
: 指定 JSX 的处理方式。allowJs
: 是否允许编译 JavaScript 文件。checkJs
: 是否检查 JavaScript 文件。declaration
: 是否生成声明文件。declarationMap
: 是否生成声明文件的 sourcemap。emitDecoratorMetadata
: 是否支持装饰器。experimentalDecorators
: 是否支持实验性装饰器。listEmittedFiles
: 是否列出所有输出的文件。listFiles
: 是否列出所有编译过的文件。locale
: 指定本地化语言。mapRoot
: 指定 sourcemap 文件的根目录。moduleResolution
: 指定模块解析策略。noEmit
: 是否禁止输出 JavaScript 代码。noEmitHelpers
: 是否禁止输出辅助函数。noEmitOnError
: 是否在发生错误时禁止输出 JavaScript 代码。noImplicitReturns
: 是否禁止隐式返回。noUnusedLocals
: 是否检查未使用的局部变量。noUnusedParameters
: 是否检查未使用的参数。preserveConstEnums
: 是否保留 const 枚举。pretty
: 是否格式化输出的 JavaScript 代码。removeComments
: 是否移除注释。skipLibCheck
: 是否跳过检查库文件。sourceRoot
: 指定源文件的根目录。suppressExcessPropertyErrors
: 是否禁止过多属性错误。suppressImplicitAnyIndexErrors
: 是否禁止隐式 any 类型索引错误。typeRoots
: 指定类型声明文件的根目录。types
: 指定需要包含在编译中的类型声明文件。watch
: 是否监视文件变化并重新编译。
这里要说明一点,因为我们使用的是typescript在进行开发,所以我们在写代码的时候是需要类型支持的,这个时候我们可以安装各个包的类型包
"devDependencies": {
"@types/express": "^4.17.14",
"@types/jsonwebtoken": "^8.5.9",
"@types/multer": "^1.4.7",
"@types/node": "^18.11.11",
"nodemon": "^2.0.20",
"ts-node": "^10.9.1",
"typescript": "^4.9.3"
},
"dependencies": {
"@types/body-parser": "^1.19.2",
"body-parser": "^1.20.1",
"class-validator": "^0.13.2",
"cors": "^2.8.5",
"express": "^4.18.2",
"express-async-errors": "^3.1.1",
"jsonwebtoken": "^8.5.1",
"md5": "^2.3.0",
"multer": "^1.4.5-lts.1",
"mysql2": "^2.3.3",
"reflect-metadata": "^0.1.13",
"typeorm": "^0.3.11"
}
在上面的依赖中,那个@type
开始的就是类型包
四、创建数据库连接的配置文件
本次项目使用的是typeorm来完成,所以需要按typeorm进行数据库连接的配置
在src的目录下面创建config文件夹,再创建DBConfig.ts的文件
/**
* 数据库的配置文件
*/
export default {
name: "default",
type: "mysql",
host: "127.0.0.1",
port: 3306,
database: "scenic_spot",
username: "dev",
password: "123456",
multipleStatements: true,
synchronize: false,
entities: [
]
}
五、创建app.ts项目启动文件
import * as express from "express";
import {Express, Request, Response, NextFunction} from "express";
import * as bodyParser from "body-parser";
const app: Express = express();
app.use(bodyParser.urlencoded({extended:false,limit:"30m"}));
app.use(bodyParser.json({limit:'30m'}));
app.listen(9999,"[::]",()=>{
console.log("服务器启动成功");
})
六、编写package.json的启动命令
在package.json
的script
选项下面,创建下面的启动命令
"main": "app.ts",
"scripts": {
"dev": "nodemon --watch ./src",
"build": "tsc -p ./",
"start": "node dist/app.js"
},
dev
代表开发模式build
代表生产模式start
是启动生产模式下面的代码
同时这里要注意,要将main
选项下面的index.js
换成src/app.ts
,这代表项目的默认启动文件
注:如果项目没有安装
nodemon
的,通过下面的命令安装$ npm install nodemon --save-dev
当编写完启动命令以后,我们的项目就可以启动了,如下所示
七、编写entity文件
项目使用了typeorm的框架,所以这里需要编译数据表的entity文件,同时使用class-validator进行最终的数据校验
$ npm install class-validator
UserInfo.ts
/**
* 数据表user_info
*/
import {Column, Entity, OneToMany, PrimaryGeneratedColumn} from "typeorm";
import {IsNotEmpty} from "class-validator";
import {UserPermissionInfo} from "./UserPermissionInfo";
@Entity({
name:"user_info"
})
export class UserInfo{
@PrimaryGeneratedColumn()
id:number;
@IsNotEmpty()
@Column({
nullable:false
})
account:string;
@IsNotEmpty()
@Column({
select:false,
nullable:false
})
password:string;
@Column()
avatar:string;
@IsNotEmpty()
@Column()
phone_number:string;
@IsNotEmpty()
@Column({
type:"int"
})
status:number;
@Column({
type:"datetime"
})
login_time:Date;
@Column()
remark:string;
@Column({
type:"datetime"
})
create_time:Date;
@Column({
type:"datetime"
})
update_time:Date;
@Column({
default:false,
select:false
})
is_del:boolean;
@OneToMany(()=>UserPermissionInfo,userPermissionInfo=>userPermissionInfo.userInfo)
userPermissionInfoList:UserPermissionInfo[];
}
PermissionInfo.ts
/**
* 数据表permission_info
*/
import {Column, Entity, OneToMany, PrimaryGeneratedColumn} from "typeorm";
import {IsInt, IsNotEmpty} from "class-validator";
import {UserPermissionInfo} from "./UserPermissionInfo";
@Entity({
name:"permission_info"
})
export class PermissionInfo {
@PrimaryGeneratedColumn()
id:number;
@IsNotEmpty()
@Column()
permission_name:string;
@Column()
icon:string;
@IsNotEmpty()
@IsInt()
@Column()
parent_id:number;
@IsNotEmpty()
@IsInt()
@Column()
order_num:number;
@Column()
front_path:string;
@Column()
api_path:string;
@Column()
component:string;
@IsNotEmpty()
@IsInt()
@Column()
permission_type:number;
@IsNotEmpty()
@Column()
permission_key:string;
@Column({
type:"datetime"
})
create_time:Date;
@Column({
type:"datetime"
})
update_time:Date;
@Column()
remark:string;
@Column({
default:false
})
is_del:boolean;
@OneToMany(()=>UserPermissionInfo,userPermissionInfo=>userPermissionInfo.permissionInfo)
userPermissionInfoList:UserPermissionInfo[];
}
UserPermissionInfo.ts
import {Column, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn} from "typeorm";
import {IsNotEmpty} from "class-validator";
import {UserInfo} from "./UserInfo";
import {PermissionInfo} from "./PermissionInfo";
@Entity({
name:"user_permission_info"
})
export class UserPermissionInfo{
@PrimaryGeneratedColumn()
id:number;
@IsNotEmpty()
@Column()
user_id:number;
@IsNotEmpty()
@Column()
permission_id:number;
@Column()
create_time:Date;
@Column()
update_time:Date;
@Column({
default:false
})
is_del:boolean;
@ManyToOne(()=>UserInfo,userInfo=>userInfo.userPermissionInfoList)
@JoinColumn({name:"user_id"})
userInfo:UserInfo;
@ManyToOne(()=>PermissionInfo,permissionInfo=>permissionInfo.userPermissionInfoList)
@JoinColumn({name:"permission_id"})
permissionInfo:PermissionInfo;
}
八、将实例类文件添加到DBConfig.ts
所有的entity实体对象都要添加到typeorm的配置文件,随着项目的启动去启动
后续如果还有其它的新增的实体对象,也要添加进去
九、express与typeorm结合
之前我们已经编写了app.ts的 express项目启动文件,现在需要将typeorm结合进去,如下
app.ts
import * as express from "express";
import {Express, Request, Response, NextFunction} from "express";
import * as bodyParser from "body-parser";
import DBConfig from "./config/DBConfig";
import {createConnection, DataSourceOptions} from "typeorm";
createConnection(DBConfig as DataSourceOptions).then(()=>{
const app: Express = express();
app.use(bodyParser.urlencoded({extended:false,limit:"30m"}));
app.use(bodyParser.json({limit:'30m'}));
app.listen(9999,"[::]",()=>{
console.log("服务器启动成功");
})
})
十、添加跨域
当前的接口是没有跨域的,现在的项目如果进行前后端分离式开始是需要添加跨域的请求头的,这里直接在后端处理了
在这里我们需要使用一个第三方的依赖包cors
$ npm install cors --save
十一、编辑DBUtils.ts
正常情部钙,DBUtils.ts是数据库的连接文件,但是我们现在使用typeorm来完成以后,这里就不需这个文件了,所以这个文件可以简写,如下
/**
* 数据库操作的核心对象
*/
import {getConnection} from "typeorm";
export default class DBUtils {
public getConnection = getConnection
}
十二、编写Services
数据库的操作还是离不开Service的
BaseService.ts
/**
* 数据库操作的基类BaseService
*/
import DBUtils from "../utils/DBUtils";
import {validate} from "class-validator";
export default class BaseService<T> extends DBUtils {
public repositoryType: any;
constructor(repositoryType: any) {
super();
this.repositoryType = repositoryType;
}
/**
* 根据id去查询
*/
async findById(id: number, ...relations) {
let connection = this.getConnection();
let repository = connection.getRepository(this.repositoryType);
let result = await repository.find({
where: {
id,
is_del:false
},
relations
});
return result;
}
/**
* 根据id删 除
* @param id
* @returns
*/
async deleteById(id: number): Promise<boolean> {
let connection = this.getConnection();
let result = await connection.getRepository(this.repositoryType)
.update({ id }, { id, is_del: true });
return result.affected > 0;
}
/**
* 查询所有数据
* @returns
*/
async getAllList(): Promise<any> {
let connection = this.getConnection();
let result = await connection.getRepository(this.repositoryType)
.find({
where: {
is_del: false
}
});
return result;
}
async add(model: T): Promise<any> {
const errors = await validate(model as any);
if (errors.length > 0) {
throw new Error("表单验证失败");
}
else {
let connection = this.getConnection();
let result = await connection.getRepository(this.repositoryType)
.save(model);
return result;
}
}
async update(model: T): Promise<boolean> {
let connection = this.getConnection();
let result = await connection.getRepository(this.repositoryType)
.update({ id: (model as any).id,is_del:false }, model);
return result.affected > 0;
}
}
UserInfoService.ts
/**
* 数据表user_info的操作
*/
import BaseService from "./BaseService";
import {UserInfo} from "../entity/UserInfo";
export default class UserInfoService extends BaseService<UserInfo>{
constructor() {
super(UserInfo);
}
}
PermissionService.ts
/**
* 数据表permission_info的操作
*/
import BaseService from "./BaseService";
import {PermissionInfo} from "../entity/PermissionInfo";
export default class PermissionInfoService extends BaseService<PermissionInfo>{
constructor() {
super(PermissionInfo);
}
}
UserPermissionInfoService.ts
/**
* 数据表user_permission_info的操作
*/
import BaseService from "./BaseService";
import {UserPermissionInfo} from "../entity/UserPermissionInfo";
export default class UserPermissionInfoService extends BaseService<UserPermissionInfo>{
constructor() {
super(UserPermissionInfo);
}
}
十三、创建Service的工厂
为了更好的实现服务层高类聚低耦合,这里使用工厂模式
-
首先在项目的src目录下面创建factory文件夹
-
在文件夹下面创建ServiceFactory.ts文件,代码如下
/**
* 服务层工厂
*/
import UserPermissionInfoService from "../services/UserPermissionInfoService";
import UserInfoService from "../services/UserInfoService";
import PermissionInfoService from "../services/PermissionInfoService";
export default class ServiceFactory{
static createUserInfoService(){
return new UserInfoService();
}
static createPermissionInfoService(){
return new PermissionInfoService();
}
static createUserPermissionInfoService(){
return new UserPermissionInfoService();
}
}
十四、服务器返回数据的编写
在服务端返回数据给客户端的时候,格式是需要固定的,这里我们列举以下常见的格式
-
ResultJson.ts
这个文件是专门用于返回JSON格式给前端的/** * @description 返回结果 */ export default class ResultJson { public status: string; public msg: string; public data: any; constructor(status: boolean, msg: string, data?: any) { this.status = status ? "success" : "fail"; this.msg = msg; this.data = data; } }
-
PageList.ts
这是分页以后的数据,当进行分页查询的时候,我们可以使用这个格式的数据/** * @description 分页的数据模型 * @author 杨标 * @version 1.0 */ export default class PageList { public pageIndex: number; public totalCount: number; public pageCount: number; public listData: any[] public pageStart: number; public pageEnd: number; /** * @param {number} pageIndex 当前第几页 * @param {number} totalCount 共多少条数据 * @param {number} pageSize 每页显示多少 * @param {any[]} listData 查询的分页列表数据 */ constructor(pageIndex: number, totalCount: number, pageSize: number, listData: any[]) { this.pageIndex = pageIndex; this.totalCount = totalCount; this.pageCount = Math.ceil(totalCount / pageSize); this.listData = listData; this.pageStart = this.pageIndex - 3 > 0 ? this.pageIndex - 3 : 1; this.pageEnd = this.pageStart + 6 > this.pageCount ? this.pageCount : this.pageStart + 6; } }
十五、进行userInfoRouter.ts的编写
后端路由是用于处理不同的HTTP请求的,路由的使用可以更好的实现模块的低耦合,我们在项目的src目录下面新建一个router
的目录,然后新建一个userInfoRouter.ts
的文件,代码如下
import * as express from "express";
import {Express, Request, Response, NextFunction} from "express";
import {checkPermission} from "../utils/permissionUtils";
import ServiceFactory from "../factory/ServiceFactory";
const router = express.Router();
router.get("/findById/:id", checkPermission("userInfo:findById"), (req: Request, resp: Response) => {
});
export default router;
上面我们列举了一个findById
的请求,同时使用了一个中间件,这个中间件叫checkPermission
,这是我们本次项目的核心,做于做权限验证的,这个中间件的代码如下
src/utils/checkPermission.ts
import {NextFunction, Request, Response} from "express";
export const checkPermission = (permissionKey: string) => {
console.log(permissionKey);
return (req: Request, resp: Response, next: NextFunction) => {
next();
}
}
将路由的框架搭建起来以后,后面就可以在app.ts
里面导入了
src/app.ts
import * as express from "express";
import {Express, Request, Response, NextFunction} from "express";
import * as bodyParser from "body-parser";
import DBConfig from "./config/DBConfig";
import {createConnection, DataSourceOptions} from "typeorm";
import * as cors from "cors";
import userInfoRouter from "./router/userInfoRouter";
const app: Express = express();
createConnection(DBConfig as DataSourceOptions).then(() => {
app.use(cors({
origin: "*"
}));
app.use(bodyParser.urlencoded({extended: false, limit: "30m"}));
app.use(bodyParser.json({limit: '30m'}));
app.use("/userInfo", userInfoRouter);
app.listen(9999, "[::]", () => {
console.log("服务器启动成功");
})
})
十六、全局异常处理
在整个程序里面,如果在每个请求里面,每个模块里面都处理异常,这样会显得非常麻烦 ,我们可以使用一个模块来实现全局异常的处理,模块的名子叫express-async-errors
$ npm install express-async-errors
十七、完成的userInfoRouter.ts
整个路由的基本结构完成以后,现在就开始实现具体业务请求(通俗一点说就是增删改查的请求操作),这里我先写一个模板,后面其它的模块与此大同小异
import * as express from "express";
import {Express, Request, Response, NextFunction} from "express";
import {checkPermission} from "../utils/permissionUtils";
import ServiceFactory from "../factory/ServiceFactory";
import ResultJson from "../dto/ResultJson";
const router = express.Router();
router.get("/findById/:id", checkPermission("userInfo:findById"), async (req: Request, resp: Response) => {
let result = await ServiceFactory.createUserInfoService().findById(Number(req.params.id));
let flag = Boolean(result);
resp.json(new ResultJson(flag, flag ? "获取数据成功" : "获取数据失败", result));
});
router.post("/add", checkPermission("userInfo:add"), async (req: Request, resp: Response) => {
let result = await ServiceFactory.createUserInfoService().add(req.body);
let flag = Boolean(result);
resp.json(new ResultJson(flag, flag ? "新增成功" : "新增失败"));
});
router.put("/update/:id", checkPermission("userInfo:update"), async (req: Request, resp: Response) => {
let result = await ServiceFactory.createUserInfoService().update({...req.body, ...req.params});
resp.json(new ResultJson(result, result ? "修改成功" : "修改失败"));
});
router.delete("/deleteById/:id", checkPermission("userInfo:deleteById"), async (req: Request, resp: Response) => {
let result = await ServiceFactory.createUserInfoService().deleteById(Number(req.params.id));
resp.json(new ResultJson(result, result ? "删除成功" : "删除失败"));
});
export default router;
十八、密码的md5加密
在上面,我们新增用户信息的时候,用户这个时候的密码是不能进行明文存储的,应该要进行加密,目前加密的方法有很多,这里我们使用md5加密再加盐的方式去处理
- 先在config的目录下面新建一个文件APPConfig.ts
- 在APPConfig.ts里面进行相应的md5盐的配置
src/config/APPConfig.ts
export default {
md5Salt:"098lskdf.!@#09sdfj"
}
完成上面的配置以后,接下来我们就可以安装md5的包了
$ npm install md5
最后在UserInfoService.ts
重写add
的方法
十九、头像上传
在新增用户的时候,有一个头像上传的环节,这里我们也是第三方包multer
来进行完成
当然这里也可以使用七牛云进行图片的存储,后面讲到
$ npm install multer
依赖包安装完成以后,我们在项目的根目录下面创建 uploadImages/avatar
用于存放用户上传的头像
router.post("/uploadAvatar", [checkPermission("userInfo:uploadAvatar"), uploadAvatar.single("avatar")], async (req: Request, resp: Response) => {
let file = req.file;
if (file) {
let newFileName = file.filename + file.originalname
fs.renameSync(file.path, path.join(__dirname, "../../uploadImages/avatar", newFileName));
resp.json(new ResultJson(true, "图片上传成功", `/uploadImages/avatar/${newFileName}`))
}
else {
resp.json(new ResultJson(false, "未接收到文件"))
}
});
完成上面的路由操作以后,接下来其它的模块的路由编写就差这多了,这里就不再细述了,接下来的工作转向前端的操作
二十、使用vite来构建vue的项目
在空文件夹下面输入如下命令,建构项目
$ npm create vite@latest
项目创建完成以后,使用npm install
来安装项目的依赖信息
二十一、安装element-plus
项目依然使用element-ui
这个组件库来进行页面的构建
$ npm install element-plus --save
element-plus的导入方式有2种,这里我们配置按需引入
按需导入
您需要使用额外的插件来导入要使用的组件。
自动导入推荐
首先你需要安装unplugin-vue-components
和 unplugin-auto-import
这两款插件
$ npm install -D unplugin-vue-components unplugin-auto-import
然后把下列代码插入到你的 Vite
或 Webpack
的配置文件中
Vite
// vite.config.ts
import { defineConfig } from 'vite'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
export default defineConfig({
// ...
plugins: [
// ...
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
})
Webpack
// webpack.config.js
const AutoImport = require('unplugin-auto-import/webpack')
const Components = require('unplugin-vue-components/webpack')
const { ElementPlusResolver } = require('unplugin-vue-components/resolvers')
module.exports = {
// ...
plugins: [
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
}
全局配置
import {createApp} from 'vue'
import App from './App.vue'
import ElementPlus from "element-plus";
const app = createApp(App)
app.use(ElementPlus).mount('#app')
二十二、配置pinia及持久化插件
vue在之前的版本里面使用的全局状态管理是vuex,这里我们更新到pinia,同时添加持久化插件
$ npm install pinia pinia-plugin-persistedstate web-storage-cache
上面的pinia-plugin-persistedstate
是持久化插件,同时使用web-storage-cache来进行缓存的处理
在src/store/index.js下面,来进行pinia 的配置
/**
* pinia的存储区域
*/
import { defineStore } from "pinia";
import WebStorageCache from "web-storage-cache";
const cache = new WebStorageCache({
storage: window.localStorage
})
export const mainStore = defineStore("main", {
state() {
return {
token: null,
}
},
actions: {
},
// 配置状态的持久化
persist: {
key: "permission_vite_vue_20230525",
storage: {
getItem: key => cache.get(key),
// 到期时间默认以秒为单位
setItem: (key, value) => cache.set(key, value, { exp: 60 * 60 * 24 }),
removeItem: key => cache.delete(key),
clear: () => cache.clear()
},
//指定需要持久化的字段
paths: ["token"]
}
});
将pinia配置好了以后,需要在main.js里面进行加载
二十三、配置项目的路径及文件别名
当我们在使用import进行文件的导入的时候,有时候为了方便需要快速使用路径,同时也需要快速导入文件,这个时候我们可以在vite.config.js
里面进行别名的处理
上面的extensions
配置是后缀名,如果在上面配置了以后,则在导入文件的时候,以上的后缀名就可以省略掉,下面的alias
代表的是别名配置,如果配置了以后可以直接 使用@
来代表src
的目录,这样更方便快速
二十四、配置vue-router前端路由
默认情况下,vite并没有帮我们添加路由,这里我们需要自己手动来进行
通过下面的命令安装路由依赖
$ npm install vue-router
在src/store/index.js下面进行配置
import {createRouter,createWebHashHistory} from "vue-router";
const router = createRouter({
history:createWebHashHistory(),
routes:[
]
})
export default router;
最后在src/main.js
下面加载这个配置文件
二十五、第一个页面-登录
在项的src目录下面,我们新建一个views
的目录,这是用于存放页面的,然后我们在里面创建一个Login.vue
的文件,用于完成登录页面的布局
做了一个简单的页面以后,这个时候需要在路由的配置文件里面配置路径
当我们去配置完路由信息以后,这个时候控制台会报错,如下所示
这个时候我们会发现是因为之前的Login.vue
这个文件里面,我们使用了Sass
,但是vite在创建项目的时候并没有直接帮我们安装sass的依赖包,我们需要安装一下
$ npm install sass --save-dev
安装包依赖包以后,这个时候就不会有错误了
当路径配置完成以后,我们就要在App.vue
里面配置路由的出口了
<template>
<router-view></router-view>
</template>
<script setup>
</script>
<style >
</style>
这个时候启动项目,在浏览器的地址栏输入地址以后就可以访问了
最后,我们还需要配置默认路径
这样当我们在打开项目上以后,默认就会跳转到Login的页面了
二十六、配置项目的样式初始化
我们刚刚打开Login的登录页以后,可以看到这里面的样式是没有初始化的,所以这里我们来进行初始化一下
在assets
的目录下面新建一个css
目录,然后建立一个comm.css
的文件,代码如下
@charset "utf-8";
*{
margin: 0;
padding: 0;
list-style-type: none;
}
将上面的文件在main.js
里面进行导入
二十七、配置tailwind.css
在进行页面样式与布局的时候,我们要频繁的使用CSS,这里我推荐大家在项目里面使用tailwind.css
这个框架,中文官网Tailwind CSS - 只需书写 HTML 代码,无需书写 CSS,即可快速构建美观的网站。 | TailwindCSS中文文档 | TailwindCSS中文网
https://www.tailwindcss.cn/
-
安装依赖包
$ npm install -D tailwindcss postcss autoprefixer
-
初始化配置文件
$ npx tailwindcss init -p
初始化配置文件完成以后,在项目的根目录下面分多出2个文件,一个是
postcss.config.js
,另一个是tailwind.config.js
-
编辑
tailwind.config.js
文件/** @type {import('tailwindcss').Config} */ export default { content: ["./index.html", "./src/*.{vue,js}", "./src/**/*.{vue,js}"], theme: { extend: {}, }, plugins: [], }
-
在之前创建的
src/assets/css/comm.css
下面建立tailwindcss 的指令,如下@tailwind base; @tailwind components; @tailwind utilities;
-
编写测试样式
在Login.vue的文件下面,编写代码
<template> <div class="w-[100px] h-[100px] bg-red-400"></div> </template> <script setup> </script> <style lang="scss" scoped> </style>
打开页面,可以看到如下效果,说明成功
至此,我们以后在书写项目的时候就可以直接 使用tailwind.css
来完成了
二十八、加载animate.css
在项目当中经常会使用动画 ,这里我们加载animate.css
$ npm install animate.css
安装完成以后,我们就在main.js里面进行加载配置
二十八、完成登录页面布局
页面完成以后,现在需要进行前后端的数据请求
在进行前后端的数据请求的时候,我们要使用axios
来进行,这里我们来安装
二十九、axios的安装与配置
通过以下的命令安装依赖包
$ npm install axios
安装完成以后,我们在src/utils
下面新建一个axiosInstance.js
的文件,进行相应的配置
import axios from "axios";
const axiosInstance = axios.create({
baseURL: "http://127.0.0.1:9999"
});
export default axiosInstance;
三十、配置前端请求的接口
现在是前后端分离的开发,前后端为了实现低耦合,需要进行前端的接口配置,我们在src/api
下面新建api的文件夹,同时根据不同的模块来编写API接口请求
import axiosInstance from "@/utils/axiosInstance.js";
const userInfo = {
findById(id) {
return axiosInstance.get(`/userInfo/findById/${id}`);
},
add({account, password, avatar, phone_number, status, login_time, remark}) {
return axiosInstance.post(`/userInfo/add`, {
account, password, avatar, phone_number, status, login_time, remark
});
},
update({id, account, password, avatar, phone_number, status, login_time, remark}) {
return axiosInstance.put(`/userInfo/update/${id}`, {
id, account, password, avatar, phone_number, status, login_time, remark
})
},
deleteById(id) {
return axiosInstance.delete(`/userInfo/deleteById/${id}`);
},
uploadAvatar(fileFormData) {
return axiosInstance.post(`/userInfo/uploadAvatar`, fileFormData, {
headers: {
"Content-Type": "multipart/form-data"
}
});
}
}
export default userInfo;
完成上面的操作以后,其它的模块依次类型。
当所有的的模块完成以后,后面就会有很多的文件,我们要将这些模块进行高内聚的操作,把这些接口对象全部集合在index.js
里面
将上面的东西设置完成以后,这个时候我们还发现一个问题,基本上所有的的表当中都会有
create_time
与update_time
,一个是新增的时候时间,一个是更新的时候的时间,这两个时间应该是在后端处理,所以我们需要在后端来进行调整
三十一、调整create_time与update_time
我们分别在UserInfoService
与PermissionInfoService
里面重写了add
与update
的方法,如上所示
三十二、编写登录的后台接口
src/services/UserInfoServices.ts
/**
* 数据表user_info的操作
*/
import BaseService from "./BaseService";
import {UserInfo} from "../entity/UserInfo";
import * as md5 from "md5";
import APPConfig from "../config/APPConfig";
export default class UserInfoService extends BaseService<UserInfo> {
constructor() {
super(UserInfo);
}
async add(model: UserInfo): Promise<any> {
model.password = <string>md5(model.password + APPConfig.md5Salt);
model.create_time = new Date();
return super.add(model);
}
async update(model: UserInfo): Promise<boolean> {
model.update_time = new Date();
return super.update(model);
}
async checkLogin(account, password) {
password = md5(password+APPConfig.md5Salt);
let connection = this.getConnection();
let result = await connection.getRepository(UserInfo).find({
where:{
account,
password,
is_del:false
}
});
return result;
}
}
上面的checkLogin就是用于检测登录的,account代表账号,password代表的是密码,同时这里要注意因为之前的密码是进行了md5的加密的,这里登录的时候密码也是要进行md5加密
service完成以后,现在就要开始进行路由的编写
src/router/userInfoRouter.ts
router.post(`/checkLogin`,async (req:Request,resp:Response)=>{
let {account,password} = req.body;
let result = await ServiceFactory.createUserInfoService().checkLogin(account,password);
let flag = Boolean(result);
if(flag){
//登录成功
}
else{
//登录失败
}
});
上面就是路由的方法,这里我们可以通过判断来指定用户登录是否成功。
当然,再登录成功以后,我们需要向用户颁发令牌,这个时候我们就要使用jsonwebtoken
这个依赖包了
三十三、jsonwebtoken的安装与使用
jsonwebtoken前后端分离开发的过程当中经常使用的一个包,用于向客户颁发令牌
$ npm install jsonwebtoken
安装完成以后,我们就要开始配置
-
先在config的目录下面的AppConfig.ts里面配置token的secret
export default { md5Salt:"098lskdf.!@#09sdfj", tokenSecret:"asdfk90/.,!@#$213" }
-
在登录成功以后向用户发放令牌
router.post(`/checkLogin`, async (req: Request, resp: Response) => { let {account, password} = req.body; let result = await ServiceFactory.createUserInfoService().checkLogin(account, password); let flag = Boolean(result); if (flag) { //登录成功 let token = jwt.sign({ ...result, }, APPConfig.tokenSecret, { expiresIn: "7d" }); resp.json(new ResultJson(true, "登录成功", { token, userInfo: result })) } else { //登录失败 resp.json(new ResultJson(false, "登录失败,用户名或密码不正确")); } });
在上面的令牌颁发过程当中,我们可以看到我们将令牌设置了一个到期时间,expiresIn
就是一个到期时间
三十四、前端登录
我们现在回到前端的项目
在api/userInfo.js
下面,新增下面的接口请求方法
回到Login.vue
的页面
<script setup>
import {ref, reactive} from "vue";
import {Loading} from "@element-plus/icons-vue";
import Api from "@/api/index.js";
const loginFormData = reactive({
account: "",
password: ""
})
const loginFormDataRules = {
account:[{required:true,message:"账号不能为空",trigger:"blur"}],
password:[{required:true,message:"密码不能为空",trigger:"blur"}],
}
const isShowLoginBox = ref(true);
const loginFormEl = ref(null)
const submitLoginForm = () => {
loginFormEl.value.validate(valid => {
if (valid) {
checkLogin();
} else {
console.log("验证失败");
}
})
}
const checkLogin = () => {
Api.userInfo.checkLogin(loginFormData.account,loginFormData.password)
.then(resp=>{
console.log(resp.data);
})
}
</script>
通过上面的东西我们可以看出,已经可以请求接口,并获取到数据了,但是数据的格式是有问题的,因为我们可以看到,我们真正需要的数据其实是在data
下面,所以我们可以在响应的时候,做一次响应拦截
三十五、实现axios的响应拦截
src/utils/axiosIntance.js
上面我们设置了一个请求拦截,我们直接将resp.data的数据返回了出来,这个我们就可以直接拿到数据了
三十六、登录以后的pinia存储用户令牌
当用户登录成功以后,我们要将用户的token以及用户的基本令牌保存起来,并做持久化处理,这个时候我们就可以使用pinia来进行了
src/store/index.js
我们添加了上面的代码以后,就可以在登录的时候使用pinia
当我们登录成功以后,我们可以在浏览器里面看到相应的登录用户信息,如下
三十七、后端初始鉴权
当我们登录成功以后,这个时候服务器会颁发一个令牌给用户,用户以后的每次ajax请求都需要携带这个令牌,我们可以通过axios
来设置请求拦截,完成这个功能
当我们完成上面的操作以后,这样在每次请求的时候,都会通过header里面的authorization进行token的携带,到服务端
服务端在接收到token以后就可以对token进行鉴权了
现在我们回到后台的程序,在之前的src/utils/permissionUtils.ts
下面进行处理
import {NextFunction, Request, Response} from "express";
import ResultJson from "../dto/ResultJson";
import * as jwt from "jsonwebtoken";
import APPConfig from "../config/APPConfig";
import ServiceFactory from "../factory/ServiceFactory";
export const checkPermission = (permissionKey: string) => {
console.log(permissionKey);
return async (req: Request, resp: Response, next: NextFunction) => {
//第1步:如果是options的预检请求,这个时候我们可以直接放行
if (req.method.toUpperCase() === "OPTIONS") {
return next();
} else {
//第2步:从header或query里面得到authorization的token
let token = req.header("authorization") || req.query.authorization;
//第3步:如果有token
if (token) {
try {
//第4步:对token进行鉴权,看token是否有效
let decode = jwt.verify(String(token), APPConfig.tokenSecret);
//第5步:如果token有效,我们就将得到的token保存在header方向后面继续使用
req.header["userInfo"] = decode;
//第5步:检测一下当前这个用户是否有这个权限
let flag = await ServiceFactory.createUserPermissionInfoService().checkUserPermission((decode as any).id, permissionKey)
if (flag) {
//说明当前用户有这个API的权限
return next();
}
else{
//说明当前用户没有这个API的权限
let resultJson = new ResultJson(false, "请求未授权");
resp.status(403).json(resultJson);
}
} catch (e) {
let resultJson = new ResultJson(false, "令牌验证失败");
resp.status(403).json(resultJson);
}
} else {
//如果没有token
let resultJson = new ResultJson(false, "请求未授权");
resp.status(403).json(resultJson);
}
}
}
}
我们在上面用到了一个Service,就是根据当前用户检测当前是否有权限,代码如下
src/services/UserPermissionInfoService.ts
/**
* 检查某一个用户是否有某一个权限
* @param user_id
* @param permission_key
*/
async checkUserPermission(user_id, permission_key):Promise<boolean> {
let connection = this.getConnection();
let result = await connection.getRepository(UserPermissionInfo).createQueryBuilder("user_permission_info")
.innerJoinAndSelect("user_permission_info.permissionInfo", "permission_info")
.where("user_permission_info.is_del=false")
.andWhere("permission_info.permission_key=:permission_key", {permission_key})
.andWhere("user_permission_info.user_id=:user_id", {user_id})
.getOne();
return result ? true : false;
}
目前阶段,我们先暂时不开启权限控制,我们会把权限控制的中间件放开,方便后面的开发,等到最后再打开
三十八、后面控制面板创建
在src/views
目录下面新建一个Manager.vue
的文件,并配置路由
三十九、注册所有图标
因为我们使用的是element-plus框架,同时使用的图标也是element-plus里面的图标,我们现在将所有的图标都注册一下,方便后面使用
四十、左边菜单的完成
我们可以将左边的菜单单独放在一个组件里面进行完成,如下操作
- 在components里面创建一个LeftMenu.vue的文件
- 在Manager.vue里面导入这个组件,并使用
有了上面的操作以后,我们就可以看到下面的效果了
现在我们只需要将左边的菜单完成即可
<template>
<div class="menu-box">
<div class="h-[60px] bg-[#263445] font-bold flex flex-row justify-center items-center text-white italic">
<el-icon size="28" class="is-loading mr-[10px]">
<Loading/>
</el-icon>
通用权限管理系统
</div>
<el-menu
active-text-color="#ffd04b"
background-color="#304156"
default-active="2"
router
text-color="#fff">
<el-sub-menu index="1">
<template #title>
<el-icon>
<setting/>
</el-icon>
<span>系统管理</span>
</template>
<el-menu-item index="/Manager/UserInfoList" >
<el-icon>
<user/>
</el-icon>
用户管理
</el-menu-item>
<el-menu-item index="1-2">
<el-icon><user-filled /></el-icon>
角色管理
</el-menu-item>
<el-menu-item index="1-3">
<el-icon><stamp /></el-icon>
菜单管理
</el-menu-item>
</el-sub-menu>
</el-menu>
</div>
</template>
<script setup>
</script>
<style scoped lang="scss">
.menu-box {
@apply bg-[#304156] w-full h-full overflow-hidden;
}
</style>
四十一、用户列表页面
本期项目的核心是通用权限管理,管理的是用户,所以我们主要围绕的仍然是用户,权限这两个点,这里我们先完成用户列表的页面
后端项目的src/services/UserInfoServices.ts文件中,编写分页查询的方法
/**
* 分页查询用户信息
*/
async getListByPage({pageIndex, account, phone_number}) {
let connection = this.getConnection();
let queryBuilder = connection.getRepository(UserInfo).createQueryBuilder("user_info")
.innerJoinAndSelect("user_info.userPermissionInfoList", "user_permission_info")
.where("user_info.is_del=false");
if (account) {
queryBuilder.andWhere("user_info.account like :account", {account: `%${account}%`});
}
if (phone_number) {
queryBuilder.andWhere("user_info.phone_number like :phone_number", {phone_number: `%${phone_number}%`});
}
let [listData,totalCount] = await queryBuilder.orderBy("id","ASC").skip((pageIndex-1)*10).take(10).getManyAndCount();
return new PageList(pageIndex, totalCount, 10, listData);
}
后端项目的src/services/userInfoRouter.ts文件中,编写分页查询的方法
router.get("/getListByPage",checkPermission("userInfo:getListByPage"),async(req:Request,resp:Response)=>{
let pageList = await ServiceFactory.createUserInfoService().getListByPage(req.query as any);
resp.json(new ResultJson(true, "获取数据成功", pageList));
});
当后端的功能完成了以后,现在我们就在要前端的项目里面编写API接口请求了
前端项目api/userInfo.js
下面
完成以后,就可以进行相应的页面布局了
<template>
<el-card>
<template #header>
<div class="font-bold">用户列表</div>
</template>
<el-form :inline="true">
<el-form-item label="用户名">
<el-input placeholder="输入用户名查询" v-model="queryParamsData.account"></el-input>
</el-form-item>
<el-form-item label="手机号">
<el-input placeholder="输入手机号查询" v-model="queryParamsData.phone_number"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="queryData" :loading="isLoading">查询</el-button>
<el-button type="success" icon="CirclePlus" @click="$router.push({name:'AddUserInfo'})">新增用户</el-button>
</el-form-item>
</el-form>
<div v-loading="isLoading">
<el-table
:data="listData"
border
stripe>
<el-table-column label="ID" prop="id" width="50" align="center"></el-table-column>
<el-table-column label="账号" prop="account" width="180"></el-table-column>
<el-table-column label="头像" width="100" align="center">
<template #default="{row}">
<el-image class="w-55px h-[55px] object-fit" :src="BASEURL+row.avatar"></el-image>
</template>
</el-table-column>
<el-table-column label="手机号" prop="phone_number" align="center" width="180"></el-table-column>
<el-table-column label="状态" width="100" align="center">
<template #default="{row}">
<el-tag type="success" v-if="row.status==0">启用</el-tag>
<el-tag type="danger" v-else-if="row.status==1">禁用</el-tag>
</template>
</el-table-column>
<el-table-column label="备注" prop="remark"></el-table-column>
<el-table-column label="操作">
<el-button type="warning" size="small" icon="EditPen">编辑</el-button>
<el-button type="primary" size="small" icon="Key">分配权限</el-button>
</el-table-column>
</el-table>
<div class="mt-[10px] flex flex-row justify-end">
<el-pagination :total="totalCount" background layout="prev, pager, next"></el-pagination>
</div>
</div>
</el-card>
</template>
<script setup>
import {reactive, ref, inject, onMounted} from "vue";
import Api from "@/api/index.js";
import {ElNotification} from "element-plus";
const BASEURL = inject("BASEURL");
const queryParamsData = reactive({
account: "",
phone_number: "",
pageIndex: 1
});
const listData = ref([]);
const totalCount = ref(0);
const isLoading = ref(false);
const queryData = () => {
Api.userInfo.getListByPage(queryParamsData)
.then(result => {
totalCount.value = result.data.totalCount;
listData.value = result.data.listData;
queryParamsData.pageIndex = +result.data.pageIndex;
}).catch(error => {
ElNotification.error(error.msg);
})
}
onMounted(() => {
queryData();
})
</script>
<style scoped lang="scss">
</style>
四十二、新增用户
在src/views/userInfo
下面新建AddUserInfo.vue
这个文件
<template>
<el-card>
<template #header>
<div class="flex flex-row justify-between items-center">
<span class="font-bold">新增用户</span>
<el-link type="primary" @click="$router.back()" underline>返回列表</el-link>
</div>
</template>
<el-form ref="formEl" label-width="150" :model="formParamsData" :rules="formParamsDataRules">
<el-form-item label="账号" prop="account">
<el-input placeholder="请输入账号" v-model="formParamsData.account"/>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input placeholder="请输入密码" v-model="formParamsData.password" show-password/>
</el-form-item>
<el-form-item label="确认密码" prop="secondPassword">
<el-input placeholder="请输入确认密码" v-model="formParamsData.secondPassword" show-password/>
</el-form-item>
<el-form-item label="手机号">
<el-input placeholder="请输入手机号" v-model="formParamsData.phone_number"/>
</el-form-item>
<el-form-item label="头像">
<el-upload
class="avatar-uploader"
:action="`${BASEURL}/userInfo/uploadAvatar`"
name="avatar"
:show-file-list="false"
:on-success="handleAvatarSuccess"
:before-upload="beforeAvatarUpload"
>
<img v-if="formParamsData.avatar" :src="BASEURL+formParamsData.avatar" class="avatar"/>
<el-icon v-else class="avatar-uploader-icon">
<Plus/>
</el-icon>
</el-upload>
</el-form-item>
<el-form-item label="备注">
<el-input :rows="2" type="textarea" placeholder="备注信息"/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm">保存数据</el-button>
<el-button type="danger" @click="formEl.resetFields()">重置表单</el-button>
</el-form-item>
</el-form>
</el-card>
</template>
<script setup>
import {reactive, inject, ref} from "vue";
import {ElMessage, ElNotification} from "element-plus";
import Api from "@/api/index.js";
import {useRouter} from "vue-router";
const router = useRouter();
const BASEURL = inject("BASEURL");
const formParamsData = reactive({
account: "",
password: "",
secondPassword: "",
phone_number: "",
avatar: "",
status: 0,
remark: ""
});
const formParamsDataRules = {
account: [{required: true, message: "账号不能为空", trigger: "blur"}],
password: [{required: true, message: "密码不能为空", trigger: "blur"}],
secondPassword: [{required: true, message: "确认密码不能为空", trigger: "blur"}, {
validator(rule, value, callBack) {
if (value === "") {
callback(new Error('请输入确认密码'))
}
if (value != formParamsData.password) {
callBack(new Error("再次密码必须相同"))
}
callBack();
}, trigger: "blur"
}],
}
const handleAvatarSuccess = (response, uploadFile) => {
if (response.status == "success") {
formParamsData.avatar = response.data;
} else {
ElMessage.error(response.msg);
}
}
const beforeAvatarUpload = (rawFile) => {
if (rawFile.size / 1024 / 1024 > 2) {
ElMessage.error('头像大小不能超过2M!')
return false
}
return true
}
const formEl = ref(null);
const submitForm = () => {
formEl.value.validate(valid => {
if (valid) {
addUserInfo();
} else {
console.log("表单验证失败");
}
})
}
const addUserInfo = () => {
Api.userInfo.add(formParamsData).then(result => {
router.replace({name: "UserInfoList"});
}).catch(error => {
ElNotification.error(error.msg);
})
}
</script>
<style lang="scss" scoped>
.avatar-uploader {
width: 100px;
height: 100px;
border: 1px dashed #808080;
border-radius: 6px;
display: flex;
justify-content: center;
align-content: center;
.avatar-uploader-icon {
font-size: 36px;
color: #808080;
width: 100%;
height: 100%;
}
.avatar {
width: 100%;
height: 100%;
object-fit: contain;
}
}
</style>
页面新增完成以后,这个时候我们就要进行路由配置了
src/router/index.js
import {createRouter, createWebHashHistory} from "vue-router";
import Login from "@/views/Login.vue";
import Manager from "@/views/Manager.vue";
const router = createRouter({
history: createWebHashHistory(),
routes: [
{
path: "/",
redirect: {
name: "Login"
}
},
{
path: "/Login",
name: "Login",
component: Login,
meta: {
title: "登录信息"
}
}, {
path: "/Manager",
name: "Manager",
component: Manager,
children:[
{
path: "/Manager/UserInfoList",
name: "UserInfoList",
component: () => import("@/views/userInfo/UserInfoList.vue")
},
{
path: "/Manager/AddUserInfo",
name: "AddUserInfo",
component: () => import("@/views/userInfo/AddUserInfo.vue")
}
]
},
]
})
export default router;
四十三、权限列表
这一个其它就是我们的菜单管理页面
这里我们模仿软帝邱芬芬老师的界面来完成,模拟的界面效果如下
展开以后的效果
新增权限的界面
编辑权限的界面
在这个界面里面,我们首先要完成的应该是新增权限的过程,这里的新增权限有三种类型,一个是目录,一个是页面,还有一个 就是按钮操作了
四十四、新增权限
首先在之前的PermissionList.vue
里面完成基本的布局,使用el-dialog
完成模态框的弹出
<template>
<el-card>
<template #header>
<div class="font-bold">权限列表</div>
</template>
<div>
<el-button type="success" icon="CirclePlus" @click="isModelVisible=true">新增权限菜单</el-button>
</div>
<el-dialog
v-model="isModelVisible"
title="新增权限菜单"
:isModelVisible="true"
:close-on-click-modal="false"
:close-on-press-escape="false">
</el-dialog>
</el-card>
</template>
<script setup>
import {ref} from "vue";
const isModelVisible = ref(false);
</script>
<style scoped>
</style>
再到src/views/permissionInfo
下面新建AddPermissionInfo.vue
的文件
完成上面的布局以垢,我们现在就要开始去填充数据了,如上级菜单中的那个树形结构图,如下所示
这里没有数据,所以我们现在先将填充一个根目录
完成上面的界面布局以后,我们现在就开始后端接口的编写 了
后台src/router/permissionInfoRouter.ts代码
后台src/app.ts代码
现在回到前台,我们来进行接口请求接口的编写
src/api目录下面,新建 permissionInfo.js
的文件
import axiosInstance from "@/utils/axiosInstance.js";
const permissionInfo = {
add({permission_name, icon, parent_id, order_num, front_path, component, permission_type, permission_key,remark}){
return axiosInstance.post(`/permissionInfo/add`,{
ermission_name, icon, parent_id, order_num, front_path, component, permission_type, permission_key,remark
});
}
}
export default permissionInfo;
最后仍然将这个对象聚合在src/api/index.js
这个文件里面
完成上面的操作以后,我们就可以新增权限菜单了,这里我们只添加目录与页面的菜单
<template>
<el-form label-width="120px" :model="permissionFormData" :rules="permissionFormDataRules" ref="formEl">
<el-form-item label="上级菜单" prop="parent_id">
<el-select placeholder="选择上级菜单" v-model="permissionFormData.parent_id">
<el-option :value="0" label="根目录"></el-option>
</el-select>
</el-form-item>
<el-form-item label="菜单类型" prop="permission_type">
<el-radio-group v-model="permissionFormData.permission_type">
<el-radio :label="0">目录</el-radio>
<el-radio :label="1">菜单</el-radio>
<el-radio :label="2">按钮</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="菜单图标">
<el-input placeholder="使用element-plus里面的图标" v-model="permissionFormData.icon"/>
</el-form-item>
<el-form-item label="菜单名称" prop="permission_name">
<el-input placeholder="请输入菜单的名称" v-model="permissionFormData.permission_name"/>
</el-form-item>
<el-form-item label="权限标识" prop="permission_key">
<el-input placeholder="输入权限标识 ,如userInfo:findById" v-model="permissionFormData.permission_key"/>
</el-form-item>
<el-form-item label="路由地址">
<el-input placeholder="如果是菜单页面,则要输入路由地址" v-model="permissionFormData.front_path"/>
</el-form-item>
<el-form-item label="组件路径">
<el-input placeholder="如果是菜单页面,则要输入组件的路径,实现动态加载"
v-model="permissionFormData.component"></el-input>
</el-form-item>
<el-form-item label="显示顺序" prop="order_num">
<el-input-number :min="0" :step="1" v-model="permissionFormData.order_num"></el-input-number>
</el-form-item>
<el-form-item label="备注信息">
<el-input type="textarea" :rows="3" v-model="permissionFormData.remark"/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm" :loading="isSubmiting" loading-icon="loading">保存</el-button>
<el-button type="default" @click.stop="$emit('closeDialog')">关闭</el-button>
</el-form-item>
</el-form>
</template>
<script setup>
import {ref, reactive,defineEmits} from "vue";
import Api from "@/api/index.js";
import {ElMessage, ElNotification} from "element-plus";
const emit = defineEmits(["closeDialog"])
const permissionFormData = reactive({
permission_name: "",
icon: "",
parent_id: 0,
order_num: 0,
front_path: "",
component: "",
permission_type: 0,
permission_key: "",
remark: ""
});
const permissionFormDataRules = {
permission_name: [{required: true, message: "权限名称不能为空", trigger: "blur"}],
parent_id: [{required: true, message: "父级菜单不能为空", trigger: "blur"}],
order_num: [{required: true, message: "排列序号不为能为空", trigger: "blur"}],
permission_type: [{required: true, message: "权限类型不能为空", trigger: "blur"}],
permission_key: [{required: true, message: "权限标识不能为空", trigger: "blur"}]
}
const isSubmiting = ref(false);
const formEl = ref(null);
const submitForm = () => {
formEl.value.validate(valid => {
if (valid) {
addPermissionInfo();
} else {
console.log("表单验证错误");
}
})
}
const addPermissionInfo = () => {
isSubmiting.value = true;
Api.permissionInfo.add(permissionFormData)
.then(result => {
ElMessage.success("新增成功");
emit("closeDialog");
}).catch(error => {
ElNotification.error(error.msg);
}).finally(() => {
isSubmiting.value = false;
})
}
</script>
<style scoped>
</style>
插入的结果如下
四十五、获取菜单目录
菜单的目录其实就是permission_type
为0和1的类型,所以现在我们在后台先编写相应的接口
src/services/PermissionInfoServices.ts代码
src/router/permissionInfoRouter.ts代码
router.get("/getAllFolderAndPageList", checkPermission("permissionInfo:getAllFolderAndPageList"), async (req: Request, resp: Response) => {
let result = await ServiceFactory.createPermissionInfoService().getAllFolderAndPageList();
let flag = Boolean(result);
resp.json(new ResultJson(flag, flag ? "获取成功" : "获取失败", result));
});
测试以求以后,得到如果的数据结果
{
"status": "success",
"msg": "获取成功",
"data": [
{
"id": 1,
"permission_name": "系统管理",
"icon": "icon",
"parent_id": 0,
"order_num": 0,
"front_path": "",
"component": "",
"permission_type": 0,
"permission_key": "manager",
"create_time": "2023-05-26T02:53:27.000Z",
"update_time": null,
"remark": "系统管理目录",
"is_del": false,
"children": [
{
"id": 2,
"permission_name": "用户管理",
"icon": "user",
"parent_id": 1,
"order_num": 0,
"front_path": "/Manager/UserInfoList",
"component": "uesrInfo/UserInfoList.vue",
"permission_type": 1,
"permission_key": "page:userInfo:list",
"create_time": "2023-05-26T02:58:28.000Z",
"update_time": null,
"remark": "用户管理的页面",
"is_del": false
},
{
"id": 3,
"permission_name": "新增用户",
"icon": "CirclePlus",
"parent_id": 1,
"order_num": 1,
"front_path": "/Manager/AddUserInfo",
"component": "userInfo/AddUserInfo.vue",
"permission_type": 1,
"permission_key": "page:userInfo:add",
"create_time": "2023-05-26T03:01:09.000Z",
"update_time": null,
"remark": "新增用户的页面",
"is_del": false
},
{
"id": 4,
"permission_name": "菜单管理",
"icon": "key",
"parent_id": 1,
"order_num": 3,
"front_path": "/Manager/PermissionInfoList",
"component": "permissionInfo/PermissionInfoList.vue",
"permission_type": 1,
"permission_key": "page:permissionInfo:list",
"create_time": "2023-05-26T03:04:10.000Z",
"update_time": null,
"remark": "菜单管理的页面",
"is_del": false
}
]
}
]
}
我们现在就可以通过这个接口来完成如下图的功能
在前端src/api/permissionInfo.js
下面编写请求接口
src/views/permissionInfo/AddPermissionInfo.vue
最终的效果如下
四十六、权限列表
效果图如下
后台代码src/services/PermissionInfoServices.ts
/**
* 获取所有权限,并实现table的级联
*/
async getAllPermissionList() {
let connection = this.getConnection();
let result = await connection.getRepository(PermissionInfo).find({
where: {
is_del: false
}
});
//开始构建层级数据
let level0 = [];
let level1 = [];
let level2 = [];
result.forEach(item=>{
//@ts-ignore
item.children = [];
if(item.permission_type===0){
level0.push(item);
}
else if(item.permission_type===1){
level1.push(item);
}
else if(item.permission_type===2){
level2.push(item);
}
});
level2.forEach(item=>{
let parent = level1.find(itemParent=>itemParent.id===item.parent_id);
if(parent){
parent.children.push(item);
}
});
level1.forEach(item=>{
let parent = level0.find(itemParent=>itemParent.id===item.parent_id);
if(parent){
parent.children.push(item);
}
});
return level0;
}
后台代码src/services/PermissionInfoServices.ts
/**
* 获取所有权限,并实现table的级联
*/
router.get("/getAllPermissionList",checkPermission("permissionInfo:getAllPermission"),async(req,resp)=>{
let result = await ServiceFactory.createPermissionInfoService().getAllPermissionList();
let flag = Boolean(result);
resp.json(new ResultJson(flag, flag ? "获取成功" : "获取失败", result));
});
前台代码src/api/permissionInfo.js
里面编写接口请求
完成接口请求以后,我们就可以开始使用这个接口来渲染数据了【此处的代码就省略了,具体可以看permissionInfoList.vue】
四十七、编辑权限列表
在权限列表里面,我们还可以对某个权限列表进行编辑,这里我们仍然使用弹窗的形式去完成
编辑权限列表的代码与新增权限列表的代码基本上大同事小异,前端页面的布局,后端接口的编写等这里就省略掉了,具体可以看代码
<template>
<el-form label-width="120px" :model="permissionFormData" :rules="permissionFormDataRules" ref="formEl">
<el-form-item label="上级菜单" prop="parent_id">
<el-tree-select
v-model="permissionFormData.parent_id"
:data="allFolderAndPageList" check-strictly
:render-after-expand="false"></el-tree-select>
</el-form-item>
<el-form-item label="菜单类型" prop="permission_type">
<el-radio-group v-model="permissionFormData.permission_type">
<el-radio :label="0">目录</el-radio>
<el-radio :label="1">菜单</el-radio>
<el-radio :label="2">按钮</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="菜单图标">
<el-input placeholder="使用element-plus里面的图标" v-model="permissionFormData.icon"/>
</el-form-item>
<el-form-item label="菜单名称" prop="permission_name">
<el-input placeholder="请输入菜单的名称" v-model="permissionFormData.permission_name"/>
</el-form-item>
<el-form-item label="权限标识" prop="permission_key">
<el-input placeholder="输入权限标识 ,如userInfo:findById" v-model="permissionFormData.permission_key"/>
</el-form-item>
<el-form-item label="路由地址">
<el-input placeholder="如果是菜单页面,则要输入路由地址" v-model="permissionFormData.front_path"/>
</el-form-item>
<el-form-item label="组件路径">
<el-input placeholder="如果是菜单页面,则要输入组件的路径,实现动态加载"
v-model="permissionFormData.component"></el-input>
</el-form-item>
<el-form-item label="显示顺序" prop="order_num">
<el-input-number :min="0" :step="1" v-model="permissionFormData.order_num"></el-input-number>
</el-form-item>
<el-form-item label="备注信息">
<el-input type="textarea" :rows="3" v-model="permissionFormData.remark"/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm" :loading="isSubmiting" loading-icon="loading">保存</el-button>
<el-button type="default" @click.stop="$emit('closeDialog')">关闭</el-button>
</el-form-item>
</el-form>
</template>
<script setup>
import {ref, reactive, onMounted} from "vue";
import Api from "@/api/index.js";
import {ElMessage, ElNotification} from "element-plus";
const emit = defineEmits(["closeDialog"]);
const props = defineProps({
currentEditPermissionId: {
required: true,
}
})
const permissionFormData = ref({
id:"",
permission_name: "",
icon: "",
parent_id: 0,
order_num: 0,
front_path: "",
component: "",
permission_type: 0,
permission_key: "",
remark: ""
});
const permissionFormDataRules = {
permission_name: [{required: true, message: "权限名称不能为空", trigger: "blur"}],
parent_id: [{required: true, message: "父级菜单不能为空", trigger: "blur"}],
order_num: [{required: true, message: "排列序号不为能为空", trigger: "blur"}],
permission_type: [{required: true, message: "权限类型不能为空", trigger: "blur"}],
permission_key: [{required: true, message: "权限标识不能为空", trigger: "blur"}]
}
//获取菜单目录
const allFolderAndPageList = ref([]);
const getAllFolderAndPageList = () => {
Api.permissionInfo.getAllFolderAndPageList().then(result => {
//生成el-tree-select的数据
buildElTreeData(result.data);
allFolderAndPageList.value = [{
value: 0,
label: "根目录",
children: result.data
}];
}).catch(error => {
ElNotification.error(error.msg);
})
}
/**
* 构建el-tree-select的数据
* @param arrData {Array}
*/
const buildElTreeData = (arrData) => {
if (Array.isArray(arrData)) {
arrData.forEach(item => {
item.value = item.id,
item.label = item.permission_name;
if (item.children) {
buildElTreeData(item.children);
}
})
}
}
const findCurrentEditItem = ()=>{
Api.permissionInfo.findById(props.currentEditPermissionId)
.then(result=>{
permissionFormData.value = result.data;
}).catch(error=>{
ElNotification.error(error.msg);
})
}
onMounted(() => {
getAllFolderAndPageList();
findCurrentEditItem();
})
const isSubmiting = ref(false);
const formEl = ref(null);
const submitForm = () => {
formEl.value.validate(valid => {
if (valid) {
updatePermissionInfo();
} else {
console.log("表单验证错误");
}
})
}
const updatePermissionInfo = ()=>{
isSubmiting.value = true;
Api.permissionInfo.update(permissionFormData.value)
.then(result=>{
emit("closeDialog");
}).catch(error=>{
ElNotification.error(error.msg);
})
}
</script>
<style scoped>
</style>
后面关于编辑与新增的操作,代码将会省略一些
四十八、用户权限分配
这是本次项目的核心了,界面如下图所示
点击“分配权限”以后将会进行下面的操作
弹出类似的模态框
在当前这个页面,使用的是element-plus里面的el-tree来完成的,这里主要有2个点需要完成
- 获取所有的权限列表,并形成这样的一个树形结构
- 获取当前这个用户已经具备的权限列表,对时在树形结构上面钩选已经具备的权限
有了这2点思路以后,我们先从后台开始
后台src/services/PermissionInfoService.ts文件
/**
* 获取所有权限,并实现table的级联
*/
async getAllPermissionList() {
let connection = this.getConnection();
let result = await connection.getRepository(PermissionInfo).find({
where: {
is_del: false
}
});
//开始构建层级数据
let level0 = [];
let level1 = [];
let level2 = [];
result.forEach(item=>{
//@ts-ignore
item.children = [];
//@ts-ignore
item.label = item.permission_name;
if(item.permission_type===0){
level0.push(item);
}
else if(item.permission_type===1){
level1.push(item);
}
else if(item.permission_type===2){
level2.push(item);
}
});
level2.forEach(item=>{
let parent = level1.find(itemParent=>itemParent.id===item.parent_id);
if(parent){
parent.children.push(item);
}
});
level1.forEach(item=>{
let parent = level0.find(itemParent=>itemParent.id===item.parent_id);
if(parent){
parent.children.push(item);
}
});
return level0;
}
上面的代码是获取所有的权限,并形成数形结构
后台src/services/UserPermissionInfoService.ts代码
/**
* 根据user_id来查询他的权限
*/
async getUserPermissionByUserId(user_id: number) {
let connection = this.getConnection();
let result = await connection.getRepository(UserPermissionInfo).find({
where: {
is_del: false,
user_id
}
});
return result;
}
完成Services的操作以后,接下来是后台的路由操作
后台src/router/userPermissionInfoRouter.ts文件
router.get("/getUserPermissionByUserId/:user_id", checkPermission("userPermissionInfo:getUserPermissionByUserId"), async (req, resp) => {
let result = await ServiceFactory.createUserPermissionInfoService().getUserPermissionByUserId(+req.params.user_id);
let flag = Boolean(result);
resp.json(new ResultJson(flag, flag ? "获取成功" : "获取失败", result));
});
后台src/router/permissionInfoRouter.ts文件
/**
* 获取所有权限,并实现table的级联
*/
router.get("/getAllPermissionList", checkPermission("permissionInfo:getAllPermission"), async (req, resp) => {
let result = await ServiceFactory.createPermissionInfoService().getAllPermissionList();
let flag = Boolean(result);
resp.json(new ResultJson(flag, flag ? "获取成功" : "获取失败", result));
});
完成后台的操作以后,接下来,我们就会转向前台操作
前台src/views/userInfo/ControlAuth.vue文件
<template>
<div>
<el-tree
:check-strictly="true"
ref="authorElTree"
:data="allPermissionList"
show-checkbox
node-key="id"
:default-checked-keys="currentUserPermissionList"
default-expand-all></el-tree>
<div class="mt-[10px] flex flex-row justify-end">
<el-button type="primary" @click="saveAuth" :loading="isSubmiting" loading-icon="loading">确定</el-button>
<el-button type="default" @click="$emit('closeDialog')">关闭</el-button>
</div>
</div>
</template>
<script setup>
/**
* 在这个页面上面必须有2个操作,第1个是获取所有的权限,形成树形图,第2个就是把自己的权限钩上
*/
import {onMounted, ref} from "vue";
import Api from "@/api/index.js";
import {ElNotification} from "element-plus";
const emit = defineEmits(["closeDialog"]);
const allPermissionList = ref([]);
const props = defineProps({
currentControlAuthorUserId: {
required: true
}
})
const getAllPermissionList = () => {
Api.permissionInfo.getAllPermissionList()
.then(result => {
allPermissionList.value = result.data;
}).catch(error => {
ElNotification.error(error.msg || "操作失败");
})
}
//当前用户的权限
const currentUserPermissionList = ref([]);
const getUserPermissionByUserId = () => {
Api.userPermissionInfo.getUserPermissionByUserId(props.currentControlAuthorUserId)
.then(result => {
currentUserPermissionList.value = result.data.map(item=>item.permission_id);
}).catch(error => {
ElNotification.error(error.msg);
})
}
onMounted(() => {
getAllPermissionList();
getUserPermissionByUserId();
})
//点击确定授权以后
const isSubmiting = ref(false);
const authorElTree = ref(null);
const saveAuth = () => {
//第一步:先获取选中的结点
let keys = authorElTree.value.getCheckedKeys();
//第二步:发送请求
Api.userPermissionInfo.controlAuth(props.currentControlAuthorUserId, keys)
.then(result => {
ElNotification.success("授权成功");
emit("closeDialog");
}).catch(error => {
ElNotification.error(error.msg);
})
}
</script>
<style lang="scss" scoped>
</style>
完成上面的操作以后,我们就可以动态的用户的权限进行分配了
四十九、保存用户分配的权限
这里承接上面的章节,当我们完成el-tree的数据渲染以后,我们肯定是需要保存用户的数据,这个时候,要将前台选中的权限传递到后台,这个操作有几点要注意
- 前台选择的可能是有多个权限,这个时候要将选中的权限组成一个数组,传递到后台,问题就在于怎么样向后台传递一个数组
- 后台接收到这个数组以后如果操作这个数组。初步的方案是数据库里面原来用户的权限全部删除掉,最后再重新添加权限,这样就会涉及到数据的一致性,所以这个地方要使用数据库的事务进行操作
前端src/api/userPermissionInfo.js代码
在上面的代码里面,我们可以看到,我们使用axios的时候更改了它的请求头,将Content-Type
设置成了application/json
,这样就方便我们向后台传递数据了
后台src/router/userPermissionRouter.ts代码
router.post("/controlAuth", checkPermission("userPermissionInfo:controlAuth"), async (req: Request, resp: Response) => {
let {user_id, permissionList} = req.body;
let flag = await ServiceFactory.createUserPermissionInfoService().controlAuth(user_id, permissionList);
resp.json(new ResultJson(flag, flag ? "授权成功" : "授权失败"));
});
请求到达路由以后,再次调用Services里面的方法,如下
后台src/services/UserPermissionInfoService.ts文件
/**
* 开始授权
* @param user_id
* @param permissionList
*/
async controlAuth(user_id: number, permissionList: Array<any>) {
console.log(user_id);
console.log(permissionList);
//注意,这里要开启事务
let connection = this.getConnection();
return await connection.transaction(async transactionalEntityManager => {
try {
//第一步:在用户权限表里面,将这个用户的所有权限全部删除掉。
await transactionalEntityManager.getRepository(UserPermissionInfo).createQueryBuilder()
.update()
.set({is_del: true, update_time: new Date()})
.where({user_id})
.execute();
for (let permission_id of permissionList) {
const userPermissionInfo = transactionalEntityManager.create(UserPermissionInfo);
userPermissionInfo.user_id = user_id;
userPermissionInfo.permission_id = permission_id;
userPermissionInfo.create_time = new Date();
await transactionalEntityManager.save(userPermissionInfo);
}
return true;
} catch (e) {
console.log(e);
return false;
}
})
}
上面的代码就是保存用户权限的时候的操作
五十、根据用户的权限生成路由信息
当我们对当同的用户分配不同的权限以后,我们就可以根据它的权限来动态的生成路由了
后台src/services/UserPermissionInfoServices.ts代码
async getUserRouterByUserId(user_id: number) {
let connection = this.getConnection();
let result = await connection.getRepository(UserPermissionInfo).createQueryBuilder("user_permission_info")
.innerJoinAndSelect("user_permission_info.permissionInfo", "permission_info")
.where("user_permission_info.is_del=false")
.andWhere("user_permission_info.user_id=:user_id", {user_id})
.orderBy("permission_info.order_num", "ASC")
.getMany();
return result;
}
上面的代码是获取当前用户的权限(这里之前写错了,只是获取了permission_type=0或1的权限,后来将这里删除了)
在用户登录的时候,获取用户的权限信息,并通过currentUserPagePermission返回到前端
前端登录成功以后,将数据保存在了pinia
里面
pinia
在保存数据的时候,将数据进行保存的时候调用了buildRouterFromPagePermission
来动态生成路由表
现在我们回到router/index.js
下面看一下buildRouterFromPagePermission
方法
完成上面的操作以后,我们就生成了动态路由了,现在就需要到LeftMenu.vue
里面来生成菜单了
<template>
<div class="menu-box">
<div class="h-[60px] bg-[#263445] font-bold flex flex-row justify-center items-center text-white italic">
<el-icon size="28" class="is-loading mr-[10px]">
<Loading/>
</el-icon>
通用权限管理系统
</div>
<el-menu
active-text-color="#ffd04b"
background-color="#304156"
:default-active="$route.fullPath"
router
text-color="#fff">
<el-sub-menu v-for="(item,index) in menuData" :index="index" :key="index">
<template #title>
<el-icon>
<component :is="item.permissionInfo.icon"></component>
</el-icon>
<span>{{ item.permissionInfo.permission_name }}</span>
</template>
<el-menu-item
v-for="(sub_item,sub_index) in item.children"
:index="sub_item.permissionInfo.front_path"
:key="sub_item.permissionInfo.front_path">
<el-icon>
<component :is="sub_item.permissionInfo.icon"></component>
</el-icon>
{{ sub_item.permissionInfo.permission_name }}
</el-menu-item>
</el-sub-menu>
</el-menu>
</div>
</template>
<script setup>
import {computed} from "vue";
import {mainStore} from "@/store";
const store = mainStore();
//这里要根据页面权限来生成菜单了
const menuData = computed(() => {
let menuList = [];
let subMenuLsit = [];
console.log(store.currentUserPagePermission);
store.currentUserPagePermission.forEach(item => {
if (item.permissionInfo.permission_type === 0) {
menuList.push({
...item,
children: []
})
} else if (item.permissionInfo.permission_type === 1) {
subMenuLsit.push(item);
}
})
subMenuLsit.forEach(item => {
let parentMenu = menuList.find(v => v.permission_id === item.permissionInfo.parent_id);
parentMenu.children.push(item);
})
return menuList;
});
</script>
<style scoped lang="scss">
.menu-box {
@apply bg-[#304156] w-full h-full overflow-hidden;
}
</style>
经过上面的代码,我们就可以在而菜单的动态加载了
五十一、解决动态路由刷新页面空白的问题
这一个问题是一个非常常见的问题,在百度上面搜索的时候也有很多解决办法,这里我只讲解一种常见的方法
因为我们的路由数据是使用 pinia进行了持久化存储,并且存储在sessionStorage里面,在持久化的时候,我们使用的是web-storage-cache
这个插件,所以我们可以在加载页面的时候使用这个插件读取数据
现在我们回到main.js
里面进行相关的操作
完成上面的操作以后,我们再刷新的时候,就不会存在路由加载不及时而造成的页面空白了
五十二、按钮权限的控制
在前面的操作中,我们已经通过权限列表完成了页面的权限操作,现在我们需要对页面上面的按钮进行权限操作
在之前构建权限的数据表的时候,我们在数据表里面添加了一项数据permission_key
的字段,这一项字段就是用于记录每一项权限的值的,如userInfo:add
代表userInfo下面的add操作
现在我们就可以通过这个permission_key
来进行
前端src/Login.vue代码
前端src/store/index.js代码
在上面的代码里面,我们可以看到,当我们在设置用户的权限的时候,我们就已经将用户的permission_key
提取出来 ,放在了一个数组里面,存放在了pinia
当中
有了这个数组以后,我们就可以通过全局指令来完成相应的按钮权限控制了
前端src/utils/directive.js代码
import {mainStore} from "@/store";
export const permissionCheck = (el,binding,vnode)=>{
const store = mainStore();
let permission_key = binding.value;
let index = store.permissionKeyList.findIndex(item=>item===permission_key);
if(index===-1){
el.remove();
}
}
上面我们定义了一个自定义指令,如果在permissionKeyList
里面找到了这个数组我们就保存这个元素,如果没有找到,就不保留这个元素
现在去使用一下
经过使用以后发现,按钮权限控制正常
五十三、后台权限的控制
前台完成权限控制以后,我们的后台API接口也要完成权限控制
重新回到后台src/utils/permissionUtils.ts
里面
import {NextFunction, Request, Response} from "express";
import ResultJson from "../dto/ResultJson";
import * as jwt from "jsonwebtoken";
import APPConfig from "../config/APPConfig";
import ServiceFactory from "../factory/ServiceFactory";
export const checkPermission = (permissionKey?: string) => {
console.log(permissionKey);
return async (req: Request, resp: Response, next: NextFunction) => {
next();
return ;
//第1步:如果是options的预检请求,这个时候我们可以直接放行
if (req.method.toUpperCase() === "OPTIONS") {
return next();
} else {
//第2步:从header或query里面得到authorization的token
let token = req.header("authorization") || req.query.authorization;
//第3步:如果有token
if (token) {
try {
//第4步:对token进行鉴权,看token是否有效
let decode = jwt.verify(String(token), APPConfig.tokenSecret);
//第5步:如果token有效,我们就将得到的token保存在header方向后面继续使用
req.header["userInfo"] = decode;
//第6步:是否是泛权限,如果不是泛权限,就进一步检测权限
if(permissionKey){
//第7步:检测一下当前这个用户是否有这个权限
let flag = await ServiceFactory.createUserPermissionInfoService().checkUserPermission((decode as any).id, permissionKey)
if (flag) {
//说明当前用户有这个API的权限
return next();
}
else{
//说明当前用户没有这个API的权限
let resultJson = new ResultJson(false, "请求未授权");
resp.status(403).json(resultJson);
}
}
else{
//第8部:说明是泛权限,只要登录了的就可以放行
return next();
}
} catch (e) {
let resultJson = new ResultJson(false, "令牌验证失败");
resp.status(403).json(resultJson);
}
} else {
//如果没有token
let resultJson = new ResultJson(false, "请求未授权");
resp.status(403).json(resultJson);
}
}
}
}
上面的代码就是一个权限验证的中间件代码,在路由的路径里面,我们就可以使用这个中间件进行权限验证
在之前我们其实已经讲过了,这里的权限划分的情况,后台的权限 们划分了3个等级
所以在checkPermission
这一个中间件里面,我们也做了3 次等级的验证
在使用的时候,可以通过下面的方式来进行
无需权限
router.get("/Login",async(req:Request,resp:Response)=>{})
上面的请求我们没有使用checkPermission
则代表当前这个API接口不需要进行权限验证
泛权限
router.get("/getAllPermissionInfoList",checkPermission(),async(req:Request,resp:Response)=>{});
上面的请求我们使用了checkPermission
中间件,但是我们并没有指定它需要使用权限,只是要验证token,所以这一种情况我们叫泛权限验证
具体权限
router.get("/deleteById/:id",checkPermission("userInfo:deleteById"),async(req:Request,resp:Response)=>{});
上面的请求我们使用了checkPermission("userInfo:deleteById")
这个中间件,并传递了具体的permission_key
这就说明一定要具备这个权限的用户才能使用这个API接口
五十四、问题汇总
-
如果时间充足的情况下,尽量做到,用户,角色,权限的完整操作,而不是直接使用用户,权限的操作
-
接口的编写一定要统一,不然后期的操作会非常麻烦,如下
新增使用add,查询使用query,编辑使用edit,删除使用delete,这样后台在编写接口的时候也可以直接使用这几个基本操作,同时前台在进行页面与按钮的权限控制的时候也不容易弄混淆
-
这里的用户表只有一张,这样做起来比较简单,但是不方便,如果后期有的用户分为管理员,学生,老师等,而学生与老师又需要在不同的数据表里面,这样权限操作起来就会变得非常麻烦