jsonのフィールドをvalidateするGatewayFilterFactoryを作ってみた

# 概要

- SpringCloudGatewayでRequestBodyをいじってみるついでにjsonのフィールドをvalidateするGatewayFilterFactoryを作ってみた
- 名付けて`ValidateRequestBodyGatewayFilterFactory`!!（笑）

# できること/制約

- フィールドに対して指定した正規表現を満たすかチェックできる
- 指定されていないフィールドを落とせる
- 多階層のjsonには対応していない
- 数値が文字列でバックポストされる（"value":123 → "value":"123"）

# 作ったもの

SpringCloudGatewayに用意されている`ModifyRequestBodyGatewayFilterFactory`を参考にしました

```java:ValidateRequestBodyGatewayFilterFactory
@Component
public class ValidateRequestBodyGatewayFilterFactory extends AbstractGatewayFilterFactory<ValidateRequestBodyGatewayFilterFactory.Config> {
    public ValidateRequestBodyGatewayFilterFactory() {
        super(Config.class);
    }

    private final List<HttpMessageReader<?>> messageReaders = HandlerStrategies.withDefaults().messageReaders();

    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            var serverRequest = ServerRequest.create(exchange, messageReaders);
            var validatedBody = serverRequest.bodyToMono(StringMap.class)
                    .map(originalBody -> {
                        var verifiedParams = new StringMap();
                        for (var key: originalBody.keySet()) {
                            var pattern = config.getPattern(key);
                            if (pattern != null) {
                                var param = originalBody.get(key);
                                if (pattern.matcher(param).find()) {
                                    verifiedParams.put(key, param);
                                }
                            }
                        }

                        return verifiedParams;
                    });

            HttpHeaders headers = new HttpHeaders();
            headers.putAll(exchange.getRequest().getHeaders());
            headers.remove(HttpHeaders.CONTENT_LENGTH);

            CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(
                    exchange, headers);

            BodyInserter bodyInserter = BodyInserters.fromPublisher(validatedBody, StringMap.class);
            return bodyInserter.insert(outputMessage, new BodyInserterContext())
                    .then(Mono.defer(() -> {
                        var decorator = new ServerHttpRequestDecorator(exchange.getRequest()) {
                            @Override
                            public Flux<DataBuffer> getBody() {
                                return outputMessage.getBody();
                            }

                            @Override
                            public HttpHeaders getHeaders() {
                                return headers;
                            }
                        };

                        return chain.filter(exchange.mutate().request(decorator).build());
                    }));
        };
    }

    @Getter
    public static class Config {
        private Map<String, Pattern> roles = new HashMap<>();

        public void setRoles(String role) {
            try {
                Map<String, String> originalRoles = new ObjectMapper().readValue(role, StringMap.class);
                for (var key: originalRoles.keySet()) {
                    this.roles.put(key, Pattern.compile(originalRoles.get(key)));
                }
            } catch (Exception ex) {
                throw new IllegalArgumentException(ex);
            }
        }

        public Pattern getPattern(String key) {
            return this.roles.getOrDefault(key, null);
        }
    }

    public static class StringMap extends HashMap<String, String> { }
}
```

# 使い方

```yaml:application.yml
spring:
  cloud:
    gateway:
      routes:
        - id: validate
          uri: <バックポスト先>
          filters:
            - name: ValidateRequestBody
              args:
                # jsonで「フィールド:正規表現」の組み合わせを定義する 
                roles: |
                  {"number":"^[0-9]+$"}
```

この設定だと

```
{"number":123,"hoge":"hoge"}
↓
{"number":"123"}
```

のようにnumberだけが透過する



何かの役に立つ時が来たらいいな😇