拦截器对接口细粒度权限校验

文章目录

      • 一、逻辑分析
      • 二、校验规则
        • 1.规则类型
        • 2.规则划分
        • 3.规则配置信息
        • 4.规则案例说明
        • 5.规则加载
      • 三、拦截器定义
        • 1.自定义拦截器
        • 2.注册拦截器
      • 四、获取请求参数
        • 1.获取get提交方式参数
        • 2.获取post提交方式参数
          • (1)定义RequestWrapper类
          • (2)定义过滤器
          • (3)注册过滤器
          • (4)获取post提交方式参数
        • 3.上传文件参数处理
        • 4.获取动态接口参数
        • 5.获取系统固定参数
      • 五、拦截请求
        • 1.获取校验规则
        • 2.固定接口地址匹配
        • 3.动态接口地址匹配
      • 六、执行校验
        • 1.执行校验入口
        • 2.反射执行校验
        • 3.执行sql校验数量
        • 4.校验服务截止时间
        • 5.禁用接口校验
        • 6.允许操作类型校验

背景

      传统的管理系统一般是这样进行权限设置的:用户与角色绑定,角色与菜单绑定,这样某个用户可以访问哪些菜单就已经定下来了;为了防止绕过权限去调用没有分配菜单对应的接口,java项目可以结合着spring security权限框架使用注解方式对具体的接口配置权限码,访问接口的用户绑定的角色下有此权限码才能访问接口,这是基于接口维度进行权限控制。

      像有些对权限细粒度划分的场景,传统的权限控制就满足不了,例如下面这样的场景:

      场景一:对同一个接口的操作,若接口处理的资源是A,用户在A下是管理员权限,可以正常访问此接口,若接口处理的资源是B,用户在B下是查看者权限,此时就需要拦截请求,这样的需求就不能单纯的从接口是否能访问来限制。

      场景二:用户购买服务,花费不同的价格购买不同的套餐,每种套餐有不同的限制,初级版限制可以新建的数量为10,中级版为20,高级版不限制,这样的需求可以在具体的接口上做判断,先获取用户购买的服务等级,然后查询已有的数量,大于阈值则进行拦截。这样的方式对代码侵入性太强,后期有调整数量或者再增加版本划分,都是不好扩展的。

      为了满足权限细粒度的划分、减轻业务代码的侵入性、易于扩展,可以使用拦截器进行权限校验,权限规则使用配置的方式添加。

一、逻辑分析

      定义好权限校验规则,key为请求的接口名,value为校验的规则集合,当请求进来时,拦截器拦截请求,获取接口名,判断规则中是否配置了此接口的校验,若是配置了校验,则获取请求参数作为校验规则需要的参数执行校验,校验通过才放行。流程图:
在这里插入图片描述

二、校验规则

      权限校验规则需要做成配置的方式,允许动态增减,可以使用配置文件或者数据库存储,在程序启动时加载到内存中,供拦截校验使用。校验规则的key使用接口名,value为规则的集合,加载到内存中使用map的方式存放,这样拦截器拦截到一个接口时,判断这个接口是否有配置校验规则可以使用map.containsKey()在时间复杂度为O(1)的情况下完成。

      这里使用Json文件的方式存储校验规则,校验规则有不同的类型,例如校验资源数量、校验是否有权访问、校验是否已过期等。我们可以使用java的多态来接收不同的规则,定义不同的实体类来接收配置信息,每种实体类约定好怎么去处理校验。当对某个接口进行校验时,遍历它配置的规则集合,根据规则的实体类是哪种类型,来调用对应的校验方法。

1.规则类型

      校验类型需要根据具体业务来定,我们来定义下面几种类型,后面也是基于这些类型来实现,类型如下:(1)一个用户关联着多个空间,在不同的空间下有不同的权限,分为管理员、编辑者、查看者,管理员可以进行删除操作,编辑者可以修改数据,查看者只能查看数据。当操作空间下的资源时需要判断用户在此空间下是哪种权限,符合权限要求才能操作资源;一个空间下包含多个图表,当用户操作某个图表时,需要判断此图表属于哪个空间,用户在此空间下是哪种权限,这样就涉及到联查的操作,出于性能考虑需要使用缓存redis记录用户在某个空间下的权限,图表属于哪个空间这样的信息,类型记为workspace

(2)用户购买不同的服务版本,可以享受不同的服务,例如初级版只能创建10个图表,中级版可以创建20个,这就需要对数量进行限制,类型记为num

(3)用户购买的服务到截止时间以后,不能再访问某些接口,需要做限制,类型记为deadline

(4)用户购买了初级版,需要对中级版才能访问的业务接口进行限制,类型记为disabled

(5)用户购买了初级版,需要对操作的业务数据类型进行限制,总的业务类型包含5种,初级版只能操作里面的2种,类型记为disabledtype

      设置多少种类型,需要根据具体的需求来定。

2.规则划分

      规则类型定义好后,基于需求,有些规则是用户购买任何版本都需要做校验,有些规则是初级版校验,有些规则是中极版校验,例如数量这样的校验,初级和中级分别对应不同的值。这里按公共校验(记为publicConfig)、初级版校验(记为noviceConfig)、中级版校验(记为intermeConfig)划分,若还有其它版本,再建对应的划分。每种划分使用list集合存放规则,这样在拦截到请求时,先获取用户开通的是哪种版本,然后遍历公共校验、开通版本对应的校验集合进行校验。

      划分为多少种大类,需要根据具体的需求来定。

3.规则配置信息

      每种规则类型都约定好按怎么的逻辑去执行,执行规则校验需要相应的参数和配置信息,每种类型创建对应的实体类接收配置信息。基于上面定义的5种规则类型进行配置说明:

(1)workspace:需要校验用户在操作资源所属的空间下是哪种权限,有什么样的权限码才可以操作此资源,并且这些所属关系需要使用缓存redis存放,所以这里使用反射的方式执行校验,具体要执行的方法写在业务service层中,拦截器根据配置信息获取到service,从request请求中获取到参数值,带着参数值使用反射invoke执行它的方法,方法返回的结果值与配置的权限码进行比较,符合了才放行。看下workspace实体类:

@EqualsAndHashCode(callSuper = true)
@Data
@NoArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
public class WorkspaceAuthority extends AuthorityConfigOne {Integer code;                  //空间权限码String beanName;               //bean名称,配置调用service层的名称,开头小写String methodName;             //执行的方法名称ArrayList methodParamType;     //执行的方法参数类型,Integer:"java.lang.Integer",String:"java.lang.String"ArrayList methodParamKey;      //执行方法需要的参数名称,用户id默认userId,其他参数根据方法需要的参数来配置
}

(2)num:需要校验用户操作资源的数量,使用sql查询的方式进行校验,配置一个允许的最大数量,配置sql需要参数值的key,参数值从request请求中获取,使用jdbcTemplate执行sql,结果值与配置的阈值比较,小于阈值才放行。看下num实体类:

@EqualsAndHashCode(callSuper = true)
@Data
@NoArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
public class NumAuthority extends AuthorityConfigOne {String querySql;       //查询数量的sqlArrayList paramKey;    //参数值集合Integer upLimit;       //最大阈值
}

(3)deadline:访问接口时需要获取用户开通服务的时间是否已到期,到期的话,直接拦截请求。看下deadline实体类:

@EqualsAndHashCode(callSuper = true)
@Data
@NoArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
public class DeadlineAuthority extends AuthorityConfigOne {
}

(4)disabled:访问接口时需要判断是否有权访问此接口,购买了初级版的服务,访问中级版才有权访问的接口时,需要拦截。看下disabled实体类:

@EqualsAndHashCode(callSuper = true)
@Data
@NoArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
public class DisabledAuthority extends AuthorityConfigOne {
}

(5)disabledtype:校验接口可以访问的类型,从request请求中获取需要校验参数的值,判断这个值是否在允许的集合里面,在集合里面才放行,这里需要配置通过key获取到的value值的具体类型,因为判断list是否包含某个值,需要是同类型的值。看下disabledtype实体类:

@EqualsAndHashCode(callSuper = true)
@Data
@NoArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
public class DisabledTypeAuthority extends AuthorityConfigOne {String checkKey;           //需要校验的keyString keyValueType;       //key值的类型,需要设置得与allowValues的类型一致ArrayList allowValues;     //允许配置的值,checkKey获取到的参数值需要在allowValues集合中才能放行
}

