feat: 重构filter

This commit is contained in:
zengxiaobo 2024-07-11 15:25:29 +08:00
parent 4654f80222
commit af1ad3cda2
10 changed files with 577 additions and 456 deletions

View File

@ -4,36 +4,29 @@ import cn.axzo.foundation.gateway.support.entity.GateResponse;
import cn.axzo.foundation.gateway.support.entity.ProxyContext;
import cn.axzo.foundation.gateway.support.entity.RequestContext;
import cn.axzo.foundation.gateway.support.plugin.ProxyHook;
import cn.axzo.foundation.page.IPageReq;
import cn.axzo.foundation.page.PageResp;
import cn.axzo.foundation.result.ResultCode;
import cn.axzo.foundation.util.DataAssembleHelper;
import cn.axzo.foundation.gateway.support.plugin.impl.filters.*;
import cn.axzo.foundation.util.FastjsonUtils;
import cn.axzo.foundation.web.support.rpc.RequestParams;
import cn.axzo.foundation.web.support.rpc.RpcClient;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.JSONPath;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.ImmutableMap;
import lombok.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.CollectionUtils;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
/**
* 对请求外部扩展插件 使用如下
@ -62,9 +55,6 @@ import java.util.stream.IntStream;
* </pre>
*/
@Slf4j
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RequestFilterHook implements ProxyHook {
public static final String FILTER_BEAN = "bean";
public static final String BEAN_SEPARATOR = "|";
@ -74,9 +64,24 @@ public class RequestFilterHook implements ProxyHook {
* key: bean 名称
* value: transformer 对象
*/
@NonNull
private Function<String, RequestFilter> filterBeanResolver;
private static final Map<String, RequestFilter> DEFAULT_FILTERS = ImmutableMap.<String, RequestFilter>builder()
.put("convertListAsObjectFilter", new ConvertListAsObjectFilter())
.put("convertListAsPageFilter", new ConvertListAsPageFilter())
.put("convertPageAsListFilter", new ConvertPageAsListFilter())
.put("cropContentFilter", new CropContentFilter())
.put("requestParamCheckFilter", new RequestParamCheckFilter())
.put("moveInputFieldFilter", new MoveInputFieldFilter())
.put("keyNoQueryFilter", new KeyNoQueryFilter())
.build();
public RequestFilterHook(@NonNull Function<String, RequestFilter> filterBeanResolver) {
//优先从外部传入, 没有时则从default获取
this.filterBeanResolver = s -> Optional.ofNullable(filterBeanResolver.apply(s)).orElse(DEFAULT_FILTERS.get(s));
}
@Override
public RequestParams preRequest(RequestContext reqContext, ProxyContext proxyContext, RpcClient rpcClient, String postURL, RequestParams postParams) {
if (proxyContext.getParameterFilter() == null || proxyContext.getParameterFilter().getInFilterExtends().isEmpty()) {
@ -183,446 +188,6 @@ public class RequestFilterHook implements ProxyHook {
}
}
/**
* 后台页面中通过关键字段查询时, 需要移除其他字段的查询场景. 添加通用的请求过滤器来处理此类需求.
* 使用时, 需要单独注入.
*/
public final static class KeyNoQueryFilter implements RequestFilter {
@Override
public JSON filterIn(RequestContext reqContext, JSON params, JSONObject config) {
if (params == null) {
return params;
}
Set<String> keyNoFields = Splitter.on(",").omitEmptyStrings().trimResults()
.splitToStream(Strings.nullToEmpty(config.getString("keyNoFields")))
.collect(Collectors.toSet());
Set<String> removeFields = Splitter.on(",").omitEmptyStrings().trimResults()
.splitToStream(Strings.nullToEmpty(config.getString("removeFields")))
.collect(Collectors.toSet());
if (keyNoFields.isEmpty() || removeFields.isEmpty()) {
return params;
}
JSONObject paramsJson = (JSONObject) params;
if (paramsJson.keySet().stream().noneMatch(keyNoFields::contains)) {
return paramsJson;
}
removeFields.forEach(paramsJson::remove);
return paramsJson;
}
}
public abstract static class ListOrPageRecordsFilter implements RequestFilter {
@Override
public JSON filterOut(RequestContext reqContext, JSON response) {
return filterOut(reqContext, response, new JSONObject());
}
@Override
public JSON filterOut(RequestContext reqContext, JSON response, JSONObject config) {
if (!(response instanceof JSONObject)) {
return response;
}
JSONObject jsonResp = (JSONObject) response;
Object content = jsonResp.get("data");
if (!(content instanceof JSONObject) && !(content instanceof JSONArray)) {
return response;
}
if (content instanceof JSONArray) {
List<JSONObject> records = ((JSONArray) content).toJavaList(JSONObject.class);
if (CollectionUtils.isEmpty(records)) {
return response;
}
JSONArray filtered = new JSONArray().fluentAddAll(filterRecords(reqContext, records, config));
return jsonResp.fluentPut("data", filtered);
}
JSONObject page = (JSONObject) content;
JSONArray records = page.getJSONArray("data");
if (CollectionUtils.isEmpty(records)) {
return response;
}
List<JSONObject> filtered = filterRecords(reqContext, records.toJavaList(JSONObject.class), config);
page.put("data", new JSONArray().fluentAddAll(filtered));
return response;
}
public abstract List<JSONObject> filterRecords(RequestContext reqContext, List<JSONObject> records, JSONObject config);
}
public abstract static class ConvertContentFilter implements RequestFilter {
@Override
public JSON filterOut(RequestContext reqContext, JSON response) {
return filterOut(reqContext, response, new JSONObject());
}
@Override
public JSON filterOut(RequestContext reqContext, JSON response, JSONObject config) {
if (!(response instanceof JSONObject)) {
return response;
}
JSONObject jsonResp = (JSONObject) response;
Object content = jsonResp.get("data");
if (content == null) {
return response;
}
Preconditions.checkArgument(content instanceof JSON, "ConvertContentFilter不支持原始response.content非json的情况");
return jsonResp.fluentPut("data", filterContent(reqContext, (JSON) content, config));
}
public abstract JSON filterContent(RequestContext reqContext, JSON content, JSONObject config);
}
public static class ConvertPageAsListFilter implements RequestFilter {
@Override
public JSON filterOut(RequestContext reqContext, JSON response) {
return filterOut(reqContext, response, new JSONObject());
}
@Override
public JSON filterOut(RequestContext reqContext, JSON response, JSONObject config) {
if (!(response instanceof JSONObject)) {
return response;
}
JSONObject jsonResp = (JSONObject) response;
Object content = jsonResp.get("data");
if (!(content instanceof JSONObject)) {
return response;
}
JSONArray records = ((JSONObject) content).getJSONArray("data");
return jsonResp.fluentPut("data", Optional.ofNullable(records).orElseGet(JSONArray::new));
}
}
public static class ConvertListAsObjectFilter implements RequestFilter {
@Override
public JSON filterOut(RequestContext reqContext, JSON response) {
return filterOut(reqContext, response, new JSONObject());
}
@Override
public JSON filterOut(RequestContext reqContext, JSON response, JSONObject config) {
if (!(response instanceof JSONObject)) {
return response;
}
JSONObject jsonResp = (JSONObject) response;
Object content = jsonResp.get("data");
JSONArray arrayContent = null;
if (content instanceof JSONArray) {
arrayContent = (JSONArray) content;
} else if (content instanceof JSONObject) {
JSONArray records = ((JSONObject) content).getJSONArray("data");
arrayContent = records == null ? new JSONArray() : records;
} else {
return response;
}
JSONObject obj = IntStream.range(0, arrayContent.size())
.mapToObj(arrayContent::getJSONObject)
.findFirst()
.orElseGet(JSONObject::new);
return jsonResp.fluentPut("data", filterObject(reqContext, obj, config));
}
public JSONObject filterObject(RequestContext reqContext, JSONObject obj) {
return obj;
}
public JSONObject filterObject(RequestContext reqContext, JSONObject obj, JSONObject config) {
return filterObject(reqContext, obj);
}
}
public static class ConvertListAsPageFilter implements RequestFilter {
@Override
public JSON filterOut(RequestContext reqContext, JSON response) {
return filterOut(reqContext, response, new JSONObject());
}
@Override
public JSON filterOut(RequestContext reqContext, JSON response, JSONObject config) {
if (!(response instanceof JSONObject)) {
return response;
}
JSONObject jsonResp = (JSONObject) response;
Object content = jsonResp.get("data");
if (!(content instanceof JSONArray)) {
return response;
}
List<JSONObject> records = ((JSONArray) content).toJavaList(JSONObject.class);
PageResp<JSONObject> page = PageResp.<JSONObject>builder().total(records.size()).build();
IPageReq pageParam = JSONObject.parseObject(reqContext.getRequestBody(), IPageReq.class);
Optional.ofNullable(pageParam).map(IPageReq::getPage).ifPresent(page::setCurrent);
Optional.ofNullable(pageParam).map(IPageReq::getPageSize).ifPresent(page::setSize);
page.setData(records.stream()
.skip((page.getCurrent() - 1) * page.getSize())
.limit(page.getSize())
.collect(Collectors.toList()));
return jsonResp.fluentPut("data", page);
}
}
public static class RequestParamCheckFilter implements RequestFilter {
@Override
public JSON filterIn(RequestContext reqContext, JSON params, JSONObject config) {
boolean isConfigRulePresent = config != null && !config.isEmpty() && config.containsKey("rules");
Preconditions.checkArgument(isConfigRulePresent, "RequestParamCheckFilter必须指定检查规则");
Preconditions.checkArgument(JSONObject.class.isAssignableFrom(params.getClass()),
"RequestParamCheckFilter只能检查JSONObject类型参数");
//spring list默认读取为 map(key=0...N). 将values转换为Rule
List<Rule> rules = config.getJSONObject("rules").entrySet().stream()
.map(e -> ((JSONObject) e.getValue()).toJavaObject(Rule.class))
.collect(Collectors.toList());
rules.stream().forEach(p ->
Preconditions.checkArgument(!StringUtils.isAllBlank(p.getField(), p.getJsonPath()),
"RequestParamCheckFilter规则错误field, jsonPath不能都为空"));
Operator operator = Optional.ofNullable(config.getString("operator"))
.map(e -> Operator.valueOf(e)).orElse(Operator.AND);
List<Optional<String>> errors = rules.stream()
.map(rule -> rule.check((JSONObject) params))
.collect(Collectors.toList());
//如果operator=AND. & 任意一个检查错误 则告警
if (operator == Operator.AND && errors.stream().anyMatch(Optional::isPresent)) {
throw ResultCode.INVALID_PARAMS.toException(errors.stream().filter(Optional::isPresent).findFirst().get().get());
}
//如果operator=OR. & 所有检查都是错误 则告警
if (operator == Operator.OR && errors.stream().allMatch(Optional::isPresent)) {
throw ResultCode.INVALID_PARAMS.toException(errors.stream().filter(Optional::isPresent).findFirst().get().get());
}
return params;
}
@Data
private static class Rule {
String jsonPath;
String field;
boolean required;
String regex;
private transient Pattern pattern;
/** 检查值并返回错误信息, 正确则返回empty */
protected Optional<String> check(JSONObject param) {
Object value = getValue(param);
if (required && value == null) {
return Optional.of(field + "不能为空");
}
Optional<Pattern> pattern = tryGetPattern();
if (pattern.isPresent() && value != null && !pattern.get().matcher(value.toString()).find()) {
return Optional.of(field + "参数校验失败:" + regex);
}
return Optional.empty();
}
/** 优先根据field获取value. 否则根据jsonPath */
private Object getValue(JSONObject param) {
if (!Strings.isNullOrEmpty(field)) {
return param.get(field);
}
return JSONPath.eval(param, jsonPath);
}
private Optional<Pattern> tryGetPattern() {
if (Strings.isNullOrEmpty(regex)) {
return Optional.empty();
}
if (pattern == null) {
pattern = Pattern.compile(regex);
}
return Optional.of(pattern);
}
}
public enum Operator {
AND,
OR;
}
}
/**
* 裁剪返回的content内容
* 仅支持:
* 1.content是JSONObject
* 2.content是JSONArray且其中是JSONObject
* 3.content是分页返回对象{"data":[{}]}
*/
public static class CropContentFilter extends ConvertContentFilter implements RequestFilter {
@Override
public JSON filterOut(RequestContext reqContext, JSON response) {
throw new UnsupportedOperationException("使用CropContentFilter必须指定裁剪配置");
}
@Override
public JSON filterContent(RequestContext reqContext, JSON content, JSONObject config) {
// see class: CropConfig
if (CollectionUtils.isEmpty(config)) {
throw new UnsupportedOperationException("使用CropContentFilter必须指定裁剪配置");
}
return cropContent(content, config);
}
private JSON cropContent(JSON content, JSONObject config) {
// content是json数组
if (content instanceof JSONArray) {
return cropJSONArrayContent((JSONArray) content, config);
}
// content是json对象
if (content instanceof JSONObject) {
JSONObject contentJSONObject = (JSONObject) content;
// 可能是分页content此时支持裁剪分页列表中的json对象
if (contentJSONObject.containsKey("data")) {
contentJSONObject.put("data",
cropJSONArrayContent(contentJSONObject.getJSONArray("data"), config));
}
return doCrop(contentJSONObject, config);
}
return content;
}
private JSONArray cropJSONArrayContent(JSONArray content, JSONObject config) {
// 只考虑JSONArray中是JSONObject对象的情况
List<JSONObject> contentList = content.stream()
.filter(obj -> obj instanceof JSONObject)
.map(obj -> JSONObject.parseObject(JSON.toJSONString(obj)))
.collect(Collectors.toList());
if (contentList.isEmpty()) {
return content;
}
return JSON.parseArray(JSON.toJSONString(contentList.stream()
.map(c -> doCrop(c, config))
.collect(Collectors.toList())));
}
/**
* 裁剪data中的字段
*
* @param data
* @param config {@link CropConfig}
* @return
*/
private static JSONObject doCrop(JSONObject data, JSONObject config) {
if (CollectionUtils.isEmpty(config) || CollectionUtils.isEmpty(data)) {
return data;
}
CropConfig cropConfig = config.toJavaObject(CropConfig.class);
// 优先用includeKeys
if (!CollectionUtils.isEmpty(cropConfig.getIncludeKeys())) {
return DataAssembleHelper.filterBean(data, cropConfig.getIncludeKeys(), true);
}
if (!CollectionUtils.isEmpty(cropConfig.getExcludeKeys())) {
return DataAssembleHelper.filterBean(data, cropConfig.getExcludeKeys(), false);
}
return data;
}
/**
* 裁剪content时的配置
*
* @see CropContentFilter
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
private static class CropConfig {
/**
* 希望只包含这些key只支持第一层key
* 如果有值将忽略excludeKeys
*/
private String includeKeys;
/**
* 希望排除的key只支持第一层key
*/
private String excludeKeys;
public Set<String> getIncludeKeys() {
if (Strings.isNullOrEmpty(includeKeys)) {
return Collections.emptySet();
}
return ImmutableSet.copyOf(Splitter.on(",").trimResults().omitEmptyStrings().splitToList(includeKeys));
}
public Set<String> getExcludeKeys() {
if (Strings.isNullOrEmpty(excludeKeys)) {
return Collections.emptySet();
}
return ImmutableSet.copyOf(Splitter.on(",").trimResults().omitEmptyStrings().splitToList(excludeKeys));
}
}
}
/**
* 移动请求输入参数中字段到指定字段下.
*/
public final static class MoveInputFieldFilter implements RequestFilter {
@Override
public JSON filterIn(RequestContext reqContext, JSON params, JSONObject configJSON) {
if (params == null) {
return params;
}
Config config = configJSON.toJavaObject(Config.class);
if (!config.isValid()) {
throw new IllegalArgumentException("不正确的配置参数");
}
Set<String> sourceFields = config.getSourceFields();
JSONObject sourceValue = DataAssembleHelper.filterBean(params, sourceFields);
JSONObject res = DataAssembleHelper.filterBean(params, sourceFields, false);
Preconditions.checkArgument(!res.containsKey(config.getTargetField()), "目标字段已经存在, 不能被覆盖");
res.put(config.getTargetField(), config.getTargetFieldType() == Config.FieldType.PROPERTY
// 如果是把字段move到listField作为它的第一个元素
? sourceValue : Lists.newArrayList(sourceValue));
return res;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
static class Config {
private String targetField;
private String sourceFields;
private FieldType targetFieldType;
public Set<String> getSourceFields() {
return Splitter.on(",").omitEmptyStrings().trimResults()
.splitToStream(Strings.nullToEmpty(sourceFields))
.collect(Collectors.toSet());
}
boolean isValid() {
return !getSourceFields().isEmpty() && !StringUtils.isBlank(targetField);
}
public FieldType getTargetFieldType() {
if (targetFieldType == null) {
return FieldType.PROPERTY;
}
return targetFieldType;
}
enum FieldType {
PROPERTY,
/**
* 将Field看做一个列表, moveields 放在toField下作为第一个元素.
*/
LIST;
}
}
}
@Data
@Builder
@NoArgsConstructor

View File

@ -0,0 +1,30 @@
package cn.axzo.foundation.gateway.support.plugin.impl.filters;
import cn.axzo.foundation.gateway.support.entity.RequestContext;
import cn.axzo.foundation.gateway.support.plugin.impl.RequestFilterHook;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.google.common.base.Preconditions;
public abstract class ConvertContentFilter implements RequestFilterHook.RequestFilter {
@Override
public JSON filterOut(RequestContext reqContext, JSON response) {
return filterOut(reqContext, response, new JSONObject());
}
@Override
public JSON filterOut(RequestContext reqContext, JSON response, JSONObject config) {
if (!(response instanceof JSONObject)) {
return response;
}
JSONObject jsonResp = (JSONObject) response;
Object content = jsonResp.get("data");
if (content == null) {
return response;
}
Preconditions.checkArgument(content instanceof JSON, "ConvertContentFilter不支持原始response.content非json的情况");
return jsonResp.fluentPut("data", filterContent(reqContext, (JSON) content, config));
}
public abstract JSON filterContent(RequestContext reqContext, JSON content, JSONObject config);
}

View File

@ -0,0 +1,48 @@
package cn.axzo.foundation.gateway.support.plugin.impl.filters;
import cn.axzo.foundation.gateway.support.entity.RequestContext;
import cn.axzo.foundation.gateway.support.plugin.impl.RequestFilterHook;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import java.util.stream.IntStream;
public class ConvertListAsObjectFilter implements RequestFilterHook.RequestFilter {
@Override
public JSON filterOut(RequestContext reqContext, JSON response) {
return filterOut(reqContext, response, new JSONObject());
}
@Override
public JSON filterOut(RequestContext reqContext, JSON response, JSONObject config) {
if (!(response instanceof JSONObject)) {
return response;
}
JSONObject jsonResp = (JSONObject) response;
Object content = jsonResp.get("data");
JSONArray arrayContent = null;
if (content instanceof JSONArray) {
arrayContent = (JSONArray) content;
} else if (content instanceof JSONObject) {
JSONArray records = ((JSONObject) content).getJSONArray("data");
arrayContent = records == null ? new JSONArray() : records;
} else {
return response;
}
JSONObject obj = IntStream.range(0, arrayContent.size())
.mapToObj(arrayContent::getJSONObject)
.findFirst()
.orElseGet(JSONObject::new);
return jsonResp.fluentPut("data", filterObject(reqContext, obj, config));
}
public JSONObject filterObject(RequestContext reqContext, JSONObject obj) {
return obj;
}
public JSONObject filterObject(RequestContext reqContext, JSONObject obj, JSONObject config) {
return filterObject(reqContext, obj);
}
}

View File

@ -0,0 +1,46 @@
package cn.axzo.foundation.gateway.support.plugin.impl.filters;
import cn.axzo.foundation.gateway.support.entity.RequestContext;
import cn.axzo.foundation.gateway.support.plugin.impl.RequestFilterHook;
import cn.axzo.foundation.page.IPageReq;
import cn.axzo.foundation.page.PageResp;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
public class ConvertListAsPageFilter implements RequestFilterHook.RequestFilter {
@Override
public JSON filterOut(RequestContext reqContext, JSON response) {
return filterOut(reqContext, response, new JSONObject());
}
@Override
public JSON filterOut(RequestContext reqContext, JSON response, JSONObject config) {
if (!(response instanceof JSONObject)) {
return response;
}
JSONObject jsonResp = (JSONObject) response;
Object content = jsonResp.get("data");
if (!(content instanceof JSONArray)) {
return response;
}
List<JSONObject> records = ((JSONArray) content).toJavaList(JSONObject.class);
PageResp<JSONObject> page = PageResp.<JSONObject>builder().total(records.size()).build();
IPageReq pageParam = JSONObject.parseObject(reqContext.getRequestBody(), IPageReq.class);
Optional.ofNullable(pageParam).map(IPageReq::getPage).ifPresent(page::setCurrent);
Optional.ofNullable(pageParam).map(IPageReq::getPageSize).ifPresent(page::setSize);
page.setData(records.stream()
.skip((page.getCurrent() - 1) * page.getSize())
.limit(page.getSize())
.collect(Collectors.toList()));
return jsonResp.fluentPut("data", page);
}
}

View File

@ -0,0 +1,31 @@
package cn.axzo.foundation.gateway.support.plugin.impl.filters;
import cn.axzo.foundation.gateway.support.entity.RequestContext;
import cn.axzo.foundation.gateway.support.plugin.impl.RequestFilterHook;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import java.util.Optional;
public class ConvertPageAsListFilter implements RequestFilterHook.RequestFilter {
@Override
public JSON filterOut(RequestContext reqContext, JSON response) {
return filterOut(reqContext, response, new JSONObject());
}
@Override
public JSON filterOut(RequestContext reqContext, JSON response, JSONObject config) {
if (!(response instanceof JSONObject)) {
return response;
}
JSONObject jsonResp = (JSONObject) response;
Object content = jsonResp.get("data");
if (!(content instanceof JSONObject)) {
return response;
}
JSONArray records = ((JSONObject) content).getJSONArray("data");
return jsonResp.fluentPut("data", Optional.ofNullable(records).orElseGet(JSONArray::new));
}
}

View File

@ -0,0 +1,133 @@
package cn.axzo.foundation.gateway.support.plugin.impl.filters;
import cn.axzo.foundation.gateway.support.entity.RequestContext;
import cn.axzo.foundation.gateway.support.plugin.impl.RequestFilterHook;
import cn.axzo.foundation.util.DataAssembleHelper;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.util.CollectionUtils;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 裁剪返回的content内容
* 仅支持:
* 1.content是JSONObject
* 2.content是JSONArray且其中是JSONObject
* 3.content是分页返回对象{"data":[{}]}
*/
public class CropContentFilter extends ConvertContentFilter implements RequestFilterHook.RequestFilter {
@Override
public JSON filterOut(RequestContext reqContext, JSON response) {
throw new UnsupportedOperationException("使用CropContentFilter必须指定裁剪配置");
}
@Override
public JSON filterContent(RequestContext reqContext, JSON content, JSONObject config) {
// see class: CropConfig
if (CollectionUtils.isEmpty(config)) {
throw new UnsupportedOperationException("使用CropContentFilter必须指定裁剪配置");
}
return cropContent(content, config);
}
private JSON cropContent(JSON content, JSONObject config) {
// content是json数组
if (content instanceof JSONArray) {
return cropJSONArrayContent((JSONArray) content, config);
}
// content是json对象
if (content instanceof JSONObject) {
JSONObject contentJSONObject = (JSONObject) content;
// 可能是分页content此时支持裁剪分页列表中的json对象
if (contentJSONObject.containsKey("data")) {
contentJSONObject.put("data",
cropJSONArrayContent(contentJSONObject.getJSONArray("data"), config));
}
return doCrop(contentJSONObject, config);
}
return content;
}
private JSONArray cropJSONArrayContent(JSONArray content, JSONObject config) {
// 只考虑JSONArray中是JSONObject对象的情况
List<JSONObject> contentList = content.stream()
.filter(obj -> obj instanceof JSONObject)
.map(obj -> JSONObject.parseObject(JSON.toJSONString(obj)))
.collect(Collectors.toList());
if (contentList.isEmpty()) {
return content;
}
return JSON.parseArray(JSON.toJSONString(contentList.stream()
.map(c -> doCrop(c, config))
.collect(Collectors.toList())));
}
/**
* 裁剪data中的字段
*
* @param data
* @param config {@link CropConfig}
* @return
*/
private static JSONObject doCrop(JSONObject data, JSONObject config) {
if (CollectionUtils.isEmpty(config) || CollectionUtils.isEmpty(data)) {
return data;
}
CropConfig cropConfig = config.toJavaObject(CropConfig.class);
// 优先用includeKeys
if (!CollectionUtils.isEmpty(cropConfig.getIncludeKeys())) {
return DataAssembleHelper.filterBean(data, cropConfig.getIncludeKeys(), true);
}
if (!CollectionUtils.isEmpty(cropConfig.getExcludeKeys())) {
return DataAssembleHelper.filterBean(data, cropConfig.getExcludeKeys(), false);
}
return data;
}
/**
* 裁剪content时的配置
*
* @see CropContentFilter
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
private static class CropConfig {
/**
* 希望只包含这些key只支持第一层key
* 如果有值将忽略excludeKeys
*/
private String includeKeys;
/**
* 希望排除的key只支持第一层key
*/
private String excludeKeys;
public Set<String> getIncludeKeys() {
if (Strings.isNullOrEmpty(includeKeys)) {
return Collections.emptySet();
}
return ImmutableSet.copyOf(Splitter.on(",").trimResults().omitEmptyStrings().splitToList(includeKeys));
}
public Set<String> getExcludeKeys() {
if (Strings.isNullOrEmpty(excludeKeys)) {
return Collections.emptySet();
}
return ImmutableSet.copyOf(Splitter.on(",").trimResults().omitEmptyStrings().splitToList(excludeKeys));
}
}
}

View File

@ -0,0 +1,42 @@
package cn.axzo.foundation.gateway.support.plugin.impl.filters;
import cn.axzo.foundation.gateway.support.entity.RequestContext;
import cn.axzo.foundation.gateway.support.plugin.impl.RequestFilterHook;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 后台页面中通过关键字段查询时, 需要移除其他字段的查询场景. 添加通用的请求过滤器来处理此类需求.
* 使用时, 需要单独注入.
*/
public class KeyNoQueryFilter implements RequestFilterHook.RequestFilter {
@Override
public JSON filterIn(RequestContext reqContext, JSON params, JSONObject config) {
if (params == null) {
return params;
}
Set<String> keyNoFields = Splitter.on(",").omitEmptyStrings().trimResults()
.splitToStream(Strings.nullToEmpty(config.getString("keyNoFields")))
.collect(Collectors.toSet());
Set<String> removeFields = Splitter.on(",").omitEmptyStrings().trimResults()
.splitToStream(Strings.nullToEmpty(config.getString("removeFields")))
.collect(Collectors.toSet());
if (keyNoFields.isEmpty() || removeFields.isEmpty()) {
return params;
}
JSONObject paramsJson = (JSONObject) params;
if (paramsJson.keySet().stream().noneMatch(keyNoFields::contains)) {
return paramsJson;
}
removeFields.forEach(paramsJson::remove);
return paramsJson;
}
}

View File

@ -0,0 +1,49 @@
package cn.axzo.foundation.gateway.support.plugin.impl.filters;
import cn.axzo.foundation.gateway.support.entity.RequestContext;
import cn.axzo.foundation.gateway.support.plugin.impl.RequestFilterHook;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import org.springframework.util.CollectionUtils;
import java.util.List;
public abstract class ListOrPageRecordsFilter implements RequestFilterHook.RequestFilter {
@Override
public JSON filterOut(RequestContext reqContext, JSON response) {
return filterOut(reqContext, response, new JSONObject());
}
@Override
public JSON filterOut(RequestContext reqContext, JSON response, JSONObject config) {
if (!(response instanceof JSONObject)) {
return response;
}
JSONObject jsonResp = (JSONObject) response;
Object content = jsonResp.get("data");
if (!(content instanceof JSONObject) && !(content instanceof JSONArray)) {
return response;
}
if (content instanceof JSONArray) {
List<JSONObject> records = ((JSONArray) content).toJavaList(JSONObject.class);
if (CollectionUtils.isEmpty(records)) {
return response;
}
JSONArray filtered = new JSONArray().fluentAddAll(filterRecords(reqContext, records, config));
return jsonResp.fluentPut("data", filtered);
}
JSONObject page = (JSONObject) content;
JSONArray records = page.getJSONArray("data");
if (CollectionUtils.isEmpty(records)) {
return response;
}
List<JSONObject> filtered = filterRecords(reqContext, records.toJavaList(JSONObject.class), config);
page.put("data", new JSONArray().fluentAddAll(filtered));
return response;
}
public abstract List<JSONObject> filterRecords(RequestContext reqContext, List<JSONObject> records, JSONObject config);
}

View File

@ -0,0 +1,82 @@
package cn.axzo.foundation.gateway.support.plugin.impl.filters;
import cn.axzo.foundation.gateway.support.entity.RequestContext;
import cn.axzo.foundation.gateway.support.plugin.impl.RequestFilterHook;
import cn.axzo.foundation.util.DataAssembleHelper;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 移动请求输入参数中字段到指定字段下.
*/
public class MoveInputFieldFilter implements RequestFilterHook.RequestFilter {
@Override
public JSON filterIn(RequestContext reqContext, JSON params, JSONObject configJSON) {
if (params == null) {
return params;
}
Config config = configJSON.toJavaObject(Config.class);
if (!config.isValid()) {
throw new IllegalArgumentException("不正确的配置参数");
}
Set<String> sourceFields = config.getSourceFields();
JSONObject sourceValue = DataAssembleHelper.filterBean(params, sourceFields);
JSONObject res = DataAssembleHelper.filterBean(params, sourceFields, false);
Preconditions.checkArgument(!res.containsKey(config.getTargetField()), "目标字段已经存在, 不能被覆盖");
res.put(config.getTargetField(), config.getTargetFieldType() == Config.FieldType.PROPERTY
// 如果是把字段move到listField作为它的第一个元素
? sourceValue : Lists.newArrayList(sourceValue));
return res;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
static class Config {
private String targetField;
private String sourceFields;
private Config.FieldType targetFieldType;
public Set<String> getSourceFields() {
return Splitter.on(",").omitEmptyStrings().trimResults()
.splitToStream(Strings.nullToEmpty(sourceFields))
.collect(Collectors.toSet());
}
boolean isValid() {
return !getSourceFields().isEmpty() && !StringUtils.isBlank(targetField);
}
public Config.FieldType getTargetFieldType() {
if (targetFieldType == null) {
return Config.FieldType.PROPERTY;
}
return targetFieldType;
}
enum FieldType {
PROPERTY,
/**
* 将Field看做一个列表, moveields 放在toField下作为第一个元素.
*/
LIST;
}
}
}

View File

@ -0,0 +1,95 @@
package cn.axzo.foundation.gateway.support.plugin.impl.filters;
import cn.axzo.foundation.gateway.support.entity.RequestContext;
import cn.axzo.foundation.gateway.support.plugin.impl.RequestFilterHook;
import cn.axzo.foundation.result.ResultCode;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.JSONPath;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import java.util.List;
import java.util.Optional;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class RequestParamCheckFilter implements RequestFilterHook.RequestFilter {
@Override
public JSON filterIn(RequestContext reqContext, JSON params, JSONObject config) {
boolean isConfigRulePresent = config != null && !config.isEmpty() && config.containsKey("rules");
Preconditions.checkArgument(isConfigRulePresent, "RequestParamCheckFilter必须指定检查规则");
Preconditions.checkArgument(JSONObject.class.isAssignableFrom(params.getClass()),
"RequestParamCheckFilter只能检查JSONObject类型参数");
//spring list默认读取为 map(key=0...N). 将values转换为Rule
List<Rule> rules = config.getJSONObject("rules").entrySet().stream()
.map(e -> ((JSONObject) e.getValue()).toJavaObject(Rule.class))
.collect(Collectors.toList());
rules.stream().forEach(p ->
Preconditions.checkArgument(!StringUtils.isAllBlank(p.getField(), p.getJsonPath()),
"RequestParamCheckFilter规则错误field, jsonPath不能都为空"));
Operator operator = Optional.ofNullable(config.getString("operator"))
.map(e -> Operator.valueOf(e)).orElse(Operator.AND);
List<Optional<String>> errors = rules.stream()
.map(rule -> rule.check((JSONObject) params))
.collect(Collectors.toList());
//如果operator=AND. & 任意一个检查错误 则告警
if (operator == Operator.AND && errors.stream().anyMatch(Optional::isPresent)) {
throw ResultCode.INVALID_PARAMS.toException(errors.stream().filter(Optional::isPresent).findFirst().get().get());
}
//如果operator=OR. & 所有检查都是错误 则告警
if (operator == Operator.OR && errors.stream().allMatch(Optional::isPresent)) {
throw ResultCode.INVALID_PARAMS.toException(errors.stream().filter(Optional::isPresent).findFirst().get().get());
}
return params;
}
@Data
private static class Rule {
String jsonPath;
String field;
boolean required;
String regex;
private transient Pattern pattern;
/** 检查值并返回错误信息, 正确则返回empty */
protected Optional<String> check(JSONObject param) {
Object value = getValue(param);
if (required && value == null) {
return Optional.of(field + "不能为空");
}
Optional<Pattern> pattern = tryGetPattern();
if (pattern.isPresent() && value != null && !pattern.get().matcher(value.toString()).find()) {
return Optional.of(field + "参数校验失败:" + regex);
}
return Optional.empty();
}
/** 优先根据field获取value. 否则根据jsonPath */
private Object getValue(JSONObject param) {
if (!Strings.isNullOrEmpty(field)) {
return param.get(field);
}
return JSONPath.eval(param, jsonPath);
}
private Optional<Pattern> tryGetPattern() {
if (Strings.isNullOrEmpty(regex)) {
return Optional.empty();
}
if (pattern == null) {
pattern = Pattern.compile(regex);
}
return Optional.of(pattern);
}
}
public enum Operator {
AND,
OR;
}
}