Files
YwxAppThink/README.md
T

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 权限控制流程

  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
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实现了前后端分离的权限管理系统,具有以下特点:

  1. 完整的RBAC权限控制:支持菜单级、按钮级、API接口级的细粒度权限管理
  2. 高性能架构:使用JWT无状态认证,Redis缓存权限数据,前后端分离提升性能
  3. 安全可靠:多层安全防护,包括认证、授权、数据安全等
  4. 易于扩展:模块化设计,便于功能扩展和二次开发
  5. 用户体验良好:使用Layui框架,界面简洁易用,操作流畅

通过本方案,可以快速构建一个安全、高效、易维护的权限管理系统,适用于各类企业级应用。