4.规则案例说明

      创建一个名称为AuthorityConfig.json的配置文件,放到resources配置目录下。规则案例:

[{"key": "/data/addData","config":{"publicConfig": [{"type":"workspace","code":4,"beanName":"xxxDataService","methodName":"getPrivilegeByIdFromRedisOrDatabase","methodParamType":["java.lang.Integer","java.lang.String"],"methodParamKey":["id","userId"]}],"noviceConfig": [{"type":"num","querySql":"select count(1) from table_name where xxx_id=?","paramKey":["xxxId"],"upLimit":3}],"intermeConfig": [{"type":"num","querySql":"select count(1) from table_name where xxx_id=?","paramKey":["xxxId"],"upLimit":5}]}},{"key": "/data/info","config":{"publicConfig": [{"type":"deadline"},{"type":"workspace","code":4,"beanName":"yyyDataService","methodName":"getPrivilegeByYyyIdFromRedisOrDatabase","methodParamType":["java.lang.Integer","java.lang.String"],"methodParamKey":["yyyId","userId"]}],"noviceConfig": [{"type":"disabled"}],"intermeConfig": [{"type":"disabledtype","checkKey":"yyyId","keyValueType":"java.lang.Integer","allowValues":[1,2,4,5]}]}}
]

规则放到json文件中,使用数组的方式存储,每个条目对应一个接口校验。配置的参数说明:

(1)key:需要进行校验的接口后缀;

(2)config:校验的规则信息;

(3)publicConfig:公共校验规则,只要访问对应接口,必须判断里面的校验,数组格式,可以配置多个校验类型;

(4)noviceConfig:初级版校验规则,当用户购买的服务为初级版时,必须判断里面的校验,数组格式,可以配置多个校验类型;

(5)intermeConfig:中级版校验规则,当用户购买的服务为中级版时,必须判断里面的校验,数组格式,可以配置多个校验类型;

(6)type:指明规则是哪种类型,后面把规则信息反序列化时,转成哪种实体类也是用这个字段标识;

(7)其他参数:其他参数根据规则类型来定,某种规则类型需要哪些参数,使用对应key来指定,当执行校验时需要根据配置参数取到对应的值。

对上面配置案例的解释:

      对/data/addData、/data/info两个接口进行权限校验配置,有公共规则、初级版规则、中级版规则配置。/data/addData接口访问时,需要校验它的权限码是否大于等于4,具体的校验方法写在业务service层,此处使用反射的方式去调用对应方法,执行反射需要用到方法所在的bean对象、方法名、方法参数类型、方法传递的参数值,参数值需要从request请求中获取,所以这里配置上取值的key;初级版配置了校验数量,最大值为3,当请求这个接口的用户是初级版时,执行查询数量的sql,sql需要的参数值从request中获取;中极版配置了校验数量,最大值为5。/data/info接口访问时,需要校验用户购买的服务是否已到期、空间下的权限码;初级版是不允许访问此接口;中级版时请求的id值要在[1,2,4,5]中才放行。

小提示tip

      当项目打包时,若是在pom.xml中指定了导出resource的文件项,需要把json文件也配置上,否则导出的jar包里不包含json文件。配置导出文件的方式:

 <build><resources><resource><!-- 指定配置文件所在的resource目录 --><directory>src/main/resources</directory><!-- 指定导出时包含的文件 --><includes><include>application.yml</include><include>application-${environment}.yml</include><include>logback-xxx.xml</include><include>AuthorityConfig.json</include></includes><filtering>true</filtering></resource></resources></build>

5.规则加载

      在程序启动时,读取规则配置文件,使用实体类接收。因为校验的类型type是不确定的,可以随意扩展,我们具体使用哪个实体类来接收,需要根据type来决定,不同类型的type体现了java的多态性。这里使用jackson的JsonTypeInfo实现不同type使用不同的实体类接收。

(1)为了方便type的扩展和维护,我们定义一个枚举type类。type枚举类:

@ToString
@AllArgsConstructor
public enum AuthorityType{Workspace("workspace"),Num("num"),Deadline("deadline"),Disabled("disabled"),DisabledType("disabledtype");@JsonValue@Getterprivate final String value;//提供一个根据value值来获取枚举值的方法public static AuthorityType valueOfNew(Object value) {if (value != null) {for (AuthorityType item:AuthorityType.values()) {if (item.value.equals(value)) {return item;}}}return null;}
}

(2)定义与json文件对应的实体类接收规则信息,最外层包含key、config字段,定义AuthorityConfigAll类:

@Data
@FieldDefaults(level = AccessLevel.PRIVATE)
public class AuthorityConfigAll {String key;AuthorityConfigType config;
}

(3)config里面包含着公共、初级、中级的权限划分,定义AuthorityConfigType类:

@Data
@FieldDefaults(level = AccessLevel.PRIVATE)
public class AuthorityConfigType {List<AuthorityConfigOne> publicConfig;     //公共的权限控制List<AuthorityConfigOne> noviceConfig;     //初级版的权限控制List<AuthorityConfigOne> intermeConfig;    //中级版的权限控制
}

(4)jackson的JsonTypeInfo根据不同的type使用不同的实体类接收,定义一个抽象父类AuthorityConfigOne,每种类型都继承此父类,使用父类型来存放规则集合。遍历规则的时候可以根据它具体是哪种子类型来调用此种类型的校验逻辑,这体现了java的多态性。AuthorityConfigOne类:

@Data
@FieldDefaults(level = AccessLevel.PRIVATE)
@JsonTypeInfo(use = JsonTypeInfo.Id.CUSTOM, include = JsonTypeInfo.As.EXISTING_PROPERTY,visible = true,property = "type")
@JsonTypeIdResolver(AuthorityTypeIdResolver.class)
@JsonIgnoreProperties(ignoreUnknown = true)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public abstract class AuthorityConfigOne {AuthorityType type;
}

@JsonTypeInfo注解的property属性指定了按哪个字段来确定接收规则的实体类,property属性的值需要对应上AuthorityConfigOne上的某个字段,此处对应上的是type字段;

@JsonTypeIdResolver注解指定了序列化(java对象转成字符串)、反序列化(字符串转成java对象)时的对应关系,这也是能够根据不同type使用不同实体接收的原因,AuthorityTypeIdResolver.class类需要自己定义。

(5)AuthorityTypeIdResolver指定了序列化与反序列化为哪种类型,AuthorityTypeIdResolver实体类:

public class AuthorityTypeIdResolver  extends TypeIdResolverBase {private JavaType superType;@Overridepublic void init(JavaType bt) {superType = bt;}@Overridepublic String idFromValue(Object value) {return idFromValueAndType(value, value.getClass());}//序列化调用的方法@Overridepublic String idFromValueAndType(Object value, Class<?> suggestedType) {if (!(value instanceof AuthorityConfigOne)) {return null;}AuthorityConfigOne filter = (AuthorityConfigOne) value;return filter.getType().getValue();}@Overridepublic JsonTypeInfo.Id getMechanism() {return JsonTypeInfo.Id.NAME;}//反序列化时,根据指定的property字段值,匹配按哪种实体类来接收@Overridepublic JavaType typeFromId(DatabindContext context, String id) throws IOException {AuthorityType authorityType = AuthorityType.valueOfNew(id);if (authorityType == null) {throw new IOException(String.format("id:%s not filter type", id));}final Class<? extends AuthorityConfigOne> authorityClassType;switch (authorityType) {case Workspace:authorityClassType = WorkspaceAuthority.class;break;case Num:authorityClassType = NumAuthority.class;break;case Deadline:authorityClassType = DeadlineAuthority.class;break;case Disabled:authorityClassType = DisabledAuthority.class;break;case DisabledType:authorityClassType = DisabledTypeAuthority.class;break;default:throw new IOException(String.format("not supported filterType:%s", authorityType));}return context.constructSpecializedType(superType, authorityClassType);}
}

idFromValueAndType()方法是序列化时确定type的值;typeFromId()方法是反序列化时,根据指定的property字段值,匹配按哪种实体类来接收。这样对实体类进行序列化后,再反序列化时才能找到具体的接收实体。

(6)程序启动加载规则,使用jackson下的ObjectMapper把文件流按类型引用转成对应的类型,这里使用配置类记录转好的规则集合,这样后面拦截器直接注入这个配置类就能获取到规则集合。使用spring的注解@PostConstruct初始化加载,在程序启动时,会执行bean中被@PostConstruct修饰的方法。AuthorityInit初始化类:

@Configuration
@Data
public class AuthorityInit {//转成的类型引用private static final TypeReference<List<AuthorityConfigAll>> AUTHORITY_LIST_TYPE =new TypeReference<List<AuthorityConfigAll>>() {};//记录规则信息,key为接口名,这样判断某个接口是否有配置校验规则,可以在时间复杂度为O(1)下完成private Map<String,AuthorityConfigType> authorityMap = new HashMap<String,AuthorityConfigType>();//程序启动时会执行bean下被此注解修饰的方法@PostConstructpublic void init() throws IOException {InputStream inputStream = null;try {//读取权限配置文件inputStream = ClassLoader.getSystemResourceAsStream("AuthorityConfig.json");//使用jackson下的ObjectMapper类读取文件流ObjectMapper objectMapper = new ObjectMapper();//把读取到的文件流按某种类型来接收List<AuthorityConfigAll> list = objectMapper.readValue(inputStream, AUTHORITY_LIST_TYPE);if(null != list && list.size() > 0) {//把list转成map,list每条记录的key字段值作为map的key值,config字段值作为map的value值authorityMap = list.stream().collect(Collectors.toMap(AuthorityConfigAll::getKey,AuthorityConfigAll::getConfig));}} catch (Exception e){e.printStackTrace();} finally {//关闭文件流if(null != inputStream) {inputStream.close();}}}
}

从json文件中读取到文件流,按类型引用把json文件反序列化到实体类中,获取到的list集合再转成map类型存放规则集合。程序启动后map存放的记录截图:
在这里插入图片描述
从截图中可以看出,每个接口是一条map记录,key为接口名,value为规则集合,分为公共、初级、中级规则集合,具体的规则已经根据type用不同的实体接收。

三、拦截器定义

      需要定义拦截器来拦截请求,拦截器可以配置哪些请求要拦截,哪些请求加白放行。自定义拦截器只需要实现HandlerInterceptor接口即可,把自定义拦截器添加到管理所有拦截器的InterceptorRegistry拦截器注册类中。不管用户定义了多少个拦截器,都是由InterceptorRegistry类统一管理。把自定义拦截器添加到InterceptorRegistry中的方式为:创建一个配置类,类实现WebMvcConfigurer接口,重写它的addInterceptors添加拦截器方法,在方法中把自定义拦截器以bean的方式加入进去。当请求进来时,InterceptorRegistry会遍历注册到它下面的拦截器,根据配置的拦截规则,依次执行拦截器的三个默认方法preHandle()、postHandle()、afterCompletion(),preHandle是业务Controller层处理之前执行,可以用于校验、检查等操作;postHandle是Controller层处理完,在进行视图渲染之前执行;afterCompletion是视图渲染结束之后调用,一般用于销毁资源。

1.自定义拦截器

      自定义拦截器,重写preHandle方法,此方法作为权限校验的入口点。自定义拦截器AuthorityHandlerInterceptor类:

@Slf4j
public class AuthorityHandlerInterceptor implements HandlerInterceptor {//业务controller层响应之前调用@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//只针对于方法进行处理if (!(handler instanceof HandlerMethod)) {return true;}if(!(request instanceof HttpServletRequest)){return true;}return true;}
}

2.注册拦截器

      把自定义拦截器注册到InterceptorRegistry类中进行管理。AuthorityHandlerConfig类:

@Configuration
public class AuthorityHandlerConfig implements WebMvcConfigurer {//自定义拦截器注册为bean@Beanpublic AuthorityHandlerInterceptor getAuthorityHandlerInterceptor(){return  new AuthorityHandlerInterceptor();}//添加自定义拦截器@Overridepublic void addInterceptors(@NotNull InterceptorRegistry registry) {  registry.addInterceptor(getAuthorityHandlerInterceptor()).order(Ordered.HIGHEST_PRECEDENCE);}
}

