Spring Cloud Gateway关于body

1,697 阅读5分钟

Spring Cloud Gateway关于body

前言

当前需求:

  1. 仅读取--客户端传送数据的content_type: application/json, post请求,服务器需要取出body进行某些字段验证.
  2. 读取并修改---客户端传送数据的content_type: application/json, post请求,服务器需要取出body进行修改并传入下级.

1. 仅读取 判断是否拦截

pom.xml

使用的应该是挺新的版本了

	<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.0.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.mydemo</groupId>
    <artifactId>gateway</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>gateway</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
            <version>2.2.3.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.70</version>
        </dependency>
            <version>2.2.3.RELEASE</version>
        </dependency>
    </dependencies>

router

这里仅定义一个就行, 流的顺序不要变了, path filter放在最后面

猜测: 根据链式来走的,readybody已经调用了,但是下面的path 之类的的不匹配,就不往下走了

其他的router可以放在yml 正常即可

​ 如果get请求也有body的话.. 就看一下代码中的注释

/**
 * @author kun.han
 */
@EnableAutoConfiguration
@Component
public class ApiLocator {

    @Resource
    private RequestFilter requestFilter;

    public static Logger LOGGER = LoggerFactory.getLogger(ApiLocator.class);

    @Bean
    public RouteLocator myRoutes(RouteLocatorBuilder builder) {
        RouteLocatorBuilder.Builder routes = builder.routes();
        RouteLocatorBuilder.Builder serviceProvider = routes
                .route("love",
                        r -> r
                                .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
                                .or()
                                .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                       // 如果get请求也有body的话.. 就把下面两行注释掉
                                .and()
                                .method(HttpMethod.POST)
                                .and()
                                .readBody(String.class, readBody -> {
                                    LOGGER.info("request method POST, body  is:{}", readBody);
                                    return true;
                                })
                                .and()
                                .path("/一个项目用不到的路径/**")
                                .filters(f -> {
                                    f.stripPrefix(1);
                                    return f;
                                })
                                .uri("http://localhost:10005"));
        RouteLocator routeLocator = serviceProvider.build();
        System.out.println("custom RouteLocator is loading ... {}" + routeLocator);
        return routeLocator;
    }
}

filter

/**
 * 全局过滤器
 *
 * @author kun.han
 */
@Component
public class AuthorizeFilterNew implements GlobalFilter, Ordered {

    public static Logger LOGGER = LoggerFactory.getLogger(AuthorizeFilterNew.class);

    private static void run() {
        LOGGER.info("请求结束");
    }
    
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();

        // 某些路径不作验证
        String path = request.getPath().toString();
        if (path.contains("/anon/")){
            return chain.filter(exchange);
        }
        
        // 试图从header获取关键验证参数
        HttpHeaders headers = request.getHeaders();
        String token = headers.getFirst(ConstantMsg.AUTHORIZE_TOKEN);
        if (StringUtils.isEmpty(token)) {
            // 试图从params获取关键验证参数
            token = request.getQueryParams().getFirst(ConstantMsg.AUTHORIZE_TOKEN);
        }
        // 试图从xxx-www-f 和 application/json body 获取关键验证参数
        String requestBody = exchange.getAttribute("cachedRequestBodyObject");
        LOGGER.info("requestBody : {}", requestBody);
        if (!StringUtils.hasText(token) && Objects.nonNull(requestBody)) {
            MediaType mediaType = request.getHeaders().getContentType();
            //  从body中获取想要的数据 自定义的方法 在utils
            token = AuthUtils.getTokenByMediaType(requestBody, mediaType);
        }
        // 获取到验证参数 进行验证
        if (AuthUtils.authTokenNormal(token)) {
            return chain.filter(exchange).then(Mono.fromRunnable(AuthorizeFilterNew::run));
        } else {
            return AuthUtils.authFailure(exchange);
        }
    }

    @Override
    public int getOrder() {
        return 0;
    }
}

utils

整理xxx-form格式数据 / 返回application/json格式信息

/**
 * @author kun.han on 2020/6/5 13:05
 */
public class AuthUtils {

    public static Mono<Void> authFailure(ServerWebExchange exchange) {
        // 验证失败状态码
        exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
        // 返回json格式错误信息
        // 自定义的一个返回bean 可自己定义
        BaseAO resultInfo = JsonResult.authFailureMap(ConstantMsg.PARAM_IS_NULL);
        String resultInfoString = JSONObject.toJSONString(resultInfo);
        // 主要是这一步
        byte[] bytes = resultInfoString.getBytes(StandardCharsets.UTF_8);
        DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes);
        return exchange.getResponse().writeWith(Flux.just(buffer));
    }

    public static Boolean authTokenNormal(String token){
        String dataToken = "test_token";
        return !StringUtils.isEmpty(token) && dataToken.equals(token);
    }

    /**
     * Decode body map.
     * xxx-www-f
     *
     * @param body the body
     * @return the map
     */
    public static JSONObject decodeBody(String body) {
        Map<String, String> map = Arrays.stream(body.split("&"))
                .map(s -> s.split("="))
                .collect(Collectors.toMap(arr -> arr[0], arr -> arr[1]));
        return JSONObject.parseObject(JSONObject.toJSONString(map));
    }

    /**
     * Gets token by media type.
     * 从body中获取想要的数据
     *
     * @param requestBody the request body
     * @param mediaType   the media type
     * @return the token by media type
     */
    public static String getTokenByMediaType(String requestBody, MediaType mediaType) {
        String token;
        if (MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(mediaType)){
            JSONObject jsonObject = AuthUtils.decodeBody(requestBody);
            token = jsonObject.getString(ConstantMsg.AUTHORIZE_TOKEN);
        }else {
            JSONObject jsonObject = JSONObject.parseObject(requestBody);
            token = jsonObject.getString(ConstantMsg.AUTHORIZE_TOKEN);
        }
        return token;
    }
}

