Interceptor checks fine-grained permissions on interfaces


background

      Traditional management systems generally set permissions like this: users are bound to roles, and roles are bound to menus, so that which menus a user can access has been determined; in order to prevent bypassing permissions and calling Interfaces, java projects can combine the spring security permission framework to use annotations to configure permission codes for specific interfaces. Users who access interfaces must have this permission code under the role bound to access the interface. This is based on the interface dimension for permission control.

      For some scenarios where permissions are fine-grained, traditional permission control cannot be satisfied, such as the following scenarios:

      Scenario 1 : For the operation of the same interface, if the resource processed by the interface is A, the user under A has the administrator authority and can access this interface normally; if the resource processed by the interface is B, the user under B has the viewer authority, At this time, the request needs to be intercepted, and such a requirement cannot be limited simply by whether the interface can be accessed.

      Scenario 2 : Users purchase services and purchase different packages at different prices. Each package has different restrictions. The number that can be created is limited to 10 for the primary version, 20 for the intermediate version, and unlimited for the advanced version. Such requirements can be specified in the specific Make a judgment on the interface, first obtain the service level purchased by the user, and then query the existing quantity, if it is greater than the threshold, it will be intercepted. This method is too intrusive to the code, and it is not easy to expand if the number is adjusted or the version division is increased later.

      In order to meet the fine-grained division of permissions, reduce the intrusion of business code, and facilitate expansion, interceptors can be used for permission verification, and permission rules can be added by configuration.

1. Logic analysis

      Define the authority verification rules, key is the interface name of the request, and value is the rule set of verification. When the request comes in, the interceptor intercepts the request, obtains the interface name, and judges whether the verification of this interface is configured in the rule. If it is configured If the verification is passed, the request parameters are obtained as the parameters required by the verification rule to perform verification, and the verification is passed before release. flow chart:
insert image description here

2. Validation rules

      Permission verification rules need to be configured in a way that allows dynamic increase or decrease. You can use configuration files or database storage, and load them into memory when the program starts for interception verification. The key of the verification rule uses the interface name, and the value is the set of rules, loaded into the memory and stored in the form of a map, so that when the interceptor intercepts an interface, it can use map.containsKey() to determine whether the interface has configuration verification rules This is done in O(1) time complexity.

      Here, Json files are used to store verification rules. There are different types of verification rules, such as the number of verification resources, whether the verification has access rights, whether the verification has expired, etc. We can use java polymorphism to receive different rules , define different entity classes to receive configuration information, and each entity class agrees on how to handle verification. When verifying an interface, traverse its configured rule set, and call the corresponding verification method according to the type of the entity class of the rule.

1. Rule type

      The verification type needs to be determined according to the specific business. Let’s define the following types, which will be implemented later based on these types. The types are as follows: (1) A user is associated with multiple spaces, and has different permissions in different spaces. It is divided into administrators, editors, and viewers. Administrators can delete, editors can modify data, and viewers can only view data. When operating resources in a space, it is necessary to determine which permission the user has in this space, and only when the permission requirements are met can the resource be operated; a space contains multiple charts, and when a user operates a certain chart, it is necessary to determine which space the chart belongs to. What kind of authority does the user have in this space, which involves the operation of joint query. For performance considerations, it is necessary to use cache redis to record the user's authority in a certain space, and which space the chart belongs to. The type is recorded as workspace .

(2) Users can enjoy different services by purchasing different service versions. For example, the primary version can only create 10 charts, and the intermediate version can create 20 charts. This requires a limit on the number, and the type is recorded as num .

(3) After the deadline for the service purchased by the user, some interfaces can no longer be accessed, and restrictions need to be made, and the type is recorded as deadline .

(4) If the user has purchased the primary version, it is necessary to restrict the business interface that can only be accessed by the intermediate version, and the type is recorded as disabled .

(5) The user purchases the primary version, and needs to restrict the type of business data to be operated. The total business types include 5 types, and the primary version can only operate 2 of them, and the type is recorded as disabledtype .

      The number of types to be set depends on specific needs.

2. Rule division

      After the rule type is defined, based on the requirements, some rules require verification of any version that the user purchases, some rules are the verification of the primary version, and some rules are the verification of the intermediate version, such as the verification of quantity, the primary and intermediate versions are respectively corresponding to different values. Here, it is divided according to public verification (recorded as publicConfig ), primary version verification (recorded as noviceConfig ), and intermediate version verification (recorded as intermeConfig ). If there are other versions, create corresponding divisions. Each division uses a list collection to store rules, so that when a request is intercepted, it first obtains which version the user has activated, and then traverses the public verification and the verification set corresponding to the activated version for verification.

      The number of categories to be divided into needs to be determined according to specific needs.

