基于SpringMVC拦截器与Annotations+Reflex做服务端权限控制
又到了我第二喜欢的玩反射时间了。真快乐啊。
如标题所说,我写了个权限控制。权限控制的精髓就是限制用户访问范围。一个系统,总得有后台、管理者这些绕不开的玩意儿。
还有用户所代表的各种不同角色,如游客、登陆者、管理员、代理商、作者、编辑,等等等等。他们这些角色在一个庞大的系统里能够操作的地方总归是不同的。
如果单单依赖前端的权限控制,难免会被人家利用抓包之类的手段获取其他角色能操作的区域信息来做一些危险操作。
后端的权限控制是必不可少的,当然,在java领域现在SpringSecurity、shiro之类的安全框架肯定是一个优选,我很久之前也写过关于SpringSecurity的配置,可见: Spring Security集成以及配置。
但是我就是tm要自己写一个,哎嘿嘿,(⁄ ⁄•⁄ω⁄•⁄ ⁄)
咳咳,关于我写的这个权限控制,我自己感觉还是特别骚的。亲自试了两天,确实没什么问题。我需要它能够做它确实做到了。
下面详细解释一下。
这个注解,是关键。里面有一个内部枚举,与其息息相关。注解的唯一一个参数就是该枚举。
@Impower 注解代表需要控制。
Level 枚举表示控制范围。
实现的效果就是:在类上(Controller层的)写入该@Impower 注解,例如: @Impower(level = Impower.Level.Admin) ,那么,即代表该类需要权限控制,并且只有 Admin 这个角色,或者比Admin角色权限更高的角色 ,才能够请求该类中的方法。
如果在方法上加入该@Impower 注解,如果@Impower同时存在类上,外部请求该方法时,则会无视类注解,按照方法上该注解来进行判定权限是否能够访问。
Level 枚举内部的权限可自己随意定义,只要保证其权限范围顺序从左到右、从上到下线性递减即可。
注:系统的用户必须有一个属性是以Impower . Level 作为其类型。才能够进行权限控制。
前后端均可使用,前台可通过thymeleaf、freemark等模板来对用户显示进行控制,若是前后端分离的项目也没事,请求后台的时候做一下check()判定就好了。
并且想修改非常方便,这已经很极限的做到了降低耦合。想要增加不同的权限也好、修改某些方法可访问的权限也好、前台对权限的控制也好。有变更只需要更改很小的地方。
package com.skypyb.annotations; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.util.Arrays; /** * 授权注解 * 使用该注解注释在 class 或者 method 上,可实现服务端的权限控制 * method 上的该注解,优先级会比 class 上的要高 * * @author pyb * @time 2018-11-22 */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, ElementType.TYPE}) public @interface Impower { /** * level 属性代表该 class/method 可以被哪个权限所访问 * 权限参数为 Impower.Level 枚举 * <p> * 所指定的范围是一个线性递减的。 * <p> * 使用注解时 : 参数代表的权限,以及比其权限更高的权限都能够访问 * 比该参数代表的权限等级更低的权限则不能够访问 * * @return */ Level level() default Level.USER; /** * 授权等级枚举 * <p> * 为每个用户的访问范围作出限定 * 每个用户都必须拥有一个权限,否则默认该用户为最低的权限级别 * <p> * <p> * 注: 权限级别为顺序下降,若需要添加新权限,请保持从上到下可访问的范围递减。 */ enum Level { SUPER, ADMIN, USER; /** * 用于检查该用户权限是否匹配传入的权限 * <p> * 前端使用: * thymeleaf为例: th:if="${session.adminUser.level.check('ADMIN')} * 则表示只有权限等级为 ADMIN 或者等级比 ADMIN 更高才能够访问 * * @param check * @return 若能够访问,则返回 true,反之 false */ public boolean check(Level check) { Level[] values = Level.values(); int index = Arrays.binarySearch(values, check);//找到该枚举下标 while (index >= 0) { if (this == values[index]) return true; index--; } return false; } public boolean check(String level) { Level check = Level.valueOf(level.toUpperCase());//能够访问的级别 return check(check); } } }
这儿是具体进行权限判定、从而决定是否放行的拦截器,关于SpringBoot的拦截器配置,若有不清楚的地方可看我之前写过的文章:
SpringBoot使用WebMvcConfigurerAdapter+HandlerInterceptorAdapter实现拦截
这里我对基本只会验证是否登录的拦截器做了拓展,获取了该请求所请求的类、方法上的注解,从而通过Session中的已登录对象来进行判定权限是否能够访问。
会使用Level 中的 check() 方法来进行判定该登陆角色的权限是否能够通过。
若通过判定,则成功访问该页面(能够正常请求响应),若判定失败,则请求失败,但是response状态码仍然会是200,只不过返回的内容是空罢了。
package com.skypyb.config; import com.rocket.annotations.Impower; import com.rocket.entity.AdminUser; import org.springframework.context.annotation.Configuration; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.config.annotation.InterceptorRegistration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; /** * 访问拦截配置 * * @author pyb * @time 2018-10-12 */ @Configuration public class AccessSecurityConfig extends WebMvcConfigurerAdapter { /** * 添加拦截器 * * @param registry */ public void addInterceptors(InterceptorRegistry registry) { InterceptorRegistration addInterceptor = registry.addInterceptor(new SecurityInterceptor())//注册拦截器,这里注册的是下边写的内部类 //excludePathPatterns() 方法接受一个String 字符串,代表不用拦截的路径 .excludePathPatterns("/**/oauth") .excludePathPatterns("/**/system/adminUser"); //拦截所有路径 addInterceptor.addPathPatterns("/**"); } /** * 拦截器 */ private class SecurityInterceptor extends HandlerInterceptorAdapter { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException { HttpSession session = request.getSession(); AdminUser adminUser = (AdminUser) session.getAttribute("adminUser"); if (adminUser == null) {//没登录就跳转到登录页 response.sendRedirect("oauth"); return false; } HandlerMethod handlerMethod = (HandlerMethod) handler; Impower beanAnn = handlerMethod.getBeanType().getAnnotation(Impower.class); Impower methodAnn = handlerMethod.getMethodAnnotation(Impower.class); //没限制,那就谁都能访问 if (methodAnn == null && beanAnn == null) return true; //没权限就默认最低权限 if (adminUser.getLevel() == null) adminUser.setLevel(Impower.Level.values()[Impower.Level.values().length - 1]); if (methodAnn != null) { return adminUser.getLevel().check(methodAnn.level()); } if (beanAnn != null) { return adminUser.getLevel().check(beanAnn.level()); } return true; } } }
又写了一些骚东西,别的不嗦,我这玩意写出来还真挺好用的。实际体感顺滑流畅,代码优雅,功能简洁却强悍。
但是后端的权限控制可不止这么点哦,最好与JWT(JSON Web Token)进行同步使用,我本来也想自个写个Token验证之类的玩玩,妈耶想起来是有点麻烦,还是算了,我还是挺懒得。