1530 lines
46 KiB
Markdown
1530 lines
46 KiB
Markdown
|
|
|
|
# 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 权限控制流程
|
|
1. 用户登录获取JWT令牌
|
|
2. 前端存储令牌并在每次请求携带
|
|
3. 后端中间件验证令牌有效性
|
|
4. 根据用户角色/权限决定是否放行请求
|
|
5. 前端根据权限数据动态渲染菜单和按钮
|
|
|
|
## 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) | 头像 |
|
|
| email | 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
|
|
<?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
|
|
<?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
|
|
<?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
|
|
<?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
|
|
<?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
|
|
<?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
|
|
<?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: 服务器内部错误
|
|
|
|
- **响应结构**:
|
|
```json
|
|
{
|
|
"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)
|
|
```javascript
|
|
// 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)
|
|
```javascript
|
|
// 权限控制类
|
|
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)
|
|
```javascript
|
|
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)
|
|
```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)
|
|
```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配置示例
|
|
```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实现了前后端分离的权限管理系统,具有以下特点:
|
|
|
|
1. **完整的RBAC权限控制**:支持菜单级、按钮级、API接口级的细粒度权限管理
|
|
2. **高性能架构**:使用JWT无状态认证,Redis缓存权限数据,前后端分离提升性能
|
|
3. **安全可靠**:多层安全防护,包括认证、授权、数据安全等
|
|
4. **易于扩展**:模块化设计,便于功能扩展和二次开发
|
|
5. **用户体验良好**:使用Layui框架,界面简洁易用,操作流畅
|
|
|
|
通过本方案,可以快速构建一个安全、高效、易维护的权限管理系统,适用于各类企业级应用。 |