跳到主要内容

角色权限管理

本文讲解系统的角色权限体系,包括角色管理、权限分配和实际应用场景。

📊 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() // 危险!容易遗漏
);

📚 相关文档