2. 读取并修改 传入下级

filter

这里仅写一下全局filter demo

如果仅对某些路径进行修改body 可以利用request.getPath()进行判断是否需要, 或者其他自定义过滤器 (implements GatewayFilter, Ordered)

请求方式为get的也可以 把判断去掉或者自己改一下

/**
 * token校验全局过滤器
 * @author kun.han
 */
@Component
public class AuthorizeFilter implements GlobalFilter, Ordered {
    private static final String AUTHORIZE_TOKEN = "token";


    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        System.out.println("AuthorizeFilter");

        Object requestBody = exchange.getAttribute("cachedRequestBodyObject");

        System.out.println(JSONObject.toJSONString(requestBody));
        ServerHttpRequest request = exchange.getRequest();
        // 某些路径不作验证
        String path = request.getPath().toString();
        if (path.contains("/lov/")){
            return chain.filter(exchange);
        }

        if (HttpMethod.POST.equals(exchange.getRequest().getMethod())){
            // 重新构造request
            ServerRequest serverRequest = ServerRequest.create(exchange, HandlerStrategies.withDefaults().messageReaders());
            MediaType mediaType = exchange.getRequest().getHeaders().getContentType();
            // 重点
            Mono modifiedBody = serverRequest.bodyToMono(String.class).flatMap(body -> {
                // APPLICATION_JSON
                if (MediaType.APPLICATION_JSON.isCompatibleWith(mediaType)) {
                    // body为string
                    JSONObject jsonObject = JSONObject.parseObject(body);
                    String newBody = "对body进行处理之后的新body";
                    return Mono.just(newBody);
                }else if (MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(mediaType)){
                    // xxx-form- 类型的数据解析成map
                    Map<String, Object> bodyMap = decodeBody(body);
                    // 对bodyMap进行处理之后的newBodyMap
                    Map<String, Object> newBodyMap = bodyMap ;
                    // 通过自定义工具重新组装 xxx-form-
                    String encodeBody = encodeBody(newBodyMap);
                    return Mono.just(encodeBody);
                }
                return Mono.empty();
            });
            BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class);
            HttpHeaders httpHeaders = new HttpHeaders();
            httpHeaders.putAll(exchange.getRequest().getHeaders());
            // 先删除 content length
            httpHeaders.remove(HttpHeaders.CONTENT_LENGTH);
            CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, httpHeaders);
            return bodyInserter.insert(outputMessage, new BodyInserterContext()).then(Mono.defer(() -> {
                ServerHttpRequest decorator = this.decorate(exchange, httpHeaders, outputMessage);
                return chain.filter(exchange.mutate().request(decorator).build());
            }));
        } else {
            return chain.filter(exchange);
        }
    }

    @Override
    public int getOrder() {
        return 0;
    }

    ServerHttpRequestDecorator decorate(ServerWebExchange exchange, HttpHeaders headers, CachedBodyOutputMessage outputMessage) {
        return new ServerHttpRequestDecorator(exchange.getRequest()) {
            @Override
            public HttpHeaders getHeaders() {
                long contentLength = headers.getContentLength();
                HttpHeaders httpHeaders = new HttpHeaders();
                httpHeaders.putAll(super.getHeaders());
                if (contentLength > 0L) {
                    // 重新写 content length
                    httpHeaders.setContentLength(contentLength);
                } else {
                    httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
                }
                return httpHeaders;
            }
            @Override
            public Flux<DataBuffer> getBody() {
                return outputMessage.getBody();
            }
        };
    }

    /**
     * Decode body map.
     * xxx-www-f
     *
     * @param body the body
     * @return the map
     */
    private Map<String, Object> decodeBody(String body) {
        return Arrays.stream(body.split("&"))
                .map(s -> s.split("="))
                .collect(Collectors.toMap(arr -> arr[0], arr -> arr[1]));
    }

    /**
     * encode body map.
     * xxx-www-f
     *
     * @param body the body
     * @return the map
     */
    private String encodeBody(Map<String, Object> map) {
        return map.entrySet().stream().map(e -> e.getKey() + "=" + e.getValue()).collect(Collectors.joining("&"));
    }
}

这种方式就只更改,传入下级

如果要对body判断匹配之类的,然后返回什么鉴权失败的话,用utils中的authFailure也可以,但是用一个问题,就是客户端可以正常接收返回数据,但是服务器会报一个异常的错误...个人不喜欢这个一大串的报错信息,所以个人是要么只读,然后做判断,要么只改,然后传入下级.

视个人而定吧.

后话

这些也是在google找的东西, 然后进行整理拿出自己需要用到的...