46 KiB
46 KiB
ThinkPHP8 + Layui/HTML 前后端分离权限管理系统设计文档
1. 概述
1.1 项目背景
随着Web应用复杂度提升,权限管理成为系统安全的基石。本文档详细设计基于ThinkPHP8(后端)和Layui/HTML(前端)实现的前后端分离权限管理系统,采用RBAC(基于角色的访问控制)模型,实现细粒度权限控制。
1.2 设计目标
- 实现完整的RBAC权限管理模型
- 支持菜单级、按钮级、API接口级权限控制
- 采用JWT进行无状态认证
- 前后端完全分离,API接口标准化
- 高性能、高可扩展性、安全可靠
- 兼容主流浏览器,适配PC端管理后台
1.3 技术栈
- 后端: ThinkPHP 8.0 + MySQL 8.0 + Redis
- 前端: Layui 2.7 + 原生HTML/JavaScript + Axios
- 认证: JWT (JSON Web Token)
- 部署: Nginx + PHP 8.1
2. 系统架构设计
2.1 整体架构
+-------------------+ +-------------------+
| 前端层 | | 后端层 |
| (Layui + HTML/JS) |<---->| (ThinkPHP 8.0) |
+-------------------+ HTTP +-------------------+
| ↑ | ↑
| | API调用 | | 业务处理
↓ | ↓ |
+-------------------+ +-------------------+
| 静态资源 | | 服务层 |
| (CDN/本地存储) | | (Service/Logic) |
+-------------------+ +-------------------+
| ↑
| | 数据操作
↓ |
+-------------------+
| 数据层 |
| (MySQL + Redis) |
+-------------------+
2.2 权限控制流程
- 用户登录获取JWT令牌
- 前端存储令牌并在每次请求携带
- 后端中间件验证令牌有效性
- 根据用户角色/权限决定是否放行请求
- 前端根据权限数据动态渲染菜单和按钮
3. 数据库设计
3.1 数据表结构
3.1.1 管理员表 (admin)
| 字段名 | 类型 | 描述 |
|---|---|---|
| id | bigint(20) unsigned | 主键ID |
| username | varchar(50) | 用户名 |
| password | varchar(100) | 密码(BCRYPT加密) |
| nickname | varchar(50) | 昵称 |
| avatar | varchar(255) | 头像 |
| varchar(100) | 邮箱 | |
| mobile | varchar(20) | 手机号 |
| last_login_ip | varchar(50) | 最后登录IP |
| last_login_time | datetime | 最后登录时间 |
| status | tinyint(1) | 状态(0:禁用,1:正常) |
| create_time | datetime | 创建时间 |
| update_time | datetime | 更新时间 |
3.1.2 角色表 (role)
| 字段名 | 类型 | 描述 |
|---|---|---|
| id | bigint(20) unsigned | 主键ID |
| name | varchar(50) | 角色名称 |
| key | varchar(50) | 角色标识 |
| description | varchar(255) | 角色描述 |
| sort | int(11) | 排序 |
| status | tinyint(1) | 状态(0:禁用,1:正常) |
| create_time | datetime | 创建时间 |
| update_time | datetime | 更新时间 |
3.1.3 权限表 (permission)
| 字段名 | 类型 | 描述 |
|---|---|---|
| id | bigint(20) unsigned | 主键ID |
| name | varchar(50) | 权限名称 |
| key | varchar(100) | 权限标识 |
| type | tinyint(1) | 类型(1:目录,2:菜单,3:按钮,4:API) |
| parent_id | bigint(20) | 父级ID |
| path | varchar(100) | 前端路由路径 |
| component | varchar(100) | 前端组件路径 |
| icon | varchar(50) | 图标 |
| sort | int(11) | 排序 |
| status | tinyint(1) | 状态(0:禁用,1:正常) |
| api_url | varchar(255) | API接口路径 |
| api_method | varchar(20) | API请求方法(GET/POST/PUT/DELETE) |
| create_time | datetime | 创建时间 |
| update_time | datetime | 更新时间 |
3.1.4 角色-管理员关联表 (role_admin)
| 字段名 | 类型 | 描述 |
|---|---|---|
| role_id | bigint(20) unsigned | 角色ID |
| admin_id | bigint(20) unsigned | 管理员ID |
3.1.5 角色-权限关联表 (role_permission)
| 字段名 | 类型 | 描述 |
|---|---|---|
| role_id | bigint(20) unsigned | 角色ID |
| permission_id | bigint(20) unsigned | 权限ID |
4. 后端实现方案 (ThinkPHP8)
4.1 JWT认证模块
4.1.1 JWT配置 (config/jwt.php)
<?php
return [
'secret_key' => 'your_strong_secret_key_here_123456', // 需要替换成强密钥
'expire' => 7200, // 2小时
'refresh_expire' => 2592000, // 30天
'header_name' => 'Authorization',
'header_prefix' => 'Bearer',
'algorithm' => 'HS256',
];
4.1.2 JWT服务类 (app/service/JwtService.php)
<?php
declare(strict_types=1);
namespace app\service;
use Firebase\JWT\JWT;
use Firebase\JWT\ExpiredException;
use think\Exception;
class JwtService
{
protected $config;
public function __construct()
{
$this->config = config('jwt');
}
/**
* 生成JWT令牌
* @param array $payload 用户信息
* @param bool $rememberMe 是否记住登录
* @return string
*/
public function generateToken(array $payload, bool $rememberMe = false): string
{
$time = time();
$expire = $rememberMe ? $this->config['refresh_expire'] : $this->config['expire'];
$token = [
'iat' => $time, // 签发时间
'exp' => $time + $expire, // 过期时间
'data' => $payload
];
return JWT::encode($token, $this->config['secret_key'], $this->config['algorithm']);
}
/**
* 验证JWT令牌
* @param string $token
* @return array
* @throws Exception
*/
public function verifyToken(string $token): array
{
try {
$decoded = JWT::decode($token, $this->config['secret_key'], [$this->config['algorithm']]);
return (array)$decoded->data;
} catch (ExpiredException $e) {
throw new Exception('登录已过期,请重新登录', 401);
} catch (\Exception $e) {
throw new Exception('无效的访问凭证', 401);
}
}
/**
* 刷新令牌
* @param string $token
* @return string
* @throws Exception
*/
public function refreshToken(string $token): string
{
$data = $this->verifyToken($token);
return $this->generateToken((array)$data, true);
}
}
4.2 RBAC权限模型
4.2.1 模型定义
管理员模型 (app/model/Admin.php)
<?php
declare(strict_types=1);
namespace app\model;
use think\Model;
use think\model\relation\BelongsToMany;
class Admin extends Model
{
// 关联角色
public function roles(): BelongsToMany
{
return $this->belongsToMany(Role::class, RoleAdmin::class, 'role_id', 'admin_id');
}
// 隐藏字段
protected $hidden = ['password'];
// 密码写入时加密
public function setPasswordAttr($value)
{
return password_hash($value, PASSWORD_BCRYPT);
}
}
角色模型 (app/model/Role.php)
<?php
declare(strict_types=1);
namespace app\model;
use think\Model;
use think\model\relation\BelongsToMany;
class Role extends Model
{
// 关联管理员
public function admins(): BelongsToMany
{
return $this->belongsToMany(Admin::class, RoleAdmin::class, 'admin_id', 'role_id');
}
// 关联权限
public function permissions(): BelongsToMany
{
return $this->belongsToMany(Permission::class, RolePermission::class, 'permission_id', 'role_id');
}
}
权限模型 (app/model/Permission.php)
<?php
declare(strict_types=1);
namespace app\model;
use think\Model;
use think\model\relation\BelongsToMany;
class Permission extends Model
{
// 关联角色
public function roles(): BelongsToMany
{
return $this->belongsToMany(Role::class, RolePermission::class, 'role_id', 'permission_id');
}
// 获取子权限
public function children()
{
return $this->where('parent_id', $this->id)->order('sort', 'asc')->select();
}
}
4.2.2 权限验证中间件 (app/middleware/Auth.php)
<?php
declare(strict_types=1);
namespace app\middleware;
use app\service\JwtService;
use Closure;
use think\Request;
use think\Response;
use app\model\Admin;
use app\model\RolePermission;
use app\model\Permission;
use think\facade\Cache;
class Auth
{
// 无需验证的路由
protected $except = [
'api/login',
'api/captcha'
];
public function handle(Request $request, Closure $next)
{
// 检查是否在白名单
$path = $request->pathinfo();
foreach ($this->except as $except) {
if (strpos($path, $except) !== false) {
return $next($request);
}
}
// 验证Token
$token = $request->header('Authorization');
if (!$token) {
return json(['code' => 401, 'msg' => '未授权访问,请登录'])->status(401);
}
$token = str_replace($this->getConfig('header_prefix') . ' ', '', $token);
try {
$jwtService = new JwtService();
$userData = $jwtService->verifyToken($token);
// 写入请求
$request->userData = $userData;
// 超级管理员无需验证API权限
if (in_array($userData['id'], config('system.super_admin_ids', []))) {
return $next($request);
}
// 权限验证
if (!$this->checkPermission($request, $userData)) {
return json(['code' => 403, 'msg' => '没有操作权限'])->status(403);
}
return $next($request);
} catch (\Exception $e) {
return json(['code' => $e->getCode() ?: 401, 'msg' => $e->getMessage()])->status(401);
}
}
/**
* 检查权限
*/
protected function checkPermission(Request $request, array $userData): bool
{
$method = $request->method();
$path = $request->pathinfo();
// 从缓存或数据库获取当前用户的所有API权限
$permissions = $this->getUserApiPermissions($userData['id']);
// API权限格式:method:path,如:GET:api/admin/user
$requiredPermission = strtoupper($method) . ':' . ltrim($path, '/');
return in_array($requiredPermission, $permissions);
}
/**
* 获取用户API权限
*/
protected function getUserApiPermissions(int $adminId): array
{
// 使用Redis缓存减少数据库查询
$cacheKey = 'admin_api_permissions:' . $adminId;
$redis = Cache::store('redis');
if ($redis->has($cacheKey)) {
return $redis->get($cacheKey);
}
// 获取用户角色ID
$admin = Admin::with(['roles'])->find($adminId);
if (!$admin) {
return [];
}
$roleIds = $admin->roles->column('id');
if (empty($roleIds)) {
return [];
}
// 获取权限
$permissions = RolePermission::whereIn('role_id', $roleIds)
->with(['permission' => function($query) {
$query->where('type', 4)->where('status', 1);
}])
->select()
->toArray();
$apiPermissions = [];
foreach ($permissions as $item) {
if (isset($item['permission']) && $item['permission']) {
$permission = $item['permission'];
$apiPermissions[] = strtoupper($permission['api_method']) . ':' . ltrim($permission['api_url'], '/');
}
}
// 缓存2小时
$redis->set($cacheKey, $apiPermissions, 7200);
return $apiPermissions;
}
protected function getConfig($name)
{
return config('jwt.' . $name);
}
}
4.2.3 登录控制器 (app/controller/api/Login.php)
<?php
declare(strict_types=1);
namespace app\controller\api;
use app\BaseController;
use app\model\Admin;
use app\service\JwtService;
use think\facade\Cache;
use think\facade\Validate;
use think\captcha\Captcha;
class Login extends BaseController
{
// 无需登录验证
protected $noAuth = ['index', 'captcha'];
/**
* 生成验证码
*/
public function captcha()
{
$captcha = new Captcha((array)config('captcha'));
return $captcha->entry();
}
/**
* 用户登录
*/
public function index()
{
$data = $this->request->post();
// 验证输入
$validate = Validate::make([
'username|用户名' => 'require',
'password|密码' => 'require',
'captcha|验证码' => 'require|captcha'
]);
if (!$validate->check($data)) {
return json(['code' => 400, 'msg' => $validate->getError()]);
}
// 查询用户
$admin = Admin::where('username', $data['username'])->find();
if (!$admin || !password_verify($data['password'], $admin->password)) {
return json(['code' => 401, 'msg' => '用户名或密码错误']);
}
if ($admin->status != 1) {
return json(['code' => 403, 'msg' => '账号已被禁用']);
}
// 生成Token
$jwtService = new JwtService();
$isSuper = in_array($admin->id, config('system.super_admin_ids', []));
$payload = [
'id' => $admin->id,
'username' => $admin->username,
'nickname' => $admin->nickname,
'is_super' => $isSuper ? 1 : 0
];
$rememberMe = isset($data['remember']) && $data['remember'];
$token = $jwtService->generateToken($payload, $rememberMe);
// 记录登录信息
$admin->last_login_time = date('Y-m-d H:i:s');
$admin->last_login_ip = $this->request->ip();
$admin->save();
// 清除权限缓存
Cache::store('redis')->rm('admin_api_permissions:' . $admin->id);
Cache::store('redis')->rm('admin_menu_permissions:' . $admin->id);
return json([
'code' => 200,
'msg' => '登录成功',
'data' => [
'token' => $token,
'expires_in' => $rememberMe ? config('jwt.refresh_expire') : config('jwt.expire'),
'user_info' => [
'id' => $admin->id,
'username' => $admin->username,
'nickname' => $admin->nickname,
'avatar' => $admin->avatar ?: '/static/admin/images/default_avatar.jpg',
'is_super' => $isSuper
]
]
]);
}
/**
* 获取用户信息和权限
*/
public function info()
{
$adminId = $this->request->userData['id'];
$isSuper = $this->request->userData['is_super'] == 1;
$cacheKey = 'admin_menu_permissions:' . $adminId;
$redis = Cache::store('redis');
if ($redis->has($cacheKey)) {
$data = $redis->get($cacheKey);
return json(['code' => 200, 'msg' => 'success', 'data' => $data]);
}
// 获取用户角色
$admin = Admin::with(['roles'])->find($adminId);
$roles = $admin->roles->toArray();
$roleKeys = array_column($roles, 'key');
// 获取菜单和按钮权限
if ($isSuper) {
// 超级管理员获取所有权限
$permissions = \app\model\Permission::where('status', 1)
->order('sort', 'asc')
->select()
->toArray();
} else {
$roleIds = array_column($roles, 'id');
$permissions = \app\model\RolePermission::whereIn('role_id', $roleIds)
->with(['permission' => function($query) {
$query->where('status', 1)->order('sort', 'asc');
}])
->select()
->hidden(['pivot'])
->toArray();
$permissions = array_column($permissions, 'permission');
}
// 构建菜单树
$menus = $this->buildMenuTree($permissions);
// 提取按钮和API权限
$buttonPermissions = [];
$apiPermissions = [];
foreach ($permissions as $permission) {
if ($permission['type'] == 3) { // 按钮权限
$buttonPermissions[] = $permission['key'];
} elseif ($permission['type'] == 4) { // API权限
$apiPermissions[] = strtoupper($permission['api_method']) . ':' . ltrim($permission['api_url'], '/');
}
}
$data = [
'roles' => $roleKeys,
'menus' => $menus,
'permissions' => [
'buttons' => array_unique($buttonPermissions),
'apis' => array_unique($apiPermissions)
],
'user_info' => [
'id' => $admin->id,
'username' => $admin->username,
'nickname' => $admin->nickname,
'avatar' => $admin->avatar ?: '/static/admin/images/default_avatar.jpg',
'is_super' => $isSuper
]
];
// 缓存1小时
$redis->set($cacheKey, $data, 3600);
return json(['code' => 200, 'msg' => 'success', 'data' => $data]);
}
/**
* 退出登录
*/
public function logout()
{
$adminId = $this->request->userData['id'];
// 清除权限缓存
Cache::store('redis')->rm('admin_api_permissions:' . $adminId);
Cache::store('redis')->rm('admin_menu_permissions:' . $adminId);
return json(['code' => 200, 'msg' => '退出成功']);
}
/**
* 构建菜单树
*/
protected function buildMenuTree($permissions, $parentId = 0)
{
$tree = [];
foreach ($permissions as $permission) {
if ($permission['parent_id'] == $parentId && in_array($permission['type'], [1, 2])) {
$children = $this->buildMenuTree($permissions, $permission['id']);
if (!empty($children)) {
$permission['children'] = $children;
}
$tree[] = $permission;
}
}
return $tree;
}
}
4.3 API接口设计
4.3.1 接口规范
-
请求方式: RESTful 风格
-
返回格式: JSON
-
状态码:
- 200: 成功
- 400: 请求参数错误
- 401: 未授权/登录过期
- 403: 没有权限
- 404: 资源不存在
- 500: 服务器内部错误
-
响应结构:
{
"code": 200,
"msg": "成功",
"data": {
// 业务数据
}
}
4.3.2 核心接口列表
| 接口路径 | 方法 | 描述 | 权限标识 |
|---|---|---|---|
| /api/login | POST | 用户登录 | 无需权限 |
| /api/captcha | GET | 获取验证码 | 无需权限 |
| /api/login/info | GET | 获取用户信息和权限 | 无需额外权限 |
| /api/login/logout | POST | 退出登录 | 无需额外权限 |
| /api/admin/menu | GET | 获取菜单列表 | system:menu:list |
| /api/admin/role | GET | 获取角色列表 | system:role:list |
| /api/admin/role | POST | 添加角色 | system:role:add |
| /api/admin/role/:id | PUT | 更新角色 | system:role:edit |
| /api/admin/role/:id | DELETE | 删除角色 | system:role:delete |
| /api/admin/permission | GET | 获取权限列表 | system:permission:list |
| /api/admin/admin | GET | 获取管理员列表 | system:admin:list |
| /api/admin/admin | POST | 添加管理员 | system:admin:add |
| /api/admin/admin/:id | PUT | 更新管理员 | system:admin:edit |
| /api/admin/admin/:id | DELETE | 删除管理员 | system:admin:delete |
5. 前端实现方案 (Layui + HTML)
5.1 项目结构
static/
├── admin/
│ ├── css/
│ │ ├── common.css # 全局样式
│ │ └── theme.css # 主题样式
│ ├── js/
│ │ ├── common.js # 公共方法
│ │ ├── http.js # 请求封装
│ │ ├── permission.js # 权限控制
│ │ └── menu.js # 菜单生成
│ ├── lib/
│ │ ├── layui/ # Layui框架
│ │ └── axios/ # Axios库
│ └── views/
│ ├── dashboard.html # 首页
│ ├── login.html # 登录页
│ ├── system/
│ │ ├── role.html # 角色管理
│ │ ├── admin.html # 管理员管理
│ │ └── menu.html # 菜单管理
│ └── ...
└── upload/ # 上传文件目录
5.2 核心功能实现
5.2.1 请求封装 (static/admin/js/http.js)
// Axios实例
const http = axios.create({
baseURL: '/api',
timeout: 10000,
headers: {
'Content-Type': 'application/json;charset=utf-8'
}
});
// 请求拦截器
http.interceptors.request.use(config => {
// 从localStorage获取token
const token = localStorage.getItem('admin_token');
if (token) {
config.headers.Authorization = 'Bearer ' + token;
}
// 显示加载层
if (!config.hideloading) {
layer.load(2);
}
return config;
}, error => {
return Promise.reject(error);
});
// 响应拦截器
http.interceptors.response.use(response => {
layer.closeAll('loading');
const res = response.data;
if (res.code === 200) {
return res.data;
} else if (res.code === 401) {
// Token过期或无效
layer.msg(res.msg || '登录已失效', { icon: 5, time: 1500 }, function() {
localStorage.removeItem('admin_token');
localStorage.removeItem('admin_info');
localStorage.removeItem('admin_routes');
localStorage.removeItem('admin_permissions');
window.location.href = '/static/admin/views/login.html';
});
return Promise.reject(new Error(res.msg || '请求失败'));
} else {
layer.msg(res.msg || '请求失败', { icon: 5 });
return Promise.reject(new Error(res.msg || '请求失败'));
}
}, error => {
layer.closeAll('loading');
if (error.message.includes('timeout')) {
layer.msg('请求超时,请重试', { icon: 5 });
} else if (error.message.includes('Network Error')) {
layer.msg('网络异常,请检查网络连接', { icon: 5 });
} else {
layer.msg('请求异常: ' + error.message, { icon: 5 });
}
return Promise.reject(error);
});
// 封装常用方法
const $http = {
get: (url, params = {}, config = {}) => http.get(url, { params, ...config }),
post: (url, data = {}, config = {}) => http.post(url, data, config),
put: (url, data = {}, config = {}) => http.put(url, data, config),
delete: (url, config = {}) => http.delete(url, config),
download: (url, params = {}, filename = 'download') => {
http.post(url, params, { responseType: 'blob', hideloading: true })
.then(res => {
const blob = new Blob([res]);
const link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = filename;
link.click();
window.URL.revokeObjectURL(link.href);
});
}
};
// 挂载到window
window.$http = $http;
5.2.2 权限控制 (static/admin/js/permission.js)
// 权限控制类
const Permission = {
// 按钮权限
hasButton: function(permissionKey) {
const permissions = JSON.parse(localStorage.getItem('admin_permissions') || '[]');
return permissions.includes(permissionKey) || this.isSuperAdmin();
},
// 检查多个按钮权限,满足一个即返回true
hasAnyButton: function(permissionKeys) {
if (!Array.isArray(permissionKeys)) return false;
return permissionKeys.some(key => this.hasButton(key));
},
// 检查所有按钮权限,全部满足才返回true
hasAllButton: function(permissionKeys) {
if (!Array.isArray(permissionKeys)) return false;
return permissionKeys.every(key => this.hasButton(key));
},
// 是否为超级管理员
isSuperAdmin: function() {
const adminInfo = JSON.parse(localStorage.getItem('admin_info') || '{}');
return adminInfo.is_super === true;
},
// 初始化权限
init: function() {
// 从本地存储获取权限
this.permissions = JSON.parse(localStorage.getItem('admin_permissions') || '[]');
this.routes = JSON.parse(localStorage.getItem('admin_routes') || '[]');
this.adminInfo = JSON.parse(localStorage.getItem('admin_info') || '{}');
// 超级管理员拥有所有权限
if (this.isSuperAdmin()) {
this.permissions = ['*'];
}
},
// 渲染带权限控制的元素
render: function() {
// 隐藏无权限的按钮
document.querySelectorAll('[permission]').forEach(el => {
const permissionKey = el.getAttribute('permission');
if (!this.hasButton(permissionKey)) {
el.style.display = 'none';
}
});
// 处理无权限提示
document.querySelectorAll('[permission-tip]').forEach(el => {
el.addEventListener('click', (e) => {
const permissionKey = el.getAttribute('permission');
if (!this.hasButton(permissionKey)) {
e.preventDefault();
e.stopPropagation();
layer.msg('您没有操作权限', { icon: 5 });
}
});
});
}
};
// 初始化
Permission.init();
// 挂载到window
window.Permission = Permission;
5.2.3 动态菜单生成 (static/admin/js/menu.js)
const Menu = {
// 生成菜单
render: function(containerId) {
const container = document.getElementById(containerId);
if (!container) return;
const menus = JSON.parse(localStorage.getItem('admin_menus') || '[]');
container.innerHTML = this.buildMenuHTML(menus);
// 绑定点击事件
this.bindMenuEvents();
},
// 递归构建菜单HTML
buildMenuHTML: function(menus) {
if (!menus || menus.length === 0) return '<li class="layui-menu-body-title">暂无菜单</li>';
let html = '';
menus.forEach(menu => {
// 菜单类型:1-目录 2-菜单 3-按钮 4-API
if (menu.type === 1) {
// 目录
const hasChildren = menu.children && menu.children.length > 0;
html += `
<li class="layui-menu-body-divider"></li>
<li class="layui-menu-body-title">
<i class="layui-icon ${menu.icon || 'layui-icon-list'}"></i>
${menu.name}
</li>
${hasChildren ? this.buildMenuHTML(menu.children) : ''}
`;
} else if (menu.type === 2) {
// 菜单项
const hasChildren = menu.children && menu.children.length > 0;
const menuItemId = 'menu-' + menu.id;
html += `
<li class="layui-menu-body-item ${hasChildren ? 'layui-menu-body-parent' : ''}" id="${menuItemId}" data-url="${menu.path || ''}" data-id="${menu.id}">
<a href="javascript:;">
${menu.icon ? `<i class="layui-icon ${menu.icon}"></i>` : ''}
<span>${menu.name}</span>
${hasChildren ? '<span class="layui-menu-body-arrow"></span>' : ''}
</a>
${hasChildren ? `<ul class="layui-menu-body-child">${this.buildMenuHTML(menu.children)}</ul>` : ''}
</li>
`;
}
});
return html;
},
// 绑定菜单事件
bindMenuEvents: function() {
// 菜单项点击
document.querySelectorAll('.layui-menu-body-item:not(.layui-menu-body-parent)').forEach(item => {
item.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const url = item.getAttribute('data-url');
const id = item.getAttribute('data-id');
if (url) {
this.openTab(id, item.querySelector('span').textContent, url);
this.setActiveMenu(item);
}
});
});
// 有子菜单的父级点击
document.querySelectorAll('.layui-menu-body-parent').forEach(item => {
const arrow = item.querySelector('.layui-menu-body-arrow');
if (arrow) {
arrow.addEventListener('click', (e) => {
e.stopPropagation();
const childMenu = item.querySelector('.layui-menu-body-child');
if (childMenu) {
childMenu.style.display = childMenu.style.display === 'block' ? 'none' : 'block';
arrow.classList.toggle('open');
}
});
}
});
},
// 打开标签页
openTab: function(id, title, url) {
const tabContainer = document.querySelector('.layui-tab[lay-filter="admin-tabs"]');
const tabContent = document.querySelector('.layui-tab-content');
const tabExists = document.querySelector(`.layui-tab[lay-filter="admin-tabs"] .layui-tab-title li[lay-id="${id}"]`);
if (!tabExists) {
// 添加标签
const tabTitle = `<li lay-id="${id}" class="layui-this">${title}</li>`;
const tabItem = `<div class="layui-tab-item layui-show" id="tab-content-${id}"><iframe src="${url}" frameborder="0" class="admin-iframe"></iframe></div>`;
tabContainer.querySelector('.layui-tab-title').insertAdjacentHTML('beforeend', tabTitle);
tabContent.insertAdjacentHTML('beforeend', tabItem);
// 重新渲染tab
layui.element.render('tab');
} else {
// 激活已有标签
tabContainer.querySelector(`.layui-tab-title li[lay-id="${id}"]`).click();
}
},
// 设置激活菜单
setActiveMenu: function(item) {
// 移除所有active
document.querySelectorAll('.layui-menu-body-item.active').forEach(el => {
el.classList.remove('active');
});
// 添加active
item.classList.add('active');
// 展开父级菜单
let parent = item.parentElement;
while (parent && parent.classList.contains('layui-menu-body-child')) {
parent.style.display = 'block';
const arrow = parent.parentElement.querySelector('.layui-menu-body-arrow');
if (arrow) arrow.classList.add('open');
parent = parent.parentElement.parentElement;
}
},
// 初始化
init: function() {
// 从本地存储获取菜单
this.menus = JSON.parse(localStorage.getItem('admin_menus') || '[]');
}
};
// 初始化
Menu.init();
// 挂载到window
window.Menu = Menu;
5.3 登录页面实现 (static/admin/views/login.html)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>系统登录 - 权限管理系统</title>
<link rel="stylesheet" href="/static/admin/lib/layui/css/layui.css">
<link rel="stylesheet" href="/static/admin/css/common.css">
<style>
.login-container {
width: 100%;
height: 100vh;
background: #393D49;
display: flex;
justify-content: center;
align-items: center;
}
.login-box {
width: 400px;
padding: 30px;
background: #fff;
border-radius: 4px;
box-shadow: 0 0 10px rgba(0,0,0,0.3);
}
.login-header {
text-align: center;
margin-bottom: 30px;
}
.login-header h1 {
font-size: 24px;
color: #333;
margin: 10px 0;
}
.login-form .layui-form-item {
position: relative;
}
.login-form .layui-icon {
position: absolute;
left: 10px;
top: 10px;
font-size: 16px;
color: #999;
}
.login-form .layui-input {
padding-left: 30px;
}
.captcha-box {
display: flex;
align-items: center;
}
.captcha-box img {
height: 38px;
cursor: pointer;
margin-left: 10px;
}
.login-footer {
margin-top: 20px;
text-align: center;
}
</style>
</head>
<body>
<div class="login-container">
<div class="login-box">
<div class="login-header">
<h1>权限管理系统</h1>
<p>LOGIN TO YOUR ACCOUNT</p>
</div>
<form class="layui-form login-form" id="login-form">
<div class="layui-form-item">
<i class="layui-icon layui-icon-username"></i>
<input type="text" name="username" required lay-verify="required" placeholder="请输入用户名" autocomplete="off" class="layui-input">
</div>
<div class="layui-form-item">
<i class="layui-icon layui-icon-password"></i>
<input type="password" name="password" required lay-verify="required" placeholder="请输入密码" autocomplete="off" class="layui-input">
</div>
<div class="layui-form-item">
<div class="captcha-box">
<input type="text" name="captcha" required lay-verify="required" placeholder="请输入验证码" autocomplete="off" class="layui-input" style="width: 160px;">
<img src="/api/captcha" alt="验证码" id="captcha-img" title="点击刷新">
</div>
</div>
<div class="layui-form-item">
<input type="checkbox" name="remember" title="记住登录状态" lay-skin="primary">
</div>
<div class="layui-form-item">
<button class="layui-btn layui-btn-fluid layui-btn-normal" lay-submit lay-filter="login">登 入</button>
</div>
<div class="login-footer">
<p>© 2023 权限管理系统</p>
</div>
</form>
</div>
</div>
<script src="/static/admin/lib/layui/layui.js"></script>
<script src="/static/admin/lib/axios/axios.min.js"></script>
<script>
layui.use(['form', 'layer'], function() {
const form = layui.form;
const layer = layui.layer;
// 刷新验证码
document.getElementById('captcha-img').addEventListener('click', function() {
this.src = '/api/captcha?t=' + new Date().getTime();
});
// 登录提交
form.on('submit(login)', function(data) {
const field = data.field;
// 提交登录
axios.post('/api/login', field)
.then(res => {
if (res.code === 200) {
// 保存token
localStorage.setItem('admin_token', res.data.token);
localStorage.setItem('admin_info', JSON.stringify(res.data.user_info));
layer.msg('登录成功', { icon: 1, time: 1000 }, function() {
window.location.href = '/static/admin/views/dashboard.html';
});
} else {
layer.msg(res.msg || '登录失败', { icon: 5 });
// 刷新验证码
document.getElementById('captcha-img').src = '/api/captcha?t=' + new Date().getTime();
}
})
.catch(error => {
layer.msg('登录异常: ' + (error.response?.data?.msg || error.message), { icon: 5 });
// 刷新验证码
document.getElementById('captcha-img').src = '/api/captcha?t=' + new Date().getTime();
});
return false;
});
});
</script>
</body>
</html>
5.4 后台主框架 (static/admin/views/dashboard.html)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>权限管理系统</title>
<link rel="stylesheet" href="/static/admin/lib/layui/css/layui.css">
<link rel="stylesheet" href="/static/admin/css/common.css">
<style>
body {
margin: 0;
padding: 0;
height: 100vh;
overflow: hidden;
}
.admin-layout {
display: flex;
height: 100vh;
}
.admin-side {
width: 220px;
height: 100%;
background: #393D49;
transition: all 0.3s;
overflow-y: auto;
}
.admin-side-collapsed {
width: 60px;
}
.admin-header {
height: 60px;
background: #292C31;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 15px;
color: #fff;
}
.admin-logo {
font-size: 20px;
font-weight: bold;
color: #fff;
}
.admin-logo-collapsed {
display: none;
}
.admin-header-right {
display: flex;
align-items: center;
}
.admin-header-right img {
width: 35px;
height: 35px;
border-radius: 50%;
cursor: pointer;
}
.admin-header-right .username {
margin-left: 10px;
color: #fff;
cursor: pointer;
}
.admin-menu {
height: calc(100% - 60px);
overflow-y: auto;
}
.layui-menu-body {
width: 100%;
}
.admin-main {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.admin-tabs {
padding: 0;
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.layui-tab-content {
flex: 1;
overflow: hidden;
}
.admin-iframe {
width: 100%;
height: 100%;
border: none;
}
.layui-menu-body-item.active,
.layui-menu-body-item:hover {
background-color: #4E5465;
}
.layui-menu-body-item.active > a {
color: #009688;
}
.layui-menu-body-title {
color: #ccc;
padding: 10px 15px;
font-size: 14px;
}
.layui-menu-body-divider {
height: 1px;
background: #4E5465;
margin: 5px 0;
}
.layui-menu-body-arrow {
position: absolute;
right: 15px;
transition: transform 0.3s;
}
.layui-menu-body-arrow.open {
transform: rotate(90deg);
}
.layui-menu-body-child {
padding-left: 20px;
display: none;
}
.layui-menu-body-parent:hover .layui-menu-body-child {
display: block;
}
</style>
</head>
<body>
<div class="admin-layout">
<!-- 侧边栏 -->
<div class="admin-side" id="admin-side">
<div class="admin-header">
<div class="admin-logo">权限系统</div>
<div class="admin-logo-collapsed">PS</div>
<div class="admin-header-right">
<i class="layui-icon layui-icon-spread-left" id="toggle-side"></i>
</div>
</div>
<div class="admin-menu">
<ul class="layui-menu-body" id="admin-menu"></ul>
</div>
</div>
<!-- 主内容区 -->
<div class="admin-main">
<!-- 标签页 -->
<div class="admin-tabs">
<div class="layui-tab" lay-filter="admin-tabs" lay-allowClose="true">
<ul class="layui-tab-title">
<li class="layui-this" lay-id="home">首页</li>
</ul>
<div class="layui-tab-content">
<div class="layui-tab-item layui-show">
<iframe src="/static/admin/views/home.html" frameborder="0" class="admin-iframe"></iframe>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="/static/admin/lib/layui/layui.js"></script>
<script src="/static/admin/lib/axios/axios.min.js"></script>
<script src="/static/admin/js/http.js"></script>
<script src="/static/admin/js/permission.js"></script>
<script src="/static/admin/js/menu.js"></script>
<script>
layui.use(['element', 'layer'], function() {
const element = layui.element;
const layer = layui.layer;
// 检查登录状态
const token = localStorage.getItem('admin_token');
if (!token) {
layer.msg('请先登录', { icon: 5, time: 1500 }, function() {
window.location.href = '/static/admin/views/login.html';
});
return;
}
// 获取用户信息和权限
$http.get('/api/login/info')
.then(data => {
// 保存到本地存储
localStorage.setItem('admin_info', JSON.stringify(data.user_info));
localStorage.setItem('admin_menus', JSON.stringify(data.menus));
localStorage.setItem('admin_permissions', JSON.stringify(data.permissions.buttons));
// 渲染菜单
Menu.render('admin-menu');
// 检查权限
Permission.init();
})
.catch(error => {
console.error('获取用户信息失败:', error);
layer.msg('获取用户信息失败,请重新登录', { icon: 5, time: 1500 }, function() {
localStorage.removeItem('admin_token');
window.location.href = '/static/admin/views/login.html';
});
});
// 切换侧边栏
document.getElementById('toggle-side').addEventListener('click', function() {
const side = document.getElementById('admin-side');
const logo = document.querySelector('.admin-logo');
const logoCollapsed = document.querySelector('.admin-logo-collapsed');
side.classList.toggle('admin-side-collapsed');
if (side.classList.contains('admin-side-collapsed')) {
logo.style.display = 'none';
logoCollapsed.style.display = 'block';
} else {
logo.style.display = 'block';
logoCollapsed.style.display = 'none';
}
});
// 退出登录
document.querySelector('.username').addEventListener('click', function() {
layer.confirm('确定要退出登录吗?', { icon: 3, title: '退出登录' }, function(index) {
$http.post('/api/login/logout')
.then(() => {
localStorage.removeItem('admin_token');
localStorage.removeItem('admin_info');
localStorage.removeItem('admin_routes');
localStorage.removeItem('admin_permissions');
layer.close(index);
layer.msg('已退出', { icon: 1, time: 1000 }, function() {
window.location.href = '/static/admin/views/login.html';
});
})
.catch(() => {
layer.close(index);
});
});
});
// 监听tab切换
element.on('tab(admin-tabs)', function(data) {
console.log('切换到tab:', this.getAttribute('lay-id'));
});
// 监听tab关闭
element.on('tabDelete(admin-tabs)', function(data) {
console.log('删除tab:', this.getAttribute('lay-id'));
});
});
</script>
</body>
</html>
6. 安全设计
6.1 认证安全
- JWT令牌: 使用HS256算法签名,设置合理过期时间
- HTTPS: 生产环境强制使用HTTPS
- 密码安全: 使用BCRYPT算法加密存储,每次登录更新salt
- 验证码: 登录时验证码防护暴力破解
- 登录限制: 同一IP频繁失败登录临时锁定
6.2 权限安全
- 最小权限原则: 每个角色只分配必要权限
- 权限缓存: 使用Redis缓存权限,减少数据库查询,提高性能
- 权限变更: 权限变更时清除相关缓存
- 接口验证: 后端每次请求验证权限,不依赖前端控制
6.3 数据安全
- 参数验证: 所有API接口进行严格参数验证
- SQL防注入: 使用ORM和参数绑定,避免SQL注入
- XSS防护: 前后端对用户输入进行过滤和转义
- CSRF防护: JWT不依赖cookie,天然防CSRF
6.4 其他安全措施
- 操作日志: 记录关键操作日志,包括操作人、时间、IP、操作内容
- 敏感操作二次验证: 如修改密码、删除重要数据需要二次验证
- IP白名单: 后台管理可配置IP白名单
- 定期安全审计: 定期进行安全漏洞扫描和审计
7. 部署与维护
7.1 服务器配置
- Web服务器: Nginx 1.18+
- PHP版本: PHP 8.1+
- 数据库: MySQL 8.0+
- 缓存: Redis 6.0+
- 运行内存: 建议2GB+
7.2 Nginx配置示例
server {
listen 80;
server_name admin.example.com;
root /var/www/permission-system/public;
index index.php index.html;
# 前端静态资源
location ~ ^/static/ {
expires 30d;
access_log off;
}
# API接口
location ~ ^/api/ {
try_files $uri $uri/ /index.php?$query_string;
}
# 后台页面
location ~ \.html$ {
try_files $uri $uri/ /static/admin/views/dashboard.html;
}
# PHP处理
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
}
# 404处理
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# 日志配置
access_log /var/log/nginx/admin-access.log;
error_log /var/log/nginx/admin-error.log;
}
7.3 性能优化
- 缓存策略: 合理使用Redis缓存权限、菜单等数据
- 静态资源: 静态资源使用CDN加速,设置长缓存
- 数据库优化: 合理设计索引,避免N+1查询问题
- 代码优化: 使用ThinkPHP8的高性能特性,避免不必要的计算
- 懒加载: 前端页面按需加载,提高首屏速度
8. 扩展与维护
8.1 代码规范
- PSR-12: 遵循PSR-12代码规范
- ESLint: 前端JavaScript代码规范
- 注释要求: 关键函数、类、复杂逻辑必须有注释
- Git提交规范: 遵循Angular提交规范
8.2 扩展点
- 多租户支持: 通过增加tenant_id字段扩展多租户功能
- 操作日志: 记录关键操作,支持审计
- 数据权限: 增加数据级权限控制,如部门数据隔离
- 第三方登录: 支持微信、钉钉等第三方登录
- API文档: 集成Swagger自动生成API文档
8.3 维护建议
- 版本控制: 使用Git进行版本控制,合理使用分支策略
- 自动化测试: 编写关键功能的单元测试和接口测试
- 监控告警: 集成系统监控,异常时及时告警
- 定期备份: 数据库和重要文件定期备份
- 日志分析: 收集并分析系统日志,及时发现潜在问题
9. 总结
本设计方案基于ThinkPHP8 + Layui/HTML实现了前后端分离的权限管理系统,具有以下特点:
- 完整的RBAC权限控制:支持菜单级、按钮级、API接口级的细粒度权限管理
- 高性能架构:使用JWT无状态认证,Redis缓存权限数据,前后端分离提升性能
- 安全可靠:多层安全防护,包括认证、授权、数据安全等
- 易于扩展:模块化设计,便于功能扩展和二次开发
- 用户体验良好:使用Layui框架,界面简洁易用,操作流畅
通过本方案,可以快速构建一个安全、高效、易维护的权限管理系统,适用于各类企业级应用。