项⽬地址:https://github.com/Xiaodie-888/Frontend.git 前端
https://github.com/Xiaodie-888/backend.git 后端
技术栈:Vue3+Vite+Typrscript+Pinia+Element-plus+Vue-Router+Express.js+MySQL
核⼼⼯作与技术:
先在后端建立JWT认证机制,使用jsonwebtoken库生成Token。用户登录时,系统验证账号密码后,将用户信息(排除密码等敏感数据)加密生成7小时有效期的JWT Token,以Bearer格式返回给前端。同时返回用户的department(部门)和position(职位)信息,为后续权限控制奠定基础。
再在前端封装统一的HTTP请求处理机制。在请求拦截器中自动从localStorage获取Token并添加到请求头的Authorization字段;在响应拦截器中统一处理各种状态码,特别是401状态码时自动清除本地存储并跳转登录页,实现Token失效的自动处理。这样开发者无需手动管理Token状态。
然后基于用户角色实现路由的动态生成。后端根据用户部门返回对应的路由配置(超级管理员拥有全部8个模块,人事部仅有用户管理,产品部拥有产品相关3个模块)。前端通过import.meta.glob动态导入组件,使用router.addRoute将权限路由添加到Vue Router中,配合Pinia的数据持久化,确保页面刷新后路由状态保持。
接着建立细粒度的权限控制体系。采用部门级 + 职位级双重权限模型:通过v-if="userAdmin||superAdmin"控制菜单显示;基于职位进行功能权限控制,如审核功能用v-if="!admin()"限制仅经理可操作;在关键操作函数中进行权限拦截,如if(position=='员工') return阻止员工编辑产品。形成了从路由→菜单→按钮→操作的四级权限防护。
最后使用Pinia管理权限状态,配合pinia-plugin-persistedstate插件实现数据持久化。将用户信息、权限数据、动态路由等关键状态进行统一管理,确保系统重启或页面刷新后用户无需重新登录。
keep-alive
是 Vue 的内置组件,主要作用是缓存组件实例,避免组件切换时重复渲染 DOM,从而保留组件状态。当它包裹动态组件时,未激活的组件会被缓存到内存中,而非销毁,再次切换时直接从缓存中恢复状态。
通过keep-alive
,Vue 实现了高效的组件状态管理,尤其适用于标签页切换、多视图缓存等场景,能显著提升用户体验和应用性能。
核心属性
include
:指定需要缓存的组件名称(字符串、正则或数组)。exclude
:指定不需要缓存的组件名称(字符串、正则或数组)。max
:限制缓存的组件数量,超出时按 LRU(最近最少使用)规则移除最早的缓存。基本用法
组件匹配规则:优先检查
name
选项,若不存在则匹配局部注册名称,匿名组件无法被缓存。
被keep-alive
缓存的组件会新增两个钩子:
activated
:组件被激活(从缓存中恢复)时调用。deactivated
:组件被停用(缓存到内存)时调用。生命周期对比:
beforeCreate
→ created
→ mounted
→ activated
activated
(直接从缓存恢复,跳过mounted
)通过路由元信息meta.keepAlive
控制组件缓存:
路由配置
const routes = [
{
path: '/list',
name: 'ItemList',
component: ItemList,
meta: { keepAlive: true } // 标记需要缓存
}
]
组件使用
缓存机制
keep-alive
通过cache
对象存储组件实例,键为组件key
,值为组件 VNode。keys
数组记录缓存顺序,用于实现max
限制下的 LRU 淘汰策略。渲染逻辑
cache
,keys
添加对应key
。key
存在于cache
中,直接从缓存中获取组件实例,避免重新创建。源码核心片段
render() {
const vnode = getFirstComponentChild(this.$slots.default)
const componentOptions = vnode?.componentOptions
const name = getComponentName(componentOptions)
// 检查是否需要缓存(通过include/exclude过滤)
if (需要缓存) {
const key = vnode.key || componentOptions.Ctor.cid + '::' + componentOptions.tag
if (this.cache[key]) {
// 命中缓存,直接使用实例
vnode.componentInstance = this.cache[key].componentInstance
this.keys.push(this.keys.splice(this.keys.indexOf(key), 1)[0]) // 调整缓存顺序
} else {
// 未命中,存入缓存
this.cache[key] = vnode
this.keys.push(key)
// 超出max时删除最早的缓存
if (this.max && this.keys.length > this.max) {
pruneCacheEntry(this.cache, this.keys[0], this.keys, this._vnode)
}
}
vnode.data.keepAlive = true
}
return vnode
}
当组件被缓存时,数据可能不会自动更新,可通过以下方式解决:
activated
钩子
activated() {
this.fetchData() // 重新请求数据
}
beforeRouteEnter
导航守卫
next(vm => ...)
访问组件实例:beforeRouteEnter(to, from, next) {
next(vm => {
vm.fetchData() // 在组件实例中调用方法
})
}
activated
和deactivated
在 SSR 期间不生效。exclude
匹配的组件会被从缓存中移除,并触发$destroy
。max
,避免缓存过多组件导致内存占用过高。Expires
或 Max - Age
设置过期时间,若未设置则随浏览器关闭失效。JSON.stringify()
)。Secure
属性限制仅在 HTTPS 下传输,HttpOnly
属性防止 XSS 攻击。// 设置 Cookie
document.cookie = "username=John; expires=Thu, 31 Dec 2025 23:59:59 GMT; path=/";
// 获取 Cookie
const cookies = document.cookie.split('; ').reduce((obj, cookie) => {
const [key, value] = cookie.split('=');
obj[key] = value;
return obj;
}, {});
// 删除 Cookie(设置过期时间为过去)
document.cookie = "username=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/";
JSON.stringify()
和 JSON.parse()
处理对象。storage
事件)。localStorage
的时候,本页面不会触发storage
事件,但是别的页面会触发storage
事件。// 存储对象
localStorage.setItem('user', JSON.stringify({ name: 'Alice', age: 25 }));
// 获取数据
const user = JSON.parse(localStorage.getItem('user'));
// 删除单个数据
localStorage.removeItem('user');
// 清空所有数据
localStorage.clear();
localStorage
也不是完美的,它有两个缺点:
Cookie
一样设置过期时间localStorage
类似,但生命周期更短。localStorage
,约 5 - 10MB。localStorage
,仅支持字符串。// 存储临时数据
sessionStorage.setItem('tempData', '临时内容');
// 在同窗口其他页面获取数据
const temp = sessionStorage.getItem('tempData');
// 打开/创建数据库
const request = indexedDB.open('MyDatabase', 1);
request.onsuccess = (event) => {
const db = event.target.result;
// 开始事务
const transaction = db.transaction(['users'], 'readwrite');
const store = transaction.objectStore('users');
// 添加数据
const user = { id: 1, name: 'Bob', email: '[email protected]' };
const addRequest = store.add(user);
addRequest.onsuccess = () => console.log('数据添加成功');
};
维度 | Cookie | localStorage | sessionStorage | IndexedDB |
---|---|---|---|---|
存储容量 | 4KB 以内 | 5 - 10MB | 5 - 10MB | 理论无上限 |
生命周期 | 可设置过期时间,或随浏览器关闭失效 | 永久存储,手动删除才失效 | 窗口关闭后失效 | 永久存储,手动删除才失效 |
数据类型 | 仅字符串 | 仅字符串(需序列化对象) | 仅字符串(需序列化对象) | 支持对象、Blob 等复杂类型 |
网络传输 | 自动携带到服务器 (每次 HTTP 请求时自动携带到服务器,增加请求头开销。) |
仅本地存储,不参与传输 | 仅本地存储,不参与传输 | 仅本地存储,不参与传输 |
异步性 | 同步操作 | 部分浏览器同步 | 部分浏览器同步 | 异步操作 |
共享性 | 同域名下共享 | 同域名下所有窗口共享 | 同域名、同窗口下共享 | 同域名下共享 |
安全性 | 可通过 Secure/HttpOnly 增强 | 本地存储,需前端自行控制 | 本地存储,需前端自行控制 | 本地存储,需前端自行控制 |
storage
事件监听)。hash 路由(锚点路由)
#
符号(锚点)及其后的内容(如 #/home
)作为路由标识。浏览器对 #
后的内容不会向服务器发送请求,仅作为前端页面内部的锚点定位。https://example.com/#/user/123
,其中 #/user/123
是 hash 部分。hashchange
事件(如用户手动修改 hash 或通过 location.hash
操作),前端解析 hash 内容并触发路由切换。history 路由(HTML5 History API)
History API
(如 pushState()
、replaceState()
)修改 URL 的路径部分(不含 #
),模拟传统服务器路由的 URL 结构。https://example.com/user/123
,路径 /user/123
由前端控制。维度 | hash 路由 | history 路由 |
---|---|---|
URL 结构 | 包含 # ,如 example.com/#/path |
无 # ,如 example.com/path |
服务器请求 | hash 变化不触发服务器请求,仅前端处理 | 路径变化可能触发服务器请求(需后端支持) |
浏览器历史记录 | 修改 hash 不会新增历史记录,仅更新当前 URL | 每次 pushState() 会新增历史记录,支持前进 / 后退 |
SEO 支持 | 搜索引擎难以解析 hash 内容,SEO 效果差 | URL 更符合传统格式,SEO 友好性更好 |
服务器配置 | 无需额外配置,兼容性强 | 需要服务器配置重定向(如将所有路径指向首页) |
参数传递 | 可通过 hash 传递简单参数(如 #/user?id=1 ) |
可通过路径参数(/user/1 )或 state 对象传递复杂数据 |
兼容性 | 支持所有主流浏览器(包括 IE8+) | 依赖 HTML5 History API,IE9 及以下不支持 |
hash 路由适用场景
history 路由适用场景
https://api.com/posts/1
),便于前后端路由统一。hash 路由:简单、兼容性强,适合纯前端场景,但 URL 不够美观且 SEO 能力弱。
history 路由:URL 更符合直觉,支持 SEO 和复杂交互,但需要后端配置支持,且兼容性略差。
history 路由的服务器配置(以 Nginx 为例)
当用户刷新 example.com/path
时,服务器需将请求重定向到首页(如 index.html
),由前端路由处理路径:
server {
listen 80;
server_name example.com;
root /path/to/your/app;
location / {
try_files $uri $uri/ /index.html; # 所有路径重定向到首页
}
}
前端路由库的选择
mode: 'hash'
启用(默认模式),React Router 需使用 HashRouter
。mode: 'history'
,React Router 使用 BrowserRouter
,同时确保后端已配置重定向。解决措施:维护一个 task queue 和 isRefresh 的标识,如果处于正在刷新 token 的阶段,之后发出的多个请求保存在一个队列中,等 token 刷新完后再请求(避免多次刷新 token)
JWT 的原理可以参考阮一峰的这篇文章:JWT 原理,需要核心掌握的就是 JWT 的结构以及 Base64Url 算法(后面会提到)
JWT 分为三个部分:
|
Base64Url 算法:
JWT 作为一个令牌(token),有些场合可能会放到 URL(比如 api.example.com/?token=xxx)。Base64 有三个字符
+
、/
和=
,在 URL 里面有特殊含义,所以要被替换掉:=
被省略、+
替换成-
,/
替换成_
。这就是 Base64URL 算法
结构与组成
{
"alg": "HS256",
"typ": "JWT"
}
iss
、exp
)和自定义声明(如userId
、roles
) {
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022,
"exp": 1516242622
}
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
工作流程
Authorization
头中添加Bearer {token}
const jwt = require('jsonwebtoken');
// 登录接口
app.post('/api/login', async (req, res) => {
// 验证用户凭证
const user = await User.findOne({ username: req.body.username });
if (!user || !comparePassword(req.body.password, user.password)) {
return res.status(401).json({ message: '认证失败' });
}
// 生成Token(包含用户ID和角色)
const token = jwt.sign(
{
userId: user._id,
roles: user.roles,
username: user.username
},
process.env.JWT_SECRET, // 私钥
{
expiresIn: '1h', // 有效期
algorithm: 'HS256' // 签名算法
}
);
res.json({ token });
});
// 登录成功后存储Token
localStorage.setItem('auth_token', token);
// 配置axios拦截器自动携带Token
axios.interceptors.request.use(config => {
const token = localStorage.getItem('auth_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
const expressJwt = require('express-jwt');
// 验证中间件
const authMiddleware = expressJwt({
secret: process.env.JWT_SECRET,
algorithms: ['HS256'],
credentialsRequired: true // 要求必须有Token
}).unless({
path: [
'/api/login', // 白名单:登录接口无需验证
'/api/register'
]
});
// 应用中间件
app.use(authMiddleware);
// 受保护的路由
app.get('/api/userInfo', (req, res) => {
// 验证通过后,用户信息会被注入到req.user中
res.json({
message: '用户信息',
user: req.user
});
});
// 错误处理中间件
app.use((err, req, res, next) => {
if (err.name === 'UnauthorizedError') {
// Token过期或无效
if (err.code === 'token_expired') {
return res.status(401).json({
message: 'Token已过期,请刷新或重新登录',
code: 40101
});
}
return res.status(401).json({ message: '无效Token' });
}
next(err);
});
// Token刷新接口
app.post('/api/refreshToken', async (req, res) => {
const oldToken = req.headers.authorization?.split(' ')[1];
if (!oldToken) {
return res.status(400).json({ message: '缺少Token' });
}
try {
// 验证旧Token签名(忽略过期时间)
const decoded = jwt.verify(oldToken, process.env.JWT_SECRET, { ignoreExpiration: true });
// 生成新Token
const newToken = jwt.sign(
{ userId: decoded.userId, roles: decoded.roles },
process.env.JWT_SECRET,
{ expiresIn: '1h' }
);
res.json({ token: newToken });
} catch (err) {
res.status(401).json({ message: '刷新失败,请重新登录' });
}
});
// 登出时将Token加入黑名单(Redis实现)
app.post('/api/logout', async (req, res) => {
const token = req.headers.authorization?.split(' ')[1];
if (token) {
const decoded = jwt.decode(token);
const expireTime = decoded.exp - Math.floor(Date.now() / 1000);
await redisClient.set(`revoked:${token}`, 'true', 'EX', expireTime);
}
res.json({ message: '登出成功' });
});
// 验证中间件中检查黑名单
authMiddleware.unless({
path: ['/api/login', '/api/refreshToken']
});
app.use(async (req, res, next) => {
if (req.user) {
const token = req.headers.authorization?.split(' ')[1];
const isRevoked = await redisClient.exists(`revoked:${token}`);
if (isRevoked) {
return res.status(401).json({ message: 'Token已失效' });
}
}
next();
});
// 生成Token(服务端)
const privateKey = fs.readFileSync('private.key');
const token = jwt.sign(payload, privateKey, { algorithm: 'RS256' });
// 验证Token(客户端/其他服务)
const publicKey = fs.readFileSync('public.key');
jwt.verify(token, publicKey, { algorithm: 'RS256' });
Session 是在用户登录成功后,由服务器自动生成的唯一标识
1 2 3 4 5 6 7 8 9 10 11 |
|
也就是说,当浏览器接收到服务器返回的 sessionID 之后,会先将此信息存入 cookie,同时 cookie 记录此 sessionID 属于哪个域名;在后续请求的时候,cookie 信息会被自动发送给服务端,服务端也会从 cookie 中获取 sessionID,再根据 ID 寻找响应的 session 信息
适合于同源 Web 应用,且无需跨域
对比项 SessionToken Token
存储位置 | 存储在服务端(如内存或 Redis) | 存储在客户端(如 Cookie 或 LocalStorage) |
状态管理 | 有状态(需要服务器存储用户会话) | 无状态(不需要服务器存储用户信息) |
跨域支持 | 不支持跨域(需要特殊配置或共享存储) | 支持跨域(特别是 JWT) |
安全性 | 相对安全,易于控制(服务器存储) | 需要注意防止 XSS 攻击,存储需小心 |
jwtSecretKey:'gbms',
定义了 JWT 的密钥,用于 token 的加密和解密
// 第四步 生成返回给前端的token
// 剔除加密后的密码,头像,创建时间,更新时间
const user = {
...results[0],
password: '',
imageUrl: '',
create_time: '',
update_time: '',
}
// 设置token的有效时长 有效期为7个小时
const tokenStr = jwt.sign(user, jwtconfig.jwtSecretKey, {
expiresIn: '7h'
})
res.send({
results: results[0],
status: 0,
message: '登录成功',
token: 'Bearer ' + tokenStr,
})
核心功能:
- 验证用户账号密码
- 生成包含用户信息的 JWT token
- 设置 7 小时的过期时间
- 返回 "Bearer" + token 格式
const superAdminRouter = [{
name: 'set',
path: '/set',
component: 'set/index'
},
{
name: 'user',
path: '/user',
component: 'user/index'
}, {
name: 'login_log',
path: '/login_log',
component: 'login_log/index'
}, {
name: 'operation_log',
path: '/operation_log',
component: 'operation_log/index'
}, {
name: 'product',
path: '/product',
component: 'product/index'
}, {
name: 'audit',
path: '/audit',
component: 'product/audit'
}, {
name: 'outbound',
path: '/outbound',
component: 'product/outbound'
},
{
name: 'statistics',
path: '/statistics',
component: 'statistics/index'
}
]
instance.interceptors.request.use(function (config) {
// 添加请求之前的逻辑 - 从localStorage获取token并添加到请求头
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = token // 后端返回的已经包含 "Bearer " 前缀
}
return config;
}, function (error) {
case 401:
ElMessage.error('未授权,请登录')
// token过期或无效,清除本地存储并跳转到登录页
localStorage.clear()
router.push('/')
break
function compilerMenu (arr:any) {
if(!arr) return
menuData.value = arr
arr.forEach((item:any)=>{
let rts = {
name:item.name,
path:item.path,
component:item.component
}
if(item.children && item.children.length){
compilerMenu(item.children)
}
if(!item.children){
let path = loadComponent(item.component)
// console.log(path)
rts.component = path;
router.addRoute('menu',rts)
}
function loadComponent(url:string){
let Module = import.meta.glob("@/views/**/*.vue")
return Module[`/src/views/${url}.vue`]
关键特性:
- 动态导入组件:使用 import.meta.glob 实现组件的动态加载
- 路由动态添加:使用 router.addRoute 将权限路由添加到路由表
- 递归处理:支持嵌套路由的处理
userStore.userInfo(account)
const routerList = await returnMenuList(id)
// console.log(routerList)
menuStore.setRouter(routerList)
router.push('/menu') // 登录成功后跳转至菜单页面
路由守卫实现 (后台前端_wz/src/router/index.ts)
// 路由守卫 - 基于token的权限控制
router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token')
// 白名单路由(不需要token验证的页面)
const whiteList = ['/']
if (token) {
// 已登录状态
if (to.path === '/') {
// 如果已登录,访问登录页则重定向到菜单页
next('/menu')
} else {
// 正常访问其他页面
next()
}
} else {
// 未登录状态
if (whiteList.indexOf(to.path) !== -1) {
// 访问白名单页面,放行
next()
} else {
// 访问需要权限的页面,重定向到登录页
ElMessage.error('请先登录')
next('/')
}
}
})
系统架构总结
你的权限控制系统解决了以下问题:
通过对你原始代码的详细分析,我发现你确实已经实现了多层次的按钮级权限控制:
12:27:后台前端_wz/src/views/menu/index.vue
用户模块
登录日志
操作日志
产品管理
18:25:后台前端_wz/src/views/product/components/notes.vue
75:82:后台前端_wz/src/views/product/components/notes.vue
const admin = () => {
if(localStorage.getItem('position')=='经理'){
return true
}else{
return false
}
}
在 user/components/user_info.vue
中:
47:49:后台前端_wz/src/views/user/components/user_info.vue
删除用户
130:137:后台前端_wz/src/views/user/components/user_info.vue
const admin = () =>{
if(localStorage.getItem('position')=='员工'){
return false
}else{
return true
}
}
在 product/index.vue
中:
147:151:后台前端_wz/src/views/product/index.vue
// 编辑产品信息
const openEdit = (row:any) =>{
if(localStorage.getItem('position')=='员工') return
bus.emit('editRow',row)
editProduct.value.open()
你的系统实现了三个层次的权限控制:
部门级权限:
超级管理员
:全部功能人事部
:用户管理产品部
:产品相关功能职位级权限:
经理
:具有审核权限、编辑权限、删除权限员工
:只有基础操作权限功能级权限:
- 审核功能:仅经理可操作
- 编辑功能:员工被限制
- 删除功能:员工无权限
- 条件渲染:使用
v-if
控制元素显示- 功能禁用:使用
:disabled
控制按钮状态- 函数拦截:在事件处理函数中进行权限检查
- LocalStorage 检查:基于用户的
department
和position
- ✅ 菜单权限控制(基于部门)
- ✅ 按钮显示 / 隐藏控制(v-if)
- ✅ 按钮禁用控制(:disabled)
- ✅ 功能级权限控制(职位检查)
- ✅ 操作拦截(函数内权限验证)
1 2 3 4 5 6 7 |
|
1 2 3 4 5 6 7 8 |
|
而我们只需要if (data.code) === 200
,就直接将data.data
进行resolve
因为上述在 axios 封装中提到了这些请求,所以这些问题也需要详细准备一下
1 2 3 4 |
|
1 2 3 4 |
|
2XX
*
200
:表示请求成功,数据放在响应体中
*
201
:表示在服务器中创建了新资源。比如创建一个新的用户
*
202
:表示服务器端已经收到了请求,但是还没有处理
*
204
:表示资源请求成功,但是响应体中没有数据
3XX
*
301
:永久重定向
*
302
:暂时重定向
*
304
:可以使用协商缓存
4XX
*
400
:笼统的错误码,表示资源请求错误
*
401
:表示需要认证后才能访问的资源
*
403
:表示服务器直接拒绝 http 请求,一般用来针对非法请求
*
404
:未找到资源
*
409
:表示请求的资源与当前服务器的状态发生冲突
*
429
:请求过多
5XX
*
500
:笼统的错误码,表示服务器发生错误
*
501
:表示请求的功能还未支持,类似于敬请期待
*
502
:访问后端服务器发生了错误
*
503
:表示服务器很忙
https = http + 加密 + 认证 + 完整性保护
具体来说,分为以下四点:
可以看出, HTTPS 相比于 HTTP,多了一层 SSL/TLS
协议,并且针对于 http 的问题,提出了相应的解决方式
HTTP 存在问题 HTTPS 对应解决方式
窃听风险 | 实现信息加密:采用混合加密的方式实现信息的机密性 |
篡改风险 | 实现校验机制:通过摘要算法来实现完整性,它能够为数据生成独一无二的指纹从而来校验数据的完整性 |
冒充风险 | 身份证书:通过第三方认证机构来对身份进行鉴定,即将服务器公钥放在了数字证书中,解决了冒充的风险 |
HTTPS 采用的是对称加密和非对称加密结合的混合加密的方式
含义:解密和加密都使用的相同的密钥
过程:
client-random
service-random
,并将这个随机数和加密套件列表返回给浏览器缺陷:传输是明文的,通过可能被监听到从而可以获得密钥
含义:公钥是每个人都可以获取到的,存储在浏览器中,私钥只有服务器才知道,不对任何人公开
过程:
缺点:
含义:在传输数据阶段使用对称加密,但是对称加密的密钥使用非对称加密来传输
具体流程:
缺点: 被 DNS 劫持替换 IP 地址在自己服务器上实现公钥和私钥,将使用 ca 证书来解决
在上述已经说过,https = http + 加密 + 认证 + 完整性保护
我们已经详细讲过加密这一小节了,接下来讲讲认证和完整性保护(摘要算法+数字签名)
CA 认证解决的是什么问题呢,混合加密的缺陷我们已经知道,可能被伪造假的公钥和私钥,从而造成数据泄露。
那么这时候,我们就需要一家权威的机构来验证身份是否合法,这家权威的机构就是 CA(数字证书认证机构),将服务器公钥放在数字证书中(由数字证书认证机构颁发),只要证书是可信的,公钥就是可信的
通过摘要算法+数字签名来确保数据的完整性
简单来说,就是为了保证传输的内容不受到篡改,我们需要对内容计算出一个【指纹】,然后同内容一同传输给对方。对方收到也会先对内容计算出一个【指纹】,然后与传输过来的指纹进行对比,如果一致,则说明内容并没有被篡改。
那么,在计算机中,会使用摘要算法(哈希算法)来计算出内容的哈希值,这个哈希值是唯一的,且无法通过哈希值推导出内容。
通过摘要算法,我们可以确保内容无法被篡改,但是无法确保【内容+哈希值】不会被中间人所替换,因为这里缺乏对客户端收到的消息是否来自服务端的认证。
为了解决这个问题,计算机中会采用非对称加密的方法来解决,跟之前讲的一样,非对称加密会有两个密钥:
这两个密钥可以双向加解密:
因为非对称加密是比较耗时的,所以我们一般不会使用非对称加密来加密时机传输的内容,而只是对内容的哈希值进行加密
所以非对称加密的用途主要通过私钥加密,公钥解密的方式来确认消息的身份,我们常说的数字签名算法,就是使用的这种方式。
私钥是由服务端保管,然后服务端会向客户端颁发对应的公钥。如果客户端收到的信息,能被公钥解密,那说明该消息是由服务器推送的
也就是 TLS 的握手过程:
1.客户端发起请求
首先,由客户端向服务器发起加密通信请求,也就是 ClientHello
请求。
在这一步,客户端主要向服务器发送以下信息:
1 2 3 |
|
2.服务器响应
服务器收到客户端请求后,向客户端发出响应,也就是 SeverHello
。服务器回应的内容有如下内容:
1 2 3 4 |
|
3.验证证书
客户端收到服务器的回应之后,首先通过浏览器或者操作系统中的 CA 公钥,确认服务器的数字证书的真实性。
如果证书没有问题,客户端会从数字证书中取出服务器的公钥,然后使用它加密报文,向服务器发送如下信息:
1 2 3 |
|
上面第一项的随机数是整个握手阶段的第三个随机数,会发给服务端,所以这个随机数客户端和服务端都是一样的
服务器和客户端有了这三个随机数(Client Random、Server Random、pre-master key),接着就用双方协商的加密算法,各自生成本次通信的「会话秘钥」。
4.服务器的最后回应
服务器收到客户端的第三个随机数(pre-master key
)之后,通过协商的加密算法,计算出本次通信的「会话秘钥」。
然后,向客户端发送最后的信息:
1 2 |
|
至此,整个 TLS 的握手阶段全部结束。接下来,客户端与服务器进入加密通信,就完全是使用普通的 HTTP 协议,只不过用「会话秘钥」加密内容
简单来说有以下五点:
|
这是你项目最突出的亮点之一,实现了三个维度的权限控制:
Search files...
javascript
// 后端:基于部门返回不同的路由配置
const superAdminRouter = [/* 所有权限 */]
const userAdminRouter = [/* 人事部权限 */]
const productAdminRouter = [/* 产品部权限 */]
// 前端:动态显示菜单
javascript
// 基于职位控制功能访问
const admin = () => {
if(localStorage.getItem('position')=='员工'){
return false // 员工无权限
}else{
return true // 经理有权限
}
}
// 实际应用:产品审核只有经理能操作
通过审核
拒绝审核
javascript
// 防止自己删除自己
const openMessageBox = () => {
let id = localStorage.getItem('id') as any as number
if(id == userId.value){
return ElMessage.error('不能删除自己!')
}
// 删除逻辑...
}
Search files...
这个动态路由系统非常巧妙,我来详细分析:
typescript
// 1. 使用 import.meta.glob 批量导入组件
function loadComponent(url: string) {
let Module = import.meta.glob("@/views/**/*.vue")
return Module[`/src/views/${url}.vue`]
}
// 2. 动态添加路由
function compilerMenu(arr: any) {
arr.forEach((item: any) => {
let rts = {
name: item.name,
path: item.path,
component: item.component
}
if (!item.children) {
let path = loadComponent(item.component)
rts.component = path
router.addRoute('menu', rts) // 关键:动态添加到menu父路由下
}
})
}
main.ts
中自动重建路由import.meta.glob
实现懒加载Read file: 后台前端_wz/src/http/index.ts
typescript
// 1. 环境变量配置 - 支持多环境部署
baseURL: import.meta.env.VITE_API_BASEURL
// 2. 业务状态码处理 - 区分HTTP状态码和业务状态码
if(response.data.status||response.data.message){
if(response.data.status==0){
ElMessage.success(response.data.message) // 业务成功
}else{
ElMessage.error(response.data.message) // 业务失败
}
}
// 3. 完善的HTTP错误处理
switch (error.response.status){
case 400: ElMessage.error('请求错误'); break
case 401: ElMessage.error('未授权,请登录'); break
case 403: ElMessage.error('拒绝访问'); break
case 404: ElMessage.error(`请求地址出错: ${error.response.config.url}`); break
case 500: ElMessage.error('服务器内部错误'); break
default: ElMessage.error(`连接出错:${error.response.status}`)
}
Search files...
typescript
// 1. 详细的操作记录 - 记录关键业务操作
const content = `删除了用户${userAccount.value}`
await operationLog(
localStorage.getItem('account'),
localStorage.getItem('name'),
content,
'高级', // 操作等级
'成功' // 操作状态
)
// 2. 操作等级分类
// 高级:删除、审核等重要操作
// 中级:编辑、修改等常规操作
// 低级:查看、搜索等只读操作
// 3. 操作状态跟踪 - 成功/失败都记录
if(res.status==0){
await operationLog(/* 参数 */, '成功')
}else{
await operationLog(/* 参数 */, '失败')
}
Read file: 后台前端_wz/src/hooks/log.ts
typescript
// 1. 高度复用的表格逻辑
const {
tableData,
paginationData,
getFirstPageList,
changePage,
clearLog
} = logHooks('operation') // 传入类型即可复用
// 2. 智能分发API调用
if(logName=='operation'){
res = await getOperationLogLength()
}else{
res = await getLoginLogLength()
}
// 3. 自动化的响应式管理
watch(paginationData, () => {
returnListLength()
getFirstPageList()
}, {immediate: true, deep: true})
首先,这个后台管理系统最突出的亮点在于其多维度权限控制体系的精妙设计。基于现代企业管理的复杂需求,系统不仅仅停留在简单的角色权限划分,而是构建了一套三层递进的权限控制架构。具体而言,系统从部门级菜单权限出发,进而细化到职位级功能权限,最终深入到业务级操作权限,形成了一个立体化的权限管控网络。
更为重要的是,这种设计理念体现了对真实企业场景的深刻理解。由于不同部门具有不同的业务职能,因此系统根据用户所属部门(人事部、产品部、超级管理员)返回相应的路由配置,确保了菜单显示的精准性。然后,在此基础上,系统进一步根据用户职位(经理vs员工)控制具体功能的访问权限,比如产品审核功能仅对经理开放,从而实现了更细粒度的权限管控。
紧接着,系统的动态路由加载机制展现了另一个技术亮点。与传统的静态路由配置不同,该系统采用了基于后端数据驱动的动态路由生成策略。具体来说,当用户登录成功后,后端会根据用户的部门信息返回对应的路由配置数组,然后前端通过import.meta.glob批量导入组件,并使用router.addRoute动态添加到路由表中。
这种设计的巧妙之处在于,它不仅实现了路由的按需加载,提升了应用性能,同时也为系统的扩展性提供了强有力的支撑。换言之,当需要新增角色或调整权限时,开发者只需要修改后端的路由配置,而无需改动前端代码,大大降低了系统维护成本。
从技术实现角度来看,系统展现出了对现代前端开发最佳实践的深刻理解。首先,在状态管理方面,系统采用了Pinia配合持久化插件的方案,确保了用户权限信息在页面刷新后依然有效。其次,在网络请求层面,系统对Axios进行了精心的二次封装,不仅支持环境变量配置以适应多环境部署,还实现了业务状态码与HTTP状态码的分离处理,使得错误处理更加精准和用户友好。
另外,系统在安全性方面的考量也颇为周全。通过JWT令牌的7小时有效期设计,既保证了用户体验的流畅性,又确保了安全风险的可控性。同时,结合bcrypt密码加密存储和CORS跨域配置,系统构建了一个相对完善的安全防护体系。
值得一提的是,系统在提升用户体验方面也做出了诸多创新。通过实现操作日志系统,系统不仅记录了用户的关键操作行为,还根据操作的重要程度进行了等级分类(高级、中级、低级),并且对操作结果(成功/失败)进行了详细追踪。这种设计不仅满足了企业内部的审计需求,也为系统问题排查提供了有力支撑。
进一步地,系统通过自定义Hooks的方式实现了代码逻辑的高度复用。以logHooks为例,通过传入不同的日志类型参数,就能够复用整套表格分页、数据加载、清空操作的逻辑,极大地提升了开发效率。这种抽象能力的展现,体现了开发者对Vue3 Composition API的深度掌握。
从代码质量角度分析,系统全面采用了TypeScript,不仅提供了类型安全保障,还增强了代码的可读性和可维护性。同时,系统采用了Vue3的组合式API,相比于Options API,这种方式能够更好地组织相关逻辑,使得代码结构更加清晰。
此外,系统在项目结构组织方面也体现了良好的工程化思维。后端采用了Router Handler的分层设计,将路由定义与业务逻辑处理分离;前端则通过统一的API管理、组件封装、样式规范等方式,确保了代码的一致性和可维护性。
在性能优化方面,系统通过路由懒加载、组件按需导入等技术手段,有效降低了首屏加载时间。特别是动态路由系统的实现,使得用户只会加载其有权限访问的页面组件,进一步优化了资源利用效率。
最后,从系统的扩展性来看,无论是权限体系的调整、新功能模块的添加,还是新角色的引入,当前的架构设计都能够很好地支撑这些变更需求。这种前瞻性的设计思维,体现了开发者对企业级应用开发的深刻理解。
综合来看,这个后台管理系统不仅在功能实现上满足了企业管理的实际需求,更在技术架构、代码质量、用户体验等多个维度展现了企业级应用开发的最佳实践。通过多维度权限控制、动态路由加载、完善的日志审计、优雅的错误处理等核心特性的有机结合,系统构建了一个既安全可靠又灵活可扩展的技术解决方案,为类似项目的开发提供了极具参考价值的技术范例。