前言
在微服务架构下开发权限控制一般的做法是,独立开发一个专门用于鉴权的服务,其它服务每次请求接口时都调用鉴权服务鉴权,这样做的好处是,代码耦合低,权限控制功能好扩展,其坏处是每次鉴权都要请求鉴权服务,增加服务器资源消耗,因此我弄了一个简单的权限验证,能满足接口级别的验证,不通过专门的鉴权服务,而是每个服务自己去验证权限。
权限验证开关注解
并非每个服务都需要验证权限,因此我们可以定义一个类似@EnableDiscoery 这样的注解开关来控制:
- @Retention(RetentionPolicy.RUNTIME)
- @Target(ElementType.TYPE)
- @Documented
- @Import(AuthenticationInterceptor.class)
- public @interface EnableAuthentication {
-
- }
关键代码是@Import,当你在代码中使用了@EnableAuthentication 注解时,spring 会自动扫描并加载Import注解中的AuthenticationInterceptor类
)
给需要鉴权的接口加上注解
先定义鉴权标记的注解,@Target({ElementType.METHOD,ElementType.TYPE}) 表示此注解是用于方法上面的。
-
- @Retention(RetentionPolicy.RUNTIME)
- @Target({ElementType.METHOD,ElementType.TYPE})
- public @interface CheckPermission {
- /**
- * @Description: 权限标识代码,请保持注解上的代码和数据库中代码一致 ,声明在类上表示该Controller下所有方法都要验证!
- * @param: @return
- * @return: String
- * @throws
- */
- OptionType[] value() default OptionType.CUSTOM;
-
- /**
- * @Description: 如果使用自定义操作权限码,请在此配置
- * @param: @return
- * @return: String
- * @throws
- */
- String customPermissionCode() default "";
-
- /**
- * @Description: 权限状态(暂时用不上)
- * @param: @return
- * @return: int
- * @throws
- */
- //int status() default 0;
- }
第二个注解是控制具体是增加、删除、修改、还是其它自定义权限,这里的枚举也可以改成字符串,其最终结果都是匹配字符串。
-
- /**
- * 权限操作类型枚举
- * @Description: DETAIL:表示查询详情,LIST:查询列表,UPDATE:全量更新该条数据,UPDATE_SELECTIVE:非全量更新数据,
- * CUSTOM:自定义权限码 ,SKIP:跳过验证
- * controller级别控制请把CheckPermission注解加控制器类上。
- */
- public enum OptionType {
-
- DETAIL,LIST,ADD,UPDATE,UPDATE_SELECTIVE,DELETE,CUSTOM,ALL,SKIP
- }
第三个是用于类似于控制器类上的@RequestMapping
- @Retention(RetentionPolicy.RUNTIME)
- @Target(ElementType.TYPE)
- @Documented
- public @interface PermissionMapping {
-
- /**
- * @Description: 权限码的前缀
- * 如:USER_INFO,数据库中权限码可配置:USER_INFO:ADD,ADD是用于区分操作类型的枚举
- * @param: @return
- * @return: String
- * @throws
- */
- String value() default "";
- }
在需要鉴权的接口上加上注解
OptionType.add标记是增加方法,在数据库配置权限码时要和枚举保持一致,两个注解加起来的字符串就成了:USER + ADD,
因此数据库中的权限码可定义为:USER:ADD ,如该用户拥有此方法的权限,可以在数据库中为该用户添加此权限码。
- @PermissionMapping("USER")
- @RequestMapping("user")
- public class UserController{
-
- @PostMapping
- @CheckPermission(OptionType.Add)
- public Object addUser(){
-
- return "add user";
- }
-
- }
鉴权拦截器
此处是首先获取该请求的token,然后根据token到redis中获取该用户的权限码,然后根据请求的接口获取该方法上配置的注解,通过两个注解来配置该用户在登陆时保存在redis的权限码,如该用户拥有权限码USER:ADD,PRODUCT:DELETE,在调用上面的方法时,通过获取注解拼接为:USER:ADD来匹配
-
- /**
- * 用于验证权限的拦截器
- * @Description:
- */
-
- public class AuthenticationInterceptor implements HandlerInterceptor{
- @Autowired
- StringRedisTemplate redisTemplate;
- @Resource
- Map<String,Set<String>> userInfoCacheMap;
- private Logger logger = LoggerFactory.getLogger(this.getClass());
-
- @SuppressWarnings("unchecked")
- @Override
- public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object handler) throws Exception {
- if(handler instanceof HandlerMethod) {
- HandlerMethod h = (HandlerMethod)handler;
- //权限码前缀
- PermissionMapping mapping = h.getBeanType().getAnnotation(PermissionMapping.class);
- //类上的权限验证注解
- CheckPermission classPermission = h.getBeanType().getAnnotation(CheckPermission.class);
- //方法上的权限验证注解
- CheckPermission methodPermission = h.getMethodAnnotation(CheckPermission.class);
- if(checkPermissionCode(methodPermission)) {
- String permissionCode = null;
- String token = null;
- CheckPermission checkPermission = methodPermission;
- OptionType[] optionTypes = checkPermission.value();
- for(OptionType optionType : optionTypes) {
-
- if(optionType==OptionType.SKIP) {
- return true;
- }
- //如果自定义权限码则拼接customPermissionCode,否则使用枚举
- permissionCode = mapping.value()+":"+(optionType==OptionType.CUSTOM?checkPermission.customPermissionCode():optionType.toString());
- token = httpServletRequest.getHeader("Access-Token");
- if(StringUtils.isEmpty(token)) {
- throw new AuthenticationException("权限验证token为空!请确认header中token信息是否丢失!");
- }
- String permissionCacheKey = token+"-permission";
- //先从缓存中获取
- Set<String> permissionCodes = userInfoCacheMap.get(permissionCacheKey);
- if(permissionCodes==null) {
- String permissionCodesJson = redisTemplate.opsForValue().get(permissionCacheKey);
- if(permissionCodesJson==null) {
- throw new AuthenticationException("非法的Token,无权限操作!");
- }
- permissionCodes = JacksonUtil.readValue(permissionCodesJson, HashSet.class);
- userInfoCacheMap.put(permissionCacheKey, permissionCodes);
- }else {
- //TODO 清除缓存可配置化
- //超过数量直接清除
- if(userInfoCacheMap.size()>500) {
- userInfoCacheMap.clear();
- userInfoCacheMap.put(permissionCacheKey, permissionCodes);
- }
- }
- //存在权限通过请求
- if(permissionCodes.contains(permissionCode)) {
- return true;
- }
- }
- //403=没有权限,401=未认证、
- logger.warn("该用户访问了没有权限的请求!请求:{},用户信息:{}",permissionCode,redisTemplate.opsForValue().get(token));
- httpServletResponse.setStatus(403);
- return false;
- }
- }
- return true;
- }
-
- private boolean checkPermissionCode(CheckPermission checkPermission) {
- return checkPermission!=null&&!StringUtils.isEmpty(checkPermission.value())?true:false;
- }
-
- }
权限开关注解最后的一点配置
要实现权限开关功能还需要在配置类中加一些代码,如果使用了@EnableAuthentication注解,那么在注入AuthenticationInterceptor 类时不会获取到null,此时将该拦截器类加入spring mvc拦截中
- @Configuration
- @EnableWebMvc
- public class WebMvcConfig extends WebMvcAutoConfiguration implements WebMvcConfigurer {
-
- /**
- * 不强制注入,如果为空,表示并没有开启权限验证开关
- */
- @Autowired(required = false)
- AuthenticationInterceptor authenticationInterceptor;
-
- @Override
- public void addCorsMappings(CorsRegistry registry) {
- // 设置允许跨域的路径
- registry.addMapping("/**")
- // 设置允许跨域请求的域名
- .allowedOrigins("*")
- // 是否允许证书 不再默认开启
- .allowCredentials(true)
- // 设置允许的方法
- .allowedMethods("*")
- // 跨域允许时间
- .maxAge(3600);
- }
-
- @Override
- public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
- configurer.enable();
- }
-
- /**
- * 配置spring mvc拦截器
- */
- @Override
- public void addInterceptors(InterceptorRegistry registry) {
- if (authenticationInterceptor != null) {
- registry.addInterceptor(authenticationInterceptor).addPathPatterns("/**");
- }
- WebMvcConfigurer.super.addInterceptors(registry);
- }
- 本文作者: reiner
- 本文链接: https://reiner.host/posts/e6208c64.html
- 版权声明: 转载请注明出处,并附上原文链接