|
|
@@ -0,0 +1,1883 @@
|
|
|
+# Spring Security + JWT 权限认证完整指南
|
|
|
+
|
|
|
+## 目录
|
|
|
+- [1. 系统架构概述](#1-系统架构概述)
|
|
|
+- [2. 数据库表设计](#2-数据库表设计)
|
|
|
+- [3. Spring Security认证流程](#3-spring-security认证流程)
|
|
|
+- [4. 核心组件详解](#4-核心组件详解)
|
|
|
+- [5. 开发者实现清单](#5-开发者实现清单)
|
|
|
+- [6. 代码示例](#6-代码示例)
|
|
|
+- [7. 测试指南](#7-测试指南)
|
|
|
+- [8. 常见问题](#8-常见问题)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 1. 系统架构概述
|
|
|
+
|
|
|
+### 1.1 技术栈
|
|
|
+- **Spring Boot 2.2.7**
|
|
|
+- **Spring Security 5.x**
|
|
|
+- **JWT (JSON Web Token)**
|
|
|
+- **Redis** - 存储Token
|
|
|
+- **MySQL 8.0** - 存储用户、角色、权限数据
|
|
|
+- **MyBatis-Plus** - 数据库操作
|
|
|
+
|
|
|
+### 1.2 权限控制级别
|
|
|
+本系统实现了**三级权限控制**:
|
|
|
+1. **接口级别** - URL路径访问控制
|
|
|
+2. **方法级别** - 使用 `@PreAuthorize` 注解
|
|
|
+3. **按钮级别** - 前端根据权限标识显示/隐藏按钮
|
|
|
+
|
|
|
+### 1.3 认证流程架构图
|
|
|
+```
|
|
|
+┌─────────────┐ ┌──────────────┐ ┌────────────────┐
|
|
|
+│ 客户端 │─────>│ 登录接口 │─────>│ Authentication │
|
|
|
+│ │ │ /auth/login │ │ Manager │
|
|
|
+└─────────────┘ └──────────────┘ └────────────────┘
|
|
|
+ ↓ ↓
|
|
|
+ ↓ ┌────────────────┐
|
|
|
+ ↓ │ UserDetails │
|
|
|
+ ↓ │ Service │
|
|
|
+ ↓ └────────────────┘
|
|
|
+ ↓ ↓
|
|
|
+ ┌──────────────┐ ↓
|
|
|
+ │ 生成JWT │ ┌────────────────┐
|
|
|
+ │ Token │ │ 查询用户 │
|
|
|
+ └──────────────┘ │ 角色+权限 │
|
|
|
+ ↓ └────────────────┘
|
|
|
+ ↓ ↓
|
|
|
+ ┌──────────────┐ ↓
|
|
|
+ │ 存储Token到 │<─────────────┘
|
|
|
+ │ Redis │
|
|
|
+ └──────────────┘
|
|
|
+ ↓
|
|
|
+ ┌──────────────┐
|
|
|
+ │ 返回Token给 │
|
|
|
+ │ 客户端 │
|
|
|
+ └──────────────┘
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 2. 数据库表设计
|
|
|
+
|
|
|
+### 2.1 RBAC权限模型
|
|
|
+本系统采用经典的 **RBAC (Role-Based Access Control)** 模型:
|
|
|
+- 用户表 (sys_user)
|
|
|
+- 角色表 (sys_role)
|
|
|
+- 菜单/权限表 (sys_menu)
|
|
|
+- 用户-角色关联表 (sys_user_role)
|
|
|
+- 角色-菜单关联表 (sys_role_menu)
|
|
|
+
|
|
|
+### 2.2 完整SQL脚本
|
|
|
+
|
|
|
+```sql
|
|
|
+-- ====================================
|
|
|
+-- 用户表
|
|
|
+-- ====================================
|
|
|
+CREATE TABLE `sys_user` (
|
|
|
+ `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
|
|
|
+ `username` VARCHAR(50) NOT NULL COMMENT '用户名',
|
|
|
+ `password` VARCHAR(100) NOT NULL COMMENT '密码(BCrypt加密)',
|
|
|
+ `nickname` VARCHAR(50) DEFAULT NULL COMMENT '昵称',
|
|
|
+ `email` VARCHAR(100) DEFAULT NULL COMMENT '邮箱',
|
|
|
+ `phone` VARCHAR(20) DEFAULT NULL COMMENT '手机号',
|
|
|
+ `avatar` VARCHAR(255) DEFAULT NULL COMMENT '头像',
|
|
|
+ `status` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '状态:0-禁用,1-正常',
|
|
|
+ `create_by` VARCHAR(50) DEFAULT NULL COMMENT '创建人',
|
|
|
+ `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
|
|
+ `update_by` VARCHAR(50) DEFAULT NULL COMMENT '更新人',
|
|
|
+ `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
|
|
+ `del_flag` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '删除标志:0-正常,1-删除',
|
|
|
+ `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
|
|
|
+ PRIMARY KEY (`id`),
|
|
|
+ UNIQUE KEY `uk_username` (`username`)
|
|
|
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户表';
|
|
|
+
|
|
|
+-- ====================================
|
|
|
+-- 角色表
|
|
|
+-- ====================================
|
|
|
+CREATE TABLE `sys_role` (
|
|
|
+ `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '角色ID',
|
|
|
+ `role_name` VARCHAR(50) NOT NULL COMMENT '角色名称',
|
|
|
+ `role_key` VARCHAR(50) NOT NULL COMMENT '角色权限字符串',
|
|
|
+ `role_sort` INT(4) NOT NULL DEFAULT 0 COMMENT '显示顺序',
|
|
|
+ `status` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '状态:0-禁用,1-正常',
|
|
|
+ `create_by` VARCHAR(50) DEFAULT NULL COMMENT '创建人',
|
|
|
+ `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
|
|
+ `update_by` VARCHAR(50) DEFAULT NULL COMMENT '更新人',
|
|
|
+ `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
|
|
+ `del_flag` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '删除标志:0-正常,1-删除',
|
|
|
+ `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
|
|
|
+ PRIMARY KEY (`id`),
|
|
|
+ UNIQUE KEY `uk_role_key` (`role_key`)
|
|
|
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统角色表';
|
|
|
+
|
|
|
+-- ====================================
|
|
|
+-- 菜单/权限表(支持2级菜单 + 按钮权限)
|
|
|
+-- ====================================
|
|
|
+CREATE TABLE `sys_menu` (
|
|
|
+ `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '菜单ID',
|
|
|
+ `menu_name` VARCHAR(50) NOT NULL COMMENT '菜单名称',
|
|
|
+ `parent_id` BIGINT(20) NOT NULL DEFAULT 0 COMMENT '父菜单ID,0表示顶级菜单',
|
|
|
+ `menu_type` CHAR(1) NOT NULL COMMENT '菜单类型:M-目录,C-菜单,F-按钮',
|
|
|
+ `path` VARCHAR(200) DEFAULT NULL COMMENT '路由地址',
|
|
|
+ `component` VARCHAR(255) DEFAULT NULL COMMENT '组件路径',
|
|
|
+ `perms` VARCHAR(100) DEFAULT NULL COMMENT '权限标识(如:system:user:add)',
|
|
|
+ `icon` VARCHAR(100) DEFAULT NULL COMMENT '菜单图标',
|
|
|
+ `order_num` INT(4) NOT NULL DEFAULT 0 COMMENT '显示顺序',
|
|
|
+ `visible` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否可见:0-隐藏,1-显示',
|
|
|
+ `status` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '状态:0-禁用,1-正常',
|
|
|
+ `create_by` VARCHAR(50) DEFAULT NULL COMMENT '创建人',
|
|
|
+ `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
|
|
+ `update_by` VARCHAR(50) DEFAULT NULL COMMENT '更新人',
|
|
|
+ `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
|
|
+ `del_flag` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '删除标志:0-正常,1-删除',
|
|
|
+ `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
|
|
|
+ PRIMARY KEY (`id`),
|
|
|
+ KEY `idx_parent_id` (`parent_id`)
|
|
|
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统菜单表';
|
|
|
+
|
|
|
+-- ====================================
|
|
|
+-- 用户-角色关联表
|
|
|
+-- ====================================
|
|
|
+CREATE TABLE `sys_user_role` (
|
|
|
+ `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
|
|
|
+ `user_id` BIGINT(20) NOT NULL COMMENT '用户ID',
|
|
|
+ `role_id` BIGINT(20) NOT NULL COMMENT '角色ID',
|
|
|
+ PRIMARY KEY (`id`),
|
|
|
+ UNIQUE KEY `uk_user_role` (`user_id`, `role_id`)
|
|
|
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户角色关联表';
|
|
|
+
|
|
|
+-- ====================================
|
|
|
+-- 角色-菜单关联表
|
|
|
+-- ====================================
|
|
|
+CREATE TABLE `sys_role_menu` (
|
|
|
+ `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
|
|
|
+ `role_id` BIGINT(20) NOT NULL COMMENT '角色ID',
|
|
|
+ `menu_id` BIGINT(20) NOT NULL COMMENT '菜单ID',
|
|
|
+ PRIMARY KEY (`id`),
|
|
|
+ UNIQUE KEY `uk_role_menu` (`role_id`, `menu_id`)
|
|
|
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色菜单关联表';
|
|
|
+
|
|
|
+-- ====================================
|
|
|
+-- 初始化数据
|
|
|
+-- ====================================
|
|
|
+-- 插入超级管理员用户(密码: admin123,已BCrypt加密)
|
|
|
+INSERT INTO `sys_user` (`id`, `username`, `password`, `nickname`, `status`)
|
|
|
+VALUES (1, 'admin', '$2a$10$N.ZOn9G6/YLFixAOPMg/h.z7pCu6v2XyFDtC4q.jeeGm/TEZySmDu', '超级管理员', 1);
|
|
|
+
|
|
|
+-- 插入普通用户(密码: user123,已BCrypt加密)
|
|
|
+INSERT INTO `sys_user` (`id`, `username`, `password`, `nickname`, `status`)
|
|
|
+VALUES (2, 'user', '$2a$10$N.ZOn9G6/YLFixAOPMg/h.z7pCu6v2XyFDtC4q.jeeGm/TEZySmDu', '普通用户', 1);
|
|
|
+
|
|
|
+-- 插入角色
|
|
|
+INSERT INTO `sys_role` (`id`, `role_name`, `role_key`, `role_sort`)
|
|
|
+VALUES (1, '超级管理员', 'admin', 1);
|
|
|
+
|
|
|
+INSERT INTO `sys_role` (`id`, `role_name`, `role_key`, `role_sort`)
|
|
|
+VALUES (2, '普通用户', 'user', 2);
|
|
|
+
|
|
|
+-- 插入一级菜单(目录)
|
|
|
+INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `icon`, `order_num`, `perms`)
|
|
|
+VALUES (1, '系统管理', 0, 'M', '/system', 'system', 1, NULL);
|
|
|
+
|
|
|
+INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `icon`, `order_num`, `perms`)
|
|
|
+VALUES (2, '用户中心', 0, 'M', '/user', 'user', 2, NULL);
|
|
|
+
|
|
|
+-- 插入二级菜单
|
|
|
+INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `order_num`, `perms`)
|
|
|
+VALUES (101, '用户管理', 1, 'C', '/system/user', 'system/user/index', 1, 'system:user:list');
|
|
|
+
|
|
|
+INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `order_num`, `perms`)
|
|
|
+VALUES (102, '角色管理', 1, 'C', '/system/role', 'system/role/index', 2, 'system:role:list');
|
|
|
+
|
|
|
+INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `order_num`, `perms`)
|
|
|
+VALUES (103, '菜单管理', 1, 'C', '/system/menu', 'system/menu/index', 3, 'system:menu:list');
|
|
|
+
|
|
|
+INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `order_num`, `perms`)
|
|
|
+VALUES (201, '个人信息', 2, 'C', '/user/profile', 'user/profile/index', 1, 'user:profile:view');
|
|
|
+
|
|
|
+-- 插入按钮权限(F类型 - 按钮级别权限控制)
|
|
|
+INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `order_num`, `perms`)
|
|
|
+VALUES (1011, '用户新增', 101, 'F', 1, 'system:user:add');
|
|
|
+
|
|
|
+INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `order_num`, `perms`)
|
|
|
+VALUES (1012, '用户修改', 101, 'F', 2, 'system:user:edit');
|
|
|
+
|
|
|
+INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `order_num`, `perms`)
|
|
|
+VALUES (1013, '用户删除', 101, 'F', 3, 'system:user:remove');
|
|
|
+
|
|
|
+INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `order_num`, `perms`)
|
|
|
+VALUES (1021, '角色新增', 102, 'F', 1, 'system:role:add');
|
|
|
+
|
|
|
+INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `order_num`, `perms`)
|
|
|
+VALUES (1022, '角色修改', 102, 'F', 2, 'system:role:edit');
|
|
|
+
|
|
|
+INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `order_num`, `perms`)
|
|
|
+VALUES (1023, '角色删除', 102, 'F', 3, 'system:role:remove');
|
|
|
+
|
|
|
+-- 用户-角色关联
|
|
|
+INSERT INTO `sys_user_role` (`user_id`, `role_id`) VALUES (1, 1);
|
|
|
+INSERT INTO `sys_user_role` (`user_id`, `role_id`) VALUES (2, 2);
|
|
|
+
|
|
|
+-- 角色-菜单关联(超级管理员拥有所有权限)
|
|
|
+INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
|
|
|
+VALUES
|
|
|
+(1, 1), (1, 2), (1, 101), (1, 102), (1, 103), (1, 201),
|
|
|
+(1, 1011), (1, 1012), (1, 1013), (1, 1021), (1, 1022), (1, 1023);
|
|
|
+
|
|
|
+-- 普通用户只有查看权限
|
|
|
+INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
|
|
|
+VALUES (2, 2), (2, 201);
|
|
|
+```
|
|
|
+
|
|
|
+### 2.3 权限标识命名规范
|
|
|
+
|
|
|
+权限标识(perms字段)采用 **模块:功能:操作** 三段式命名:
|
|
|
+
|
|
|
+| 权限标识 | 说明 | 示例 |
|
|
|
+|---------|------|------|
|
|
|
+| `system:user:list` | 用户列表查看 | 查询用户列表接口 |
|
|
|
+| `system:user:add` | 用户新增 | 新增用户按钮 |
|
|
|
+| `system:user:edit` | 用户编辑 | 编辑用户按钮 |
|
|
|
+| `system:user:remove` | 用户删除 | 删除用户按钮 |
|
|
|
+| `system:user:export` | 用户导出 | 导出Excel按钮 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 3. Spring Security认证流程
|
|
|
+
|
|
|
+### 3.1 完整认证流程图
|
|
|
+
|
|
|
+```
|
|
|
+用户登录流程:
|
|
|
+┌──────────────────────────────────────────────────────────────────┐
|
|
|
+│ 1. 用户发起登录请求 │
|
|
|
+│ POST /auth/login │
|
|
|
+│ { username, password } │
|
|
|
+└────────────────────────┬─────────────────────────────────────────┘
|
|
|
+ ↓
|
|
|
+┌──────────────────────────────────────────────────────────────────┐
|
|
|
+│ 2. Controller接收请求 │
|
|
|
+│ SysUserController.login() │
|
|
|
+│ 创建 UsernamePasswordAuthenticationToken │
|
|
|
+└────────────────────────┬─────────────────────────────────────────┘
|
|
|
+ ↓
|
|
|
+┌──────────────────────────────────────────────────────────────────┐
|
|
|
+│ 3. AuthenticationManager 认证 │
|
|
|
+│ 调用 authenticate(authenticationToken) │
|
|
|
+└────────────────────────┬─────────────────────────────────────────┘
|
|
|
+ ↓
|
|
|
+┌──────────────────────────────────────────────────────────────────┐
|
|
|
+│ 4. DaoAuthenticationProvider 处理 │
|
|
|
+│ - 调用 UserDetailsService.loadUserByUsername() │
|
|
|
+│ - 获取用户详情(包含密码、角色、权限) │
|
|
|
+└────────────────────────┬─────────────────────────────────────────┘
|
|
|
+ ↓
|
|
|
+┌──────────────────────────────────────────────────────────────────┐
|
|
|
+│ 5. UserDetailsServiceImpl.loadUserByUsername() │
|
|
|
+│ ┌──────────────────────────────────────┐ │
|
|
|
+│ │ a. 查询用户基本信息 │ │
|
|
|
+│ │ getUserWithRolesAndPermissions() │ │
|
|
|
+│ └──────────────────┬───────────────────┘ │
|
|
|
+│ ↓ │
|
|
|
+│ ┌──────────────────────────────────────┐ │
|
|
|
+│ │ b. 查询用户权限列表 │ │
|
|
|
+│ │ getPermissionsByUserId() │ │
|
|
|
+│ └──────────────────┬───────────────────┘ │
|
|
|
+│ ↓ │
|
|
|
+│ ┌──────────────────────────────────────┐ │
|
|
|
+│ │ c. 构建 LoginUser 对象 │ │
|
|
|
+│ │ 包含:用户信息 + 权限列表 │ │
|
|
|
+│ └──────────────────────────────────────┘ │
|
|
|
+└────────────────────────┬─────────────────────────────────────────┘
|
|
|
+ ↓
|
|
|
+┌──────────────────────────────────────────────────────────────────┐
|
|
|
+│ 6. 密码校验 │
|
|
|
+│ PasswordEncoder.matches(rawPassword, encodedPassword) │
|
|
|
+│ ✓ 匹配成功 → 继续 │
|
|
|
+│ ✗ 匹配失败 → 抛出 BadCredentialsException │
|
|
|
+└────────────────────────┬─────────────────────────────────────────┘
|
|
|
+ ↓
|
|
|
+┌──────────────────────────────────────────────────────────────────┐
|
|
|
+│ 7. 认证成功,生成Token │
|
|
|
+│ ┌──────────────────────────────────────┐ │
|
|
|
+│ │ a. 生成 JWT Token │ │
|
|
|
+│ │ JwtUtil.generateToken() │ │
|
|
|
+│ │ 包含:userId, username, 过期时间 │ │
|
|
|
+│ └──────────────────┬───────────────────┘ │
|
|
|
+│ ↓ │
|
|
|
+│ ┌──────────────────────────────────────┐ │
|
|
|
+│ │ b. 将Token存储到Redis │ │
|
|
|
+│ │ key: token:userId │ │
|
|
|
+│ │ value: token │ │
|
|
|
+│ │ expire: 24小时 │ │
|
|
|
+│ └──────────────────────────────────────┘ │
|
|
|
+└────────────────────────┬─────────────────────────────────────────┘
|
|
|
+ ↓
|
|
|
+┌──────────────────────────────────────────────────────────────────┐
|
|
|
+│ 8. 返回Token给客户端 │
|
|
|
+│ { success: true, token: "xxx", userInfo: {...} } │
|
|
|
+└──────────────────────────────────────────────────────────────────┘
|
|
|
+
|
|
|
+
|
|
|
+后续请求认证流程:
|
|
|
+┌──────────────────────────────────────────────────────────────────┐
|
|
|
+│ 1. 客户端发起业务请求 │
|
|
|
+│ Header: Authorization: Bearer {token} │
|
|
|
+└────────────────────────┬─────────────────────────────────────────┘
|
|
|
+ ↓
|
|
|
+┌──────────────────────────────────────────────────────────────────┐
|
|
|
+│ 2. JwtAuthenticationFilter 拦截 │
|
|
|
+│ ┌──────────────────────────────────────┐ │
|
|
|
+│ │ a. 从请求头获取Token │ │
|
|
|
+│ │ Authorization: Bearer {token} │ │
|
|
|
+│ └──────────────────┬───────────────────┘ │
|
|
|
+│ ↓ │
|
|
|
+│ ┌──────────────────────────────────────┐ │
|
|
|
+│ │ b. 验证Token有效性 │ │
|
|
|
+│ │ JwtUtil.validateToken() │ │
|
|
|
+│ └──────────────────┬───────────────────┘ │
|
|
|
+│ ↓ │
|
|
|
+│ ┌──────────────────────────────────────┐ │
|
|
|
+│ │ c. 从Redis获取LoginUser │ │
|
|
|
+│ │ TokenService.getLoginUser() │ │
|
|
|
+│ └──────────────────┬───────────────────┘ │
|
|
|
+│ ↓ │
|
|
|
+│ ┌──────────────────────────────────────┐ │
|
|
|
+│ │ d. 将LoginUser设置到SecurityContext │ │
|
|
|
+│ │ 包含用户信息和权限列表 │ │
|
|
|
+│ └──────────────────────────────────────┘ │
|
|
|
+└────────────────────────┬─────────────────────────────────────────┘
|
|
|
+ ↓
|
|
|
+┌──────────────────────────────────────────────────────────────────┐
|
|
|
+│ 3. FilterSecurityInterceptor │
|
|
|
+│ 检查用户是否有访问当前URL的权限 │
|
|
|
+└────────────────────────┬─────────────────────────────────────────┘
|
|
|
+ ↓
|
|
|
+┌──────────────────────────────────────────────────────────────────┐
|
|
|
+│ 4. 方法级别权限验证 │
|
|
|
+│ @PreAuthorize("hasAuthority('xxx')") │
|
|
|
+│ 检查用户是否有执行该方法的权限 │
|
|
|
+└────────────────────────┬─────────────────────────────────────────┘
|
|
|
+ ↓
|
|
|
+┌──────────────────────────────────────────────────────────────────┐
|
|
|
+│ 5. 执行业务逻辑,返回结果 │
|
|
|
+└──────────────────────────────────────────────────────────────────┘
|
|
|
+```
|
|
|
+
|
|
|
+### 3.2 核心过滤器链顺序
|
|
|
+
|
|
|
+Spring Security的过滤器链按以下顺序执行:
|
|
|
+
|
|
|
+```
|
|
|
+1. SecurityContextPersistenceFilter - 创建SecurityContext
|
|
|
+2. HeaderWriterFilter - 添加安全响应头
|
|
|
+3. LogoutFilter - 处理登出请求
|
|
|
+4. JwtAuthenticationFilter - JWT认证过滤器(自定义)
|
|
|
+5. RequestCacheAwareFilter - 恢复被中断的请求
|
|
|
+6. SecurityContextHolderAwareRequestFilter - 包装请求对象
|
|
|
+7. AnonymousAuthenticationFilter - 匿名认证
|
|
|
+8. SessionManagementFilter - Session管理
|
|
|
+9. ExceptionTranslationFilter - 异常转换
|
|
|
+10. FilterSecurityInterceptor - 权限验证
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 4. 核心组件详解
|
|
|
+
|
|
|
+### 4.1 SecurityConfig(安全配置类)
|
|
|
+
|
|
|
+**作用**:Spring Security的核心配置类
|
|
|
+
|
|
|
+**关键配置项**:
|
|
|
+
|
|
|
+```java
|
|
|
+@Configuration
|
|
|
+@EnableWebSecurity
|
|
|
+@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
|
|
|
+public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
|
|
+
|
|
|
+ // 1. 配置密码编码器(BCrypt)
|
|
|
+ @Bean
|
|
|
+ public PasswordEncoder passwordEncoder() {
|
|
|
+ return new BCryptPasswordEncoder();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 暴露认证管理器
|
|
|
+ @Bean
|
|
|
+ @Override
|
|
|
+ public AuthenticationManager authenticationManagerBean() throws Exception {
|
|
|
+ return super.authenticationManagerBean();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 配置认证方式
|
|
|
+ @Override
|
|
|
+ protected void configure(AuthenticationManagerBuilder auth) throws Exception {
|
|
|
+ auth.userDetailsService(userDetailsService)
|
|
|
+ .passwordEncoder(passwordEncoder());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. 配置HTTP安全策略
|
|
|
+ @Override
|
|
|
+ protected void configure(HttpSecurity http) throws Exception {
|
|
|
+ http
|
|
|
+ .csrf().disable() // 禁用CSRF(使用JWT不需要)
|
|
|
+ .sessionManagement()
|
|
|
+ .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态
|
|
|
+ .and()
|
|
|
+ .authorizeRequests()
|
|
|
+ .antMatchers("/auth/login", "/auth/logout").permitAll() // 公开接口
|
|
|
+ .anyRequest().authenticated(); // 其他接口需要认证
|
|
|
+
|
|
|
+ // 添加JWT过滤器
|
|
|
+ http.addFilterBefore(jwtAuthenticationFilter,
|
|
|
+ UsernamePasswordAuthenticationFilter.class);
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**重要说明**:
|
|
|
+- `permitAll()` vs `anonymous()`:
|
|
|
+ - `permitAll()`:允许所有人访问(包括已认证和未认证用户)
|
|
|
+ - `anonymous()`:只允许匿名用户访问(已认证用户会被拒绝)
|
|
|
+ - **登录接口必须使用 `permitAll()`**
|
|
|
+
|
|
|
+### 4.2 UserDetailsServiceImpl(用户详情服务)
|
|
|
+
|
|
|
+**作用**:加载用户信息、角色和权限
|
|
|
+
|
|
|
+**实现要点**:
|
|
|
+
|
|
|
+```java
|
|
|
+@Service
|
|
|
+public class UserDetailsServiceImpl implements UserDetailsService {
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private SysUserService userService;
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public UserDetails loadUserByUsername(String username)
|
|
|
+ throws UsernameNotFoundException {
|
|
|
+
|
|
|
+ // 1. 查询用户(包含角色信息)
|
|
|
+ SysUser user = userService.getUserWithRolesAndPermissions(username);
|
|
|
+ if (user == null) {
|
|
|
+ throw new UsernameNotFoundException("用户不存在");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 查询用户权限列表
|
|
|
+ List<String> permissions = userService.getPermissionsByUserId(user.getId());
|
|
|
+
|
|
|
+ // 3. 构建LoginUser对象(实现UserDetails接口)
|
|
|
+ return new LoginUser(user, permissions);
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**SQL查询示例**:
|
|
|
+
|
|
|
+```xml
|
|
|
+<!-- 查询用户及其角色 -->
|
|
|
+<select id="selectUserWithRolesAndPermissions" resultMap="UserWithRolesMap">
|
|
|
+ SELECT
|
|
|
+ u.id, u.username, u.password, u.nickname, u.status,
|
|
|
+ r.id AS role_id, r.role_name, r.role_key
|
|
|
+ FROM sys_user u
|
|
|
+ LEFT JOIN sys_user_role ur ON u.id = ur.user_id
|
|
|
+ LEFT JOIN sys_role r ON ur.role_id = r.id
|
|
|
+ WHERE u.username = #{username}
|
|
|
+ AND u.del_flag = 0
|
|
|
+ AND (r.del_flag = 0 OR r.id IS NULL)
|
|
|
+</select>
|
|
|
+
|
|
|
+<!-- 查询用户权限 -->
|
|
|
+<select id="selectPermissionsByUserId" resultType="java.lang.String">
|
|
|
+ SELECT DISTINCT m.perms
|
|
|
+ FROM sys_user_role ur
|
|
|
+ LEFT JOIN sys_role_menu rm ON ur.role_id = rm.role_id
|
|
|
+ LEFT JOIN sys_menu m ON rm.menu_id = m.id
|
|
|
+ WHERE ur.user_id = #{userId}
|
|
|
+ AND m.perms IS NOT NULL
|
|
|
+ AND m.perms != ''
|
|
|
+ AND m.status = 1
|
|
|
+ AND m.del_flag = 0
|
|
|
+</select>
|
|
|
+```
|
|
|
+
|
|
|
+### 4.3 LoginUser(用户认证信息)
|
|
|
+
|
|
|
+**作用**:封装用户详情和权限,实现UserDetails接口
|
|
|
+
|
|
|
+```java
|
|
|
+@Data
|
|
|
+@NoArgsConstructor
|
|
|
+public class LoginUser implements UserDetails {
|
|
|
+
|
|
|
+ private SysUser user; // 用户信息
|
|
|
+ private List<String> permissions; // 权限列表
|
|
|
+
|
|
|
+ // 构造方法
|
|
|
+ public LoginUser(SysUser user, List<String> permissions) {
|
|
|
+ this.user = user;
|
|
|
+ this.permissions = permissions;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 将权限字符串转换为GrantedAuthority
|
|
|
+ @Override
|
|
|
+ public Collection<? extends GrantedAuthority> getAuthorities() {
|
|
|
+ return permissions.stream()
|
|
|
+ .map(SimpleGrantedAuthority::new)
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public String getPassword() {
|
|
|
+ return user.getPassword();
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public String getUsername() {
|
|
|
+ return user.getUsername();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 账户是否启用(检查status字段)
|
|
|
+ @Override
|
|
|
+ public boolean isEnabled() {
|
|
|
+ return user.getStatus() == 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 其他方法返回true
|
|
|
+ @Override
|
|
|
+ public boolean isAccountNonExpired() { return true; }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public boolean isAccountNonLocked() { return true; }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public boolean isCredentialsNonExpired() { return true; }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 4.4 JwtAuthenticationFilter(JWT认证过滤器)
|
|
|
+
|
|
|
+**作用**:拦截所有请求,验证JWT Token
|
|
|
+
|
|
|
+```java
|
|
|
+@Component
|
|
|
+public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private TokenService tokenService;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private JwtUtil jwtUtil;
|
|
|
+
|
|
|
+ @Override
|
|
|
+ protected void doFilterInternal(HttpServletRequest request,
|
|
|
+ HttpServletResponse response,
|
|
|
+ FilterChain chain)
|
|
|
+ throws ServletException, IOException {
|
|
|
+
|
|
|
+ // 1. 从请求头获取token
|
|
|
+ String token = getTokenFromRequest(request);
|
|
|
+
|
|
|
+ // 2. 验证token
|
|
|
+ if (token != null && jwtUtil.validateToken(token)) {
|
|
|
+
|
|
|
+ // 3. 从Redis获取用户信息
|
|
|
+ LoginUser loginUser = tokenService.getLoginUser(token);
|
|
|
+
|
|
|
+ if (loginUser != null) {
|
|
|
+ // 4. 刷新token有效期
|
|
|
+ tokenService.verifyToken(loginUser);
|
|
|
+
|
|
|
+ // 5. 创建认证对象
|
|
|
+ UsernamePasswordAuthenticationToken authentication =
|
|
|
+ new UsernamePasswordAuthenticationToken(
|
|
|
+ loginUser, null, loginUser.getAuthorities());
|
|
|
+
|
|
|
+ // 6. 设置到SecurityContext
|
|
|
+ SecurityContextHolder.getContext()
|
|
|
+ .setAuthentication(authentication);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 7. 继续过滤器链
|
|
|
+ chain.doFilter(request, response);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 从请求头提取token
|
|
|
+ private String getTokenFromRequest(HttpServletRequest request) {
|
|
|
+ String bearerToken = request.getHeader("Authorization");
|
|
|
+ if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
|
|
|
+ return bearerToken.substring(7);
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 4.5 JwtUtil(JWT工具类)
|
|
|
+
|
|
|
+**作用**:生成和解析JWT Token
|
|
|
+
|
|
|
+```java
|
|
|
+@Component
|
|
|
+public class JwtUtil {
|
|
|
+
|
|
|
+ // 密钥(生产环境应从配置文件读取)
|
|
|
+ private String secret = "your-secret-key-must-be-very-long";
|
|
|
+
|
|
|
+ // 过期时间(24小时)
|
|
|
+ private long expiration = 86400000;
|
|
|
+
|
|
|
+ // 生成Token
|
|
|
+ public String generateToken(Map<String, Object> claims) {
|
|
|
+ return Jwts.builder()
|
|
|
+ .setClaims(claims)
|
|
|
+ .setExpiration(new Date(System.currentTimeMillis() + expiration))
|
|
|
+ .signWith(SignatureAlgorithm.HS512, secret)
|
|
|
+ .compact();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 从Token中获取用户ID
|
|
|
+ public Long getUserIdFromToken(String token) {
|
|
|
+ Claims claims = parseToken(token);
|
|
|
+ return claims.get("userId", Long.class);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 从Token中获取用户名
|
|
|
+ public String getUsernameFromToken(String token) {
|
|
|
+ Claims claims = parseToken(token);
|
|
|
+ return claims.get("username", String.class);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 验证Token
|
|
|
+ public boolean validateToken(String token) {
|
|
|
+ try {
|
|
|
+ Jwts.parser().setSigningKey(secret).parseClaimsJws(token);
|
|
|
+ return true;
|
|
|
+ } catch (Exception e) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 解析Token
|
|
|
+ private Claims parseToken(String token) {
|
|
|
+ return Jwts.parser()
|
|
|
+ .setSigningKey(secret)
|
|
|
+ .parseClaimsJws(token)
|
|
|
+ .getBody();
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 4.6 TokenService(Token服务)
|
|
|
+
|
|
|
+**作用**:管理Token的存储和刷新
|
|
|
+
|
|
|
+```java
|
|
|
+@Service
|
|
|
+public class TokenService {
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private RedisCache redisCache;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private JwtUtil jwtUtil;
|
|
|
+
|
|
|
+ // Token在Redis中的前缀
|
|
|
+ private static final String LOGIN_TOKEN_KEY = "login_tokens:";
|
|
|
+
|
|
|
+ // 刷新时间(20分钟)
|
|
|
+ private static final long REFRESH_TIME = 20 * 60 * 1000;
|
|
|
+
|
|
|
+ // 过期时间(24小时)
|
|
|
+ private static final long EXPIRE_TIME = 24 * 60 * 60 * 1000;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 创建Token
|
|
|
+ */
|
|
|
+ public String createToken(LoginUser loginUser) {
|
|
|
+ String uuid = UUID.randomUUID().toString();
|
|
|
+ loginUser.setToken(uuid);
|
|
|
+ loginUser.setLoginTime(System.currentTimeMillis());
|
|
|
+ loginUser.setExpireTime(loginUser.getLoginTime() + EXPIRE_TIME);
|
|
|
+
|
|
|
+ // 生成JWT
|
|
|
+ Map<String, Object> claims = new HashMap<>();
|
|
|
+ claims.put("userId", loginUser.getUser().getId());
|
|
|
+ claims.put("username", loginUser.getUsername());
|
|
|
+ String token = jwtUtil.generateToken(claims);
|
|
|
+
|
|
|
+ // 存储到Redis
|
|
|
+ String tokenKey = getTokenKey(token);
|
|
|
+ redisCache.setCacheObject(tokenKey, loginUser, EXPIRE_TIME, TimeUnit.MILLISECONDS);
|
|
|
+
|
|
|
+ return token;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取用户信息
|
|
|
+ */
|
|
|
+ public LoginUser getLoginUser(String token) {
|
|
|
+ String tokenKey = getTokenKey(token);
|
|
|
+ return redisCache.getCacheObject(tokenKey);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 刷新Token有效期
|
|
|
+ */
|
|
|
+ public void verifyToken(LoginUser loginUser) {
|
|
|
+ long currentTime = System.currentTimeMillis();
|
|
|
+ long expireTime = loginUser.getExpireTime();
|
|
|
+
|
|
|
+ // 距离过期时间小于20分钟,自动刷新
|
|
|
+ if (expireTime - currentTime <= REFRESH_TIME) {
|
|
|
+ String tokenKey = getTokenKey(loginUser.getToken());
|
|
|
+ redisCache.expire(tokenKey, EXPIRE_TIME, TimeUnit.MILLISECONDS);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 删除Token
|
|
|
+ */
|
|
|
+ public void deleteToken(String token) {
|
|
|
+ String tokenKey = getTokenKey(token);
|
|
|
+ redisCache.deleteObject(tokenKey);
|
|
|
+ }
|
|
|
+
|
|
|
+ private String getTokenKey(String token) {
|
|
|
+ return LOGIN_TOKEN_KEY + token;
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 4.7 全局异常处理器
|
|
|
+
|
|
|
+**作用**:统一处理认证和授权异常
|
|
|
+
|
|
|
+```java
|
|
|
+@RestController
|
|
|
+@ControllerAdvice
|
|
|
+public class ServiceExceptionHandler {
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理认证异常(用户名或密码错误)
|
|
|
+ */
|
|
|
+ @ExceptionHandler(AuthenticationException.class)
|
|
|
+ @ResponseBody
|
|
|
+ public RestResult<?> handle(AuthenticationException ex) {
|
|
|
+ ex.printStackTrace();
|
|
|
+ return RestResult.error("用户名或密码错误");
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理授权异常(权限不足)
|
|
|
+ */
|
|
|
+ @ExceptionHandler(AccessDeniedException.class)
|
|
|
+ @ResponseBody
|
|
|
+ public RestResult<?> handle(AccessDeniedException ex) {
|
|
|
+ ex.printStackTrace();
|
|
|
+ return RestResult.error("没有访问权限");
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理其他异常
|
|
|
+ */
|
|
|
+ @ExceptionHandler(Exception.class)
|
|
|
+ @ResponseBody
|
|
|
+ public RestResult<?> handle(Exception ex) {
|
|
|
+ ex.printStackTrace();
|
|
|
+ return RestResult.error("请求异常, 请稍后重试!" + ex.getMessage());
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 5. 开发者实现清单
|
|
|
+
|
|
|
+### 5.1 必须实现的组件(7个核心类)
|
|
|
+
|
|
|
+#### ✅ 1. SecurityConfig(安全配置类)
|
|
|
+**位置**:`config/SecurityConfig.java`
|
|
|
+
|
|
|
+**必须实现的方法**:
|
|
|
+- `passwordEncoder()` - 配置密码编码器
|
|
|
+- `authenticationManagerBean()` - 暴露认证管理器
|
|
|
+- `configure(AuthenticationManagerBuilder)` - 配置认证方式
|
|
|
+- `configure(HttpSecurity)` - 配置HTTP安全策略
|
|
|
+
|
|
|
+**关键点**:
|
|
|
+- 禁用CSRF和Session
|
|
|
+- 配置公开接口(登录、登出等)
|
|
|
+- 添加JWT过滤器
|
|
|
+- 启用方法级权限注解 `@EnableGlobalMethodSecurity`
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+#### ✅ 2. UserDetailsServiceImpl(用户详情服务)
|
|
|
+**位置**:`security/UserDetailsServiceImpl.java`
|
|
|
+
|
|
|
+**必须实现的方法**:
|
|
|
+- `loadUserByUsername(String username)` - 加载用户信息
|
|
|
+
|
|
|
+**关键点**:
|
|
|
+- 查询用户基本信息(包含密码)
|
|
|
+- 查询用户权限列表
|
|
|
+- 返回LoginUser对象
|
|
|
+- 用户不存在时抛出 `UsernameNotFoundException`
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+#### ✅ 3. LoginUser(用户认证信息)
|
|
|
+**位置**:`security/LoginUser.java`
|
|
|
+
|
|
|
+**必须实现的方法**(实现UserDetails接口):
|
|
|
+- `getAuthorities()` - 返回权限列表
|
|
|
+- `getPassword()` - 返回密码
|
|
|
+- `getUsername()` - 返回用户名
|
|
|
+- `isEnabled()` - 是否启用(检查status字段)
|
|
|
+- `isAccountNonExpired()` - 账户是否过期
|
|
|
+- `isAccountNonLocked()` - 账户是否锁定
|
|
|
+- `isCredentialsNonExpired()` - 密码是否过期
|
|
|
+
|
|
|
+**关键点**:
|
|
|
+- 封装用户信息和权限列表
|
|
|
+- 将权限字符串转换为 `GrantedAuthority`
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+#### ✅ 4. JwtAuthenticationFilter(JWT认证过滤器)
|
|
|
+**位置**:`security/JwtAuthenticationFilter.java`
|
|
|
+
|
|
|
+**必须实现的方法**:
|
|
|
+- `doFilterInternal()` - 过滤器逻辑
|
|
|
+
|
|
|
+**关键点**:
|
|
|
+- 从请求头提取Token
|
|
|
+- 验证Token有效性
|
|
|
+- 从Redis获取用户信息
|
|
|
+- 设置认证信息到SecurityContext
|
|
|
+- 继续过滤器链
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+#### ✅ 5. JwtUtil(JWT工具类)
|
|
|
+**位置**:`utils/JwtUtil.java`
|
|
|
+
|
|
|
+**必须实现的方法**:
|
|
|
+- `generateToken(Map<String, Object> claims)` - 生成Token
|
|
|
+- `getUserIdFromToken(String token)` - 从Token获取用户ID
|
|
|
+- `getUsernameFromToken(String token)` - 从Token获取用户名
|
|
|
+- `validateToken(String token)` - 验证Token
|
|
|
+- `parseToken(String token)` - 解析Token
|
|
|
+
|
|
|
+**关键点**:
|
|
|
+- 使用HMAC-SHA512算法签名
|
|
|
+- 设置过期时间
|
|
|
+- 异常处理(ExpiredJwtException等)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+#### ✅ 6. TokenService(Token服务)
|
|
|
+**位置**:`security/TokenService.java`
|
|
|
+
|
|
|
+**必须实现的方法**:
|
|
|
+- `createToken(LoginUser loginUser)` - 创建Token并存储到Redis
|
|
|
+- `getLoginUser(String token)` - 从Redis获取用户信息
|
|
|
+- `verifyToken(LoginUser loginUser)` - 刷新Token有效期
|
|
|
+- `deleteToken(String token)` - 删除Token(登出时使用)
|
|
|
+
|
|
|
+**关键点**:
|
|
|
+- Token存储在Redis中,key为 `login_tokens:{token}`
|
|
|
+- 自动刷新机制:距离过期时间小于20分钟时自动续期
|
|
|
+- 默认过期时间24小时
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+#### ✅ 7. 登录/登出Controller
|
|
|
+**位置**:`modules/system/controller/SysUserController.java`
|
|
|
+
|
|
|
+**必须实现的方法**:
|
|
|
+- `login(LoginBody loginBody)` - 登录接口
|
|
|
+- `logout()` - 登出接口
|
|
|
+
|
|
|
+**关键点**:
|
|
|
+- 使用 `AuthenticationManager.authenticate()` 进行认证
|
|
|
+- 认证成功后生成Token
|
|
|
+- 将Token存储到Redis
|
|
|
+- 返回Token和用户信息给客户端
|
|
|
+- 登出时删除Redis中的Token
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 5.2 数据访问层(Mapper)
|
|
|
+
|
|
|
+#### ✅ 8. SysUserMapper
|
|
|
+**位置**:`modules/system/mapper/SysUserMapper.java`
|
|
|
+
|
|
|
+**必须实现的方法**:
|
|
|
+```java
|
|
|
+// 根据用户名查询用户(包含角色)
|
|
|
+SysUser selectUserWithRolesAndPermissions(@Param("username") String username);
|
|
|
+
|
|
|
+// 根据用户ID查询权限列表
|
|
|
+List<String> selectPermissionsByUserId(@Param("userId") Long userId);
|
|
|
+```
|
|
|
+
|
|
|
+#### ✅ 9. SysUserMapper.xml
|
|
|
+**位置**:`modules/system/mapper/dao/SysUserMapper.xml`
|
|
|
+
|
|
|
+**必须实现的SQL**:
|
|
|
+- 用户+角色关联查询
|
|
|
+- 用户权限查询(用户→角色→菜单)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 5.3 服务层(Service)
|
|
|
+
|
|
|
+#### ✅ 10. SysUserService
|
|
|
+**位置**:`modules/system/service/SysUserService.java`
|
|
|
+
|
|
|
+**必须实现的方法**:
|
|
|
+```java
|
|
|
+// 根据用户名查询用户(包含角色和权限)
|
|
|
+SysUser getUserWithRolesAndPermissions(String username);
|
|
|
+
|
|
|
+// 根据用户ID查询权限列表
|
|
|
+List<String> getPermissionsByUserId(Long userId);
|
|
|
+
|
|
|
+// 根据用户名查询用户(不含角色)
|
|
|
+SysUser getUserByUsername(String username);
|
|
|
+
|
|
|
+// 验证密码是否匹配
|
|
|
+boolean matchesPassword(String rawPassword, String encodedPassword);
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 5.4 实体类(Entity)
|
|
|
+
|
|
|
+#### ✅ 11. SysUser
|
|
|
+**位置**:`modules/system/entity/SysUser.java`
|
|
|
+
|
|
|
+**必须包含的字段**:
|
|
|
+- `id` - 用户ID
|
|
|
+- `username` - 用户名
|
|
|
+- `password` - 密码(BCrypt加密)
|
|
|
+- `status` - 状态(0-禁用,1-正常)
|
|
|
+- `roles` - 角色列表(非数据库字段)
|
|
|
+- `permissions` - 权限列表(非数据库字段)
|
|
|
+
|
|
|
+**关键点**:
|
|
|
+- `password` 字段添加 `@JsonIgnore` 注解,防止返回给前端
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 5.5 配置文件
|
|
|
+
|
|
|
+#### ✅ 12. pom.xml
|
|
|
+**必须添加的依赖**:
|
|
|
+```xml
|
|
|
+<!-- Spring Security -->
|
|
|
+<dependency>
|
|
|
+ <groupId>org.springframework.boot</groupId>
|
|
|
+ <artifactId>spring-boot-starter-security</artifactId>
|
|
|
+</dependency>
|
|
|
+
|
|
|
+<!-- JWT -->
|
|
|
+<dependency>
|
|
|
+ <groupId>io.jsonwebtoken</groupId>
|
|
|
+ <artifactId>jjwt</artifactId>
|
|
|
+ <version>0.9.1</version>
|
|
|
+</dependency>
|
|
|
+
|
|
|
+<!-- JAXB API(JDK 11+必需) -->
|
|
|
+<dependency>
|
|
|
+ <groupId>javax.xml.bind</groupId>
|
|
|
+ <artifactId>jaxb-api</artifactId>
|
|
|
+ <version>2.3.1</version>
|
|
|
+</dependency>
|
|
|
+<dependency>
|
|
|
+ <groupId>org.glassfish.jaxb</groupId>
|
|
|
+ <artifactId>jaxb-runtime</artifactId>
|
|
|
+ <version>2.3.1</version>
|
|
|
+</dependency>
|
|
|
+
|
|
|
+<!-- Redis -->
|
|
|
+<dependency>
|
|
|
+ <groupId>org.springframework.boot</groupId>
|
|
|
+ <artifactId>spring-boot-starter-data-redis</artifactId>
|
|
|
+</dependency>
|
|
|
+```
|
|
|
+
|
|
|
+#### ✅ 13. application.yaml
|
|
|
+**必须配置的项**:
|
|
|
+```yaml
|
|
|
+spring:
|
|
|
+ redis:
|
|
|
+ host: 127.0.0.1
|
|
|
+ port: 6379
|
|
|
+ password: your-password
|
|
|
+ database: 0
|
|
|
+
|
|
|
+logging:
|
|
|
+ level:
|
|
|
+ org.springframework.security: DEBUG # 开发时开启,便于调试
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 5.6 完整实现清单总结
|
|
|
+
|
|
|
+| 序号 | 组件名称 | 位置 | 类型 | 优先级 |
|
|
|
+|-----|---------|------|------|-------|
|
|
|
+| 1 | SecurityConfig | config/ | 配置类 | 🔴 必须 |
|
|
|
+| 2 | UserDetailsServiceImpl | security/ | 服务类 | 🔴 必须 |
|
|
|
+| 3 | LoginUser | security/ | 实体类 | 🔴 必须 |
|
|
|
+| 4 | JwtAuthenticationFilter | security/ | 过滤器 | 🔴 必须 |
|
|
|
+| 5 | JwtUtil | utils/ | 工具类 | 🔴 必须 |
|
|
|
+| 6 | TokenService | security/ | 服务类 | 🔴 必须 |
|
|
|
+| 7 | SysUserController | controller/ | 控制器 | 🔴 必须 |
|
|
|
+| 8 | SysUserMapper | mapper/ | Mapper接口 | 🔴 必须 |
|
|
|
+| 9 | SysUserMapper.xml | mapper/dao/ | MyBatis XML | 🔴 必须 |
|
|
|
+| 10 | SysUserService | service/ | 服务接口 | 🔴 必须 |
|
|
|
+| 11 | SysUser | entity/ | 实体类 | 🔴 必须 |
|
|
|
+| 12 | ServiceExceptionHandler | exception/ | 异常处理 | 🟡 推荐 |
|
|
|
+| 13 | RedisCache | utils/ | Redis工具类 | 🟡 推荐 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 6. 代码示例
|
|
|
+
|
|
|
+### 6.1 登录接口完整实现
|
|
|
+
|
|
|
+```java
|
|
|
+@RestController
|
|
|
+@RequestMapping("/auth")
|
|
|
+public class SysUserController {
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private AuthenticationManager authenticationManager;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private JwtUtil jwtUtil;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private SysUserService sysUserService;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private RedisTemplate<String, String> redisTemplate;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 用户名密码登录
|
|
|
+ */
|
|
|
+ @PostMapping("/login")
|
|
|
+ public RestResult<?> login(@RequestBody LoginBody loginBody) {
|
|
|
+ try {
|
|
|
+ // 1. 使用Spring Security进行认证
|
|
|
+ Authentication authentication = authenticationManager.authenticate(
|
|
|
+ new UsernamePasswordAuthenticationToken(
|
|
|
+ loginBody.getUsername(),
|
|
|
+ loginBody.getPassword()
|
|
|
+ )
|
|
|
+ );
|
|
|
+
|
|
|
+ // 2. 认证成功,设置认证信息
|
|
|
+ SecurityContextHolder.getContext().setAuthentication(authentication);
|
|
|
+
|
|
|
+ // 3. 获取用户信息
|
|
|
+ SysUser user = sysUserService.getUserByUsername(loginBody.getUsername());
|
|
|
+
|
|
|
+ // 4. 生成JWT Token
|
|
|
+ Map<String, Object> claims = new HashMap<>();
|
|
|
+ claims.put("username", user.getUsername());
|
|
|
+ claims.put("userId", user.getId());
|
|
|
+ String token = jwtUtil.generateToken(claims);
|
|
|
+
|
|
|
+ // 5. 将Token存储到Redis(24小时过期)
|
|
|
+ redisTemplate.opsForValue().set(
|
|
|
+ "token:" + user.getId(),
|
|
|
+ token,
|
|
|
+ 24,
|
|
|
+ TimeUnit.HOURS
|
|
|
+ );
|
|
|
+
|
|
|
+ // 6. 返回结果
|
|
|
+ Map<String, Object> result = new HashMap<>();
|
|
|
+ result.put("token", token);
|
|
|
+ result.put("userInfo", user);
|
|
|
+
|
|
|
+ return RestResult.ok("登录成功", result);
|
|
|
+
|
|
|
+ } catch (AuthenticationException e) {
|
|
|
+ // 认证失败
|
|
|
+ return RestResult.error("用户名或密码错误");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 退出登录
|
|
|
+ */
|
|
|
+ @PostMapping("/logout")
|
|
|
+ public RestResult<?> logout() {
|
|
|
+ // 1. 获取当前用户
|
|
|
+ Authentication authentication = SecurityContextHolder.getContext()
|
|
|
+ .getAuthentication();
|
|
|
+
|
|
|
+ if (authentication != null) {
|
|
|
+ // 2. 删除Redis中的Token
|
|
|
+ String username = authentication.getName();
|
|
|
+ SysUser user = sysUserService.getUserByUsername(username);
|
|
|
+ if (user != null) {
|
|
|
+ redisTemplate.delete("token:" + user.getId());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 清空认证信息
|
|
|
+ SecurityContextHolder.clearContext();
|
|
|
+
|
|
|
+ return RestResult.ok("退出成功");
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 6.2 方法级权限控制
|
|
|
+
|
|
|
+```java
|
|
|
+@RestController
|
|
|
+@RequestMapping("/system/user")
|
|
|
+public class UserController {
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private SysUserService userService;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 查询用户列表
|
|
|
+ * 需要 system:user:list 权限
|
|
|
+ */
|
|
|
+ @GetMapping("/list")
|
|
|
+ @PreAuthorize("hasAuthority('system:user:list')")
|
|
|
+ public RestResult<?> list() {
|
|
|
+ List<SysUser> users = userService.list();
|
|
|
+ return RestResult.ok(users);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 新增用户
|
|
|
+ * 需要 system:user:add 权限
|
|
|
+ */
|
|
|
+ @PostMapping("/add")
|
|
|
+ @PreAuthorize("hasAuthority('system:user:add')")
|
|
|
+ public RestResult<?> add(@RequestBody SysUser user) {
|
|
|
+ userService.save(user);
|
|
|
+ return RestResult.ok("新增成功");
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 修改用户
|
|
|
+ * 需要 system:user:edit 权限
|
|
|
+ */
|
|
|
+ @PutMapping("/edit")
|
|
|
+ @PreAuthorize("hasAuthority('system:user:edit')")
|
|
|
+ public RestResult<?> edit(@RequestBody SysUser user) {
|
|
|
+ userService.updateById(user);
|
|
|
+ return RestResult.ok("修改成功");
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 删除用户
|
|
|
+ * 需要 system:user:remove 权限
|
|
|
+ */
|
|
|
+ @DeleteMapping("/remove/{id}")
|
|
|
+ @PreAuthorize("hasAuthority('system:user:remove')")
|
|
|
+ public RestResult<?> remove(@PathVariable Long id) {
|
|
|
+ userService.removeById(id);
|
|
|
+ return RestResult.ok("删除成功");
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 组合权限判断
|
|
|
+ * 需要同时拥有 system:user:list 和 system:user:export 权限
|
|
|
+ */
|
|
|
+ @GetMapping("/export")
|
|
|
+ @PreAuthorize("hasAuthority('system:user:list') and hasAuthority('system:user:export')")
|
|
|
+ public RestResult<?> export() {
|
|
|
+ // 导出Excel逻辑
|
|
|
+ return RestResult.ok("导出成功");
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 角色判断
|
|
|
+ * 需要admin角色
|
|
|
+ */
|
|
|
+ @GetMapping("/admin-only")
|
|
|
+ @PreAuthorize("hasRole('admin')")
|
|
|
+ public RestResult<?> adminOnly() {
|
|
|
+ return RestResult.ok("管理员专属功能");
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 6.3 前端按钮级权限控制
|
|
|
+
|
|
|
+#### Vue示例(使用自定义指令)
|
|
|
+
|
|
|
+```javascript
|
|
|
+// permission.js - 自定义权限指令
|
|
|
+import store from '@/store'
|
|
|
+
|
|
|
+export default {
|
|
|
+ install(Vue) {
|
|
|
+ // v-hasPermission 指令
|
|
|
+ Vue.directive('hasPermission', {
|
|
|
+ inserted(el, binding, vnode) {
|
|
|
+ const { value } = binding
|
|
|
+ const permissions = store.getters.permissions
|
|
|
+
|
|
|
+ if (value && value instanceof Array && value.length > 0) {
|
|
|
+ const hasPermission = permissions.some(permission => {
|
|
|
+ return value.includes(permission)
|
|
|
+ })
|
|
|
+
|
|
|
+ if (!hasPermission) {
|
|
|
+ // 没有权限,移除按钮
|
|
|
+ el.parentNode && el.parentNode.removeChild(el)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+```vue
|
|
|
+<!-- UserList.vue -->
|
|
|
+<template>
|
|
|
+ <div>
|
|
|
+ <el-table :data="userList">
|
|
|
+ <el-table-column prop="username" label="用户名"/>
|
|
|
+ <el-table-column label="操作">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <!-- 新增按钮 - 需要 system:user:add 权限 -->
|
|
|
+ <el-button
|
|
|
+ v-hasPermission="['system:user:add']"
|
|
|
+ @click="handleAdd()">
|
|
|
+ 新增
|
|
|
+ </el-button>
|
|
|
+
|
|
|
+ <!-- 编辑按钮 - 需要 system:user:edit 权限 -->
|
|
|
+ <el-button
|
|
|
+ v-hasPermission="['system:user:edit']"
|
|
|
+ @click="handleEdit(scope.row)">
|
|
|
+ 编辑
|
|
|
+ </el-button>
|
|
|
+
|
|
|
+ <!-- 删除按钮 - 需要 system:user:remove 权限 -->
|
|
|
+ <el-button
|
|
|
+ v-hasPermission="['system:user:remove']"
|
|
|
+ @click="handleDelete(scope.row)">
|
|
|
+ 删除
|
|
|
+ </el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+```
|
|
|
+
|
|
|
+#### React示例(使用Hook)
|
|
|
+
|
|
|
+```javascript
|
|
|
+// usePermission.js
|
|
|
+import { useSelector } from 'react-redux'
|
|
|
+
|
|
|
+export const usePermission = () => {
|
|
|
+ const permissions = useSelector(state => state.user.permissions)
|
|
|
+
|
|
|
+ const hasPermission = (permission) => {
|
|
|
+ if (Array.isArray(permission)) {
|
|
|
+ return permission.some(p => permissions.includes(p))
|
|
|
+ }
|
|
|
+ return permissions.includes(permission)
|
|
|
+ }
|
|
|
+
|
|
|
+ return { hasPermission }
|
|
|
+}
|
|
|
+
|
|
|
+// UserList.jsx
|
|
|
+import { usePermission } from '@/hooks/usePermission'
|
|
|
+
|
|
|
+const UserList = () => {
|
|
|
+ const { hasPermission } = usePermission()
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div>
|
|
|
+ <Table dataSource={userList}>
|
|
|
+ <Column title="用户名" dataIndex="username" />
|
|
|
+ <Column
|
|
|
+ title="操作"
|
|
|
+ render={(text, record) => (
|
|
|
+ <>
|
|
|
+ {/* 新增按钮 */}
|
|
|
+ {hasPermission('system:user:add') && (
|
|
|
+ <Button onClick={() => handleAdd()}>新增</Button>
|
|
|
+ )}
|
|
|
+
|
|
|
+ {/* 编辑按钮 */}
|
|
|
+ {hasPermission('system:user:edit') && (
|
|
|
+ <Button onClick={() => handleEdit(record)}>编辑</Button>
|
|
|
+ )}
|
|
|
+
|
|
|
+ {/* 删除按钮 */}
|
|
|
+ {hasPermission('system:user:remove') && (
|
|
|
+ <Button onClick={() => handleDelete(record)}>删除</Button>
|
|
|
+ )}
|
|
|
+ </>
|
|
|
+ )}
|
|
|
+ />
|
|
|
+ </Table>
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 7. 测试指南
|
|
|
+
|
|
|
+### 7.1 登录测试
|
|
|
+
|
|
|
+```bash
|
|
|
+# 1. 登录获取Token
|
|
|
+curl -X POST http://localhost:8080/auth/login \
|
|
|
+ -H "Content-Type: application/json" \
|
|
|
+ -d '{
|
|
|
+ "username": "admin",
|
|
|
+ "password": "admin123"
|
|
|
+ }'
|
|
|
+
|
|
|
+# 响应示例
|
|
|
+{
|
|
|
+ "success": true,
|
|
|
+ "message": "登录成功",
|
|
|
+ "code": 200,
|
|
|
+ "result": {
|
|
|
+ "token": "eyJhbGciOiJIUzUxMiJ9.eyJ1c2VySWQiOjEsInVzZXJuYW1lIjoiYWRtaW4iLCJleHAiOjE3MDAwMDAwMDB9.xxx",
|
|
|
+ "userInfo": {
|
|
|
+ "id": 1,
|
|
|
+ "username": "admin",
|
|
|
+ "nickname": "超级管理员"
|
|
|
+ }
|
|
|
+ },
|
|
|
+ "timestamp": 1700000000000
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 7.2 携带Token访问受保护接口
|
|
|
+
|
|
|
+```bash
|
|
|
+# 2. 使用Token访问受保护的接口
|
|
|
+curl -X GET http://localhost:8080/system/user/list \
|
|
|
+ -H "Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.xxx"
|
|
|
+
|
|
|
+# 有权限 - 返回数据
|
|
|
+{
|
|
|
+ "success": true,
|
|
|
+ "code": 200,
|
|
|
+ "result": [...]
|
|
|
+}
|
|
|
+
|
|
|
+# 无权限 - 返回403
|
|
|
+{
|
|
|
+ "success": false,
|
|
|
+ "message": "没有访问权限",
|
|
|
+ "code": 403
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 7.3 测试不同权限级别
|
|
|
+
|
|
|
+```bash
|
|
|
+# 测试超级管理员(拥有所有权限)
|
|
|
+# 1. 以admin身份登录
|
|
|
+TOKEN_ADMIN="eyJhbGciOiJIUzUxMiJ9.xxx"
|
|
|
+
|
|
|
+# 2. 测试新增用户(需要 system:user:add 权限)
|
|
|
+curl -X POST http://localhost:8080/system/user/add \
|
|
|
+ -H "Authorization: Bearer $TOKEN_ADMIN" \
|
|
|
+ -H "Content-Type: application/json" \
|
|
|
+ -d '{
|
|
|
+ "username": "test",
|
|
|
+ "password": "123456",
|
|
|
+ "nickname": "测试用户"
|
|
|
+ }'
|
|
|
+# 预期:成功
|
|
|
+
|
|
|
+
|
|
|
+# 测试普通用户(权限有限)
|
|
|
+# 1. 以user身份登录
|
|
|
+TOKEN_USER="eyJhbGciOiJIUzUxMiJ9.yyy"
|
|
|
+
|
|
|
+# 2. 测试新增用户(普通用户没有此权限)
|
|
|
+curl -X POST http://localhost:8080/system/user/add \
|
|
|
+ -H "Authorization: Bearer $TOKEN_USER" \
|
|
|
+ -H "Content-Type: application/json" \
|
|
|
+ -d '{...}'
|
|
|
+# 预期:403 没有访问权限
|
|
|
+```
|
|
|
+
|
|
|
+### 7.4 退出登录测试
|
|
|
+
|
|
|
+```bash
|
|
|
+# 3. 退出登录
|
|
|
+curl -X POST http://localhost:8080/auth/logout \
|
|
|
+ -H "Authorization: Bearer $TOKEN_ADMIN"
|
|
|
+
|
|
|
+# 响应
|
|
|
+{
|
|
|
+ "success": true,
|
|
|
+ "message": "退出成功",
|
|
|
+ "code": 200
|
|
|
+}
|
|
|
+
|
|
|
+# 4. 退出后再次访问受保护接口
|
|
|
+curl -X GET http://localhost:8080/system/user/list \
|
|
|
+ -H "Authorization: Bearer $TOKEN_ADMIN"
|
|
|
+
|
|
|
+# 预期:401 未认证(因为Token已从Redis中删除)
|
|
|
+```
|
|
|
+
|
|
|
+### 7.5 Postman测试集合
|
|
|
+
|
|
|
+创建Postman环境变量:
|
|
|
+```json
|
|
|
+{
|
|
|
+ "base_url": "http://localhost:8080",
|
|
|
+ "token": ""
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+测试脚本(自动保存Token):
|
|
|
+```javascript
|
|
|
+// 在登录接口的 Tests 标签页添加
|
|
|
+if (pm.response.code === 200) {
|
|
|
+ var jsonData = pm.response.json();
|
|
|
+ if (jsonData.success && jsonData.result.token) {
|
|
|
+ pm.environment.set("token", jsonData.result.token);
|
|
|
+ console.log("Token已保存:", jsonData.result.token);
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 8. 常见问题
|
|
|
+
|
|
|
+### 8.1 认证问题
|
|
|
+
|
|
|
+#### Q1: 登录时提示"Bad credentials",但密码确实正确?
|
|
|
+
|
|
|
+**原因**:
|
|
|
+1. 数据库中的密码不是正确的BCrypt加密值
|
|
|
+2. `UserDetailsService.loadUserByUsername()` 返回的密码为null
|
|
|
+3. 密码编码器配置不正确
|
|
|
+
|
|
|
+**解决方法**:
|
|
|
+```bash
|
|
|
+# 1. 测试密码是否匹配
|
|
|
+curl -X POST "http://localhost:8080/auth/test-password" \
|
|
|
+ -d "username=admin&password=admin123"
|
|
|
+
|
|
|
+# 2. 生成正确的BCrypt密码
|
|
|
+curl -X POST "http://localhost:8080/auth/generate-password" \
|
|
|
+ -d "password=admin123"
|
|
|
+
|
|
|
+# 3. 更新数据库密码
|
|
|
+UPDATE sys_user SET password = '$2a$10$xxx' WHERE username = 'admin';
|
|
|
+```
|
|
|
+
|
|
|
+#### Q2: SecurityConfig中应该使用 `permitAll()` 还是 `anonymous()`?
|
|
|
+
|
|
|
+**区别**:
|
|
|
+- `permitAll()`:允许所有人访问(已认证 + 未认证)
|
|
|
+- `anonymous()`:只允许匿名用户访问(已认证用户会被拒绝)
|
|
|
+
|
|
|
+**登录接口必须使用 `permitAll()`**,否则认证过程会失败!
|
|
|
+
|
|
|
+```java
|
|
|
+// ✓ 正确
|
|
|
+.antMatchers("/auth/login", "/auth/logout").permitAll()
|
|
|
+
|
|
|
+// ✗ 错误(会导致认证失败)
|
|
|
+.antMatchers("/auth/login", "/auth/logout").anonymous()
|
|
|
+```
|
|
|
+
|
|
|
+### 8.2 JWT问题
|
|
|
+
|
|
|
+#### Q3: JDK 11运行时报错:`NoClassDefFoundError: javax/xml/bind/DatatypeConverter`
|
|
|
+
|
|
|
+**原因**:Java 11移除了 `javax.xml.bind` 包
|
|
|
+
|
|
|
+**解决方法**:在 `pom.xml` 中添加JAXB依赖
|
|
|
+
|
|
|
+```xml
|
|
|
+<dependency>
|
|
|
+ <groupId>javax.xml.bind</groupId>
|
|
|
+ <artifactId>jaxb-api</artifactId>
|
|
|
+ <version>2.3.1</version>
|
|
|
+</dependency>
|
|
|
+<dependency>
|
|
|
+ <groupId>org.glassfish.jaxb</groupId>
|
|
|
+ <artifactId>jaxb-runtime</artifactId>
|
|
|
+ <version>2.3.1</version>
|
|
|
+</dependency>
|
|
|
+```
|
|
|
+
|
|
|
+#### Q4: Token验证失败,但Token确实有效?
|
|
|
+
|
|
|
+**检查项**:
|
|
|
+1. JWT密钥是否一致(secret)
|
|
|
+2. Token是否过期
|
|
|
+3. Redis中是否存在该Token
|
|
|
+4. Token格式是否正确(Bearer + 空格 + token)
|
|
|
+
|
|
|
+```bash
|
|
|
+# 正确的请求头格式
|
|
|
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.xxx
|
|
|
+
|
|
|
+# 错误示例
|
|
|
+Authorization: eyJhbGciOiJIUzUxMiJ9.xxx # 缺少Bearer前缀
|
|
|
+Authorization: BearereyJhbGciOiJIUzUxMiJ9.xxx # 缺少空格
|
|
|
+```
|
|
|
+
|
|
|
+### 8.3 权限问题
|
|
|
+
|
|
|
+#### Q5: @PreAuthorize 注解不生效?
|
|
|
+
|
|
|
+**原因**:未开启方法级权限验证
|
|
|
+
|
|
|
+**解决方法**:在SecurityConfig上添加注解
|
|
|
+
|
|
|
+```java
|
|
|
+@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
|
|
|
+public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
|
|
+ // ...
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### Q6: 用户明明有权限,但还是返回403?
|
|
|
+
|
|
|
+**检查步骤**:
|
|
|
+1. 确认权限标识是否正确(区分大小写)
|
|
|
+2. 确认数据库中用户-角色-权限关联是否正确
|
|
|
+3. 确认 `getPermissionsByUserId()` 是否正确返回权限列表
|
|
|
+4. 确认LoginUser的 `getAuthorities()` 方法是否正确转换权限
|
|
|
+
|
|
|
+```java
|
|
|
+// 调试:打印用户权限
|
|
|
+@GetMapping("/debug/permissions")
|
|
|
+public RestResult<?> getMyPermissions() {
|
|
|
+ Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
|
|
+ LoginUser loginUser = (LoginUser) auth.getPrincipal();
|
|
|
+ return RestResult.ok(loginUser.getPermissions());
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 8.4 Redis问题
|
|
|
+
|
|
|
+#### Q7: Token生成成功,但后续请求获取不到用户信息?
|
|
|
+
|
|
|
+**原因**:Redis连接或序列化问题
|
|
|
+
|
|
|
+**检查项**:
|
|
|
+1. Redis服务是否正常运行
|
|
|
+2. Redis连接配置是否正确
|
|
|
+3. RedisTemplate序列化配置
|
|
|
+
|
|
|
+```java
|
|
|
+@Configuration
|
|
|
+public class RedisConfig {
|
|
|
+
|
|
|
+ @Bean
|
|
|
+ public RedisTemplate<String, Object> redisTemplate(
|
|
|
+ RedisConnectionFactory factory) {
|
|
|
+ RedisTemplate<String, Object> template = new RedisTemplate<>();
|
|
|
+ template.setConnectionFactory(factory);
|
|
|
+
|
|
|
+ // 使用Jackson序列化
|
|
|
+ Jackson2JsonRedisSerializer<Object> serializer =
|
|
|
+ new Jackson2JsonRedisSerializer<>(Object.class);
|
|
|
+
|
|
|
+ template.setKeySerializer(new StringRedisSerializer());
|
|
|
+ template.setValueSerializer(serializer);
|
|
|
+ template.setHashKeySerializer(new StringRedisSerializer());
|
|
|
+ template.setHashValueSerializer(serializer);
|
|
|
+
|
|
|
+ return template;
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 8.5 跨域问题
|
|
|
+
|
|
|
+#### Q8: 前后端分离时,登录接口报跨域错误?
|
|
|
+
|
|
|
+**解决方法**:在SecurityConfig中配置CORS
|
|
|
+
|
|
|
+```java
|
|
|
+@Override
|
|
|
+protected void configure(HttpSecurity http) throws Exception {
|
|
|
+ http
|
|
|
+ .cors() // 启用CORS
|
|
|
+ .and()
|
|
|
+ .csrf().disable()
|
|
|
+ // ...
|
|
|
+}
|
|
|
+
|
|
|
+// 配置CORS规则
|
|
|
+@Bean
|
|
|
+public CorsConfigurationSource corsConfigurationSource() {
|
|
|
+ CorsConfiguration configuration = new CorsConfiguration();
|
|
|
+ configuration.setAllowedOrigins(Arrays.asList("http://localhost:3000"));
|
|
|
+ configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
|
|
|
+ configuration.setAllowedHeaders(Arrays.asList("*"));
|
|
|
+ configuration.setAllowCredentials(true);
|
|
|
+
|
|
|
+ UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
|
|
+ source.registerCorsConfiguration("/**", configuration);
|
|
|
+ return source;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 9. 最佳实践
|
|
|
+
|
|
|
+### 9.1 密码安全
|
|
|
+
|
|
|
+1. **永远使用BCrypt加密密码**
|
|
|
+ ```java
|
|
|
+ // ✓ 正确
|
|
|
+ String encodedPassword = passwordEncoder.encode(rawPassword);
|
|
|
+
|
|
|
+ // ✗ 错误
|
|
|
+ String encodedPassword = MD5(rawPassword); // MD5不安全
|
|
|
+ ```
|
|
|
+
|
|
|
+2. **密码字段添加@JsonIgnore**
|
|
|
+ ```java
|
|
|
+ @JsonIgnore
|
|
|
+ private String password;
|
|
|
+ ```
|
|
|
+
|
|
|
+3. **密码复杂度要求**
|
|
|
+ - 最小长度8位
|
|
|
+ - 包含大小写字母、数字、特殊字符
|
|
|
+ - 定期更换密码
|
|
|
+
|
|
|
+### 9.2 Token安全
|
|
|
+
|
|
|
+1. **Token存储位置**
|
|
|
+ - ✓ 前端:localStorage 或 sessionStorage
|
|
|
+ - ✗ 前端:Cookie(容易被CSRF攻击)
|
|
|
+ - ✓ 后端:Redis(可快速失效)
|
|
|
+
|
|
|
+2. **Token刷新策略**
|
|
|
+ ```java
|
|
|
+ // 距离过期时间小于20分钟时自动刷新
|
|
|
+ if (expireTime - currentTime <= 20 * 60 * 1000) {
|
|
|
+ redisCache.expire(tokenKey, 24 * 60 * 60 * 1000);
|
|
|
+ }
|
|
|
+ ```
|
|
|
+
|
|
|
+3. **Token密钥管理**
|
|
|
+ ```yaml
|
|
|
+ # application.yaml - 生产环境从环境变量读取
|
|
|
+ jwt:
|
|
|
+ secret: ${JWT_SECRET:your-default-secret-key}
|
|
|
+ expiration: 86400000 # 24小时
|
|
|
+ ```
|
|
|
+
|
|
|
+### 9.3 权限设计
|
|
|
+
|
|
|
+1. **权限粒度**
|
|
|
+ - 粗粒度:模块级(system:user)
|
|
|
+ - 中粒度:功能级(system:user:list)
|
|
|
+ - 细粒度:按钮级(system:user:add)
|
|
|
+
|
|
|
+2. **权限命名规范**
|
|
|
+ ```
|
|
|
+ 模块:功能:操作
|
|
|
+ system:user:add # 系统管理-用户管理-新增
|
|
|
+ system:user:edit # 系统管理-用户管理-编辑
|
|
|
+ report:sales:export # 报表管理-销售报表-导出
|
|
|
+ ```
|
|
|
+
|
|
|
+3. **数据权限控制**
|
|
|
+ ```java
|
|
|
+ // 根据用户所属部门过滤数据
|
|
|
+ @DataScope(deptAlias = "d", userAlias = "u")
|
|
|
+ @PreAuthorize("hasAuthority('system:user:list')")
|
|
|
+ public List<SysUser> list() {
|
|
|
+ return userService.selectUserList();
|
|
|
+ }
|
|
|
+ ```
|
|
|
+
|
|
|
+### 9.4 日志记录
|
|
|
+
|
|
|
+```java
|
|
|
+@Aspect
|
|
|
+@Component
|
|
|
+public class LoginLogAspect {
|
|
|
+
|
|
|
+ @Around("@annotation(com.xxx.annotation.LoginLog)")
|
|
|
+ public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
|
|
|
+ long startTime = System.currentTimeMillis();
|
|
|
+
|
|
|
+ try {
|
|
|
+ Object result = joinPoint.proceed();
|
|
|
+ long time = System.currentTimeMillis() - startTime;
|
|
|
+
|
|
|
+ // 记录登录成功日志
|
|
|
+ saveLog(joinPoint, time, "SUCCESS");
|
|
|
+
|
|
|
+ return result;
|
|
|
+ } catch (Exception e) {
|
|
|
+ // 记录登录失败日志
|
|
|
+ saveLog(joinPoint, 0, "FAIL");
|
|
|
+ throw e;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 10. 性能优化
|
|
|
+
|
|
|
+### 10.1 Redis优化
|
|
|
+
|
|
|
+1. **使用Redis Pipeline批量操作**
|
|
|
+ ```java
|
|
|
+ redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
|
|
|
+ connection.set("key1".getBytes(), "value1".getBytes());
|
|
|
+ connection.set("key2".getBytes(), "value2".getBytes());
|
|
|
+ return null;
|
|
|
+ });
|
|
|
+ ```
|
|
|
+
|
|
|
+2. **合理设置过期时间**
|
|
|
+ - Token:24小时
|
|
|
+ - 验证码:5分钟
|
|
|
+ - 短期缓存:1小时
|
|
|
+
|
|
|
+### 10.2 数据库优化
|
|
|
+
|
|
|
+1. **为常用查询添加索引**
|
|
|
+ ```sql
|
|
|
+ -- 用户名索引
|
|
|
+ CREATE UNIQUE INDEX idx_username ON sys_user(username);
|
|
|
+
|
|
|
+ -- 用户-角色关联索引
|
|
|
+ CREATE INDEX idx_user_id ON sys_user_role(user_id);
|
|
|
+ CREATE INDEX idx_role_id ON sys_user_role(role_id);
|
|
|
+ ```
|
|
|
+
|
|
|
+2. **使用MyBatis二级缓存**
|
|
|
+ ```xml
|
|
|
+ <!-- SysUserMapper.xml -->
|
|
|
+ <cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>
|
|
|
+ ```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 11. 附录
|
|
|
+
|
|
|
+### 11.1 完整项目结构
|
|
|
+
|
|
|
+```
|
|
|
+pacsonline_new/
|
|
|
+├── src/main/java/com/zskk/pacsonline/
|
|
|
+│ ├── config/
|
|
|
+│ │ ├── SecurityConfig.java # Security配置
|
|
|
+│ │ └── RedisConfig.java # Redis配置
|
|
|
+│ ├── security/
|
|
|
+│ │ ├── UserDetailsServiceImpl.java # 用户详情服务
|
|
|
+│ │ ├── LoginUser.java # 用户认证信息
|
|
|
+│ │ ├── JwtAuthenticationFilter.java # JWT过滤器
|
|
|
+│ │ └── TokenService.java # Token服务
|
|
|
+│ ├── utils/
|
|
|
+│ │ ├── JwtUtil.java # JWT工具类
|
|
|
+│ │ └── RedisCache.java # Redis工具类
|
|
|
+│ ├── modules/system/
|
|
|
+│ │ ├── controller/
|
|
|
+│ │ │ └── SysUserController.java # 用户控制器
|
|
|
+│ │ ├── service/
|
|
|
+│ │ │ ├── SysUserService.java # 用户服务接口
|
|
|
+│ │ │ └── impl/
|
|
|
+│ │ │ └── SysUserServiceImpl.java
|
|
|
+│ │ ├── mapper/
|
|
|
+│ │ │ ├── SysUserMapper.java # 用户Mapper接口
|
|
|
+│ │ │ └── dao/
|
|
|
+│ │ │ └── SysUserMapper.xml # MyBatis XML
|
|
|
+│ │ └── entity/
|
|
|
+│ │ ├── SysUser.java # 用户实体
|
|
|
+│ │ ├── SysRole.java # 角色实体
|
|
|
+│ │ └── SysMenu.java # 菜单实体
|
|
|
+│ └── component/
|
|
|
+│ └── exception/
|
|
|
+│ └── ServiceExceptionHandler.java # 全局异常处理
|
|
|
+├── src/main/resources/
|
|
|
+│ ├── application.yaml # 配置文件
|
|
|
+│ └── mapper/
|
|
|
+└── doc/
|
|
|
+ └── security_tables.sql # 数据库表SQL
|
|
|
+```
|
|
|
+
|
|
|
+### 11.2 参考资料
|
|
|
+
|
|
|
+- [Spring Security官方文档](https://docs.spring.io/spring-security/reference/)
|
|
|
+- [JWT官方网站](https://jwt.io/)
|
|
|
+- [BCrypt密码加密](https://www.baeldung.com/spring-security-registration-password-encoding-bcrypt)
|
|
|
+- [Spring Security架构](https://spring.io/guides/topicals/spring-security-architecture/)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 12. 总结
|
|
|
+
|
|
|
+本文档详细介绍了基于Spring Security + JWT的权限认证系统的完整实现方案,涵盖了从数据库设计到代码实现的所有关键环节。
|
|
|
+
|
|
|
+**核心要点回顾**:
|
|
|
+
|
|
|
+1. **7个核心类必须实现**:SecurityConfig、UserDetailsServiceImpl、LoginUser、JwtAuthenticationFilter、JwtUtil、TokenService、登录Controller
|
|
|
+
|
|
|
+2. **RBAC权限模型**:用户-角色-权限三级关联
|
|
|
+
|
|
|
+3. **三级权限控制**:接口级(URL)、方法级(@PreAuthorize)、按钮级(前端控制)
|
|
|
+
|
|
|
+4. **关键配置**:
|
|
|
+ - 登录接口使用 `permitAll()` 而非 `anonymous()`
|
|
|
+ - JDK 11+ 需要添加JAXB依赖
|
|
|
+ - 密码必须使用BCrypt加密
|
|
|
+ - Token存储在Redis中,支持自动刷新
|
|
|
+
|
|
|
+5. **安全最佳实践**:
|
|
|
+ - 密码加@JsonIgnore注解
|
|
|
+ - Token密钥从环境变量读取
|
|
|
+ - 记录登录日志
|
|
|
+ - 合理设置Token过期时间
|
|
|
+
|
|
|
+遵循本文档的实现方案,可以快速搭建一个安全、可靠、易扩展的权限认证系统。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+**文档版本**:v1.0
|
|
|
+**最后更新**:2025-10-29
|
|
|
+**作者**:gengjunfang
|
|
|
+**项目地址**:pacsonline_new
|