spring cloud gateway 网关路由转发及request、response的加解密处理

springcloud gateway的介绍和原理及使用请自行查阅和学习,本章内容需在对spring cloud gateway的基本的了解和一定的知识基础上进行的。附上一个可供学习的博文:SpringCloud gateway (史上最全) - 疯狂创客圈 - 博客园

需求场景

前端对post请求的数据进行加密(主流的加密方式),后端需要解密,将接口的响应数据进行加密返回,前端解密并展示或其他操作等。出于安全性的考虑和可扩展性故使用了网关来做。

涉及到的问题

1、request的body数据流读取只能读取一次,controller层获取不到抛异常问题等

2、加解密采用对称加密算法是否前后端通用包括:ios、android、js

设计思路:(尽量使网关通用且可配置减少网关的代码修改和上线)

1、框架为springcloud+nacos+gateway+feign 后期可以加入hystrix等

2、在网关层进行对token的验证

3、在网关层进行统一解密request数据,并将header中添加特定数据,业务系统拦截器中进行对header的数据进行识别处理

4、业务系统返回数据后在网关层中对response数据进行加密返回,前端进行解密

直接上代码:(对token的验证)

启动类很普通,注册发现nacos相关注解

@SpringBootApplication
@EnableDiscoveryClient
public class GatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);
    }
}
server:
  port: 8080 #网关端口号

spring:
  application:
    name: k_men-gateway
    active: dev
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848 #本地nacos需要启动默认8848端口
      config:
        server-addr: localhost:8848 #本地nacos需要启动默认8848端口
        file-extension: yaml
        group: DEFAULT_GROUP
    gateway:
      discovery:
        locator:
          enabled: true #开启动态路由的功能

对token验证的自定义filter(代码中涉及到自定义的返回体或者返回错误码之类的自行替换)

@Slf4j
public class AuthGatewayFilterFactory extends AbstractGatewayFilterFactory {

    @Autowired
    private RedisUtils redisUtils;

    public AuthGatewayFilterFactory() {
        super(Config.class);
    }

    @Autowired
    private ModifyResponseBodyGatewayFilterFactory modifyResponseBodyGatewayFilterFactory;

    @Autowired
    private ObjectMapper objectMapper;

    /**
     * 具体的过滤逻辑
     * 1.如果没有登录令牌token,直接返回没有授权的信息
     * 2.如果有登录令牌token
     * 2.1通过令牌校验,继续向下执行
     * 2.2没有通过校验,返回没有通过校验的原因
     *
     * @param config 自定义配置类
     * @return 网关过滤器
     */
    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            String token = exchange.getRequest().getHeaders().getFirst("Authorization");
            ServerHttpResponse resp = exchange.getResponse();
            resp.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
            try {
                if (!exchange.getRequest().getPath().toString().contains("admin/login")){
                    if (StrUtil.isBlank(token) || JwtUtils.getAppTokenKey(token) == null || !token.equals(redisUtils.get(JwtUtils.getAppTokenKey(token)))) {
                        String result = JSONUtil.parseObj(R.fail(ErrorCodeEnum.TOKEN_ERROR.getCode(), "token error")).toStringPretty();
                        DataBuffer buffer = resp.bufferFactory().wrap(result.getBytes(StandardCharsets.UTF_8));
                        return resp.writeWith(Flux.just(buffer));
                    }
                }
                return chain.filter(exchange.mutate().build());
            } catch (Exception e) {
                log.error(e.getMessage(), e);
                String result = JSONUtil.parseObj(R.fail(ErrorCodeEnum.TOKEN_ERROR.getCode(), e.getMessage())).toStringPretty();
                DataBuffer buffer = resp.bufferFactory().wrap(result.getBytes(StandardCharsets.UTF_8));
                return resp.writeWith(Flux.just(buffer));
            }
        };
    }

    /**
     * 自定义配置类
     */
    public static class Config {
    }

}
@Configuration
public class GatewayConfig {

