拦截器对接口细粒度权限校验
文章目录
- 一、逻辑分析
- 二、校验规则
- 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();}}
}
相关文章:

拦截器对接口细粒度权限校验
文章目录 一、逻辑分析二、校验规则1.规则类型2.规则划分3.规则配置信息4.规则案例说明5.规则加载 三、拦截器定义1.自定义拦截器2.注册拦截器 四、获取请求参数1.获取get提交方式参数2.获取post提交方式参数(1)定义RequestWrapper类(2&#…...
计算机科技历史纵横:8月6日的十大里程碑
计算机科技历史纵横:8月6日的十大里程碑 目录 引言1951年:EDSAC电脑完成第一个实际计算任务1964年:IBM发布System/360系列1973年:Xerox PARC开发出第一台个人电脑Xerox Alto1976年:Apple发布Apple I电脑1981年&#…...

知识图谱实战应用23-【知识图谱的高级用法】Neo4j图算法的Cypher查询语句实例
大家好,我是微学AI,今天给大家介绍一下知识图谱实战应用23-【知识图谱的高级用法】Neo4j图算法的Cypher查询语句实例,Neo4j图算法是一套在Neo4j图数据库上运行的算法集合。这些算法专门针对图数据结构进行设计,用于分析、查询和处理图数据。图算法可以帮助我们发现图中的模…...
C++ 头文件函数大全
<cstdio>头文件: scanf("%d",&a); cin>>a; scanf("%d%d",&a,&b); cin>>a>>b; for(i1;i<n;i) scanf("&d,&alil); cin>>a[i]; printf("%d",a); cout&l…...

智慧物流园区整体架构方案【46页PPT】
导读:原文《智慧物流园区整体架构方案【46页PPT】》(获取来源见文尾),本文精选其中精华及架构部分,逻辑清晰、内容完整,为快速形成售前方案提供参考。 完整版领取方式 完整版领取方式: 如需获取…...

llama2模型下载
介绍 LLaMA 2-CHAT与OpenAI ChatGPT效果一样好。LLaMA 2与LLaMA 1架构相同,LLaMA 2训练数据是2000000000000个tokens,还是用了1000000个人类新标注的数据。上下文长度由2048提升为4096。 本教程提供两种下载方式: 1官方下载脚本下载 2hugging face网站下载 官网资格申请 …...

C高级【day4】
思维导图: 写一个函数,获取用户的uid和gid并使用变量接收: #!/bin/bashfunction get_uid {my_uidid -umy_gidid -g }get_uid echo "当前用户的UID:$my_uid" echo "当前用户的GID:$my_gid"整理冒泡…...

【前端实习生备战秋招】—HTML 和 CSS面试题总结(一)
【前端实习生备战秋招】—HTML 和 CSS面试题总结(一) 1. 你做的页面在哪些流览器测试过?这些浏览器的内核分别是什么? IE:trident内核 Firefox:gecko内核 Safari:webkit内核 Opera:以前是presto内核,Opera现已改用Goo…...

【从零学习python 】02. 开发工具介绍
文章目录 编写Python代码一、常见的代码编辑工具二、运行Python程序三、Pycharm的下载和安装PyCharm的主要功能区域进阶案例 编写Python代码 根据我们之前介绍的知识,我们知道,所谓代码其实就是将一段普通文本按照一定的规范编写,然后交给电…...

Python:Spider爬虫工程化入门到进阶(2)使用Spider Admin Pro管理scrapy爬虫项目
Python:Spider爬虫工程化入门到进阶系列: Python:Spider爬虫工程化入门到进阶(1)创建Scrapy爬虫项目Python:Spider爬虫工程化入门到进阶(2)使用Spider Admin Pro管理scrapy爬虫项目 目录 1、使…...
CubeMap convert into Octahedral思路
看了一些介绍,大多都是如何采样Octahedral的,那么如何把cubemap转成为这个呢 首先,我们想想 Vec4 Sample(Vec3 direction) { // Some logicwait wait wait think about what weve got here UV UV UV! return SampleTexture(Image, UV); }这个…...

vue项目实战-脑图编辑管理系统kitymind百度脑图
前言 项目为前端vue项目,把kitymind百度脑图整合到前端vue项目中,显示了脑图的绘制,编辑,到处为json,png,text等格式的功能 文章末尾有相关的代码链接,代码只包含前端项目,在原始的…...
c++调用ffmpeg api录屏 并进行rtmp推流
代码及工程见https://download.csdn.net/download/daqinzl/88156528 开发工具:visual studio 2019 记得启动rtmp流媒体服务 nginx的rtmp服务见https://download.csdn.net/download/daqinzl/20478812 播放,采用ffmpeg工具集里的ffplay.exe, 执行命令 f…...

SQL分类及通用语法数据类型(超详细版)
一、SQL分类 SQL是结构化查询语言(Structured Query Language)的缩写。它是一种用于管理和操作关系型数据库系统的标准化语言。SQL分类如下: DDL: 数据定义语言,用来定义数据库对象(数据库、表、字段)DML:…...

配置Hive远程服务详细步骤
HiveServer2支持多客户端的并发和认证,为开放API客户端如JDBC、ODBC提供了更好的支持。 (1)修改hive-site.xml,在文件中添加以下内容: <property><name>hive.metastore.event.db.notification.api.auth&l…...

Java中实现图片和Base64的互相转化
文章目录 前言一、代码二、测试三、结果 前言 公司项目中用到了实名认证此,采用的第三方平台。后端中用到的单项功能为身份证信息人像对比功能,在写demo的过程中发现,它们所要求的图片信息为base64编码格式。 一、代码 package com.bajiao…...

视频添加字幕
1、依靠ffmpeg 命令 package zimu;import java.io.IOException;public class TestSrt {public static void main(String[] args) {String videoFile "/test/test1.mp4";String subtitleFile "/test/test1.SRT";String outputFile "/test/testout13…...
Vue VS React:两大前端框架的对比与分析
Vue和React是当前最流行的前端框架之一,它们都有着广泛的应用和开发者社区。下面是Vue和React之间的深度对比与分析: 学习曲线: Vue:Vue拥有简单直观的API和文档,对初学者友好。Vue的设计初衷是逐步增强的,…...
【机密计算标准解读】 基于TEE的安全计算(IEEE 2952)
目录 1.概述2.定义、术语、缩略语3.技术框架3.1 架构框架3.2 分层功能4.基础组件4.1 基础层4.2 平台层4.3 应用层4.4 服务层4.5 交叉层5.安全计算参考过程6.技术和安全要求6.1 隔离要求6.2 互操作要求6.3 性能要求6.4 可用性要求6.5 数据安全要求6.6 密码学要求 1. 概述 随着…...

程序员编写文档的 10 个技巧
编写好的文档在软件开发领域具有重大意义。文档是概述特定问题陈述、方法、功能、工作流程、架构、挑战和开发过程的书面数据或指令。文档可以让你全面了解解决方案的功能、安装和配置。 文档不仅是为其他人编写的,也是为自己编写的。它让我们自己知道我们以前做过什…...
Ubuntu系统下交叉编译openssl
一、参考资料 OpenSSL&&libcurl库的交叉编译 - hesetone - 博客园 二、准备工作 1. 编译环境 宿主机:Ubuntu 20.04.6 LTSHost:ARM32位交叉编译器:arm-linux-gnueabihf-gcc-11.1.0 2. 设置交叉编译工具链 在交叉编译之前&#x…...
java_网络服务相关_gateway_nacos_feign区别联系
1. spring-cloud-starter-gateway 作用:作为微服务架构的网关,统一入口,处理所有外部请求。 核心能力: 路由转发(基于路径、服务名等)过滤器(鉴权、限流、日志、Header 处理)支持负…...
进程地址空间(比特课总结)
一、进程地址空间 1. 环境变量 1 )⽤户级环境变量与系统级环境变量 全局属性:环境变量具有全局属性,会被⼦进程继承。例如当bash启动⼦进程时,环 境变量会⾃动传递给⼦进程。 本地变量限制:本地变量只在当前进程(ba…...

智慧工地云平台源码,基于微服务架构+Java+Spring Cloud +UniApp +MySql
智慧工地管理云平台系统,智慧工地全套源码,java版智慧工地源码,支持PC端、大屏端、移动端。 智慧工地聚焦建筑行业的市场需求,提供“平台网络终端”的整体解决方案,提供劳务管理、视频管理、智能监测、绿色施工、安全管…...
postgresql|数据库|只读用户的创建和删除(备忘)
CREATE USER read_only WITH PASSWORD 密码 -- 连接到xxx数据库 \c xxx -- 授予对xxx数据库的只读权限 GRANT CONNECT ON DATABASE xxx TO read_only; GRANT USAGE ON SCHEMA public TO read_only; GRANT SELECT ON ALL TABLES IN SCHEMA public TO read_only; GRANT EXECUTE O…...
TRS收益互换:跨境资本流动的金融创新工具与系统化解决方案
一、TRS收益互换的本质与业务逻辑 (一)概念解析 TRS(Total Return Swap)收益互换是一种金融衍生工具,指交易双方约定在未来一定期限内,基于特定资产或指数的表现进行现金流交换的协议。其核心特征包括&am…...

从零实现STL哈希容器:unordered_map/unordered_set封装详解
本篇文章是对C学习的STL哈希容器自主实现部分的学习分享 希望也能为你带来些帮助~ 那咱们废话不多说,直接开始吧! 一、源码结构分析 1. SGISTL30实现剖析 // hash_set核心结构 template <class Value, class HashFcn, ...> class hash_set {ty…...
全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比
目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec? IPsec VPN 5.1 IPsec传输模式(Transport Mode) 5.2 IPsec隧道模式(Tunne…...
Android第十三次面试总结(四大 组件基础)
Activity生命周期和四大启动模式详解 一、Activity 生命周期 Activity 的生命周期由一系列回调方法组成,用于管理其创建、可见性、焦点和销毁过程。以下是核心方法及其调用时机: onCreate() 调用时机:Activity 首次创建时调用。…...

接口自动化测试:HttpRunner基础
相关文档 HttpRunner V3.x中文文档 HttpRunner 用户指南 使用HttpRunner 3.x实现接口自动化测试 HttpRunner介绍 HttpRunner 是一个开源的 API 测试工具,支持 HTTP(S)/HTTP2/WebSocket/RPC 等网络协议,涵盖接口测试、性能测试、数字体验监测等测试类型…...