3. Rule configuration information

      Each rule type has an agreement on how to execute the logic. Executing rule verification requires corresponding parameters and configuration information. Each type creates a corresponding entity class to receive configuration information. Configuration instructions based on the five rule types defined above:

(1) workspace : It is necessary to verify which permission the user has in the space to which the resource belongs, and what kind of permission code is available to operate this resource, and these affiliation relationships need to be stored in cache redis, so reflection is used here to execute Check, the specific method to be executed is written in the business service layer, the interceptor obtains the service according to the configuration information, obtains the parameter value from the request request, and uses reflection invoke to execute its method with the parameter value, and the result value returned by the method Compare it with the configured permission code, and release it only if it matches. Look at the workspace entity class:

@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 : Need to verify the number of resources operated by the user, use sql query to verify, configure a maximum number allowed, configure sql requires the key of the parameter value, the parameter value is obtained from the request request, use jdbcTemplate to execute sql , the result value is compared with the configured threshold, and only if it is smaller than the threshold is it released. Look at the num entity class:

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

(3) deadline : When accessing the interface, it is necessary to obtain whether the time for the user to activate the service has expired, and if it expires, directly intercept the request. Look at the deadline entity class:

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

(4) disabled : When accessing an interface, you need to judge whether you have the right to access the interface. After purchasing the service of the primary version, you need to block when accessing the interface that the intermediate version has the right to access. Look at the disabled entity class:

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

(5) disabledtype : The type that can be accessed by the verification interface, obtain the value of the parameter that needs to be verified from the request request, judge whether the value is in the allowed set, and release it in the set. Here, you need to configure the value obtained through the key The specific type of value, because to judge whether the list contains a certain value, it needs to be the same type of value. Look at the disabledtype entity class:

@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. Rule Case Description

      Create a configuration file named AuthorityConfig.json and put it in the resources configuration directory. Rule case:

[
	{
    
    
		"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]
				}
			]
		}
	}
]

The rules are placed in the json file and stored in an array, and each entry corresponds to an interface check. Configuration parameter description:

(1) key: the interface suffix that needs to be verified;

(2) config: verification rule information;

(3) publicConfig: Public verification rules, as long as you access the corresponding interface, you must judge the verification inside, the array format, and multiple verification types can be configured;

(4) noviceConfig: primary version verification rules. When the service purchased by the user is the primary version, the verification inside must be judged, array format, and multiple verification types can be configured;

(5) intermeConfig: verification rules of the intermediate version. When the service purchased by the user is the intermediate version, the verification inside must be judged, the array format, and multiple verification types can be configured;

(6) type: indicates which type the rule is, and when the rule information is deserialized later, which entity class is converted to is also identified by this field;

(7) Other parameters: Other parameters are determined according to the rule type. Which parameters are required by a certain rule type are specified using the corresponding key. When performing verification, the corresponding values ​​​​need to be obtained according to the configuration parameters.

Explanation of the above configuration case:

      Perform permission verification configuration on the two interfaces of /data/addData and /data/info, including public rules, primary version rules, and intermediate version rule configurations. When accessing the /data/addData interface, it is necessary to verify whether its permission code is greater than or equal to 4. The specific verification method is written in the business service layer. Here, the reflection method is used to call the corresponding method. To execute the reflection, the method is located. The bean object, method name, method parameter type, and the parameter value passed by the method. The parameter value needs to be obtained from the request, so the key for the upper value is configured here; the primary version is configured with the number of checks, and the maximum value is 3. When requesting this When the user of the interface is the primary version, the sql of the query quantity is executed, and the parameter values ​​required by the sql are obtained from the request; the middle version is configured with the number of checks, and the maximum value is 5. When accessing the /data/info interface, it is necessary to verify whether the service purchased by the user has expired and the permission code under the space; the primary version is not allowed to access this interface; the id value requested for the intermediate version must be in [1,2,4 ,5] before release.

Tips

      When the project is packaged, if the export resource file item is specified in pom.xml, the json file needs to be configured as well, otherwise the exported jar package does not contain the json file. How to configure the export file:

 <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. Rule loading

      When the program starts, read the rule configuration file and use the entity class to receive it. Because the type of verification is uncertain and can be expanded at will, which entity class we use to receive it needs to be determined according to the type. Different types of types reflect the polymorphism of java. Here, jackson's JsonTypeInfo is used to realize that different types are received by different entity classes.