    /**
     * 注入自定义授权网关过滤器工厂
     */
    @Bean
    public AuthGatewayFilterFactory authGatewayFilterFactory() {
        return new AuthGatewayFilterFactory();
    }
}
spring:
  cloud:
    gateway:
      routes:
      - id: k_men_cp_routh_auth
        uri: lb://k_men-cp-api-test-local
        predicates:
        - Path=/**
        filters:
        - Auth

实现方式:(对header添加数据上面链接中有,下面重点讲对request和response的处理基于上述部分代码)

一、使用java代码进行配置

优点:简单,容易理解,代码量少

缺点:编码频繁,不能配置于yaml文件进行热更新,新增服务需要进行编码配置,缺少灵活性

废话不多说直接上代码:

@Slf4j
public class RequestBodyRewrite implements RewriteFunction {

    public RequestBodyRewrite() {
    }

    @Override
    public Publisher apply(ServerWebExchange exchange, String body) {
        try {
            String data = DESedeUtils.decrypt(body);//加密代码处理
            return Mono.just(data);
        } catch (Exception ex) {
            log.error("1. json process fail", ex);
            return Mono.error(new Exception("1. json process fail", ex));
        }
    }
}
@Slf4j
public class ResponseBodyRewrite implements RewriteFunction {
    public ResponseBodyRewrite() {
    }

    @Override
    public Publisher apply(ServerWebExchange exchange, String body) {
        try {
            String data = DESedeUtils.encrypt(body);//加密代码处理
            return Mono.just(data);
        } catch (Exception ex) {
            log.error("2. json process fail", ex);
            return Mono.error(new Exception("2. json process fail", ex));
        }
    }
}
    @Bean
    public RouteLocator routes(RouteLocatorBuilder builder, ObjectMapper objectMapper) {
        return builder
                .routes()
                .route("path_route_change",
                        r -> r.path("/**")
                                .filters(f -> f
                                        .modifyRequestBody(String.class,String.class,new RequestBodyRewrite())
                                        .modifyResponseBody(String.class, String.class, new ResponseBodyRewrite())
                                ).uri("lb://k_men-cp-api-test")).build();
    }

二、自定义filter及配置文件方式

优点:支持yaml配置,配置文件热更新生效,灵活通用,可扩展性好,减少网关的频繁上线

缺点:代码稍微难以理解,代码多等

废话不多说直接上代码:

启动类上加动态刷新@RefreshScope 注解

配置文件放在nacos上使用配合上面的配置文件中的名字 k_men-gateway-dev.yaml

spring:
  cloud:
    gateway:
      routes:
      - id: lc_cp_routh
        uri: lb://k_men-cp-api-test-local #需要转发到的服务在nacos上的name
        predicates:
        - Path=/**
        filters:
        - DecryptRequestBody
        - EncryptResponseBody

对request的body的修改解密处理 

@Slf4j
public class DecryptRequestBodyGatewayFilterFactory extends AbstractGatewayFilterFactory implements Ordered {

    private final List> messageReaders;

    public DecryptRequestBodyGatewayFilterFactory() {
        this.messageReaders = HandlerStrategies.withDefaults().messageReaders();
    }

    @Override
    @SuppressWarnings("unchecked")
    public GatewayFilter apply(Object config) {
        return (exchange, chain) -> {
            ServerRequest serverRequest = ServerRequest.create(exchange,
                    this.messageReaders);

            Mono modifiedBody = serverRequest.bodyToMono(String.class)
                    .flatMap(originalBody -> modifyBody().apply(exchange,Mono.just(originalBody)));

            BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody,String.class);
            HttpHeaders headers = new HttpHeaders();
            headers.putAll(exchange.getRequest().getHeaders());
            headers.remove(HttpHeaders.CONTENT_LENGTH);
            CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange,headers);

            return bodyInserter.insert(outputMessage, new BodyInserterContext())
                    .then(Mono.defer(() -> {
                        ServerHttpRequest decorator = decorate(exchange, headers,outputMessage);
                        return chain.filter(exchange.mutate().request(decorator).build());
                    }));
        };
    }

    private BiFunction, Mono> modifyBody() {
        return (exchange, body) -> {
            try {
                AtomicReference result = new AtomicReference<>();
                body.subscribe(value -> result.set(EncryptUtils.decryptStr(value)),
                        e -> log.error(e.getMessage(), e)
                );
                return Mono.just(result.get());
            } catch (Exception e) {
                log.error("gateway parameter decryption exception", e);
                throw new DecryptParamException("Parameter decryption exception");
            }
        };
    }

    private 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 > 0) {
                    httpHeaders.setContentLength(contentLength);
                }
                else {
                    httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
                }
                return httpHeaders;
            }

            @Override
            public Flux getBody() {
                return outputMessage.getBody();
            }
        };

    }

    @Override
    public int getOrder() {
        return -2;
    }
}

对response的body的修改加密处理

@Slf4j
public class EncryptResponseBodyGatewayFilterFactory extends AbstractGatewayFilterFactory{


    public EncryptResponseBodyGatewayFilterFactory() {
    }

    @Override
    public GatewayFilter apply(Object config) {
        return new EncryptResponseGatewayFilter();
    }

    public class EncryptResponseGatewayFilter implements GatewayFilter,Ordered{
        @Override
        public int getOrder() {
            return -2;
        }

        @Override
        public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            return chain.filter(exchange.mutate().response(decorate(exchange)).build());
        }

        @SuppressWarnings("unchecked")
        private ServerHttpResponse decorate(ServerWebExchange exchange) {
            return new ServerHttpResponseDecorator(exchange.getResponse()) {

                @Override
                public Mono writeWith(Publisher body) {

                    String originalResponseContentType = exchange
                            .getAttribute(ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR);
                    HttpHeaders httpHeaders = new HttpHeaders();
                    httpHeaders.add(HttpHeaders.CONTENT_TYPE,
                            originalResponseContentType);

                    ClientResponse clientResponse = ClientResponse
                            .create(exchange.getResponse().getStatusCode())
                            .headers(headers -> headers.putAll(httpHeaders))
                            .body(Flux.from(body)).build();

                    //修改body
                    Mono modifiedBody = clientResponse.bodyToMono(String.class)
                            .flatMap(originalBody -> modifyBody()
                                    .apply(exchange,Mono.just(originalBody)));

                    BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody,
                            String.class);
                    CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(
                            exchange, exchange.getResponse().getHeaders());
                    return bodyInserter.insert(outputMessage, new BodyInserterContext())
                            .then(Mono.defer(() -> {
                                Flux messageBody = outputMessage.getBody();
                                HttpHeaders headers = getDelegate().getHeaders();
                                if (!headers.containsKey(HttpHeaders.TRANSFER_ENCODING)) {
                                    messageBody = messageBody.doOnNext(data -> headers
                                            .setContentLength(data.readableByteCount()));
                                }
                                return getDelegate().writeWith(messageBody);
                            }));
                }

                /**
                 * 修改body
                 * @return apply 返回Mono,数据是修改后的body
                 */
                private BiFunction,Mono> modifyBody(){
                    return (exchange,json)-> {
                        AtomicReference result = new AtomicReference<>();
                        json.subscribe(
                                value -> result.set(EncryptUtils.encryptHex(value)),
                                Throwable::printStackTrace
                        );
                        return Mono.just(result.get());
                    };
                }

                @Override
                public Mono writeAndFlushWith(
                        Publisher> body) {
                    return writeWith(Flux.from(body).flatMapSequential(p -> p));
                }
            };
        }
    }
}
@Configuration
public class GatewayConfig {

    /**
     * 注入自定义授权网关过滤器工厂
     */
    @Bean
    public AuthGatewayFilterFactory authGatewayFilterFactory() {
        return new AuthGatewayFilterFactory();
    }

    /**
     * request body解密
     */
    @Bean
    public DecryptRequestBodyGatewayFilterFactory decryptRequestBodyGatewayFilterFactory() {
        return new DecryptRequestBodyGatewayFilterFactory();
    }

    /**
     * response body加密
     */
    @Bean
    public EncryptResponseBodyGatewayFilterFactory encryptResponseBodyGatewayFilterFactory() {
        return new EncryptResponseBodyGatewayFilterFactory();
    }

}

最后查询资料和测试运行不易,如果对您有帮助可以打赏一波小红包,或者点赞收藏再走谢谢

你可能感兴趣的:(spring,cloud,gateway,java)