四、获取请求参数

      我们执行校验时,需要获取参数值,例如获取操作资源的id、获取当前用户id等,把获取到的参数值,作为执行校验的参数。

      获取请求参数需要考虑接口的请求方式为get还是post、还需要考虑上传的文件流、动态参数作为接口后缀的情况(像http://api/getUser/{id}后面的id值是接口的一部分),有些post请求,参数可能会放到url后,像http://api/xxx?id=1。

1.获取get提交方式参数

      get方式提交,参数都是跟在url后面,可以从HttpServletRequest中获取。获取get方式参数的方式:

  private Map<String, Object> getParamMaps(HttpServletRequest request) throws IOException   {//存放参数值的集合Map<String, Object> paramsMaps = new TreeMap();//获取url后面跟的参数Map<String, String[]> parameterMap = request.getParameterMap();if(null != parameterMap && !parameterMap.isEmpty() && parameterMap.size() > 0) {Set<Map.Entry<String, String[]>> entries = parameterMap.entrySet();Iterator<Map.Entry<String, String[]>> iterator = entries.iterator();while (iterator.hasNext()) {Map.Entry<String, String[]> next = iterator.next();paramsMaps.putIfAbsent(next.getKey(), next.getValue()[0]);}}return paramsMaps;}

2.获取post提交方式参数

      post方式提交,参数需要从HttpServletRequest的输入流中获取,但是获取输入流的方法request.getInputStream()只能调用一次,拦截器中调用后,Controller层就获取不到这些参数了,所以需要重写getInputStream()方法,不管调用多少次getInputStream()都能获取到参数。

(1)定义RequestWrapper类

      RequestWrapper类默认构造函数调用request.getInputStream()获取到参数值,把参数值记录在一个内部变量中,让此类继承HttpServletRequestWrapper,这样就可以让过滤器链chain向下传递请求时传递RequestWrapper类。过滤器链chain向下传递请求的方法:

   void doFilter(ServletRequest var1, ServletResponse var2) throws IOException, ServletException;

HttpServletRequestWrapper类的继承关系:
在这里插入图片描述
所以当请求为post方式时,我们创建一个RequestWrapper类,并把此RequestWrapper类作为过滤器chain链向下传递的request。重写的getInputStream()方法是根据RequestWrapper类内部变量值生成的输入流,内部变量在创建RequestWrapper类时已经接收了请求参数值,这样无论调用多少次getInputStream()都能获取到参数值。当这样处理后,后面Controller层获取参数时执行的getInputStream()也是RequestWrapper类重写的方法,因为过滤器链向下传递的ServletRequest的具体类是自定义的RequestWrapper类。RequestWrapper类:

public class RequestWrapper extends HttpServletRequestWrapper {//内部变量,记录请求参数private String body;public RequestWrapper(HttpServletRequest request) throws IOException {//把request设置到父类中super(request);//获取请求输入流的方法request.getInputStream()只能调用一次,在此处获取后,把值设置到变量body中//后面controller层需要从HttpServletRequestWrapper类获取输入流的方法,在此类进行重写,把body的值写入到输入流中,这样从controller层调用时就能获取到输入流StringBuilder stringBuilder = new StringBuilder();InputStream inputStream = null;BufferedReader bufferedReader = null;try {inputStream = request.getInputStream();if (inputStream != null) {bufferedReader = new BufferedReader(new InputStreamReader(inputStream,"UTF-8"));char[] charBuffer = new char[128];int bytesRead = -1;while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {stringBuilder.append(charBuffer, 0, bytesRead);}} else {stringBuilder.append("");}} catch (IOException ex) {throw ex;} finally {if (inputStream != null) {try {inputStream.close();} catch (IOException e) {e.printStackTrace();}}if (bufferedReader != null) {try {bufferedReader.close();} catch (IOException ex) {throw ex;}}}body = stringBuilder.toString();}/*** 重写父类HttpServletRequestWrapper的getInputStream方法,从body中获取请求参数,这个会在controller层进行参数获取时调用*/@Overridepublic ServletInputStream getInputStream() throws IOException {final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes("UTF-8"));ServletInputStream servletInputStream = new ServletInputStream() {@Overridepublic boolean isFinished() {return false;}@Overridepublic boolean isReady() {return false;}@Overridepublic void setReadListener(ReadListener readListener) {}@Overridepublic int read() throws IOException {return byteArrayInputStream.read();}};return servletInputStream;}/*** 重写父类HttpServletRequestWrapper获取字符流的方式,这个会在controller层进行参数获取时调用*/@Overridepublic BufferedReader getReader() throws IOException {return new BufferedReader(new InputStreamReader(this.getInputStream(),"UTF-8"));}/*** 直接返回获取 body*/public String getBody() {return this.body;}
}
(2)定义过滤器

      当为post请求时,需要重新设置过滤器链chain向下传递的ServletRequest,若是get请求,不用处理,直接传递接收到的ServletRequest。过滤器负责ServletRequest的传递,拦截器不负责ServletRequest的传递,先执行过滤器,再执行拦截器。自定义过滤器需要实现Filter,重写doFilter方法,自定义过滤器HttpServletRequestFilter类:

public class HttpServletRequestFilter implements Filter {@Overridepublic void destroy() {}//过滤器负责request的传递,拦截器不负责request的传递@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse response,FilterChain chain) throws IOException, ServletException {ServletRequest requestWrapper = null;if(servletRequest instanceof HttpServletRequest){HttpServletRequest request = (HttpServletRequest) servletRequest;String methodType = request.getMethod();if("post".equalsIgnoreCase(methodType)){//当为post方式时,需要使用request.getInputStream()获取参数,此方法只能使用一次,所以创建一个方法来接收参数body,//并重写getInputStream方法,后面controller层需要从HttpServletRequestWrapper类获取输入流的方法,在此自定义类进行重写,把body的值写入到输入流中,这样从controller层调用时就能获取到输入流requestWrapper = new RequestWrapper(request);}}// 在chain.doFiler方法中传递新的request对象if (requestWrapper == null) {chain.doFilter(servletRequest, response);} else {chain.doFilter(requestWrapper, response);}}@Overridepublic void init(FilterConfig arg0) throws ServletException {}
}
(3)注册过滤器

      自定义的过滤器需要注册到配置中,使用bean管理,过滤器注册FilterRegistration类:

@Configuration
public class FilterRegistration {@Beanpublic FilterRegistrationBean httpServletRequestReplacedRegistration() {FilterRegistrationBean registration = new FilterRegistrationBean();//添加自定义过滤器registration.setFilter(new HttpServletRequestFilter());registration.addUrlPatterns("/*");registration.addInitParameter("paramName", "paramValue");registration.setName("httpServletRequestFilter");registration.setOrder(1);return registration;}
}
(4)获取post提交方式参数

      需要使用request.getInputStream()方法获取到输入流,此时的request已经在过滤器中变更为自定义的RequestWrapper,所以此处调用的是RequestWrapper类的getInputStream()方法。获取参数的方法:

 private Map<String, Object> getParamMaps(HttpServletRequest request) throws IOException   {String methodType = request.getMethod();Map<String, Object> paramsMaps = new TreeMap();//post方式时,单独处理if("post".equalsIgnoreCase(methodType)){try {String body = getParameBody(request);TreeMap paramsMapsTemp = JSONObject.parseObject(body, TreeMap.class);if(null != paramsMapsTemp) {paramsMaps = paramsMapsTemp;}} catch (Exception e) {e.printStackTrace();}}return paramsMaps;}/*** @Description: 获取请求参数的body值*/public String getParameBody(HttpServletRequest request) throws IOException {StringBuilder stringBuilder = new StringBuilder();InputStream inputStream = null;BufferedReader bufferedReader = null;try {//此处request.getInputStream()方法调用到的是自定义类RequestWrapper重写的方法getInputStream()//重写的getInputStream方法是使用过滤器检测到是post方法时,创建的RequestWrapper,每次获取都是拿接收到的body参数组织的inputStream,所以可以重复调用//controller层调用的时候也是调用到RequestWrapper重写的方法getInputStreaminputStream = request.getInputStream();if (inputStream != null) {bufferedReader = new BufferedReader(new InputStreamReader(inputStream,"UTF-8"));char[] charBuffer = new char[128];int bytesRead = -1;while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {stringBuilder.append(charBuffer, 0, bytesRead);}} else {stringBuilder.append("");}} catch (IOException ex) {throw ex;} finally {if (inputStream != null) {try {inputStream.close();} catch (IOException e) {e.printStackTrace();}}if (bufferedReader != null) {try {bufferedReader.close();} catch (IOException ex) {throw ex;}}}return stringBuilder.toString();}

3.上传文件参数处理

      上传文件都是用post方式提交,经过上面post方式对参数处理后,在Controller层获取到的文件流为空,所以需要对post方式上传文件特殊处理。在过滤器中判断是上传文件时(请求的contentType包含multipart/form-data字符),使用MultipartResolver对文件流处理一下。过滤器中doFilter方法:

   //过滤器负责request的传递,拦截器不负责request的传递@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse response,FilterChain chain) throws IOException, ServletException {ServletRequest requestWrapper = null;if(servletRequest instanceof HttpServletRequest){HttpServletRequest request = (HttpServletRequest) servletRequest;String contentType = request.getContentType();String method = "multipart/form-data";if (contentType != null && contentType.contains(method)) {//处理文件流上传的方式,把请求处理成MultipartHttpServletRequest传递下去//实现request的转换MultipartResolver resolver = new CommonsMultipartResolver(request.getSession().getServletContext());MultipartHttpServletRequest multipartRequest = resolver.resolveMultipart(request);// 将转化后的 request 放入过滤链中request = multipartRequest;requestWrapper = new RequestWrapper(request);} else {String methodType = request.getMethod();if("post".equalsIgnoreCase(methodType)){//当为post方式时,需要使用request.getInputStream()获取参数,此方法只能使用一次,所以创建一个方法来接收参数body,//并重写getInputStream方法,后面controller层需要从HttpServletRequestWrapper类获取输入流的方法,在此自定义类进行重写,把body的值写入到输入流中,这样从controller层调用时就能获取到输入流requestWrapper = new RequestWrapper(request);}}}// 在chain.doFiler方法中传递新的request对象if (requestWrapper == null) {chain.doFilter(servletRequest, response);} else {chain.doFilter(requestWrapper, response);}}

当使用MultipartResolver处理MultipartFile文件时,它需要依赖commons-fileupload包,在项目pom.xml中引入相关依赖:

        <dependency><groupId>commons-fileupload</groupId><artifactId>commons-fileupload</artifactId><version>1.3.3</version></dependency>

4.获取动态接口参数

      当接口定义为/xxx/{id},id作为动态参数拼接接口名,例如下面这样的接口:

    @RequestMapping(value = {"/xxx/{id}"}, method = RequestMethod.GET)public Object useShare(@PathVariable String id) {return xxx;}

获取到参数的key为@PathVariable指定的名称。获取动态参数具体值的方式:

   private Map<String, Object> getParamMaps(HttpServletRequest request) throws IOException {Map<String, Object> paramsMaps = new TreeMap();//获取动态参数@PathVariableMap<String, String> pathVars = (Map<String, String>) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);if(null != pathVars && !pathVars.isEmpty() && pathVars.size() > 0) {Set<Map.Entry<String, String>> entries = pathVars.entrySet();Iterator<Map.Entry<String, String>> iterator = entries.iterator();while (iterator.hasNext()) {Map.Entry<String, String> next = iterator.next();paramsMaps.putIfAbsent(next.getKey(), next.getValue());}}return paramsMaps;}

5.获取系统固定参数

      有一些参数是根据token获取的值,例如用户id,用户id在规则校验中用得特别频繁,所以按固定参数的方式获取,约定好用户id的key,后面校验时直接使用。完整的获取请求参数的方法:

  private Map<String, Object> getParamMaps(HttpServletRequest request) throws IOException    {String methodType = request.getMethod();Map<String, Object> paramsMaps = new TreeMap();if("post".equalsIgnoreCase(methodType)){try {String body = getParameBody(request);TreeMap paramsMapsTemp = JSONObject.parseObject(body, TreeMap.class);if(null != paramsMapsTemp) {paramsMaps = paramsMapsTemp;}} catch (Exception e) {e.printStackTrace();}}Map<String, String[]> parameterMap = request.getParameterMap();if(null != parameterMap && !parameterMap.isEmpty() && parameterMap.size() > 0) {Set<Map.Entry<String, String[]>> entries = parameterMap.entrySet();Iterator<Map.Entry<String, String[]>> iterator = entries.iterator();while (iterator.hasNext()) {Map.Entry<String, String[]> next = iterator.next();paramsMaps.putIfAbsent(next.getKey(), next.getValue()[0]);}}//获取动态参数@PathVariableMap<String, String> pathVars = (Map<String, String>) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);if(null != pathVars && !pathVars.isEmpty() && pathVars.size() > 0) {Set<Map.Entry<String, String>> entries = pathVars.entrySet();Iterator<Map.Entry<String, String>> iterator = entries.iterator();while (iterator.hasNext()) {Map.Entry<String, String> next = iterator.next();paramsMaps.putIfAbsent(next.getKey(), next.getValue());}}//获取用户id,这是sa-token框架的获取方式paramsMaps.put("userId", StpUtil.getLoginId());return paramsMaps;}

五、拦截请求

      拦截请求的入手点为拦截器,只针对于方法调用进行拦截,非方法的直接放行(例如加载静态资源)。经过上面的步骤,校验规则已经定义并加载到内存中,请求参数也获取到map中,接下来要对请求进行拦截,获取接口配置的校验规则集合。

1.获取校验规则

      在拦截器中注入程序启动时加载规则信息的配置类AuthorityInit,通过AuthorityInit可以获取到记录规则集合的map。

    @AutowiredAuthorityInit authorityInit;   //注入配置类authorityInit.getAuthorityMap();//获取规则配置信息,map集合

2.固定接口地址匹配

      获取到请求的接口地址,判断此接口是否配置了校验规则,规则的校验信息已经使用map存放,key为接口名,value为AuthorityConfigType(包含公共、初级版、中级版规则集合),使用map.containsKey即可判断是否包含,不包含的直接放行,包含则遍历规则执行校验。

      可以使用这样的方式获取请求接口地址:

     String servletPath = request.getServletPath();

当一个接口请求地址是这样:http://ip+port/api/xxx/getInfo,获取到的servletPath为/xxx/getInfo,所以校验配置规则的key也是接口的后缀。判断固定接口是否有配置校验规则:

//获取请求接口地址
String servletPath = request.getServletPath();
//判断接口是否配置了校验规则        
if(authorityInit.getAuthorityMap().containsKey(servletPath)){//校验规则}

3.动态接口地址匹配

      当接口为动态参数的方式时,获取到的servletPath是一个动态的,例如/xxx/{id}接口,当参数为1时,获取到的是/xxx/1,参数为2时获取到的是/xxx/2,这时候就需要使用匹配的方式比对。针对于动态参数的接口,配置规则的key使用*代替动态的部分,像/xxx{id}这个接口,配置的key为:

{"key": "/xxx/*","config":{"publicConfig": [],"noviceConfig": [],"intermeConfig": []}},

可以使用获取动态参数值的方式去获取参数,当获取到的动态参数值不为空,则表示是一个动态接口地址,需要使用匹配的方式判断包含关系,若是动态参数值为空,说明是一个固定接口地址,使用map的包含判断。

动态参数的匹配使用AntPathMatcher路径匹配类匹配获取到的servletPath与key关系,key的集合可以过滤一下只包含*号的记录,当匹配了,则获取配置的校验规则集合。

        //获取请求接口地址String servletPath = request.getServletPath();//获取动态参数请求接口的方式Map<String, String> pathVars = (Map<String, String>) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);if(null != pathVars && !pathVars.isEmpty() && pathVars.size() > 0) {   //包含动态参数,使用正则进行判断Map<String,AuthorityConfigType> authorityMap = authorityInit.getAuthorityMap();Set<String> keySet = authorityMap.keySet();//获取到key包含*的记录List<String> collect = keySet.stream().filter(x -> x.indexOf("*") != -1).collect(Collectors.toList());if(null != collect && collect.size() >0){AntPathMatcher pathMatcher = new AntPathMatcher();//url匹配工具类for(String key : collect) {if(pathMatcher.match(key,servletPath)){  //地址匹配break;}}}} 

      当接口地址匹配后,需要获取此接口配置的校验规则集合,并把这些规则集合传递到一个执行校验的service中。 此处创建一个名为CheckAuthorityService的service类,并注入到拦截器中。完整的拦截器代码:

@Slf4j
public class AuthorityHandlerInterceptor implements HandlerInterceptor {@AutowiredAuthorityInit authorityInit; //注入配置类@AutowiredCheckAuthorityService checkAuthorityService; //注入处理校验的service类//业务controller层响应之前调用@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//只针对于方法进行处理if (!(handler instanceof HandlerMethod)) {return true;}if(!(request instanceof HttpServletRequest)){return true;}//获取请求接口地址String servletPath = request.getServletPath();//获取动态参数请求接口的方式Map<String, String> pathVars = (Map<String, String>) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);if(null != pathVars && !pathVars.isEmpty() && pathVars.size() > 0) {   //包含动态参数,使用正则进行判断Map<String,AuthorityConfigType> authorityMap = authorityInit.getAuthorityMap();Set<String> keySet = authorityMap.keySet();//获取到key包含*的记录List<String> collect = keySet.stream().filter(x -> x.indexOf("*") != -1).collect(Collectors.toList());if(null != collect && collect.size() >0){AntPathMatcher pathMatcher = new AntPathMatcher(); //url匹配工具类for(String key : collect) {if(pathMatcher.match(key,servletPath)){ //地址匹配checkAuthorityService.checkAuthority(request,authorityInit.getAuthorityMap().get(key));break;}}}} else { //固定接口地址,使用map的包含判断if(authorityInit.getAuthorityMap().containsKey(servletPath)){//校验规则checkAuthorityService.checkAuthority(request,authorityInit.getAuthorityMap().get(servletPath));}}return true;}
}

六、执行校验

      经过上面的步骤,已经获取到要校验的规则集合,CheckAuthorityService类是处理校验逻辑的,根据需求分析,需要执行sql查询数据库,所以注入JdbcTemplate;需要使用反射执行业务方法,所以注入ApplicationContext程序上下文来获取bean对象。获取请求参数值的方法上面已经分析,直接把方法写到CheckAuthorityService类中调用。

1.执行校验入口

      执行入口就是CheckAuthorityService类的checkAuthority()方法,在此方法中获取到此次请求的参数值、公共规则集合、根据用户开通的版本情况获取对应的规则集合,遍历执行规则校验。看下checkAuthority()方法:

   public void checkAuthority(HttpServletRequest request, AuthorityConfigType authorityConfigType) throws Exception {//获取请求参数Map<String, Object> paramsMaps = getParamMaps(request);//配置的权限拦截不为空if(null != authorityConfigType) {//获取公共权限进行处理List<AuthorityConfigOne> publicConfig = authorityConfigType.getPublicConfig();//配置的规则不为空则处理if(null != publicConfig && publicConfig.size() > 0) {checkAuthorityConfigOne(publicConfig,paramsMaps);}//------获取用户的权限版本int versionNum = getUserVersionNum();if(versionNum == 0) {  //初级版权限List<AuthorityConfigOne> noviceConfig = authorityConfigType.getnoviceConfig();if(null != noviceConfig && noviceConfig.size() > 0) {checkAuthorityConfigOne(noviceConfig,paramsMaps);}} else if (versionNum == 1) {//中级版权限List<AuthorityConfigOne> intermeConfig = authorityConfigType.getintermeConfig();if(null != intermeConfig && intermeConfig.size() > 0) {checkAuthorityConfigOne(intermeConfig,paramsMaps);}}}}

获取用户开通的权限版本可以使用反射去执行查询方法,也可以使用JdbcTemplate执行sql的方式去查询,反射的方式可以使用缓冲redis记录用户的版本情况。这里使用sql的方式:

    private int getUserVersionNum() {String querySql = "select version_num from xxx_user where user_id = ? ";//执行sql查询return jdbcTemplate.queryForObject(querySql, Integer.class, new Object[]{StpUtil.getLoginId()});}

遍历规则集合,根据规则是哪种实体类型,调用它对应的处理逻辑,遍历处理规则的方法checkAuthorityConfigOne():

private void checkAuthorityConfigOne(List<AuthorityConfigOne> authorityConfigOneList, Map<String, Object> paramsMaps) throws Exception  {for(AuthorityConfigOne authorityConfigOne : authorityConfigOneList){if(authorityConfigOne instanceof WorkspaceAuthority) {//校验workspace类型checkWorkspace(paramsMaps,(WorkspaceAuthority)authorityConfigOne);} else if(authorityConfigOne instanceof NumAuthority){//校验num类型checkNum(paramsMaps,(NumAuthority)authorityConfigOne);} else if(authorityConfigOne instanceof DeadlineAuthority){//验证deadline会员截止时间checkDeadline(paramsMaps,(DeadlineAuthority)authorityConfigOne);} else if(authorityConfigOne instanceof DisabledAuthority){//验证disabled接口是否可以访问checkDisabled(paramsMaps,(DisabledAuthority)authorityConfigOne);} else if(authorityConfigOne instanceof DisabledTypeAuthority){//验证disabledtype接口可以访问的类型checkDisabledType(paramsMaps,(DisabledTypeAuthority)authorityConfigOne);}}}

2.反射执行校验

      workspace类型校验需要使用反射机制,从spring程序上下文获取到业务service的bean对象,执行service下定义的方法,执行方法需要先获取到此方法,获取方法的时候要传递方法的参数类型,执行方法时要带有参数值,参数值从请求的参数map里获取,执行完业务方法后,返回值与配置的阈值进行比较。看下workspace类型校验的方法checkWorkspace():

 private void checkWorkspace(Map<String, Object> paramsMaps, WorkspaceAuthority workspaceAuthority) throws Exception {//从spring容器中根据bean名称获取beanObject bean = applicationContext.getBean(workspaceAuthority.getBeanName());//根据class获取方法时需要设置方法接收的参数类型Class[] parameterTypes = new Class[workspaceAuthority.getMethodParamType().size()];//方法参数的值Object[] methodParam = new Object[workspaceAuthority.getMethodParamKey().size()];for(int i = 0;i < workspaceAuthority.getMethodParamType().size();i++) {//根据全限定类名创建classparameterTypes[i] = Class.forName(workspaceAuthority.getMethodParamType().get(i).toString());//根据配置的参数key从请求中获取参数值Object parameValue = paramsMaps.getOrDefault(workspaceAuthority.getMethodParamKey().get(i),null);if(null == parameValue){throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "获取不到参数"+workspaceAuthority.getMethodParamKey().get(i)+"的值,请确保参数的准确性");}//设置上参数值,参数值的类型需要根据它的类型进行转一下,参数接收过来默认是字符串methodParam[i] = getMethodParamWidthType(workspaceAuthority.getMethodParamType().get(i).toString(),parameValue);}//根据方法名和参数类型获取方法Method method = bean.getClass().getMethod(workspaceAuthority.getMethodName(),parameterTypes);//使用反射执行方法,接收值Object value = method.invoke(bean,methodParam);//值进行比较if(null != value){if(Integer.parseInt(value.toString()) < workspaceAuthority.getCode()){throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "您没有权限操作此记录,请确保参数的准确性");}} else {throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "程序错误,请稍后重试");}}

从请求参数里面获取到的参数值,类型为Object,反射执行时需要转成对应的参数类型,例如Integer类型的参数,参数值需要转成Integer。写一个根据类型转成对应值的方法:

    private Object getMethodParamWidthType(String type, Object parameValue) {switch (type) {case "java.lang.Integer" :return Integer.parseInt(parameValue.toString());default:return parameValue.toString();}}

3.执行sql校验数量

      num类型需要根据配置的sql,以及sql需要的参数key,从请求参数map中获取到参数key对应的值,把参数值作为sql执行的参数传递进行,执行sql,获取到sql的结果值,与配置的阈值进行比较。看下校验数量的方法checkNum():

 private void checkNum(Map<String, Object> paramsMaps, NumAuthority numAuthority) {//获取需要执行的sqlString querySql = numAuthority.getQuerySql();//构造参数集合Object[] paramKey = new Object[numAuthority.getParamKey().size()];//变量参数集合设置进数组中for(int i = 0;i < numAuthority.getParamKey().size();i++) {//从请求参数中获取参数的值Object parameValue = paramsMaps.getOrDefault(numAuthority.getParamKey().get(i),null);if(null == parameValue){throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "获取不到参数"+numAuthority.getParamKey().get(i)+"的值,请确保参数的准确性");}paramKey[i] = parameValue;}//执行sql查询Integer num = jdbcTemplate.queryForObject(querySql, Integer.class, paramKey);//判断数量是否大于配置的最大数量if(num >= numAuthority.getUpLimit()){throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "已经达到您的最大数量:"+numAuthority.getUpLimit());}}

4.校验服务截止时间

      deadline类型需要获取用户开通服务的截止时间,拿到截止时间与当前时间做差,差值小于0,表示用户服务时间已到期。获取用户服务截止时间有用缓存redis的话,可以使用反射获取,也可以用sql执行获取,此处用sql查询获取。看下校验服务截止时间的方法checkDeadline():

 private static final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); //定义日期格式private void checkDeadline(Map<String, Object> paramsMaps, DeadlineAuthority deadlineAuthority) {//获取用户的会员截止时间,与当前时间做比对String dataLineStr = getUserDeadLine();LocalDateTime deadLine = LocalDateTime.parse(dataLineStr,dateTimeFormatter);Duration duration = Duration.between(LocalDateTime.now(),deadLine);if(duration.toMillis() < 0){throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "您的会员时间已到期,请您续期再访问");}}

获取用户开通服务截止时间的方法getUserDeadLine():

 private String getUserDeadLine() {String querySql = "select dead_line from xxx_user where user_id= ? ";//执行sql查询return jdbcTemplate.queryForObject(querySql, String.class, new Object[]{StpUtil.getLoginId()});}

5.禁用接口校验

      disabled类型是禁用接口,有配置这个类型,直接拦截接口。看下禁用接口校验的方法checkDisabled():

private void checkDisabled(Map<String, Object> paramsMaps, DisabledAuthority disabledAuthority) {throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "您没有权限操作此资源");}

6.允许操作类型校验

      disabledtype类型是配置白名单的方式进行校验,用户允许操作的类型配置在集合里面,配置一个需要校验的key,根据key从请求参数里面获取值,看值是否在允许的集合里面,在才放行。看下校验允许操作类型校验的方法checkDisabledType():

    private void checkDisabledType(Map<String, Object> paramsMaps, DisabledTypeAuthority disabledTypeAuthority) {String checkKey = disabledTypeAuthority.getCheckKey();Object parameValue = paramsMaps.getOrDefault(checkKey,null);if(null == parameValue){throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "获取不到参数"+checkKey+"的值,请确保参数的准确性");}ArrayList allowValues = disabledTypeAuthority.getAllowValues();if(null == allowValues || allowValues.size() == 0) {throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "您没有权限操作此种类型的资源");}//设置上参数值,参数值的类型需要根据它的类型进行转一下,参数接收过来默认是字符串parameValue = getMethodParamWidthType(disabledTypeAuthority.getKeyValueType(),parameValue);if(!allowValues.contains(parameValue)){throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "您没有权限操作此种类型的资源");}}

list中是否包含某个元素的判断,需要把元素的类型转成与list元素一致再进行比较,所以使用了getMethodParamWidthType()方法把元素转成需要的类型值。

完整的校验service类CheckAuthorityService:

@Service
public class CheckAuthorityService {@Autowiredprivate ApplicationContext applicationContext;@Autowiredprivate JdbcTemplate jdbcTemplate;private static final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");/*** @Description: 校验权限规则*/public void checkAuthority(HttpServletRequest request, AuthorityConfigType authorityConfigType) throws Exception {//获取请求参数Map<String, Object> paramsMaps = getParamMaps(request);//配置的权限拦截不为空if(null != authorityConfigType) {//获取公共权限进行处理List<AuthorityConfigOne> publicConfig = authorityConfigType.getPublicConfig();//配置的规则不为空则处理if(null != publicConfig && publicConfig.size() > 0) {checkAuthorityConfigOne(publicConfig,paramsMaps);}//------获取用户的权限版本int versionNum = getUserVersionNum();if(versionNum == 0) {  //个人版权限List<AuthorityConfigOne> noviceConfig = authorityConfigType.getnoviceConfig();if(null != noviceConfig && noviceConfig.size() > 0) {checkAuthorityConfigOne(noviceConfig,paramsMaps);}} else if (versionNum == 1) {//创作版权限List<AuthorityConfigOne> intermeConfig = authorityConfigType.getintermeConfig();if(null != intermeConfig && intermeConfig.size() > 0) {checkAuthorityConfigOne(intermeConfig,paramsMaps);}}}}/*** @Description: 获取用户的权限版本*/private int getUserVersionNum() {String querySql = "select version_num from xxx_user where user_id = ? ";//执行sql查询return jdbcTemplate.queryForObject(querySql, Integer.class, new Object[]{StpUtil.getLoginId()});}/*** @Description: 校验一类权限*/private void checkAuthorityConfigOne(List<AuthorityConfigOne> authorityConfigOneList, Map<String, Object> paramsMaps) throws Exception  {for(AuthorityConfigOne authorityConfigOne : authorityConfigOneList){if(authorityConfigOne instanceof WorkspaceAuthority) {//校验workspace类型checkWorkspace(paramsMaps,(WorkspaceAuthority)authorityConfigOne);} else if(authorityConfigOne instanceof NumAuthority){//校验num类型checkNum(paramsMaps,(NumAuthority)authorityConfigOne);} else if(authorityConfigOne instanceof DeadlineAuthority){//验证会员截止时间checkDeadline(paramsMaps,(DeadlineAuthority)authorityConfigOne);} else if(authorityConfigOne instanceof DisabledAuthority){//验证接口是否可以访问checkDisabled(paramsMaps,(DisabledAuthority)authorityConfigOne);} else if(authorityConfigOne instanceof DisabledTypeAuthority){//验证接口可以访问的类型checkDisabledType(paramsMaps,(DisabledTypeAuthority)authorityConfigOne);}}}/*** @Description: 验证接口可以访问的类型*/private void checkDisabledType(Map<String, Object> paramsMaps, DisabledTypeAuthority disabledTypeAuthority) {String checkKey = disabledTypeAuthority.getCheckKey();Object parameValue = paramsMaps.getOrDefault(checkKey,null);if(null == parameValue){throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "获取不到参数"+checkKey+"的值,请确保参数的准确性");}ArrayList allowValues = disabledTypeAuthority.getAllowValues();if(null == allowValues || allowValues.size() == 0) {throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "您没有权限操作此种类型的资源");}//设置上参数值,参数值的类型需要根据它的类型进行转一下,参数接收过来默认是字符串parameValue = getMethodParamWidthType(disabledTypeAuthority.getKeyValueType(),parameValue);if(!allowValues.contains(parameValue)){throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "您没有权限操作此种类型的资源");}}/*** @Description: 验证接口是否可以访问,配置了这个类型的都不允许访问接口*/private void checkDisabled(Map<String, Object> paramsMaps, DisabledAuthority disabledAuthority) {throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "您没有权限操作此资源");}/*** @Description: 验证会员截止时间,有此配置则验证当前时间与用户的过期时间*/private void checkDeadline(Map<String, Object> paramsMaps, DeadlineAuthority deadlineAuthority) {//获取用户的会员截止时间,与当前时间做比对String dataLineStr = getUserDeadLine();LocalDateTime deadLine = LocalDateTime.parse(dataLineStr,dateTimeFormatter);Duration duration = Duration.between(LocalDateTime.now(),deadLine);if(duration.toMillis() < 0){throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "您的会员时间已到期,请您续期再访问");}}/*** @Description: 获取用户的会员截止时间*/private String getUserDeadLine() {String querySql = "select create_time from doravis_sys_user where id = ? ";//执行sql查询return jdbcTemplate.queryForObject(querySql, String.class, new Object[]{StpUtil.getLoginId()});}/*** @Description: 检查数量*/private void checkNum(Map<String, Object> paramsMaps, NumAuthority numAuthority) {//获取需要执行的sqlString querySql = numAuthority.getQuerySql();//构造参数集合Object[] paramKey = new Object[numAuthority.getParamKey().size()];//变量参数集合设置进数组中for(int i = 0;i < numAuthority.getParamKey().size();i++) {//从请求参数中获取参数的值Object parameValue = paramsMaps.getOrDefault(numAuthority.getParamKey().get(i),null);if(null == parameValue){throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "获取不到参数"+numAuthority.getParamKey().get(i)+"的值,请确保参数的准确性");}paramKey[i] = parameValue;}//执行sql查询Integer num = jdbcTemplate.queryForObject(querySql, Integer.class, paramKey);//判断数量是否大于配置的最大数量if(num >= numAuthority.getUpLimit()){throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "已经达到您的最大数量:"+numAuthority.getUpLimit());}}/*** @Description: 校验workspace*/private void checkWorkspace(Map<String, Object> paramsMaps, WorkspaceAuthority workspaceAuthority) throws Exception {//从spring容器中根据bean名称获取beanObject bean = applicationContext.getBean(workspaceAuthority.getBeanName());//根据class获取方法时需要设置方法接收的参数类型Class[] parameterTypes = new Class[workspaceAuthority.getMethodParamType().size()];//方法参数的值Object[] methodParam = new Object[workspaceAuthority.getMethodParamKey().size()];for(int i = 0;i < workspaceAuthority.getMethodParamType().size();i++) {//根据全限定类名创建classparameterTypes[i] = Class.forName(workspaceAuthority.getMethodParamType().get(i).toString());//根据配置的参数key从请求中获取参数值Object parameValue = paramsMaps.getOrDefault(workspaceAuthority.getMethodParamKey().get(i),null);if(null == parameValue){throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "获取不到参数"+workspaceAuthority.getMethodParamKey().get(i)+"的值,请确保参数的准确性");}//设置上参数值,参数值的类型需要根据它的类型进行转一下,参数接收过来默认是字符串methodParam[i] = getMethodParamWidthType(workspaceAuthority.getMethodParamType().get(i).toString(),parameValue);}//根据方法名和参数类型获取方法Method method = bean.getClass().getMethod(workspaceAuthority.getMethodName(),parameterTypes);//使用反射执行方法,接收值Object value = method.invoke(bean,methodParam);//值进行比较if(null != value){if(Integer.parseInt(value.toString()) < workspaceAuthority.getcode()){throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "您没有权限操作此记录,请确保参数的准确性");}} else {throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "程序错误,请稍后重试");}}/*** @Description: 获取请求参数*/private Map<String, Object> getParamMaps(HttpServletRequest request) throws IOException {String methodType = request.getMethod();Map<String, Object> paramsMaps = new TreeMap();if("post".equalsIgnoreCase(methodType)){try {String body = getParameBody(request);TreeMap paramsMapsTemp = JSONObject.parseObject(body, TreeMap.class);if(null != paramsMapsTemp) {paramsMaps = paramsMapsTemp;}} catch (Exception e) {e.printStackTrace();}}Map<String, String[]> parameterMap = request.getParameterMap();if(null != parameterMap && !parameterMap.isEmpty() && parameterMap.size() > 0) {Set<Map.Entry<String, String[]>> entries = parameterMap.entrySet();Iterator<Map.Entry<String, String[]>> iterator = entries.iterator();while (iterator.hasNext()) {Map.Entry<String, String[]> next = iterator.next();paramsMaps.putIfAbsent(next.getKey(), next.getValue()[0]);}}//获取动态参数@PathVariableMap<String, String> pathVars = (Map<String, String>) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);if(null != pathVars && !pathVars.isEmpty() && pathVars.size() > 0) {Set<Map.Entry<String, String>> entries = pathVars.entrySet();Iterator<Map.Entry<String, String>> iterator = entries.iterator();while (iterator.hasNext()) {Map.Entry<String, String> next = iterator.next();paramsMaps.putIfAbsent(next.getKey(), next.getValue());}}paramsMaps.put("userId", StpUtil.getLoginId());return paramsMaps;}/*** @Description: 获取请求参数的body值*/public String getParameBody(HttpServletRequest request) throws IOException {StringBuilder stringBuilder = new StringBuilder();InputStream inputStream = null;BufferedReader bufferedReader = null;try {//此处request.getInputStream()方法调用到的是自定义类RequestWrapper重写的方法getInputStream()//重写的getInputStream方法是使用过滤器检测到是post方法时,创建的RequestWrapper,每次获取都是拿接收到的body参数组织的inputStream,所以可以重复调用//controller层调用的时候也是调用到RequestWrapper重写的方法getInputStreaminputStream = request.getInputStream();if (inputStream != null) {bufferedReader = new BufferedReader(new InputStreamReader(inputStream,"UTF-8"));char[] charBuffer = new char[128];int bytesRead = -1;while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {stringBuilder.append(charBuffer, 0, bytesRead);}} else {stringBuilder.append("");}} catch (IOException ex) {throw ex;} finally {if (inputStream != null) {try {inputStream.close();} catch (IOException e) {e.printStackTrace();}}if (bufferedReader != null) {try {bufferedReader.close();} catch (IOException ex) {throw ex;}}}return stringBuilder.toString();}/*** @Description: 获取带类型的方法参数*/private Object getMethodParamWidthType(String type, Object parameValue) {switch (type) {case "java.lang.Integer" :return Integer.parseInt(parameValue.toString());default:return parameValue.toString();}}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/27982.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

地图 SDK gitlab 测试代码环境配置

文章目录 1、Gradle 插件版本和 Gradle 版本2、NDK 路径3、JDK 版本4、修改变量5、重新 BuildQ&A&#xff1a; test 用例启动之后问题问题描述 拉下项目的 dev 分支&#xff0c;然后依赖的 mapsdk-base 也完成下载 &#xff0c;之后就是Android Studio 配置环境 1、Gradle …

JDBC(常用类与接口、实现数据库的增删查改)

目录 1.Connection接口常用方法 2.DriverMange类 3.Statement接口 4.实现表的数据更新&#xff08;增、改、删&#xff09; 5.实现数据查找&#xff08;ResultSet接口&#xff09; 6.PreparedStatement 数据更新 1.Connection接口常用方法 用来与数据库连接的对象&#xff…

【Spring】Spring AOP 初识及实现原理解析

博主简介&#xff1a;想进大厂的打工人博主主页&#xff1a;xyk:所属专栏: JavaEE进阶 目录 文章目录 一、初识AOP 1.1 什么是AOP&#xff1f; 1.2 AOP的组成 1.2.1 切面&#xff08;Aspect&#xff09; 1.2.2 切点&#xff08;Pointcut&#xff09; 1.2.3 连接点&…

blender基础认识(选项开关、工具栏、视图等)

文章目录 引言一、大纲选项开关和保存启动文件1. 大纲选项1. 禁用选中2. 视图影藏3. 视图禁用4. 渲染禁用 2. 保存启动文件 二、工具栏和侧边栏1. 左侧工具栏2. 右侧工具栏 三、视图1. 视角2. 缩放3. 拖拽4. 摄像机视角5. 切换正交视图6. 局部视图7. 显示隐藏 四、添加删除物体…

【DMA】如何保证 DMA 和 cache 的一致性

一方面&#xff0c;当 CPU 要从cache 读取数据时&#xff0c;会先检查cache是否命中&#xff0c;如果命中就直接返回&#xff0c;此时便不再访问内存&#xff1b;另一方面&#xff0c;DMA 在 向内存写入数据。这样一来就造成了DMA 传输的内容和cache中缓存的内容不一致。 DMA 向…

关于echarts遇到的一些问题

1.echarts监听legend&#xff0c;动态设置legend属性无效 动态更改legend中的icon&#xff0c; myChart.setOption(option&#xff09;失效&#xff0c;但是设置局部就生效 myChart.on(legendselectchanged, function (params) {if (params.selected[params.name]) {data1[dat…

C++ 多态性——纯虚函数与抽象类

抽象类是一种特殊的类&#xff0c;它为一个类族提供统一的操作界面。抽象类是为了抽象和设计的目的而建立的。可以说&#xff0c;建立抽象类&#xff0c;就是为了通过它多态地使用其中的成员函数。抽象类处于类层次的上层&#xff0c;一个抽象类自身无法实例化&#xff0c;也就…

VUE+view table.exportCsv()导出.csv文档时如何防止数据格式为科学计数

当使用table.exportCsv()方法导出数据时&#xff0c;出现科学计数法问题&#xff0c;像电话号码&#xff0c;身份证号码等&#xff0c;当数据大于15位后面的会用0替代。 针对这一问题&#xff0c;解决方法如下&#xff1a;就是再数字前加上制表符“\t”注意双引号&#xff0c;…

前端技术基础-css

前端技术基础-css【了解】 一、css理解 概念&#xff1a;CSS&#xff1a;C(cascade) SS(StyleSheet) &#xff0c;级联样式表。作用&#xff1a;对网页提供丰富的视觉效果&#xff0c;进行美化页面(需要在html页面基础上)样式规则&#xff1a;样式1&#xff1a;值1;样式2&…

0基础学习VR全景平台篇 第79篇:全景相机-泰科易如何直播推流

泰科易科技是中国的一家研发全景相机的高科技公司&#xff0c;前不久&#xff0c;在2020世界VR产业大会上发布了新一代5G VR直播影像采集终端--360starlight。以其出色的夜景成像效果和一“部”到位的直播方案重新定义了VR慢直播相机&#xff0c;对行业具有高度借鉴意义。 本文…

【工具插件类教学】电脑端移动端缩放大图自适应Simple Zoom

目录 简介 1.创建Canvas并设置 2.使用预制体Zoom 3.商店地址 简介 特点: •易于使用和高度可定制。 •支持鼠标(桌面)和触摸(移动)。 •指定最小和最大缩放的限制。 •缩放指针(鼠标/手指)或屏幕上预定义的自定义位置。 •变焦时使用夹紧/弹性变焦类型。 •定义缩…

MySQL插入数据的方法

插入数据方法&#xff1a; 1.insert into 表 values(value1, value2, value3....) 2.insert into 表 (字段1&#xff0c; 字段3&#xff0c; 字段5) values(value1, value2, value3) 3.insert into 表 [(字段1&#xff0c; 字段2&#xff0c; 字段3....)] values(value1, val…

【CSS】网格布局(简单布局、网格合并、网格嵌套)

文章目录 CSS网格布局&#xff08;Grid Layout&#xff09;1. 简单布局2. 网格合并3. 网格嵌套4. 总结 CSS网格布局&#xff08;Grid Layout&#xff09; CSS网格布局&#xff08;Grid Layout&#xff09;是一种强大且灵活的CSS布局系统&#xff0c;允许开发者以网格形式组织和…

Spring源码解析(八):bean后置处理器CommonAnnotationBeanPostProcessor

Spring源码系列文章 Spring源码解析(一)&#xff1a;环境搭建 Spring源码解析(二)&#xff1a;bean容器的创建、默认后置处理器、扫描包路径bean Spring源码解析(三)&#xff1a;bean容器的刷新 Spring源码解析(四)&#xff1a;单例bean的创建流程 Spring源码解析(五)&…

opencv基础-34 图像平滑处理-双边滤波cv2.bilateralFilter()

双边滤波&#xff08;BilateralFiltering&#xff09;是一种图像处理滤波技术&#xff0c;用于平滑图像并同时保留边缘信息。与其他传统的线性滤波方法不同&#xff0c;双边滤波在考虑像素之间的空间距离之外&#xff0c;还考虑了像素之间的灰度值相似性。这使得双边滤波能够有…

数据结构初阶--二叉树的顺序结构之堆

目录 一.堆的概念及结构 1.1.堆的概念 1.2.堆的存储结构 二.堆的功能实现 2.1.堆的定义 2.2.堆的初始化 2.3.堆的销毁 2.4.堆的打印 2.5.堆的插入 向上调整算法 堆的插入 2.6.堆的删除 向下调整算法 堆的删除 2.7.堆的取堆顶元素 2.8.堆的判空 2.9.堆的求堆的…

[Microsoft][ODBC 驱动程序管理器] 未发现数据源名称并且未指定默认驱动程序

1.今天开发了一套服务程序&#xff0c;使用的是Odbc连接MySql数据库&#xff0c; 在我本机用VS打开程序时&#xff0c;访问一切正常&#xff0c;当发布出来装在电脑上&#xff0c;连接数据库时提示&#xff1a; [Microsoft][ODBC 驱动程序管理器] 未发现数据源名称并且未指定…

VoxWeekly|The Sandbox 生态周报|20230731

欢迎来到由 The Sandbox 发布的《VoxWeekly》。我们会在每周发布&#xff0c;对上一周 The Sandbox 生态系统所发生的事情进行总结。 如果你喜欢我们内容&#xff0c;欢迎与朋友和家人分享。请订阅我们的 Medium 、关注我们的 Twitter&#xff0c;并加入 Discord 社区&#xf…

Zebec Protocol 将进军尼泊尔市场,通过 Zebec Card 推动该地区金融平等

流支付正在成为一种全新的支付形态&#xff0c;Zebec Protocol 作为流支付的主要推崇者&#xff0c;正在积极的推动该支付方案向更广泛的应用场景拓展。目前&#xff0c;Zebec Protocol 成功的将流支付应用在薪酬支付领域&#xff0c;并通过收购 WageLink 将其纳入旗下&#xf…

C#实现SqlServer数据库同步

实现效果&#xff1a; 设计思路&#xff1a; 1. 开启数据库及表的cdc&#xff0c;定时查询cdc表数据&#xff0c;封装sql语句(通过执行类型&#xff0c;主键;修改类型的cdc数据只取最后更新的记录)&#xff0c;添加到离线数据表&#xff1b; 2. 线程定时查询离线数据表&#xf…