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

文章目录

      • 一、逻辑分析
      • 二、校验规则
        • 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.rhkb.cn/news/79841.html

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

相关文章

Flink正常消费一段时间后,大量反压,看着像卡住了,但又没有报错。

文章目录 前言一、原因分析二、解决方案 前言 前面我也有提到&#xff0c;发现flink运行一段时间后&#xff0c;不再继续消费的问题。这个问题困扰了我非常久&#xff0c;一开始也很迷茫。又因为比较忙&#xff0c;所以一直没有时间能够去寻找答案&#xff0c;只是通过每天重启…

IDEA中maven项目失效,pom.xml文件橙色/橘色

IDEA中maven项目失效&#xff0c;pom.xml文件橙色/橘色 IDEA中Maven项目失效 IDEA中创建的maven项目中的文件夹都变成普通格式&#xff0c;pom.xml变成橙色 右键点击橙色的pom.xml文件&#xff0c;选择add as maven project maven项目开始重新导入相应依赖&#xff0c;恢复…

字符串查找匹配算法

概述 字符串匹配&#xff08;查找&#xff09;是字符串的一种基本操作&#xff1a;给定带匹配查询的文本串S和目标子串T&#xff0c;T也叫做模式串。在文本S中找到一个和模式T相符的子字符串&#xff0c;并返回该子字符串在文本中的位置。 暴力匹配 Brute Force Algorithm&a…

安全狗V3.512048版本绕过

安全狗安装 安全狗详细安装、遇见无此服务器解决、在windows中命令提示符中进入查看指定文件夹手动启动Apache_安全狗只支持 glibc_2.14 但是服务器是2.17_黑色地带(崛起)的博客-CSDN博客 安全狗 safedogwzApacheV3.5.exe 右键电脑右下角安全狗图标-->选择插件-->安装…

vscode中无法使用git解决方案

1 首先查看git安装目录 where git 2 找到bash.exe 的路径 比如&#xff1a;C:/Users/Wangzd/AppData/Local/Programs/Git/bin/bash 3 找到vscode的配置项setting.json 4 添加 "terminal.integrated.shell.windowns": "C:/Users/Wangzd/AppData/Local/Pr…

架构训练营学习笔记:6-2 微服务基础选型

基础选型 微服务基础设施架构 优先级 其中&#xff0c;核心 就是服务注册、服务发现、服务路由。 模式1-嵌入SDK 模式2-反向代理式 模式3-网络代理式&#xff08;Service Mesh&#xff09; 模式对比 常见微服务框架选择 嵌入SDK-dubbo Spring Cloud 反向代理式 APISIX …

跨境B2B2C多用户购物网站源码快速部署

​ 搭建跨境B2B2C多用户购物网站需要以下步骤&#xff1a; 1. 确定业务模式和定位&#xff1a;确定网站的业务模式&#xff0c;包括跨境B2B2C的商业模式以及目标用户定位。 2. 营业执照和域名注册&#xff1a;根据当地法律要求&#xff0c;注册一家具有法人资格的公司&#xff…

基于Citespace、vosviewer、R语言的文献计量学可视化分析技术及全流程文献可视化SCI论文高效写作方法

跨尺度预测模式&#xff08;The Model for Prediction Across Scales - MPAS&#xff09;是由洛斯阿拉莫斯实验室和美国国家大气研究中心(NCAR)共同开发&#xff0c;其由3个部分组成&#xff0c;分别称为 MPAS-A&#xff08;大气模型&#xff09;、MPAS-O&#xff08;海洋模型&…

一百四十一、Kettle——kettle8.2在Windows本地开启carte服务以及配置子服务器

一、目的 在kettle建好共享资源库后&#xff0c;为了给在服务器上部署kettle的carte服务躺雷&#xff0c;先在Windows本地测试一下怎么玩carte服务 二、Kettle版本以及在Windows本地安装路径 kettle版本是8.2 pdi-ce-8.2.0.0-342 kettle本地安装路径是D:\j…

全面讲解最小二乘法

常见的最小二乘法我们就不多说了&#xff0c;下面主要介绍一下最小二乘法的一些先进方法。 正则化的最小二乘法 在使用常见的最小二乘法进行回归分析时&#xff0c;常常会遇到过拟合的问题&#xff0c;也就是在训练数据集上表现的很好&#xff0c;但是在测试数据集上表现的很…