(1) In order to facilitate the expansion and maintenance of type, we define an enumeration type class. type enumeration class:

@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) Define the entity class corresponding to the json file to receive rule information, the outermost layer contains key and config fields, and define the AuthorityConfigAll class:

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

(3) config contains public, primary, and intermediate authority divisions, and defines the AuthorityConfigType class:

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

(4) Jackson's JsonTypeInfo uses different entity classes to receive according to different types, defines an abstract parent class AuthorityConfigOne, each type inherits this parent class, and uses the parent type to store the rule set. When traversing the rules, you can call the verification logic of this type according to which subtype it is, which reflects the polymorphism of java. AuthorityConfigOne class:

@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;
}

The property attribute of the @JsonTypeInfo annotation specifies which field is used to determine the entity class of the receiving rule. The value of the property attribute needs to correspond to a certain field on AuthorityConfigOne, and here corresponds to the type field;

The @JsonTypeIdResolver annotation specifies the correspondence between serialization (java objects are converted into strings) and deserialization (strings are converted into java objects). This is also the reason why different entities can be used to receive according to different types. The AuthorityTypeIdResolver.class class needs arrogant.

(5) AuthorityTypeIdResolver specifies the type of serialization and deserialization, AuthorityTypeIdResolver entity class:

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);
    }
}

The idFromValueAndType() method determines the type value during serialization; the typeFromId() method determines which entity class to receive according to the specified property field value during deserialization. In this way, after the entity class is serialized, the specific receiving entity can be found when it is deserialized.

(6) The program starts loading rules, using ObjectMapper under jackson to convert the file stream according to the type reference into the corresponding type. Here, the configuration class is used to record the converted rule set, so that the subsequent interceptor can directly inject this configuration class to obtain the rules. gather. Use the spring annotation @PostConstruct to initialize the loading. When the program starts, the method modified by @PostConstruct in the bean will be executed. AuthorityInit initialization class:

@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();
            }
        }
    }
}

Read the file stream from the json file, deserialize the json file into the entity class according to the type reference, and convert the obtained list set into a map type to store the rule set. Screenshot of the record stored in the map after the program starts:
insert image description here
From the screenshot, it can be seen that each interface is a map record, the key is the interface name, and the value is the rule set, which is divided into public, primary, and intermediate rule sets. The specific rules have been determined according to the type Received with a different entity.

3. Interceptor definition

      Interceptors need to be defined to intercept requests. Interceptors can configure which requests to intercept and which requests to pass. A custom interceptor only needs to implement the HandlerInterceptor interface, and add the custom interceptor to the InterceptorRegistry interceptor registration class that manages all interceptors. No matter how many interceptors are defined by the user, they are managed uniformly by the InterceptorRegistry class. The way to add a custom interceptor to the InterceptorRegistry is: create a configuration class that implements the WebMvcConfigurer interface, rewrite its addInterceptors add interceptor method, and add the custom interceptor as a bean in the method. When a request comes in, the InterceptorRegistry will traverse the interceptors registered under it, and execute the three default methods preHandle(), postHandle(), and afterCompletion() of the interceptor in turn according to the configured interception rules. preHandle is before the processing of the business controller layer Execute, which can be used for verification, inspection and other operations; postHandle is executed before the view rendering after the Controller layer is processed; afterCompletion is called after the view rendering is completed, and is generally used to destroy resources.

1. Custom interceptor

      Customize the interceptor and override the preHandle method, which serves as the entry point for permission verification. Custom interceptor AuthorityHandlerInterceptor class:

@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. Register interceptor

      Register the custom interceptor to the InterceptorRegistry class for management. AuthorityHandlerConfig class:

@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);
    }
}

