根据个人上一篇文章,权限设计的杂谈中,涉及到了有关于先后端分离中,页面和api接口断开表与表层面的关联,另辟蹊径从其余角度找到方式进行关联。这里咱们主要采起了shiro的自定义注解的方案。本篇文章主要解决如下的问题。java
在表与表的结构关系中,页面和接口表最终都是与权限表进行的关联(详情请查看个人上一篇文章《权限设计的杂谈》)。
咱们如今但愿用另外一种方案去替代他,实现一个低成本同时兼顾必定程度的权限控制。这里咱们引入两个概念。业务模块,操做类型。数据库
业务模块apache
操做类型segmentfault
如今提出了这两个概念,他们最终的实际的使用方式是什么,咱们先从如下几个角度去思考一下。后端
咱们从结论出发来看这几个问题,首先“控制对象”的存储除了在数据库中也能够代码中,也能够在配置文件中,并不必定非得在数据库;那么接着回答第二个问题,当数据库存在的接口信息,而服务端并无开发这个接口的时候,数据库的信自己就有问题,亦或者,数据库里新增的接口一定是服务端上已经部署的接口才能生效;接着就是第一个问题,那么数据库中关于“控制对象”的表中的数据并不必定是真实有效的。因此咱们能够得出如下的解决方案api
可是这种方案仅适用于非强控制接口型的项目,在强控制型的接口项目仍然要将页面与接口进行绑定,虽然这会带来巨大的运维成本。另外也能够经过接口路由规则进行划分,例如:/api/page/xxxx/(仅对页面使用),/api/mobile/xxxxx(仅对移动端使用)将仅供页面使用的接口进行分类,这类接口仅作认证不作受权,也能够达到目的。
经过一个理论上的思路承认以后,剩下的则是付诸技术上的实践,咱们这边采用的是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这个咱们尽量不采用,由于角色的组合形式太多,角色名没有办法在接口中具象惟一化(很难指定接口归某个角色调用,可是必定能知道接口归属于某些业务模块的某些操做。)app
如今咱们来回顾一下整个运转的流程。框架
可是仅仅是拥有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("没有访问此接口的权限"); } } } }
/** * 拦截器 */ 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切面 */ public class AuthAopInterceptor extends AopAllianceAnnotationsAuthorizingMethodInterceptor { public AuthAopInterceptor() { super(); // 添加自定义的注解拦截器 this.methodInterceptors.add(new AuthMethodInterceptor(new SpringAnnotationResolver())); } }
/** * 启动自定义注解 */ 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的自定义注解启用类。其余的自定义的注解的编写思路和这个也是相似的。