【Maven】常用命令、插件管理、私服nexus

【Maven】常用命令、插件管理、私服nexus 常用命令 插件管理 私服nexus Nexus3 配置私服 项目pom中的配置 发布时区分正式版、快照版 常用命令 Maven提供了一系列常用命令&#xff0c;用于构建、测试和管理项目。以下是一些常用的Maven命令示例&#xff1a; mvn clean:…

Cadence学习

Cadence学习 Cadence内容涵盖Cadence主要功能Cadence功能模块Allegro Design Entry CIS 和 OrCAD Capture CIS 的区别Cadence 公司简介Allegro Design Entry CISOrCAD Capture CIS OrCAD中part和database part区别OrCAD中不同页面的连接关系应该怎么处理&#xff08;1&#xff…

【Unity3D】消融特效

1 前言 选中物体消融特效中基于 Shader 实现了消融特效&#xff0c;本文将基于 Shader Graph 实现消融特效&#xff0c;两者原理一样&#xff0c;只是表达方式不同&#xff0c;另外&#xff0c;选中物体消融特效中通过 discard 丢弃片元&#xff0c;本文通过 alpha 测试丢弃片元…

【华秋推荐】物联网入门学习模块 ESP8266

随着全球信息技术的不断进步和普及&#xff0c;物联网成为当今备受关注的技术热点之一。通过物理和数字设备之间的连接来实现自动化和互联互通的网络。无线传感器、云计算和大数据分析等技术&#xff0c;物联网使设备能够相互交流和共享信息&#xff0c;实现智能化的自动化操作…

RocketMQ第二课-核心编程模型以及生产环境最佳实践

一、回顾RocketMQ的消息模型 ​ 上一章节我们从试验整理出了RocketMQ的消息模型&#xff0c;这也是我们使用RocketMQ时最直接的指导。 二、深入理解RocketMQ的消息模型 1、RocketMQ客户端基本流程 <dependency><groupId>org.apache.rocketmq</groupId>&…

数据结构 | 搜索和排序——搜索

目录 一、顺序搜索 二、分析顺序搜索算法 三、二分搜索 四、分析二分搜索算法 五、散列 5.1 散列函数 5.2 处理冲突 5.3 实现映射抽象数据类型 搜索是指从元素集合中找到某个特定元素的算法过程。搜索过程通常返回True或False&#xff0c;分别表示元素是否存在。有时&a…

gradle项目Connection timed out,build时先下载gradle问题download gradle-x.x-bin.zip

IDEA 导入 Gradle 项目&#xff0c;编译的时候会默认下载 配置版本的Gradle.zip问题&#xff0c;一般会下载失败&#xff0c;提示Connection timed out&#xff0c;连接超时。 解决办法&#xff1a; 修改项目根目录下gradle目录下的gradle-wrapper.properties文件&#xff0c;…

Kafka3.0.0版本——生产者如何提高吞吐量

目录 一、生产者提高吞吐量参数设置二、产者提高吞吐量代码示例 一、生产者提高吞吐量参数设置 batch.size&#xff1a;设置批次大小&#xff0c;默认16klinger.ms&#xff1a;设置等待时间&#xff0c;修改为5-100msbuffer.memory&#xff1a;设置缓冲区大小&#xff0c; 默认…

JGJ80-2016建筑施工高处作业安全技术规范

为规范建筑施工高处作业及其管理&#xff0c;做到防护安全、技术先进、经济合理&#xff0c;制定本规范。 本规范适用于建筑工程施工高处作业中的临边、洞口攀登、悬空、操作平台、交叉作业及安全网搭设等项作业。 本规范亦适用于其他高处作业的各类洞、坑、沟、槽等部位的施…

高性能计算集群使用

一、PuTTY的下载与安装 PuTTY是一款开源的连接软件&#xff0c;是 SSH、Telnet、Rlogin 和 SUPDUP 网络协议的客户端程序。 下载网址&#xff1a;Download PuTTY - a free SSH and telnet client for Windows 安装好后连接自己的服务器 输入用户名和密码&#xff0c;回车登录…