基于shiro的自定义注解的扩展
基于shiro的自定义注解的扩展
根据我的上一篇文章,权限设计的杂谈中,涉及到了有关于前后端分离中,页面和api接口断开表与表层面的关联,另辟蹊径从其他角度找到方式进行关联。这里我们主要采取了shiro的自定义注解的方案。本篇文章主要解决以下的问题。
- 如何通过逻辑进行页面与api接口的关联。
- shiro的自身注解的用法。
- 如何编写自定义注解。
如何通过逻辑进行页面与api接口的关联
在表与表的结构关系中,页面和接口表最终都是与权限表进行的关联(详情请查看我的上一篇文章《权限设计的杂谈》)。
我们现在希望用另一种方案去替代他,实现一个低成本同时兼顾一定程度的权限控制。这里我们引入两个概念。业务模块,操作类型。
业务模块
- 概念:将系统中的业务模块抽象成一种数据,我们可以用字符串的形式去表示,例如:角色管理对应是role-manage、用户管理对应是user-manage等等。我们将系统中所存在的业务模块通过“最小特权原则”进行划分,最终形成一批可分配的数据。
- 使用原则:api接口和页面以及功能从本质上来说,都和业务模块有逻辑关系,于是,我们可以对api接口与页面(以及功能点)进行逻辑匹配,来判断页面与接口的关系。
操作类型
- 概念:将系统中的所有的操作类型抽象成一种数据,我们也可以用字符串的形式去表示,例如:新增对应的是add、分配对应的是allot等等。我们将系统中所有的操作类型根据业务模块通过“数据许可证”进行划分,最终形成一批可分配的数据。
- 使用原则:页面是展示,功能点是动作,而接口是最终动作的资源提供,通过“业务模块”确定了调取的资源,通过“操作类型”确定了资源的使用方式。通过两者可以大致无误的判断页面的功能点触发的接口是否在鉴权之内。
现在提出了这两个概念,他们最终的实际的使用方式是什么,我们先从以下几个角度去思考一下。
- 数据库中的页面表或的api接口表中的数据就是真实有效吗?
- 页面或接口的实际使用,是以功能存在为前提,还是以数据库表中的数据存在为前提。
- 权限结构中,“控制对象”的存储只有数据库这一种途径吗?
我们从结论出发来看这几个问题,首先“控制对象”的存储除了在数据库中也可以代码中,也可以在配置文件中,并不一定非得在数据库;那么接着回答第二个问题,当数据库存在的接口信息,而服务端并没有开发这个接口的时候,数据库的信本身就有问题,亦或者,数据库里新增的接口必定是服务端上已经部署的接口才能生效;接着就是第一个问题,那么数据库中关于“控制对象”的表中的数据并不一定是真实有效的。所以我们可以得出以下的解决方案
- 我们可以在接口上用注解的形式补充“业务模块”和“操作类型”的数据信息,这两类信息都可以存于常量类中,
- 在数据库添加创建页面表结构和页面功能表结构的时候,添加“业务模块”和“操作类型”字段。
- 可以将“业务模块”和“操作类型”的信息存于数据库的字典表中。
- 模块的新增或操作的新增,必定带来了接口的新增,那么就会带来一次系统部署活动,这个运维成本是无法减少的,并不能通过表结构来减少。
但是这种方案仅适用于非强控制接口型的项目,在强控制型的接口项目仍然要将页面与接口进行绑定,虽然这会带来巨大的运维成本。另外也可以通过接口路由规则进行划分,例如:/api/page/xxxx/(仅对页面使用),/api/mobile/xxxxx(仅对移动端使用)将仅供页面使用的接口进行分类,这类接口仅做认证不做授权,也可以达到目的。
shiro的自身注解的用法
通过一个理论上的思路认可之后,剩下的则是付诸技术上的实践,我们这边采用的是Apache Shiro的安全框架,在Spring Boot的环境下应用。简要说明以下几个shiro的注解。
注解名 | 作用 |
---|---|
@RequiresAuthentication | 作用于的类、方法、实例上。调用时,当前的subject是必须经过了认证的。 |
@RequiresGuest | 作用于的类、方法、实例上。调用时,subject可以是guest状态。 |
@RequiresPermissions | 作用于的类、方法、实例上。调用时,需要判断suject中是否包含当前接口中的Permission(权限信息)。 |
@RequiresRoles | 作用于的类、方法、实例上。调用时,需要判断subject中是否包含当前接口中的Role(角色信息)。 |
@RequiresUser | 作用于的类、方法、实例上。调用时,需要判断subject中是否当前应用中的用户。 |
/** * 1.当前接口需要经过"认证"过程 * @return */ @RequestMapping(value = "/info",method = RequestMethod.GET) @RequiresAuthentication public String test(){ return "恭喜你,拿到了参数信息"; } /** * 2.1.当前接口需要经过权限校验(需包含 角色的查询 或 菜单的查询) * @return */ @RequestMapping(value = "/info",method = RequestMethod.GET) @RequiresPermissions(value={"role:search","menu":"search"},logical=Logical.OR) public String test(){ return "恭喜你,拿到了参数信息"; } /** * 2.2.当前接口需要经过权限校验(需包含 角色的查询 与 菜单的查询) * @return */ @RequestMapping(value = "/info",method = RequestMethod.GET) @RequiresPermissions(value={"role:search","menu":"search"},logical=Logical.OR) public String test(){ return "恭喜你,拿到了参数信息"; } /** * 3.1.当前接口需要经过角色校验(需包含admin的角色) * @return */ @RequestMapping(value = "/info",method = RequestMethod.GET) @RequiresRoles(value={"admin"}) public String test(){ return "恭喜你,拿到了参数信息"; } /** * 3.2.当前接口需要经过角色与权限的校验(需包含admin的角色,以及角色的查询 或 菜单的查询) * @return */ @RequestMapping(value = "/info",method = RequestMethod.GET) @RequiresRoles(value={"admin"}) @RequiresPermissions(value={"role:search","menu":"search"},logical=Logical.OR) public String test(){ return "恭喜你,拿到了参数信息"; }
在我们的实际使用过程中,实际上只需要使用@RequiresPermissions和@RequiresAuthentication就可以了这一个注解就可以了,在上一小节的结尾,我们采取了业务模块与操作的结合方案来解耦页面和api接口的关系,和apache Shiro的这种方式正好一致。但是@RequiresRoles这个我们尽可能不采用,因为角色的组合形式太多,角色名没有办法在接口中具象唯一化(很难指定接口归某个角色调用,但是一定能知道接口归属于某些业务模块的某些操作。)
现在我们来回顾一下整个运转的流程。
如何编写自定义注解
但是仅仅是拥有shiro中的这5个注解肯定是不够使用的。在实际的使用过程中,根据需求,我们会在权限认证中加入我们自己特有的业务逻辑的,我们为了便捷则可以采用自定义注解的方式进行使用。这种方法不仅仅适用于Apache Shiro,很多其他的框架如:Hibernate Validator、SpringMVC、甚至我们可以写一套校验体系,在aop中去验证权限,这都是没问题的。所以自定义注解的作用很广。但是在这里,我仅仅基于shiro的来实现适用于它的自定义注解。
- 定义注解类
/** * 用于认证的接口的注解,组合形式默认是“或”的关系 */ @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface Auth { /** * 业务模块 * @return */ String[] module(); /** * 操作类型 */ String[] action(); }
- 定义注解的处理类
/** * Auth注解的操作类 */ public class AuthHandler extends AuthorizingAnnotationHandler { public AuthHandler() { //写入注解 super(Auth.class); } @Override public void assertAuthorized(Annotation a) throws AuthorizationException { if (a instanceof Auth) { Auth annotation = (Auth) a; String[] module = annotation.module(); String[] action = annotation.action(); //1.获取当前主题 Subject subject = this.getSubject(); //2.验证是否包含当前接口的权限有一个通过则通过 boolean hasAtLeastOnePermission = false; for(String m:module){ for(String ac:action){ //使用hutool的字符串工具类 String permission = StrFormatter.format("{}:{}",m,ac); if(subject.isPermitted(permission)){ hasAtLeastOnePermission=true; break; } } } if(!hasAtLeastOnePermission){ throw new AuthorizationException("没有访问此接口的权限"); } } } }
- 定义shiro拦截处理类
/** * 拦截器 */ public class AuthMethodInterceptor extends AuthorizingAnnotationMethodInterceptor { public AuthMethodInterceptor() { super(new AuthHandler()); } public AuthMethodInterceptor(AnnotationResolver resolver) { super(new AuthHandler(), resolver); } @Override public void assertAuthorized(MethodInvocation mi) throws AuthorizationException { // 验证权限 try { ((AuthHandler) this.getHandler()).assertAuthorized(getAnnotation(mi)); } catch (AuthorizationException ae) { if (ae.getCause() == null) { ae.initCause(new AuthorizationException("当前的方法没有通过鉴权: " + mi.getMethod())); } throw ae; } } }
- 定义shiro的aop切面类
/** * shiro的aop切面 */ public class AuthAopInterceptor extends AopAllianceAnnotationsAuthorizingMethodInterceptor { public AuthAopInterceptor() { super(); // 添加自定义的注解拦截器 this.methodInterceptors.add(new AuthMethodInterceptor(new SpringAnnotationResolver())); } }
- 定义shiro的自定义注解启动类
/** * 启动自定义注解 */ public class ShiroAdvisor extends AuthorizationAttributeSourceAdvisor { public ShiroAdvisor() { // 这里可以添加多个 setAdvice(new AuthAopInterceptor()); } @SuppressWarnings({"unchecked"}) @Override public boolean matches(Method method, Class targetClass) { Method m = method; if (targetClass != null) { try { m = targetClass.getMethod(m.getName(), m.getParameterTypes()); return this.isFrameAnnotation(m); } catch (NoSuchMethodException ignored) { } } return super.matches(method, targetClass); } private boolean isFrameAnnotation(Method method) { return null != AnnotationUtils.findAnnotation(method, Auth.class); } }
总体的思路顺序:定义注解类(定义业务可使用的变量)->定义注解处理类(通过注解中的变量做业务逻辑处理)->定义注解的拦截器->定义aop的切面类->最后定义shiro的自定义注解启用类。其他的自定义的注解的编写思路和这个也是类似的。