Spring Security 权限控制深度实战——RBAC 模型落地的完整代码实现
Spring Security 权限控制深度实战——RBAC 模型落地的完整代码实现
适读人群:需要在Spring Boot项目中实现细粒度权限控制的Java后端开发者 | 阅读时长:约19分钟 | 核心价值:从数据库设计到代码实现,完整落地RBAC权限模型
一个让我加班到凌晨的权限需求
2022年做一个SaaS系统,运营提了需求:不同角色的用户看到的菜单不一样,能操作的按钮也不一样,而且管理员可以自定义角色、自定义每个角色能看什么、能做什么。
我当时按照常规思路做:Controller上加@PreAuthorize("hasRole('ADMIN')"),写死角色。
上线一周后,运营又来了:我想给客服加一个权限,让他们能看用户列表但不能编辑。
我改代码,上线。
两天后又来了:我想把"导出功能"单独拆成一个权限,部分用户可以看数据但不能导出。
我意识到:这种频繁变更的权限需求,不能靠改代码解决。必须把权限数据化,做成可配置的RBAC系统。
那个周末,我重新设计了整个权限系统。
RBAC 模型设计
RBAC(Role-Based Access Control)的核心思想:
- 用户 被分配给 角色
- 角色 被赋予 权限
- 用户 通过 角色 间接获得 权限
数据库表设计
-- 用户表
CREATE TABLE t_user (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(100) NOT NULL,
email VARCHAR(100),
status TINYINT NOT NULL DEFAULT 1, -- 1:正常 0:禁用
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- 角色表
CREATE TABLE t_role (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
role_code VARCHAR(50) NOT NULL UNIQUE, -- ADMIN, USER, VIEWER
role_name VARCHAR(50) NOT NULL, -- 管理员、普通用户、访客
description VARCHAR(200),
status TINYINT NOT NULL DEFAULT 1
);
-- 权限表(细粒度权限点)
CREATE TABLE t_permission (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
perm_code VARCHAR(100) NOT NULL UNIQUE, -- user:list, user:create, order:export
perm_name VARCHAR(50) NOT NULL, -- 用户列表, 创建用户, 导出订单
resource_type VARCHAR(50), -- USER, ORDER, REPORT
action VARCHAR(50), -- LIST, CREATE, UPDATE, DELETE, EXPORT
description VARCHAR(200),
parent_id BIGINT, -- 支持权限分组
sort_order INT DEFAULT 0
);
-- 用户-角色关联(多对多)
CREATE TABLE t_user_role (
user_id BIGINT NOT NULL,
role_id BIGINT NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (user_id, role_id)
);
-- 角色-权限关联(多对多)
CREATE TABLE t_role_permission (
role_id BIGINT NOT NULL,
perm_id BIGINT NOT NULL,
PRIMARY KEY (role_id, perm_id)
);
-- 创建索引
CREATE INDEX idx_user_role_user ON t_user_role(user_id);
CREATE INDEX idx_role_perm_role ON t_role_permission(role_id);Spring Security 集成
自定义 UserDetails 实现
/**
* 扩展UserDetails,携带权限码列表
*/
public class LoginUser implements UserDetails {
private final Long userId;
private final String username;
private final String password;
private final List<String> roles;
private final Set<String> permissions; // 所有权限码
private final String tenantId;
private final boolean enabled;
// 将权限码转换为Spring Security的GrantedAuthority
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> authorities = new ArrayList<>();
// 添加角色(Spring Security约定:角色以ROLE_开头)
roles.forEach(role ->
authorities.add(new SimpleGrantedAuthority("ROLE_" + role))
);
// 添加权限码(直接作为authority)
permissions.forEach(perm ->
authorities.add(new SimpleGrantedAuthority(perm))
);
return authorities;
}
// 其他UserDetails方法
@Override public String getPassword() { return password; }
@Override public String getUsername() { return username; }
@Override public boolean isAccountNonExpired() { return true; }
@Override public boolean isAccountNonLocked() { return true; }
@Override public boolean isCredentialsNonExpired() { return true; }
@Override public boolean isEnabled() { return enabled; }
// Getter方法
public Long getUserId() { return userId; }
public String getTenantId() { return tenantId; }
public Set<String> getPermissions() { return permissions; }
}
/**
* 从数据库加载用户及其权限
*/
@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserMapper userMapper;
private final RoleMapper roleMapper;
private final PermissionMapper permissionMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.selectByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用户不存在: " + username);
}
if (user.getStatus() == 0) {
throw new DisabledException("账号已被禁用");
}
// 加载用户角色
List<String> roles = roleMapper.selectRoleCodesByUserId(user.getId());
// 加载所有权限码(通过角色关联)
Set<String> permissions = new HashSet<>(
permissionMapper.selectPermCodesByUserId(user.getId())
);
return new LoginUser(
user.getId(), user.getUsername(), user.getPassword(),
roles, permissions, user.getTenantId(), user.getStatus() == 1
);
}
}权限校验 Bean(核心)
/**
* 自定义权限校验Bean,用于@PreAuthorize中的SpEL表达式
* 用法:@PreAuthorize("@perm.has('user:create')")
*/
@Component("perm")
public class PermissionChecker {
/**
* 检查当前用户是否有指定权限
*/
public boolean has(String permCode) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth == null || !auth.isAuthenticated()) return false;
return auth.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.anyMatch(a -> a.equals(permCode));
}
/**
* 检查当前用户是否有任意一个指定权限(OR)
*/
public boolean hasAny(String... permCodes) {
return Arrays.stream(permCodes).anyMatch(this::has);
}
/**
* 检查当前用户是否同时拥有所有指定权限(AND)
*/
public boolean hasAll(String... permCodes) {
return Arrays.stream(permCodes).allMatch(this::has);
}
/**
* 检查当前用户是否有指定角色
*/
public boolean hasRole(String roleCode) {
return has("ROLE_" + roleCode);
}
/**
* 数据权限:检查是否有权限访问指定数据(自己的或管理员才能访问全部)
*/
public boolean canAccessData(Long dataOwnerId) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (!(auth.getPrincipal() instanceof LoginUser loginUser)) return false;
// 管理员可以访问所有数据
if (has("ROLE_ADMIN")) return true;
// 普通用户只能访问自己的数据
return loginUser.getUserId().equals(dataOwnerId);
}
}完整的权限控制示例
/**
* 用户管理Controller - 展示完整的RBAC权限控制
*/
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
/**
* 查询用户列表 - 需要 user:list 权限
*/
@GetMapping
@PreAuthorize("@perm.has('user:list')")
public PageResult<UserVO> listUsers(UserQueryDTO query) {
return userService.queryUsers(query);
}
/**
* 查看用户详情 - 需要 user:detail 权限,且只能查看自己或管理员才能查所有
*/
@GetMapping("/{userId}")
@PreAuthorize("@perm.has('user:detail') and @perm.canAccessData(#userId)")
public UserVO getUserDetail(@PathVariable Long userId) {
return userService.getUserDetail(userId);
}
/**
* 创建用户 - 需要 user:create 权限
*/
@PostMapping
@PreAuthorize("@perm.has('user:create')")
@OperationLog(type = "USER_CREATE", desc = "创建用户")
public UserVO createUser(@RequestBody @Valid CreateUserDTO dto) {
return userService.createUser(dto);
}
/**
* 更新用户 - 需要 user:update 权限
*/
@PutMapping("/{userId}")
@PreAuthorize("@perm.has('user:update')")
public UserVO updateUser(@PathVariable Long userId,
@RequestBody @Valid UpdateUserDTO dto) {
return userService.updateUser(userId, dto);
}
/**
* 删除用户 - 需要 user:delete 权限,且不能删除自己
*/
@DeleteMapping("/{userId}")
@PreAuthorize("@perm.has('user:delete')")
public void deleteUser(@PathVariable Long userId,
@AuthenticationPrincipal LoginUser currentUser) {
if (currentUser.getUserId().equals(userId)) {
throw new BusinessException("不能删除自己的账号");
}
userService.deleteUser(userId);
}
/**
* 导出用户数据 - 需要同时有 user:list 和 user:export 两个权限
*/
@GetMapping("/export")
@PreAuthorize("@perm.hasAll('user:list', 'user:export')")
public void exportUsers(HttpServletResponse response) {
userService.exportUsers(response);
}
/**
* 重置密码 - 需要是管理员角色,或者是本人
*/
@PostMapping("/{userId}/reset-password")
@PreAuthorize("@perm.hasRole('ADMIN') or @perm.canAccessData(#userId)")
public void resetPassword(@PathVariable Long userId,
@RequestBody ResetPasswordDTO dto) {
userService.resetPassword(userId, dto);
}
}权限缓存(避免每次请求都查DB)
/**
* 带缓存的UserDetailsService实现
* 权限数据缓存10分钟,管理员修改权限后可手动刷新
*/
@Service
@RequiredArgsConstructor
@CacheConfig(cacheNames = "user-permissions")
public class CachedUserDetailsService implements UserDetailsService {
private final UserDetailsServiceImpl delegate;
private final RedisTemplate<String, Object> redisTemplate;
@Override
@Cacheable(key = "#username", unless = "#result == null")
public UserDetails loadUserByUsername(String username) {
return delegate.loadUserByUsername(username);
}
/**
* 权限变更后,清除该用户的缓存
* 在RoleService.assignPermission()等变更方法后调用
*/
@CacheEvict(key = "#username")
public void evictUserCache(String username) {
// 缓存被清除,下次请求会重新从DB加载
}
/**
* 批量清除(如某角色权限被修改时,清除所有该角色用户的缓存)
*/
public void evictRoleUsersCache(Long roleId) {
List<String> usernames = userMapper.selectUsernamesByRoleId(roleId);
usernames.forEach(this::evictUserCache);
}
}三个踩坑实录
坑一:@PreAuthorize 注解在 Controller 内部调用无效
现象: Controller A有方法调用了同一类中另一个带@PreAuthorize的方法,权限注解失效,无权限用户也能执行。
原因: Spring的AOP代理是基于动态代理实现的,同一个Bean的内部方法调用不经过代理,@PreAuthorize的AOP切面不会执行。
解法: 把需要保护的方法移到另一个Bean,或者通过注入自身的代理对象来调用:
@Service
public class UserService {
@Autowired
@Lazy // 避免循环依赖
private UserService self;
public void doSomething() {
self.protectedMethod(); // 通过代理调用,@PreAuthorize 生效
}
@PreAuthorize("@perm.has('user:create')")
public void protectedMethod() { ... }
}坑二:权限码大小写不一致导致授权失败
现象: 数据库里存的是User:List,代码里写的是user:list,@PreAuthorize("@perm.has('user:list')")永远返回false。
原因: Spring Security的GrantedAuthority是区分大小写的,user:list和User:List是不同的authority。
解法: 统一规范权限码格式,强制全小写(或全大写),在存入数据库时做格式化,在加载时也做统一转换:
// 加载权限时统一转小写
Set<String> permissions = new HashSet<>(permissionMapper.selectPermCodesByUserId(userId))
.stream()
.map(String::toLowerCase)
.collect(Collectors.toSet());坑三:SuperAdmin 角色被权限码过滤器拦截
现象: 超级管理员(SUPER_ADMIN)可以访问所有接口,但需要把每一个权限码都配置给这个角色,非常繁琐,而且新增功能时容易漏配。
解法: 扩展PermissionChecker,让SUPER_ADMIN绕过具体权限检查:
@Component("perm")
public class PermissionChecker {
public boolean has(String permCode) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth == null || !auth.isAuthenticated()) return false;
// 超级管理员拥有所有权限
if (auth.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals("ROLE_SUPER_ADMIN"))) {
return true;
}
return auth.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.anyMatch(a -> a.equals(permCode));
}
}小结
RBAC落地的关键不在于Spring Security的配置,而在于数据模型的设计:
- 权限要细粒度,不能只到角色级别
- 权限码要有命名规范(
资源:操作格式) - 权限变更要有缓存刷新机制
- SuperAdmin要特殊处理,避免每次新增权限都要配置
配合@PreAuthorize("@perm.has('xxx')"),可以做到任意细粒度的权限控制,且权限可以在运行时动态调整而不需要改代码。