4. Get request parameters

      When we perform verification, we need to obtain parameter values, such as obtaining the id of the operating resource, obtaining the current user id, etc., and use the obtained parameter values ​​as parameters for performing verification.

      To obtain request parameters, you need to consider whether the request method of the interface is get or post, and you also need to consider the uploaded file stream and dynamic parameters as the interface suffix (like the id value behind http://api/getUser/{id} is part of the interface ), for some post requests, the parameters may be placed after the url, like http://api/xxx?id=1.

1. Obtain the get submission method parameters

      Submit in get mode, the parameters are all followed by url, and can be obtained from HttpServletRequest. The way to get get method parameters:

  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. Obtain post submission method parameters

      Submitted by post, the parameters need to be obtained from the input stream of HttpServletRequest, but the method request.getInputStream() to obtain the input stream can only be called once. After calling in the interceptor, the Controller layer cannot obtain these parameters, so it needs to be rewritten The getInputStream() method can get parameters no matter how many times getInputStream() is called.

(1) Define the RequestWrapper class

      The default constructor of the RequestWrapper class calls request.getInputStream() to get the parameter value, records the parameter value in an internal variable, and lets this class inherit HttpServletRequestWrapper, so that the filter chain chain can pass the RequestWrapper class when passing the request down. The method of passing the request down the filter chain chain:

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

The inheritance relationship of the HttpServletRequestWrapper class:
insert image description here
So when the request is in the post mode, we create a RequestWrapper class and use this RequestWrapper class as the request passed down the filter chain chain. The rewritten getInputStream() method is an input stream generated based on the internal variable value of the RequestWrapper class. The internal variable has received the request parameter value when the RequestWrapper class was created, so that no matter how many times getInputStream() is called, the parameter value can be obtained. After processing in this way, the getInputStream() executed when the Controller layer obtains parameters is also a method rewritten by the RequestWrapper class, because the specific class of the ServletRequest passed down by the filter chain is a custom RequestWrapper class. RequestWrapper class:

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) Define the filter

      When it is a post request, it is necessary to reset the ServletRequest passed down by the filter chain chain. If it is a get request, it does not need to be processed, and the received ServletRequest is directly passed on. The filter is responsible for the delivery of ServletRequest, and the interceptor is not responsible for the delivery of ServletRequest. The filter is executed first, and then the interceptor is executed. A custom filter needs to implement Filter, rewrite the doFilter method, and customize the filter HttpServletRequestFilter class:

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) Register filter

      Custom filters need to be registered in the configuration, using bean management, filter registration FilterRegistration class:

@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) Obtain post submission method parameters

      You need to use the request.getInputStream() method to obtain the input stream. At this time, the request has been changed to a custom RequestWrapper in the filter, so the getInputStream() method of the RequestWrapper class is called here. Method to get parameters:

 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. Upload file parameter processing

      The uploaded files are all submitted in the post method. After the parameters are processed by the post method above, the file stream obtained at the Controller layer is empty, so special processing is required for the uploaded files in the post method. When it is judged in the filter that the file is uploaded (the requested contentType contains multipart/form-data characters), use MultipartResolver to process the file stream. The doFilter method in the filter:

   //过滤器负责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);
        }
    }

When using MultipartResolver to process MultipartFile files, it needs to rely on the commons-fileupload package, and introduce related dependencies in the project pom.xml:

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

4. Obtain dynamic interface parameters

      When the interface is defined as /xxx/{id}, id is used as a dynamic parameter to join the interface name, such as the following interface:

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

The obtained parameter key is the name specified by @PathVariable. Ways to get specific values ​​of dynamic parameters:

   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. Obtain system fixed parameters

      Some parameters are obtained according to the value of the token, such as user id, which is used frequently in rule verification, so it is obtained as a fixed parameter, and the key of the user id is agreed upon, which will be used directly in the verification later. The complete method of obtaining request parameters:

  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;
    }

5. Intercept request

      The starting point for intercepting requests is the interceptor, which only intercepts method calls, and directly releases non-methods (such as loading static resources). After the above steps, the verification rules have been defined and loaded into the memory, and the request parameters are also obtained into the map. Next, the request needs to be intercepted to obtain the verification rule set of the interface configuration.

1. Obtain verification rules

      The configuration class AuthorityInit, which loads the rule information when the program is injected into the interceptor, can obtain the map that records the rule set through AuthorityInit.

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

2. Fixed interface address matching

      Obtain the requested interface address, and determine whether the interface is configured with a verification rule. The verification information of the rule has been stored in a map. The key is the interface name, and the value is AuthorityConfigType (including public, primary, and intermediate rule sets). Use the map .containsKey can be used to determine whether it is included, if it is not included, it will be released directly, and if it is included, it will traverse the rules to perform verification.

      You can get the request interface address in this way:

     String servletPath = request.getServletPath();

When an interface request address is like this: http://ip+port/api/xxx/getInfo, the obtained servletPath is /xxx/getInfo, so the key for verifying configuration rules is also the suffix of the interface. Determine whether a fixed interface has a configuration verification rule:

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

3. Dynamic interface address matching

      When the interface is a dynamic parameter, the obtained servletPath is a dynamic one, such as /xxx/{id} interface, when the parameter is 1, the obtained is /xxx/1, and when the parameter is 2, the obtained is /xxx/2, at this time, you need to use the matching method to compare. For the interface with dynamic parameters, the key of the configuration rule uses * instead of the dynamic part, such as the interface /xxx{id}, the configured key is:

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

