角色权限管理
本文讲解系统的角色权限体系,包括角色管理、权限分配和实际应用场景。
📊 RBAC 权限模型
系统采用 RBAC (Role-Based Access Control) 基于角色的访问控制:
用户 (User) ← 拥有 → 角色 (Role) ← 包含 → 权限 (Permission)
例如:
- 用户"张三" 拥有 "编辑" 角色
- "编辑" 角色 拥有 "文章编写"、"文章修改" 权限
- 因此张三可以编写和修改文章
数据模型
erDiagram
SYS_USER ||--o{ SYS_USER_ROLE : has
SYS_ROLE ||--o{ SYS_USER_ROLE : belongs_to
SYS_USER {
bigint id PK
string username
string password
int status
}
SYS_ROLE {
bigint id PK
string role_name
string role_key
string description
int status
}
SYS_USER_ROLE {
bigint user_id FK
bigint role_id FK
}
🎭 系统角色
内置角色
| 角色 Key | 角色名称 | 权限范围 | 用途 |
|---|---|---|---|
ADMIN | 超级管理员 | 所有权限 | 系统管理、用户管理 |
USER | 普通用户 | 基础功能 | 浏览文章、评论 |
EDITOR | 编辑 | 内容管理 | 编写、修改文章 |
GUEST | 访客 | 只读 | 仅浏览公开内容 |
角色数据结构
@Data
@TableName("sys_role")
public class SysRole {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private String roleName; // 角色名称:管理员
private String roleKey; // 角色标识:ADMIN
private String description; // 描述:系统管理员
private Integer status; // 状态:0=禁用, 1=启用
// 公共字段
private Integer version;
private Long createBy;
private LocalDateTime createTime;
private Long updateBy;
private LocalDateTime updateTime;
private Integer isDeleted;
}
👥 用户角色关联
数据表设计
CREATE TABLE `sys_user_role` (
`user_id` BIGINT NOT NULL COMMENT '用户ID',
`role_id` BIGINT NOT NULL COMMENT '角色ID',
PRIMARY KEY (`user_id`, `role_id`), -- 联合主键
KEY `idx_role_id` (`role_id`)
) COMMENT='用户角色关联表';
关联示例
user_id | role_id | 说明
--------|---------|------------------
1 | 1 | 用户1 拥有角色1 (ADMIN)
1 | 2 | 用户1 同时拥有角色2 (USER) - 一个用户可以有多个角色
2 | 2 | 用户2 拥有角色2 (USER)
3 | 3 | 用户3 拥有角色3 (EDITOR)
🔐 权限控制实现
1. 注册时分配默认角色
@Override
@Transactional
public UserVO register(RegisterDTO registerDTO) {
// ... 创建用户 ...
// ===== 分配默认角色 =====
SysRole defaultRole = roleMapper.selectOne(
new LambdaQueryWrapper<SysRole>()
.eq(SysRole::getRoleKey, RoleConstants.DEFAULT_USER_ROLE) // "USER"
);
if (defaultRole != null) {
// 插入用户角色关联
userMapper.assignRole(user.getId(), defaultRole.getId());
}
return userConverter.entityToVo(user);
}
2. 登录时加载用户角色
@Override
public LoginVO login(LoginDTO loginDTO) {
// ... 验证用户名密码 ...
// ===== 查询用户角色 =====
List<String> roleKeys = getUserRoleKeys(user.getId());
// 返回: ["ADMIN", "USER"]
// ===== 转换为 Spring Security 权限 =====
List<SimpleGrantedAuthority> authorities = roleKeys.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role))
.collect(Collectors.toList());
// 转换后: [ROLE_ADMIN, ROLE_USER]
// ===== 构建 UserDetails =====
UserDetails userDetails = User.builder()
.username(user.getUsername())
.password(user.getPassword())
.authorities(authorities) // ⭐ 设置权限
.build();
// ===== 生成 JWT Token(包含角色信息)=====
String token = jwtTokenProvider.generateToken(userDetails, user.getId());
return loginVO;
}
3. 接口权限验证
@RestController
@RequestMapping("/api/v1/users")
public class UserController {
// ===== 无需权限(所有已登录用户)=====
@GetMapping("/me")
public Result<UserVO> getCurrentUser() {
// Token 有效即可访问
}
// ===== 需要 ADMIN 角色 =====
@GetMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')") // ⭐ 权限注解
public Result<UserVO> getUserById(@PathVariable Long id) {
// 1. Spring Security 检查当前用户是否有 ROLE_ADMIN
// 2. 没有则抛出 AccessDeniedException (403)
// 3. 有则继续执行
}
// ===== 需要多个角色之一 =====
@PostMapping
@PreAuthorize("hasAnyRole('ADMIN', 'MANAGER')")
public Result<Void> createUser(@RequestBody UserDTO dto) {
// ADMIN 或 MANAGER 都可以访问
}
// ===== 复杂权限表达式 =====
@PutMapping("/{id}")
@PreAuthorize("hasRole('ADMIN') or #id == principal")
public Result<Void> updateUser(@PathVariable Long id) {
// ADMIN 可以修改任何人
// 普通用户只能修改自己(id == 当前用户ID)
}
}
🚀 角色管理 API
1. 创建角色
POST /api/v1/roles
Authorization: Bearer {admin_token}
Content-Type: application/json
{
"roleName": "编辑",
"roleKey": "EDITOR",
"description": "文章编辑人员",
"status": 1
}
响应:
{
"code": 0,
"data": {
"id": 4,
"roleName": "编辑",
"roleKey": "EDITOR",
"description": "文章编辑人员",
"status": 1
}
}
2. 分配角色给用户
POST /api/v1/roles/{roleId}/users/{userId}
Authorization: Bearer {admin_token}
代码实现:
@PostMapping("/{roleId}/users/{userId}")
@PreAuthorize("hasRole('ADMIN')")
public Result<Void> assignRoleToUser(
@PathVariable Long roleId,
@PathVariable Long userId) {
// 1. 插入用户角色关联
userMapper.assignRole(userId, roleId);
// 2. 清除该用户的角色缓存
userService.evictUserRolesCache(userId);
return Result.success();
}
3. 移除用户角色
DELETE /api/v1/roles/{roleId}/users/{userId}
Authorization: Bearer {admin_token}
💾 角色缓存策略
为什么需要缓存?
没有缓存的问题:
- 每次请求都查数据库获取角色
- 高并发下数据库压力大
- 响应时间慢
使用缓存的好处:
- 第一次查数据库,后续读 Redis
- 响应时间从 50ms 降到 5ms
- 减少 90% 的数据库查询
缓存实现
/**
* 获取用户角色(带缓存)
*
* 缓存配置:
* - 缓存名:user:roles
* - 缓存键:{userId}
* - 过期时间:30 分钟
*/
@Cacheable(value = "user:roles", key = "#userId")
public List<String> getUserRoleKeys(Long userId) {
// 1. Spring 先检查 Redis 是否有缓存
// 2. 有缓存直接返回
// 3. 无缓存执行方法并存入 Redis
List<SysRole> roles = userMapper.selectRolesByUserId(userId);
return roles.stream()
.map(role -> "ROLE_" + role.getRoleKey())
.collect(Collectors.toList());
}
缓存失效
/**
* 失效用户角色缓存
*
* 调用时机:
* 1. 分配角色时
* 2. 移除角色时
* 3. 删除用户时
*/
@CacheEvict(value = "user:roles", key = "#userId")
public void evictUserRolesCache(Long userId) {
// Spring 自动删除 Redis 中的缓存
// key = "user:roles::{userId}"
}
缓存键示例
Redis 中的存储:
KEY: user:roles::1
VALUE: ["ROLE_ADMIN", "ROLE_USER"]
TTL: 1800 秒 (30 分钟)
KEY: user:roles::2
VALUE: ["ROLE_USER"]
TTL: 1800 秒
🔍 权限检查原理
Spring Security 执行流程
flowchart TD
A[请求到达] --> B{解析 JWT Token}
B -->|Token 无效| C[返回 401]
B -->|Token 有效| D[提取用户角色]
D --> E{检查 @PreAuthorize}
E -->|无注解| F[允许访问]
E -->|有注解| G{评估权限表达式}
G -->|hasRole 'ADMIN'| H{用户有 ROLE_ADMIN?}
H -->|是| F
H -->|否| I[返回 403]
G -->|hasAnyRole| J{用户有任一角色?}
J -->|是| F
J -->|否| I
F --> K[执行 Controller 方法]
权限表达式语言 (SpEL)
// ===== 1. 基础角色检查 =====
@PreAuthorize("hasRole('ADMIN')")
// 等价于:authentication.authorities.contains('ROLE_ADMIN')
// ===== 2. 多角色检查 =====
@PreAuthorize("hasAnyRole('ADMIN', 'MANAGER')")
// 等价于:有 ROLE_ADMIN 或 ROLE_MANAGER 之一
// ===== 3. 权限检查 =====
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
// 与 hasRole 类似,但不自动添加 ROLE_ 前缀
// ===== 4. 逻辑运算 =====
@PreAuthorize("hasRole('ADMIN') and hasRole('MANAGER')")
@PreAuthorize("hasRole('ADMIN') or hasRole('USER')")
@PreAuthorize("!hasRole('GUEST')")
// ===== 5. 参数引用 =====
@PreAuthorize("#id == principal") // principal 是当前用户 ID
@PreAuthorize("#userId == authentication.principal")
// ===== 6. 方法调用 =====
@PreAuthorize("@customPermissionEvaluator.check(authentication, #id)")
🎯 实战场景
场景 1:用户只能修改自己的资料
@PutMapping("/me")
public Result<Void> updateCurrentUser(@RequestBody UserDTO dto) {
// 方案1:从 Token 获取当前用户 ID
Long userId = SecurityUtils.getCurrentUserId();
dto.setId(userId); // 强制设置为当前用户
userService.updateByDto(dto);
return Result.success();
}
场景 2:管理员可以查看所有用户,普通用户只能查自己
@GetMapping("/{id}")
@PreAuthorize("hasRole('ADMIN') or #id == principal")
public Result<UserVO> getUserById(@PathVariable Long id) {
// ADMIN: 可以查任何 ID
// 普通用户: 只能查 id == 自己的 ID
Optional<UserVO> userVO = userService.getVoById(id);
return userVO.map(Result::success)
.orElseGet(() -> Result.error(404, "用户不存在"));
}
场景 3:编辑只能修改自己的文章
@PutMapping("/articles/{id}")
@PreAuthorize("hasRole('ADMIN') or @articleService.isAuthor(#id, principal)")
public Result<Void> updateArticle(
@PathVariable Long id,
@RequestBody ArticleDTO dto) {
// ADMIN: 可以修改任何文章
// EDITOR: 只能修改自己的文章(通过自定义方法检查)
articleService.updateById(id, dto);
return Result.success();
}
🛡️ 最佳实践
1. 角色命名规范
✅ 推荐:
- ADMIN (全大写)
- USER
- EDITOR
- VIEWER
❌ 避免:
- admin (小写)
- Admin (混合)
- ROLE_ADMIN (代码中会自动添加 ROLE_ 前缀)
2. 权限粒度
✅ 粗粒度(适合大多数场景):
- 基于角色:ADMIN、USER、EDITOR
❌ 过细粒度(增加复杂度):
- 基于操作:CREATE_USER、UPDATE_USER、DELETE_USER
- 除非必要,否则不推荐
3. 默认拒绝原则
// ✅ 推荐:默认需要认证
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll() // 明确允许
.anyRequest().authenticated() // 其他都需要认证
);
// ❌ 不推荐:默认允许
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/admin/**").authenticated()
.anyRequest().permitAll() // 危险!容易遗漏
);
📚 相关文档
- 用户管理详解 - 用户 CRUD
- 认证授权详解 - JWT Token
- Security 配置 - 安全架构