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


背景

      传统的管理系统一般是这样进行权限设置的:用户与角色绑定,角色与菜单绑定,这样某个用户可以访问哪些菜单就已经定下来了;为了防止绕过权限去调用没有分配菜单对应的接口,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;       //查询数量的sql
     ArrayList 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;           //需要校验的key
    String 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
    @Getter
    private 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;

    @Override
    public void init(JavaType bt) {
        superType = bt;
    }

    @Override
    public String idFromValue(Object value) {
        return idFromValueAndType(value, value.getClass());
    }

    //序列化调用的方法
    @Override
    public String idFromValueAndType(Object value, Class<?> suggestedType) {
        if (!(value instanceof AuthorityConfigOne)) {
            return null;
        }
        AuthorityConfigOne filter = (AuthorityConfigOne) value;
        return filter.getType().getValue();
    }

    @Override
    public JsonTypeInfo.Id getMechanism() {
        return JsonTypeInfo.Id.NAME;
    }

    //反序列化时,根据指定的property字段值,匹配按哪种实体类来接收
    @Override
    public 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下被此注解修饰的方法
    @PostConstruct
    public 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层响应之前调用
    @Override
    public 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
    @Bean
    public AuthorityHandlerInterceptor getAuthorityHandlerInterceptor(){
        return  new AuthorityHandlerInterceptor();
    }

    //添加自定义拦截器
    @Override
    public 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层进行参数获取时调用
     */
    @Override
    public ServletInputStream getInputStream() throws IOException {
        final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes("UTF-8"));
        ServletInputStream servletInputStream = new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setReadListener(ReadListener readListener) {

            }

            @Override
            public int read() throws IOException {
                return byteArrayInputStream.read();
            }
        };
        return servletInputStream;
    }

    /**
     * 重写父类HttpServletRequestWrapper获取字符流的方式,这个会在controller层进行参数获取时调用
     */
    @Override
    public 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 {

    @Override
    public void destroy() {

    }

    //过滤器负责request的传递,拦截器不负责request的传递
    @Override
    public 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);
        }
    }

    @Override
    public void init(FilterConfig arg0) throws ServletException {

    }
}
(3)注册过滤器

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

@Configuration
public class FilterRegistration {

    @Bean
    public 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重写的方法getInputStream
            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;
                }
            }
        }
        return stringBuilder.toString();
    }
3.上传文件参数处理

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

   //过滤器负责request的传递,拦截器不负责request的传递
    @Override
    public 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();
        //获取动态参数@PathVariable
        Map<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]);
            }
        }
        //获取动态参数@PathVariable
        Map<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。

    @Autowired
    AuthorityInit 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 {

    @Autowired
    AuthorityInit authorityInit; //注入配置类

    @Autowired
    CheckAuthorityService checkAuthorityService; //注入处理校验的service类

    //业务controller层响应之前调用
    @Override
    public 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名称获取bean
        Object 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++) {
            //根据全限定类名创建class
            parameterTypes[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) {
        //获取需要执行的sql
        String 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 {

    @Autowired
    private ApplicationContext applicationContext;

    @Autowired
    private 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 xxx_user where id = ? ";
        //执行sql查询
        return jdbcTemplate.queryForObject(querySql, String.class, new Object[]{StpUtil.getLoginId()});

    }

    /**
     * @Description: 检查数量
     */
    private void checkNum(Map<String, Object> paramsMaps, NumAuthority numAuthority) {
        //获取需要执行的sql
        String 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名称获取bean
        Object 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++) {
            //根据全限定类名创建class
            parameterTypes[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]);
            }
        }
        //获取动态参数@PathVariable
        Map<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重写的方法getInputStream
            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;
                }
            }
        }
        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();
        }
    }
}
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Boot拦截器是一种用于在请求到达目标方法之前或之后执行一些操作的机制。在实现登录校验时,可以使用拦截器来拦截请求,并进行登录状态的验证。 首先,需要编写一个拦截器类来实现登录校验的逻辑。拦截器类需要继承HandlerInterceptorAdapter,并重写preHandle方法。在preHandle方法中,可以获取到请求的信息,如请求路径和参数。根据业务需要,可以从请求中获取登录状态信息,进行验证。 其次,需要配置拦截器类生效的路径。在Spring Boot中,可以通过配置类或注解来配置拦截器类。可以使用@Configuration注解标注一个配置类,并通过implements WebMvcConfigurer接口来添加拦截器。 在配置类中,可以通过重写addInterceptors方法来配置拦截器。将拦截器对象添加到InterceptorRegistry中,并设置拦截的路径。可以使用addPathPatterns方法来设置拦截的路径模式,如"/user/*"表示拦截以/user/开头的路径。 在拦截器生效后,当发起请求时,拦截器会拦截请求并调用preHandle方法进行登录校验。如果登录状态验证失败,可以根据业务需要进行处理,如返回错误信息或进行重定向。 需要注意的是,拦截器拦截的是请求,而不是方法。也就是说,拦截器会在请求到达目标方法之前执行,但不会影响目标方法的执行。如果需要在拦截器中进行一些后续操作,可以使用postHandle和afterCompletion方法。 通过使用Spring Boot拦截器校验登录,可以在请求到达目标方法之前进行登录状态的验证,确保只有已登录的用户才能访问敏感接口或页面,提高系统的安全性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值