Ruoyi-Android-App 的权限模块思考
Ruoyi-Android-App 的权限模块中,实现「授权菜单查看 + 按钮级权限控制」是基于「角色-菜单-按钮」三级权限模型来实现的,以下是实战步骤和技巧
一、先理清权限模型基础
Ruoyi 前后端分离架构的权限逻辑是一致的,Android 端只是把后端返回的权限数据做本地处理:
-
顶级: 菜单(分为目录、菜单两类,用于侧边栏/页面导航展示) -
子级: 按钮(对应页面内的操作按钮,比如新增、编辑、删除,每个按钮绑定一个权限标识) -
权限绑定:用户 → 角色 → 菜单+按钮权限,后端登录后会返回当前用户拥有的所有菜单和按钮权限集合。
二、第一步:获取并缓存权限数据
用户登录成功后,后端接口会返回当前用户的路由菜单(包含按钮权限),Android 端需要先把权限缓存到本地,后续做过滤和控制。
1. 后端返回的数据结构示例(按钮绑定在菜单下)
json {"code": 200,"data": [ {"menuId": 1,"menuName": "系统管理","menuType": "M", // 目录"path": "/system","children": [ {"menuId": 101,"menuName": "用户管理","menuType": "C", // 菜单"path": "/system/user","component": "UserActivity", // Android端对应Activity/Fragment路径// 按钮权限集合,绑定在当前菜单下"buttons": ["system:user:add", "system:user:edit", "system:user:remove", "system:user:query"] } ] } ]}
2. Android 端缓存权限数据
我们一般用MMKV或者SP缓存,同时内存中保留一份方便读取:
// 权限工具类,单例保存权限数据publicclassPermissionUtils{privatestatic PermissionUtils instance;// 当前用户所有授权菜单集合private List<Menu> mAuthMenuList;// 当前用户所有授权按钮权限标识集合private Set<String> mAuthButtonSet;privatePermissionUtils(){}publicstatic PermissionUtils getInstance(){if (instance == null) {synchronized (PermissionUtils.class) {if (instance == null) { instance = new PermissionUtils(); } } }return instance; }// 登录成功后初始化权限publicvoidinitPermission(List<Menu> menuList){this.mAuthMenuList = filterAuthMenu(menuList);this.mAuthButtonSet = parseAllButtons(menuList);// 序列化缓存到本地 MMKV.defaultMMKV().encode("auth_menu", GsonUtils.toJson(mAuthMenuList)); MMKV.defaultMMKV().encode("auth_button_set", mAuthButtonSet); }// 过滤出用户授权的菜单(后端其实已经过滤,前端可以再做一次兜底过滤)private List<Menu> filterAuthMenu(List<Menu> menuList){ List<Menu> result = new ArrayList<>();for (Menu menu : menuList) {// 如果有子菜单递归过滤if (menu.getChildren() != null && !menu.getChildren().isEmpty()) { menu.setChildren(filterAuthMenu(menu.getChildren()));// 如果子菜单过滤后不为空,才保留父菜单if (!menu.getChildren().isEmpty()) { result.add(menu); } } else {// 叶子菜单判断是否授权(后端已经返回授权的,这里可以加自己的逻辑) result.add(menu); } }return result; }// 把所有菜单下的按钮权限收集到Set中,方便后续判断private Set<String> parseAllButtons(List<Menu> menuList){ Set<String> buttonSet = new HashSet<>();for (Menu menu : menuList) {if (menu.getButtons() != null && !menu.getButtons().isEmpty()) { buttonSet.addAll(menu.getButtons()); }if (menu.getChildren() != null && !menu.getChildren().isEmpty()) { buttonSet.addAll(parseAllButtons(menu.getChildren())); } }return buttonSet; }// 判断是否拥有某个按钮权限publicbooleanhasButtonPermission(String permission){return mAuthButtonSet != null && mAuthButtonSet.contains(permission); }// 获取授权后的菜单列表,用于展示侧边栏/菜单列表public List<Menu> getAuthMenuList(){return mAuthMenuList == null ? new ArrayList<>() : mAuthMenuList; }}三、第二步:实现授权菜单的查看功能
拿到过滤后的授权菜单,直接绑定到 RecyclerView 展示即可,做菜单导航功能:
示例:菜单列表展示
// 菜单页面ActivitypublicclassMenuListActivityextendsBaseActivity{private RecyclerView rvMenu;private MenuAdapter menuAdapter;@OverrideprotectedvoidonCreate(Bundle savedInstanceState){super.onCreate(savedInstanceState); setContentView(R.layout.activity_menu_list); rvMenu = findViewById(R.id.rv_menu); rvMenu.setLayoutManager(new LinearLayoutManager(this));// 直接从工具类拿已经过滤好的授权菜单 List<Menu> authMenus = PermissionUtils.getInstance().getAuthMenuList(); menuAdapter = new MenuAdapter(authMenus); rvMenu.setAdapter(menuAdapter);// 点击菜单跳转对应页面 menuAdapter.setOnItemClickListener((adapter, view, position) -> { Menu menu = authMenus.get(position); jumpToPage(menu); }); }// 根据菜单配置的组件路径跳转对应Activity/FragmentprivatevoidjumpToPage(Menu menu){try { Class<?> clazz = Class.forName(menu.getComponent()); startActivity(new Intent(this, clazz)); } catch (ClassNotFoundException e) { Toast.makeText(this, "页面未找到", Toast.LENGTH_SHORT).show(); } }}
// 菜单页面ActivitypublicclassMenuListActivityextendsBaseActivity{private RecyclerView rvMenu;private MenuAdapter menuAdapter;@OverrideprotectedvoidonCreate(Bundle savedInstanceState){super.onCreate(savedInstanceState); setContentView(R.layout.activity_menu_list); rvMenu = findViewById(R.id.rv_menu); rvMenu.setLayoutManager(new LinearLayoutManager(this));// 直接从工具类拿已经过滤好的授权菜单 List<Menu> authMenus = PermissionUtils.getInstance().getAuthMenuList(); menuAdapter = new MenuAdapter(authMenus); rvMenu.setAdapter(menuAdapter);// 点击菜单跳转对应页面 menuAdapter.setOnItemClickListener((adapter, view, position) -> { Menu menu = authMenus.get(position); jumpToPage(menu); }); }// 根据菜单配置的组件路径跳转对应Activity/FragmentprivatevoidjumpToPage(Menu menu){try { Class<?> clazz = Class.forName(menu.getComponent()); startActivity(new Intent(this, clazz)); } catch (ClassNotFoundException e) { Toast.makeText(this, "页面未找到", Toast.LENGTH_SHORT).show(); } }}菜单展示技巧:
-
分级展示:目录类型的菜单可以做成二级折叠列表,用 ExpandableListView或者RecyclerView的多视图类型实现,和Ruoyi后台端的侧边栏效果一致; -
本地缓存持久化:下次打开APP不需要重新请求菜单接口,直接从本地缓存加载权限,提升启动速度; -
权限刷新:如果管理员在后台修改了当前用户的权限,APP退出登录清空缓存,重新登录拉取最新权限即可。
四、第三步:实现按钮级权限控制
核心逻辑:页面渲染按钮的时候,判断当前用户是否有该按钮的权限标识,没有权限就隐藏/禁用按钮。
技巧1:自定义权限注解+反射控制(Activity/Fragment通用)
这种方式入侵性低,代码简洁:
// 1. 定义权限注解@Target(ElementType.FIELD)@Retention(RetentionPolicy.RUNTIME)public@interface RequiresPermission {String value();}在页面中给需要控制权限的按钮加上注解:
publicclassUserManagerActivityextendsBaseActivity{// 需要新增权限才显示@RequiresPermission("system:user:add")private Button btnAdd;// 需要编辑权限才显示@RequiresPermission("system:user:edit")private Button btnEdit;// 需要删除权限才显示@RequiresPermission("system:user:remove")private Button btnDelete;@OverrideprotectedvoidonCreate(Bundle savedInstanceState){super.onCreate(savedInstanceState); setContentView(R.layout.activity_user_manager);// 绑定控件...// 注入权限控制 PermissionInjector.injectPermission(this); }}权限注入工具类,通过反射处理按钮显示隐藏:
publicclassPermissionInjector{publicstaticvoidinjectPermission(Activity activity){// 获取Activity里所有的成员变量 Field[] fields = activity.getClass().getDeclaredFields();for (Field field : fields) {// 判断是否有我们的权限注解 RequiresPermission annotation = field.getAnnotation(RequiresPermission.class);if (annotation != null) { String permission = annotation.value();// 判断是否有权限if (!PermissionUtils.getInstance().hasButtonPermission(permission)) {try {// 设置可访问,拿到控件实例 field.setAccessible(true); Object view = field.get(activity);if (view instanceof View) {// 没有权限直接隐藏,也可以改成禁用 ((View) view).setVisibility(View.GONE); } } catch (IllegalAccessException e) { e.printStackTrace(); } } } } }}技巧2:适配器中动态控制Item按钮权限
如果是列表项中的按钮(比如每行的编辑删除按钮),在RecyclerView.Adapter中绑定数据的时候直接判断即可:
publicclassUserAdapterextendsBaseQuickAdapter<User, BaseViewHolder> {@Overrideprotectedvoidconvert(BaseViewHolder holder, User item){// ...绑定其他数据// 控制删除按钮显示boolean hasDeletePermission = PermissionUtils.getInstance() .hasButtonPermission("system:user:remove"); holder.setGone(R.id.btn_delete, !hasDeletePermission); }}技巧3:接口请求前做权限二次校验
APP端的权限控制是做交互层面的隐藏,后端接口一定要做二次校验,不过前端也可以在请求前做判断,避免无效请求:
// 点击删除按钮之前先判断权限btnDelete.setOnClickListener(v -> {if (!PermissionUtils.getInstance().hasButtonPermission("system:user:remove")) { Toast.makeText(this, "无删除权限", Toast.LENGTH_SHORT).show();return; }// 执行删除请求...});五、常见问题处理技巧
-
动态权限刷新:如果需要不重新登录刷新权限,调用 PermissionUtils.getInstance().initPermission(newMenuList),然后重新加载菜单列表,重启当前Activity即可刷新按钮状态; -
空菜单处理:如果用户没有授权任何菜单,展示「暂无权限」提示页; -
混淆问题:使用反射注入注解的方式,需要在混淆规则中保留 RequiresPermission注解和你的View字段,避免被混淆导致注解失效;
pro -keepattributes *Annotation*-keep class 你的包名.permission.** { *; }
-
性能优化:反射注入只会在页面创建的时候执行一次,对性能影响很小,如果担心反射,可以改成在 onCreate中手动调用判断,代码稍微多一点但是更直接。
总结
整个流程的核心就是:
-
登录拉取权限 → 本地缓存分类:菜单集合+按钮权限集合 -
展示菜单的时候直接用过滤后的授权菜单 -
按钮通过权限标识判断,没有权限就隐藏/禁用,接口层面二次校验,既保证了交互体验,也保证了权限安全。
夜雨聆风