一、数据库设计:理清角色-权限-用户的关系
RBAC 的核心是三张核心表 + 两张关联表,具体如下(简化版):
举个例子:
用户A的角色是「运维组」,「运维组」角色拥有「server:manage」权限;
服务器1被分配了「运维组」角色,所以用户A能管理服务器1;
服务器2被分配了「管理员」角色,用户A没有「管理员」角色,所以管不了服务器2。
二、服务端实现:SpringSecurity 整合 JWT + RBAC
我们用 SpringSecurity 做权限校验,结合 JWT 实现无状态认证,核心步骤如下:
1. 自定义用户认证(UserDetailsService)
用户登录时,前端传账号密码,服务端通过 UserDetailsService
加载用户信息(包括角色、权限)。这里需要从数据库查用户,再查关联的角色和权限,封装成 UserDetails
对象返回。
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private SysUserMapper userMapper; // MyBatis-Plus 用户表Mapper
@Override
public UserDetails loadUserByUsername(String username) {
// 1. 查用户是否存在
SysUser user = userMapper.selectOne(Wrappers.lambdaQuery(SysUser.class).eq(SysUser::getUsername, username));
if (user == null) {
throw new UsernameNotFoundException("用户不存在");
}
// 2. 查用户的所有角色(通过 sys_user_role 关联表)
List<SysRole> roles = roleMapper.selectRolesByUserId(user.getId());
// 3. 查角色对应的所有权限(通过 sys_role_perm 关联表)
List<String> perms = roles.stream()
.flatMap(role -> permMapper.selectPermsByRoleId(role.getId()).stream())
.collect(Collectors.toList());
// 4. 封装成 SpringSecurity 的 UserDetails(包含权限)
return new CustomUserDetails(
user.getUsername(),
user.getPassword(),
user.getStatus() == 1, // 是否启用
true, true, true,
Collections.emptyList(), // 权限集合(这里用字符串列表,实际可用 Permission 对象)
roles,
perms
);
}
}
2. 权限拦截与校验
通过 @PreAuthorize
注解或自定义拦截器,校验用户是否有权限访问某个接口或操作。比如:
@RestController
@RequestMapping("/server")
public class ServerController {
// 只有拥有 'server:manage' 权限的用户才能访问
@PreAuthorize("hasAuthority('server:manage')")
@PostMapping("/add")
public Result addServer(@RequestBody Server server) {
// 添加服务器逻辑
}
// 拥有 'server:view' 权限的用户都能查看
@PreAuthorize("hasAuthority('server:view')")
@GetMapping("/list")
public Result listServers() {
// 查询服务器列表
}
}
3. 动态权限加载(解决角色/权限变更后缓存问题)
因为用了 JWT(无状态),用户权限变更后,旧 JWT 仍然有效,可能导致权限未及时更新。我们的解决方法是:
在
sys_user
表加version
字段(每次修改用户权限时version+1
);JWT 中携带
version
信息;每次请求时,服务端校验 JWT 中的
version
是否与数据库一致,不一致则拒绝请求并让用户重新登录。
// 自定义 JWT 校验过滤器(关键逻辑)
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) {
String token = extractToken(request);
if (token != null) {
try {
// 解析 JWT 得到用户信息和 version
Claims claims = jwtUtil.parseToken(token);
String username = claims.getSubject();
Integer jwtVersion = claims.get("version", Integer.class);
// 查数据库用户的当前 version
SysUser user = userMapper.selectByUsername(username);
if (user.getVersion() == null || !user.getVersion().equals(jwtVersion)) {
// 版本不一致,权限可能变更,拒绝请求
response.setStatus(HttpStatus.UNAUTHORIZED.value());
return;
}
// 校验通过,生成 UserDetails 并设置到 SecurityContext
UserDetails userDetails = customUserDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities()
);
SecurityContextHolder.getContext().setAuthentication(auth);
filterChain.doFilter(request, response);
} catch (JwtException e) {
// Token 无效,返回未授权
response.setStatus(HttpStatus.UNAUTHORIZED.value());
}
} else {
filterChain.doFilter(request, response);
}
}
}
4. 服务器-角色关联的权限控制
除了用户角色的权限,还要控制「用户是否能管理某台具体服务器」。比如:
用户A有「运维组」角色,该角色被分配了服务器1和服务器2;
用户A访问服务器3的管理接口时,需要校验「运维组」是否拥有服务器3的管理权限。
这部分在服务端接口中额外处理:
@PostMapping("/operate/{serverId}")
public Result operateServer(@PathVariable Long serverId, @RequestBody OperateReq req) {
// 1. 当前用户
String username = SecurityContextHolder.getContext().getAuthentication().getName();
// 2. 查用户拥有的角色
List<SysRole> userRoles = roleService.getUserRoles(username);
// 3. 查这些角色是否被分配了当前服务器
boolean hasPermission = serverRoleService.checkServerAssignedToRoles(serverId, userRoles);
if (!hasPermission) {
throw new AccessDeniedException("无权限操作该服务器");
}
// 4. 执行操作...
}
三、前端配合:动态菜单与按钮权限
前端(Vue3)需要根据用户权限动态渲染菜单和按钮,避免显示无权限的功能。具体步骤:
1. 登录后获取权限信息
用户登录成功后,后端返回用户的角色、权限列表(比如 ['server:manage', 'monitor:view']
),前端存储到 Vuex 或 Pinia 中。
2. 动态生成菜单
菜单数据从后端获取(根据用户权限过滤),比如:
管理员看到「服务器管理」「监控看板」「用户管理」;
普通运维只看到「服务器管理」「监控看板」。
// Vue 组件中获取菜单
async function getMenus() {
const res = await axios.get('/api/menus', {
headers: { Authorization: `Bearer ${token}` }
});
// 根据用户权限过滤菜单(后端已处理,前端直接渲染)
store.commit('setMenus', res.data);
}
3. 按钮级权限控制
通过自定义指令 v-permission
控制按钮是否显示:
// 注册全局指令
app.directive('permission', {
mounted(el, binding) {
const perms = store.state.user.permissions; // 用户权限列表
const requiredPerm = binding.value; // 需要的权限(如 'server:manage')
if (!perms.includes(requiredPerm)) {
el.parentNode?.removeChild(el); // 无权限则移除按钮
}
}
});
// 使用示例
<button v-permission="'server:manage'">删除服务器</button>
四、关键细节与踩坑
权限缓存:用户权限信息存在 Redis 中(键:
user:perm:${username}
),避免每次请求都查数据库。用户登出或权限变更时,删除对应缓存。JWT 与 RBAC 结合:JWT 中除了用户信息,还要存角色/权限的摘要(比如角色ID列表),避免每次请求都查数据库(但最终校验还是以数据库为准)。
动态路由:前端路由需要根据权限动态添加(比如用
router.addRoute()
),避免无权限的路由被访问。服务器-角色关联的灵活性:通过
sys_server_role
表实现「多对多」关系,一个服务器可分配给多个角色,一个角色可管理多个服务器,满足复杂权限需求。
总结来说, RBAC 实现围绕「用户-角色-权限-服务器」四者关系,通过 SpringSecurity 做权限校验,JWT 做无状态认证,前端动态渲染,最终实现了「不同账户管理不同服务器」的灵活权限控制。