1. 项目概述
RuoYi-Vue作为国内广泛使用的开源后台管理系统,其权限体系设计一直是开发者关注的焦点。在实际项目中,我发现很多团队虽然使用了RuoYi-Vue,但对其中权限机制的理解仅停留在表面配置层面。本文将基于我在三个大型后台系统开发中的实战经验,深度剖析RuoYi-Vue权限系统的三层架构设计。
2. 核心架构解析
2.1 登录鉴权实现原理
RuoYi-Vue采用JWT+Spring Security的混合鉴权方案,其核心流程如下:
- 用户登录时系统生成包含用户标识的JWT Token
- 前端将Token存储在localStorage并附加到后续请求的Header
- 后端通过SecurityConfig配置的JwtAuthenticationTokenFilter进行拦截验证
关键配置类说明:
java复制// 安全配置核心类
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/login").anonymous()
.anyRequest().authenticated()
.and().addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
}
// JWT过滤器
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) {
// 解析Header中的Token并验证
String token = request.getHeader("Authorization");
if(StringUtils.isNotEmpty(token) && token.startsWith("Bearer ")){
String authToken = token.substring(7);
String username = jwtTokenUtil.getUsernameFromToken(authToken);
// 构建Authentication对象并存入SecurityContext
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(username, null, null);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(request, response);
}
}
关键经验:生产环境建议将Token有效期设置为2-4小时,并配合Redis实现续签机制。我曾遇到过因Token过期策略不当导致的用户体验问题,最终采用双Token方案(access_token+refresh_token)解决。
2.2 功能权限控制机制
功能权限通过@PreAuthorize注解与前端路由守卫双重控制:
后端实现:
java复制// 菜单权限校验
@PreAuthorize("@ss.hasPermi('system:user:list')")
@GetMapping("/list")
public TableDataInfo list(SysUser user) {
// 业务逻辑
}
// 角色权限校验
@PreAuthorize("@ss.hasRole('admin')")
@PostMapping("/export")
public void export(HttpServletResponse response) {
// 导出逻辑
}
前端路由控制(Vue实现):
javascript复制// 路由守卫
router.beforeEach((to, from, next) => {
if (to.meta.roles && !hasAnyRole(to.meta.roles)) {
next({ path: '/401' });
} else {
next();
}
});
// 按钮权限指令
Vue.directive('hasPermi', {
inserted: function(el, binding) {
if (!checkPermi(binding.value)) {
el.parentNode.removeChild(el);
}
}
});
权限数据表关键设计:
sql复制CREATE TABLE `sys_menu` (
`menu_id` bigint NOT NULL AUTO_INCREMENT,
`menu_name` varchar(50) NOT NULL,
`perms` varchar(100) DEFAULT NULL COMMENT '权限标识',
PRIMARY KEY (`menu_id`)
);
CREATE TABLE `sys_role_menu` (
`role_id` bigint NOT NULL,
`menu_id` bigint NOT NULL,
PRIMARY KEY (`role_id`,`menu_id`)
);
3. 数据权限深度实践
3.1 数据过滤原理
RuoYi通过AOP+MyBatis插件实现数据权限过滤,核心类DataScopeAspect会解析注解:
java复制@DataScope(deptAlias = "d", userAlias = "u")
public List<SysUser> selectUserList(SysUser user) {
return userMapper.selectUserList(user);
}
SQL自动注入逻辑:
java复制public class DataPermissionHelper {
public static void dataScopeFilter(String joinPoint, String alias) {
String permission = StringUtils.format(
" OR {}.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = {} ) ",
alias, roleId);
// 将条件注入到SQL中
}
}
3.2 自定义数据权限方案
在电商后台项目中,我们扩展了基于组织架构的数据权限:
- 新建数据权限类型表:
sql复制CREATE TABLE `sys_data_scope` (
`role_id` bigint NOT NULL,
`data_scope_type` tinyint NOT NULL COMMENT '1-全部 2-自定义 3-本部门 4-本部门及子部门',
PRIMARY KEY (`role_id`)
);
- 扩展AOP处理逻辑:
java复制switch (dataScopeType) {
case 2: // 自定义数据范围
sqlFilter.append(" AND {}.dept_id IN ({}) ", alias, customDeptIds);
break;
case 4: // 部门及子部门
sqlFilter.append(" AND {}.dept_id IN (
SELECT dept_id FROM sys_dept WHERE dept_id = {}
OR ancestors LIKE '%,{},%')",
alias, currentDeptId, currentDeptId);
break;
}
4. 性能优化实践
4.1 权限缓存方案
通过Redis缓存权限数据提升系统响应速度:
java复制// 权限缓存加载
public Set<String> getMenuPermission(Long userId) {
String key = "sys:perms:" + userId;
Set<String> perms = redisTemplate.opsForSet().members(key);
if (CollectionUtils.isEmpty(perms)) {
perms = menuMapper.selectPermsByUserId(userId);
redisTemplate.opsForSet().add(key, perms.toArray(new String[0]));
redisTemplate.expire(key, 2, TimeUnit.HOURS);
}
return perms;
}
4.2 前端权限优化
采用按需加载权限指令避免重复计算:
javascript复制// 优化后的权限检查
const checkPermi = _.memoize((permission) => {
return store.getters.permissions.includes(permission);
}, (permission) => permission);
5. 常见问题排查
5.1 权限失效场景分析
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 按钮突然消失 | 1. 权限标识变更 2. Vue指令未更新 |
1. 检查v-hasPermi绑定值2. 使用 Vue.set强制更新 |
| 接口返回403 | 1. Token过期 2. 权限配置错误 |
1. 检查Network请求头 2. 核对 @PreAuthorize注解 |
| 数据过滤异常 | 1. 别名配置错误 2. SQL注入失败 |
1. 检查@DataScope注解2. 开启MyBatis日志 |
5.2 多租户权限改造
在SAAS项目中改造数据权限方案:
- 增加租户字段:
sql复制ALTER TABLE sys_dept ADD tenant_id bigint NOT NULL DEFAULT 0;
- 修改数据范围过滤逻辑:
java复制String tenantFilter = " AND {}.tenant_id = {} ";
sqlFilter.append(tenantFilter, alias, TenantContext.getCurrentTenant());
6. 扩展应用场景
6.1 字段级权限控制
通过MyBatis拦截器实现敏感字段过滤:
java复制@Intercepts(@Signature(type= ResultSetHandler.class,
method="handleResultSets",
args={Statement.class}))
public class FieldPermissionInterceptor implements Interceptor {
public Object intercept(Invocation invocation) {
// 根据权限标识过滤结果集字段
if (!hasFieldPermission(fieldName)) {
field.setAccessible(true);
field.set(object, null);
}
}
}
6.2 权限变更通知
使用Spring事件机制实现权限实时更新:
java复制// 权限变更事件
public class PermissionUpdateEvent extends ApplicationEvent {
private Long userId;
// 构造方法
}
// 事件监听器
@Component
public class PermissionUpdateListener {
@EventListener
public void handleEvent(PermissionUpdateEvent event) {
redisTemplate.delete("sys:perms:" + event.getUserId());
}
}
在权限管理模块中,当修改用户权限后发布事件:
java复制applicationContext.publishEvent(new PermissionUpdateEvent(this, userId));
7. 最佳实践建议
-
权限标识命名规范:
- 系统模块:
system:user:add - 业务模块:
order:manage:export - 避免使用特殊字符和中文
- 系统模块:
-
权限分配原则:
- 角色权限采用最小够用原则
- 敏感操作必须二次验证
- 定期审计权限分配情况
-
性能监控指标:
java复制@Aspect @Component public class PermissionMonitor { @Around("@annotation(preAuthorize)") public Object monitor(ProceedingJoinPoint pjp, PreAuthorize preAuthorize) { long start = System.currentTimeMillis(); try { return pjp.proceed(); } finally { long cost = System.currentTimeMillis() - start; Metrics.record("permission.check", cost); } } }
在实际项目部署中,我们发现合理设置权限缓存时间(建议30-120秒)能显著降低数据库压力。同时建议对权限变更操作进行日志记录,便于后续审计追踪。
