没有角色表的权限管理,动态加载用户权限菜单(改造Spring security)
spring security
传统的spring security权限结构涉及到五张表
- 用户表
- 角色表
- 用户角色表
- 系统资源表
- 角色资源表
给用户分配不同的角色,就可以动态加载该角色下的权限菜单。
新需求
不是根据角色进行菜单权限的分配,而是在用户注册后,用户进行自行勾选需要的权限菜单(根据不同的菜单选择,进行不同的缴费),也就是说用户自己选择需要开通哪些菜单的权限,弱化了用户角色的概念。
因此,对sprIng security进行改造
设计三张表
- 用户表 (记录注册的用户信息)
- 系统菜单资源表(记录所有的菜单信息)
- 用户资源表(记录用户自己勾选的菜单权限,可更新)
设计思路及代码
用户在注册之后,只有默认的几个菜单可操作,对于没有开通的菜单没有操作权限。
所以在用户注册完之后,先进行所需要权限的开通。
@Data public class AddUserPermissionVO { @ApiModelProperty(value = "用户ID") private String userId; @ApiModelProperty(value = "用户所选权限列表", notes = "传过来的是路径地址list") private List<String> permissionLists; }
如果是第一次进行菜单权限开通,则进行用户资源表的新增操作,如果是后续想开通其他的菜单权限,则进行更新操作。使用加密算法进行tokenId的加解密。
/** * 用户菜单权限添加 * @param param */ public void addUserPermission(AddUserPermissionVO param) { SysUserPermission model = new SysUserPermission(); List<String> permissionLists = param.getPermissionLists(); // 把用户id也封装进token permissionLists.add(param.getUserId()); Map<String, Object> payload = new HashMap<>(); // 把用户权限菜单封装成key payload.put(SIGNING_KEY, permissionLists); String token = JwtUtil.createToken(payload, EXP); model.setId(param.getUserId()); model.setUserPermission(token); SysUserPermission user = sysUserPermissionService.findById(param.getUserId()); if (CommonUtil.isEmpty(user)) { sysUserPermissionService.add(model); } else { sysUserPermissionService.update(model); } }
添加完用户权限之后,用户每一次登录,都给前端返回用户资源表里的tokenId,前端根据返回的tokenId里的资源菜单路径信息,进行动态加载用户可操作的菜单。
SysUserPermission sysUserPermission = sysUserPermissionService.findById(oper.getOperatorId()); String tokenId = sysUserPermission.getUserPermission(); LoginResp loginResp = new LoginResp(); // 将tokenId返回给web页面,好做后续权限比对 loginResp.setTokenId(tokenId); return loginResp;
做到这一步,前端已经能够动态展示用户可看到可操作的权限菜单列表了。但是,没有做到权限的控制(即前端的不可信任性),用户如果知道其他菜单的路径也能进行操作。
在spring security 里面,在后端的方法接口上添加角色的注解,根据用户的角色进行接口操作的控制。
因此,使用AOP横切过滤的方式,再次改造sprIng security,使用注解的形式,进行用户菜单权限的控制。
前端的不可信任,后端权限控制改造
/** * 权限自定义注解 * * @author LH * @date 2022/1/11 11:26 */ @Target({ ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Documented @Component public @interface UserPermission
{ String value() default ""; }
AOP横切过滤
@Component @Aspect public class PermissionFilter { // 自定义加密密钥SIGNING_KEY private static final String SIGNING_KEY = "你的加密key(自定义)"; /** * 定义切点 * @annotation 以注解作为切点,参数为注解定义路径 */ @Pointcut("@annotation(com.xxx.security.annotation.UserPermission)") private void permissionAspect() { } /** * @Before 方法执行前校验, permissionAspect()是切点,@annotation(userPermission)是注解参数,point是切点对象 * @param userPermission * @return */ @Before(value = "permissionAspect()&&@annotation(userPermission)") public Object checkPermissionkey(UserPermission userPermission) { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()) .getRequest(); // 得到tokenId并进行比对 String tokenId = request.getHeader("X-Token"); if (CommonUtil.isEmpty(tokenId)) { // 这里还要加一层判断,当没有登录的用户进行操作时,没有tokenId,设置一个默认的default tokenId throw new RuntimeException("用户无访问权限!"); } JwtUtil.parseToken(tokenId); // 这里进行分割,去掉路径前缀,匹对后面的请求路径 String str = StrUtil.removeAll(JwtUtil.parseToken(tokenId).get(SIGNING_KEY).toString(), '[', ']'); List<String> keyLists = BwFunc.getListSplitStr(str, ","); // userPermission.value()是注解参数 if (keyLists.contains(userPermission.value())) { return null; } else { throw new RuntimeException("用户无访问权限!"); } } }
在接口方法上添加自定义注解进行测试
@RequestMapping(value = "/addUser", method = RequestMethod.POST) @ApiOperation(value = "用户添加", notes = "用户添加", httpMethod = "POST") @UserPermission("addUser") // 只有在用户携带的token里面携带了 addUser参数,才能够对改接口方法进行后续的操作,否则返回用户无操作权限 public BwResult addUserPermission(@RequestBody AddUserVO param) { sysUserPermissionAppService.addUser(param); return BwResult.success("用户添加成功!"); }
到此,简单demo改造完成,后续看是否可进行优化。