org.shoulder.web.validate.ValidateRuleEndPoint Maven / Gradle / Ivy
package org.shoulder.web.validate;
import cn.hutool.core.util.StrUtil;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Nonnull;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Pattern;
import org.shoulder.core.dto.response.BaseResult;
import org.shoulder.core.dto.response.ListResult;
import org.shoulder.core.log.Logger;
import org.shoulder.core.log.ShoulderLoggers;
import org.shoulder.validate.support.dto.FieldValidationRuleDTO;
import org.shoulder.validate.support.extract.ConstraintExtract;
import org.shoulder.validate.support.model.ValidConstraint;
import org.springframework.core.MethodParameter;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerExecutionChain;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* 统一获取表单校验规则
* 两种拉取方式
* A表单的保存url为 [POST] http://ip:port/projectName/role/save
* 第一种(通过增加前缀/api/v1/validate/rule/)
* 那么获取A表单的验证规则url: [GET] http://ip:port/projectName/api/v1/validate/rule/role/save?method=post
*
* 第二种(通过参数传递uri路径的方式来拉取):
* [GET] http://ip:port/projectName/api/v1/validate/rule?method=post&uri=/projectName/role/save
*
* 固定了验证uri地址,而要验证的表单地址作为参数进行传输。当然,可以一次性拿多个表单验证地址。有些界面可能同时存在多个表单需要提交。
*
* @author lym
*/
@Tag(name = "ValidateRuleEndPoint", description = "接口校验规则-查询(只读);识别后端代码 JSR 校验规则注解,生成校验规则。")
@RestController
@RequestMapping(value = ValidateRuleEndPoint.VALIDATION_RULE_URL_VALUE_EXPRESSION)
public class ValidateRuleEndPoint {
private final Logger log = ShoulderLoggers.SHOULDER_WEB;
public static final String VALIDATION_RULE_URL_VALUE_EXPRESSION = "${shoulder.web.ext.dynamic-validate.path:/api/v1/validate/rule}";
/**
* 动态路径
*/
private final String validationRuleUrl;
/**
* 请求 mapping
*/
private final RequestMappingHandlerMapping requestMappingHandlerMapping;
/**
* 约束抽取
*/
private final ConstraintExtract constraintExtract;
public ValidateRuleEndPoint(ConstraintExtract constraintExtract,
RequestMappingHandlerMapping requestMappingHandlerMapping,
String validationRuleUrl) {
this.validationRuleUrl = validationRuleUrl;
this.constraintExtract = constraintExtract;
this.requestMappingHandlerMapping = requestMappingHandlerMapping;
}
/**
* 支持第一种拉取方式
* 注意: 具体的方法必须在参数上面标注 @Validated 才有效
*
* @param request 请求
* @return 验证规则
* @throws Exception 异常
*/
@GetMapping("/**")
@ResponseBody
public BaseResult> viaPathVariable(@RequestParam(value = "method")
@Pattern(regexp = "POST|GET|DELETE|PUT|HEAD|PATCH|OPTIONS|TRACE")
String method,
HttpServletRequest request) throws Exception {
String requestUri = request.getRequestURI();
String uri = StrUtil.subAfter(requestUri, validationRuleUrl, false);
return BaseResult.success(localFieldValidatorDescribe(
new HttpServletRequestValidatorWrapper(request, method, uri)
));
}
/**
* 支持第二种拉取方式
*
* @param uri 表单地址
* @param request 请求
* @return 验证规则
* @throws Exception 异常
*/
@GetMapping
@ResponseBody
public BaseResult> viaQueryParam(@RequestParam(value = "method", required = false) String method,
@RequestParam(value = "uri", required = false) String uri,
HttpServletRequest request) throws Exception {
return BaseResult.success(localFieldValidatorDescribe(
new HttpServletRequestValidatorWrapper(request, method, uri)
));
}
private List localFieldValidatorDescribe(HttpServletRequest request) throws Exception {
HandlerExecutionChain chains = requestMappingHandlerMapping.getHandler(request);
if (chains == null) {
// 避免被黑客拿该接口来探测接口,故若接口不存在,返回内容与无校验规则一致
log.info("ValidateRuleEndPoint can't find handler match method={}, uri={}", request.getMethod(), request.getRequestURI());
return Collections.emptyList();
}
HandlerMethod method = (HandlerMethod) chains.getHandler();
log.debug("method is {}", method);
return loadValidatorDescribe(method);
}
/**
* 伪装成 目标请求,骗过 spring mvc 获取 handler
*/
public static class HttpServletRequestValidatorWrapper extends HttpServletRequestWrapper {
private final String method;
private final String requestUri;
public HttpServletRequestValidatorWrapper(HttpServletRequest request, String method, String requestUri) {
super(request);
this.method = method;
this.requestUri = requestUri;
}
@Override
public String getRequestURI() {
return this.requestUri;
}
@Override
public String getServletPath() {
return this.requestUri;
}
@Override
public String getMethod() {
return method;
}
}
/**
* Spring 触发校验条件
* A, 普通对象形:
* B、@RequestBody形式:
*
* 1,类 / 方法 上有 {@link Validated}
* 2,参数有 {@link Valid} / {@link Validated}
*
*
* C、普通参数形式:
* 类上有 有 @Validated
* 参数有 任意注解
*
*
* 步骤:
* 1,先判断类上是否存在
* 2,判断方法上是否存在
* 3,判断
*
* @param handlerMethod 处理方法
* @return 验证规则
* @throws Exception 异常
*/
private List loadValidatorDescribe(HandlerMethod handlerMethod) throws Exception {
Method method = handlerMethod.getMethod();
Parameter[] methodParams = method.getParameters();
if (methodParams == null || methodParams.length < 1) {
return Collections.emptyList();
}
MethodParameter[] methodParameters = handlerMethod.getMethodParameters();
if (methodParameters.length < 1) {
return Collections.emptyList();
}
Validated classValidated = method.getDeclaringClass().getAnnotation(Validated.class);
List validatorStandard = getValidConstraints(methodParams, classValidated);
return constraintExtract.extract(validatorStandard);
}
@Nonnull
private List getValidConstraints(Parameter[] methodParams, Validated classValidated) {
List validatorStandard = new ArrayList<>(methodParams.length);
for (Parameter methodParam : methodParams) {
Validated methodParamValidate = methodParam.getAnnotation(Validated.class);
if (classValidated == null && methodParamValidate == null
&& methodParam.getAnnotation(Valid.class) == null) {
// 无校验注解
continue;
}
// 优先获取方法上的 验证组,在取类上的验证组
Class>[] groupsOnMethod = null;
if (methodParamValidate != null) {
groupsOnMethod = methodParamValidate.value();
} else if (classValidated != null) {
groupsOnMethod = classValidated.value();
}
validatorStandard.add(new ValidConstraint(methodParam.getType(), groupsOnMethod, methodParam.getDeclaredAnnotations()));
}
return validatorStandard;
}
}