You can use the method of obtaining dynamic parameter values ​​to obtain parameters. When the obtained dynamic parameter value is not empty, it means that it is a dynamic interface address. You need to use the matching method to determine the inclusion relationship. If the dynamic parameter value is empty, it means it is a dynamic interface address. Fixed interface address, using map inclusion judgment.

The matching of dynamic parameters uses the AntPathMatcher path matching class to match the relationship between the obtained servletPath and the key. The set of keys can be filtered to only contain * records. When it matches, the set of verification rules configured will be obtained.

        //获取请求接口地址
        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;
                    }
                }
            }
        } 

      When the interface address matches, it is necessary to obtain the verification rule set configured on this interface, and pass these rule sets to a service that performs verification. Create a service class named CheckAuthorityService here and inject it into the interceptor. Complete interceptor code:

@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;
    }
}

6. Perform verification

      After the above steps, the set of rules to be verified has been obtained. The CheckAuthorityService class handles the verification logic. According to the requirement analysis, it is necessary to execute SQL to query the database, so inject JdbcTemplate; it needs to use reflection to execute business methods, so inject the ApplicationContext program context to get the bean object. The method of obtaining the request parameter value has been analyzed above, and the method is directly written into the CheckAuthorityService class and called.

1. Execute the verification entry

      The execution entry point is the checkAuthority() method of the CheckAuthorityService class. In this method, the parameter value of the request, the public rule set, and the corresponding rule set are obtained according to the version activated by the user, and the rule verification is traversed. Take a look at the checkAuthority() method:

   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);
                }
            }
        }
    }

To obtain the permission version activated by the user, you can use reflection to execute the query method, or you can use JdbcTemplate to execute SQL to query. The reflection method can use buffered redis to record the user's version status. Here is how to use 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()});
    }

Traverse the rule set, call its corresponding processing logic according to which entity type the rule is, and traverse the rule checkAuthorityConfigOne() method:

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. Reflective execution verification

      The workspace type verification needs to use the reflection mechanism to obtain the bean object of the business service from the spring program context, and execute the method defined under the service. The execution method needs to obtain this method first. When obtaining the method, the parameter type of the method must be passed, and the execution method It must have a parameter value. The parameter value is obtained from the requested parameter map. After the business method is executed, the return value is compared with the configured threshold. Look at the workspace type check method 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, "程序错误,请稍后重试");
        }
    }

The parameter value obtained from the request parameter is of type Object, which needs to be converted to the corresponding parameter type when the reflection is executed, for example, for a parameter of type Integer, the parameter value needs to be converted to Integer. Write a method that converts to the corresponding value according to the type:

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

3. Execute the number of sql checks

      The num type needs to obtain the value corresponding to the parameter key from the request parameter map according to the configured sql and the parameter key required by sql, pass the parameter value as the parameter of sql execution, execute sql, and obtain the result value of sql, and Configured thresholds are compared. Look at the method checkNum() for checking the number:

 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. Verification service deadline

      The deadline type needs to obtain the deadline for the user to activate the service. The difference between the deadline and the current time is obtained. If the difference is less than 0, it means that the user's service time has expired. If it is useful to cache redis to get the user service deadline, you can use reflection to get it, or you can use sql to execute the get, here use sql query to get it. Look at the method checkDeadline() for checking the service deadline:

 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, "您的会员时间已到期,请您续期再访问");
        }
    }

The method getUserDeadLine() to obtain the deadline for the user to open the service:

 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. Disable interface verification

      The disabled type is a disabled interface. If this type is configured, the interface is directly intercepted. Look at the method checkDisabled() that disables interface verification:

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

6. Allow operation type verification

      The disabledtype type is verified by configuring the whitelist. The type of operation allowed by the user is configured in the collection, configure a key that needs to be verified, and obtain the value from the request parameter according to the key to see if the value is in the allowed collection. let go. Look at the checkDisabledType() method for checking the type of operation allowed:

    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, "您没有权限操作此种类型的资源");
        }
    }

To determine whether a certain element is contained in the list, the type of the element needs to be converted to be consistent with the list element before comparison, so the getMethodParamWidthType() method is used to convert the element into the required type value.

Complete verification service class 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 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) {
    
    
        //获取需要执行的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();
        }
    }
}

Guess you like

Origin blog.csdn.net/ZHANGLIZENG/article/details/132156759