zoukankan      html  css  js  c++  java
  • SpringCloud:搭建基于Gateway的微服务网关(二)

    0.代码

    https://github.com/fengdaizang/OpenAPI

    1.引入相关依赖

    pom文件如下:

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <parent>
            <artifactId>OpenAPI</artifactId>
            <groupId>com.fdzang.microservice</groupId>
            <version>1.0-SNAPSHOT</version>
        </parent>
        <modelVersion>4.0.0</modelVersion>
    
        <artifactId>api-gateway</artifactId>
    
        <dependencies>
         <!-- 公共模块引入了web模块,会与gateway产生冲突,故排除 -->
    <dependency> <groupId>com.fdzang.microservice</groupId> <artifactId>api-common</artifactId> <version>1.0-SNAPSHOT</version> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-collections4</artifactId> </dependency> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> </dependency> <dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> </dependency> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> </dependency> <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>${fastjson.version}</version> </dependency>
         <!-- 引入gateway模块 -->     <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> <version>${spring.cloud.starter.version}</version> </dependency>
         <!-- 引入eureka模块 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> <version>${spring.cloud.starter.version}</version> </dependency>
         <!-- 引入openfeign模块,这里不要用feign,Springboot2.0已弃用 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> <version>${spring.cloud.starter.version}</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> <version>${spring.cloud.starter.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <version>${spring.boot.version}</version> <optional>true</optional> </dependency> </dependencies> </project>

    2.配置Gateway

    server:
      port: 7000
    
    #注册到eureka eureka: client: service-url: defaultZone: http://127.0.0.1:7003/eureka/
    #配置gateway拦截规则 spring: application: name: api-gateway cloud: gateway: discovery: locator: enabled: true routes: - id: gateway uri: http://www.baidu.com predicates: - Path=/**
    #这里定义了鉴权的服务名,以及白名单 auth: service-id: api-auth-v1 gateway: white: - /login #这里是id生成器的配置,Twitter-Snowflake IdWorker: workerId: 122 datacenterId: 1231

    3.过滤器

    3.1.ID生成拦截

    对每个请求生成一个唯一的请求id

    package com.fdzang.microservice.gateway.gateway;
    
    import com.fdzang.microservice.gateway.util.GatewayConstant;
    import com.fdzang.microservice.gateway.util.IdWorker;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.lang.StringUtils;
    import org.springframework.cloud.gateway.filter.GatewayFilterChain;
    import org.springframework.cloud.gateway.filter.GlobalFilter;
    import org.springframework.core.Ordered;
    import org.springframework.http.server.reactive.ServerHttpRequest;
    import org.springframework.stereotype.Component;
    import org.springframework.web.server.ServerWebExchange;
    import reactor.core.publisher.Mono;
    
    /**
     * 生成一个请求的特定id
     * @author tanghu
     * @Date: 2019/11/5 18:42
     */
    @Slf4j
    @Component
    public class SerialNoFilter implements GlobalFilter, Ordered {
    
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            ServerHttpRequest request = exchange.getRequest();
    
            String requestId= request.getHeaders().getFirst(GatewayConstant.REQUEST_TRACE_ID);
            if (StringUtils.isEmpty(requestId)) {
                Object attribute = exchange.getAttribute(GatewayConstant.REQUEST_TRACE_ID);
                if (attribute == null) {
                    requestId = String.valueOf(IdWorker.getWorkerId());
                    exchange.getAttributes().put(GatewayConstant.REQUEST_TRACE_ID,requestId);
                }
            }else{
                exchange.getAttributes().put(GatewayConstant.REQUEST_TRACE_ID,requestId);
            }
    
            return chain.filter(exchange);
        }
    
        @Override
        public int getOrder() {
            return GatewayConstant.Order.SERIAL_NO_ORDER;
        }
    }

    3.2.鉴权拦截

    获取请求头中的鉴权信息,对信息校验,这里暂时没有做(AuthResult authService.auth(AuthRequest request)),这里需求请求其他模块对请求信息进行校验,返回校验结果

    package com.fdzang.microservice.gateway.gateway;
    
    import com.fdzang.microservice.common.entity.auth.AuthCode;
    import com.fdzang.microservice.common.entity.auth.AuthRequest;
    import com.fdzang.microservice.common.entity.auth.AuthResult;
    import com.fdzang.microservice.gateway.service.AuthService;
    import com.fdzang.microservice.gateway.util.GatewayConstant;
    import com.fdzang.microservice.gateway.util.WhiteUrl;
    import com.google.common.base.Joiner;
    import com.google.common.base.Splitter;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.collections4.MapUtils;
    import org.apache.commons.lang.StringUtils;
    import org.apache.commons.lang.math.NumberUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.cloud.gateway.filter.GatewayFilterChain;
    import org.springframework.cloud.gateway.filter.GlobalFilter;
    import org.springframework.core.Ordered;
    import org.springframework.http.HttpHeaders;
    import org.springframework.http.server.reactive.ServerHttpRequest;
    import org.springframework.stereotype.Component;
    import org.springframework.util.CollectionUtils;
    import org.springframework.util.MultiValueMap;
    import org.springframework.web.server.ServerWebExchange;
    import reactor.core.publisher.Mono;
    
    import java.util.List;
    import java.util.Map;
    import java.util.TreeMap;
    
    /**
     * 权限校验
     * @author tanghu
     * @Date: 2019/10/22 18:00
     */
    @Slf4j
    @Component
    public class AuthFilter implements GlobalFilter, Ordered {
    
        @Autowired
        private AuthService authService;
    
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            String requestId = exchange.getAttribute(GatewayConstant.REQUEST_TRACE_ID);
            String url = exchange.getRequest().getURI().getPath();
    
            ServerHttpRequest request = exchange.getRequest();
    
            //跳过白名单
            if(null != WhiteUrl.getWhite() && WhiteUrl.getWhite().contains(url)){
                return chain.filter(exchange);
            }
    
            //获取权限校验部分
            //Authorization: gateway:{AccessId}:{Signature}
            String authHeader = exchange.getRequest().getHeaders().getFirst(GatewayConstant.AUTH_HEADER);
            if(StringUtils.isBlank(authHeader)){
                log.warn("request has no authorization header, uuid:{}, request:{}",requestId, url);
    
                throw new IllegalArgumentException("bad request");
            }
    
            List<String> auths = Splitter.on(":").trimResults().omitEmptyStrings().splitToList(authHeader);
            if(CollectionUtils.isEmpty(auths) || auths.size() != 3 || !GatewayConstant.AUTH_LABLE.equals(auths.get(0))){
                log.warn("bad authorization header, uuid:{}, request:[{}], header:{}",
                        requestId, url, authHeader);
    
                throw new IllegalArgumentException("bad request");
            }
    
            //校验时间戳是否合法
            String timestamp = exchange.getRequest().getHeaders().getFirst(GatewayConstant.TIMESTAMP_HEADER);
            if (StringUtils.isBlank(timestamp) || isTimestampExpired(timestamp)) {
                log.warn("wrong timestamp:{}, uuid:{}, request:{}",
                        timestamp, requestId, url);
            }
    
            String accessId = auths.get(1);
            String sign = auths.get(2);
    
            String stringToSign = getStringToSign(request, timestamp);
    
            AuthRequest authRequest = new AuthRequest();
            authRequest.setAccessId(accessId);
            authRequest.setSign(sign);
            authRequest.setStringToSign(stringToSign);
            authRequest.setHttpMethod(request.getMethodValue());
            authRequest.setUri(url);
    
            AuthResult authResult = authService.auth(authRequest);
    
            if (authResult.getStatus() != AuthCode.SUCEESS.getAuthCode()) {
                log.warn("checkSign failed, uuid:{},  accessId:{}, request:[{}], error:{}",
                        requestId, accessId, url, authResult.getDescription());
                throw new RuntimeException(authResult.getDescription());
            }
    
            log.info("request auth finished, uuid:{}, orgCode:{}, userName:{}, accessId:{}, request:{}, serviceName:{}",
                    requestId, authResult.getOrgCode(),
                    authResult.getUsername(), accessId,
                    url, authResult.getServiceName());
    
            exchange.getAttributes().put(GatewayConstant.SERVICE_NAME,authResult.getServiceName());
    
            return chain.filter(exchange);
        }
    
        /**
         * 获取原始字符串(签名前)
         * @param request
         * @param timestamp
         * @return
         */
        private String getStringToSign(ServerHttpRequest request, String timestamp){
            // headers
            TreeMap<String, String> headersInSign = new TreeMap<>();
            HttpHeaders headers = request.getHeaders();
            for (Map.Entry<String,List<String>> header:headers.entrySet()) {
                String key = header.getKey();
                if (key.startsWith(GatewayConstant.AUTH_HEADER_PREFIX)) {
                    headersInSign.put(key, header.getValue().get(0));
                }
            }
    
            StringBuilder headerStringBuilder = new StringBuilder();
            for (Map.Entry<String, String> entry : headersInSign.entrySet()) {
                headerStringBuilder.append(entry.getKey()).append(":").append(entry.getValue()).append("
    ");
            }
            String headerString = null;
            if (headerStringBuilder.length() != 0) {
                headerString = headerStringBuilder.deleteCharAt(headerStringBuilder.length()-1).toString();
            }
    
            // Url_String
            TreeMap<String, String> paramsInSign = new TreeMap<>();
            MultiValueMap<String, String> parameterMap = request.getQueryParams();
            if (MapUtils.isNotEmpty(parameterMap)) {
                for (Map.Entry<String, List<String>> entry : parameterMap.entrySet()) {
                    paramsInSign.put(entry.getKey(), entry.getValue().get(0));
                }
            }
    
            // 原始url
            String originalUrl = request.getURI().getPath();
    
            StringBuilder uriStringBuilder = new StringBuilder(originalUrl);
            if (!parameterMap.isEmpty()) {
                uriStringBuilder.append("?");
                for (Map.Entry<String, String> entry : paramsInSign.entrySet()) {
                    uriStringBuilder.append(entry.getKey());
                    if (StringUtils.isNotBlank(entry.getValue())) {
                        uriStringBuilder.append("=").append(entry.getValue());
                    }
                    uriStringBuilder.append("&");
                }
                uriStringBuilder.deleteCharAt(uriStringBuilder.length()-1);
            }
    
            String uriString = uriStringBuilder.toString();
    
            String contentType = headers.getFirst(HttpHeaders.CONTENT_TYPE);
    
            //这里可以对请求参数进行MD5校验,暂时不做
            String contentMd5 = headers.getFirst(GatewayConstant.CONTENTE_MD5);
    
            String[] parts = {
                    request.getMethodValue(),
                    StringUtils.isNotBlank(contentMd5) ? contentMd5 : "",
                    StringUtils.isNotBlank(contentType) ? contentType : "",
                    timestamp,
                    headerString,
                    uriString
            };
    
            return Joiner.on(GatewayConstant.STRING_TO_SIGN_DELIM).skipNulls().join(parts);
        }
    
        /**
         * 校验时间戳是否超时
         * @param timestamp
         * @return
         */
        private boolean isTimestampExpired(String timestamp){
            long l = NumberUtils.toLong(timestamp, 0L);
            if (l == 0) {
                return true;
            }
    
            return Math.abs(System.currentTimeMillis() - l) > GatewayConstant.EXPIRE_TIME_SECONDS *1000;
        }
    
        @Override
        public int getOrder() {
            return GatewayConstant.Order.AUTH_ORDER;
        }
    }

    3.3.服务分发

    根据鉴权后的结果能得到服务名,然后重写路由以及请求,对该次请求进行转发

    package com.fdzang.microservice.gateway.gateway;
    
    import com.fdzang.microservice.gateway.util.GatewayConstant;
    import com.fdzang.microservice.gateway.util.WhiteUrl;
    import com.google.common.base.Splitter;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.cloud.gateway.filter.GatewayFilterChain;
    import org.springframework.cloud.gateway.filter.GlobalFilter;
    import org.springframework.cloud.gateway.route.Route;
    import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
    import org.springframework.core.Ordered;
    import org.springframework.http.server.reactive.ServerHttpRequest;
    import org.springframework.stereotype.Component;
    import org.springframework.web.server.ServerWebExchange;
    import reactor.core.publisher.Mono;
    
    import java.util.List;
    
    /**
     * @author tanghu
     * @Date: 2019/11/6 15:39
     */
    @Slf4j
    @Component
    public class ModifyRequestFilter implements GlobalFilter, Ordered {
    
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            String url = exchange.getRequest().getURI().getPath();
            ServerHttpRequest request = exchange.getRequest();
    
            //跳过白名单
            if(null != WhiteUrl.getWhite() && WhiteUrl.getWhite().contains(url)){
                return chain.filter(exchange);
            }
    
            String serviceName = exchange.getAttribute(GatewayConstant.SERVICE_NAME);
    
            //修改路由
            Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
            Route newRoute = Route.async()
                    .asyncPredicate(route.getPredicate())
                    .filters(route.getFilters())
                    .id(route.getId())
                    .order(route.getOrder())
                    .uri(GatewayConstant.URI.LOAD_BALANCE+serviceName).build();
    
            exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR,newRoute);
    
            //修改请求路径
            List<String> strings = Splitter.on("/").omitEmptyStrings().trimResults().limit(3).splitToList(url);
            String newServletPath = "/" + strings.get(2);
    
            ServerHttpRequest newRequest = request.mutate().path(newServletPath).build();
    
            return chain.filter(exchange.mutate().request(newRequest).build());
        }
    
        @Override
        public int getOrder() {
            return GatewayConstant.Order.MODIFY_REQUEST_ORDER;
        }
    }

    3.4.统一响应

    对响应进行统一封装

    package com.fdzang.microservice.gateway.gateway;
    
    import com.alibaba.fastjson.JSON;
    import com.fdzang.microservice.common.entity.ApiResult;
    import com.fdzang.microservice.gateway.entity.GatewayError;
    import com.fdzang.microservice.gateway.entity.GatewayResult;
    import com.fdzang.microservice.gateway.entity.GatewayResultEnums;
    import com.fdzang.microservice.gateway.util.GatewayConstant;
    import lombok.extern.slf4j.Slf4j;
    import org.reactivestreams.Publisher;
    import org.springframework.cloud.gateway.filter.GatewayFilterChain;
    import org.springframework.cloud.gateway.filter.GlobalFilter;
    import org.springframework.cloud.gateway.filter.NettyWriteResponseFilter;
    import org.springframework.core.Ordered;
    import org.springframework.core.io.buffer.DataBuffer;
    import org.springframework.core.io.buffer.DataBufferFactory;
    import org.springframework.core.io.buffer.DataBufferUtils;
    import org.springframework.http.server.reactive.ServerHttpResponse;
    import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
    import org.springframework.stereotype.Component;
    import org.springframework.web.server.ServerWebExchange;
    import reactor.core.publisher.Flux;
    import reactor.core.publisher.Mono;
    
    import java.nio.charset.Charset;
    
    /**
     * @author tanghu
     * @Date: 2019/11/7 8:58
     */
    @Slf4j
    @Component
    public class ModifyResponseFilter implements GlobalFilter, Ordered {
    
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            String requestId = exchange.getAttribute(GatewayConstant.REQUEST_TRACE_ID);
            ServerHttpResponse originalResponse = exchange.getResponse();
            DataBufferFactory bufferFactory = originalResponse.bufferFactory();
            ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) {
                @Override
                public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
                    if (body instanceof Flux) {
                        Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) body;
                        return super.writeWith(fluxBody.map(dataBuffer -> {
                            byte[] content = new byte[dataBuffer.readableByteCount()];
                            dataBuffer.read(content);
                            //释放掉内存
                            DataBufferUtils.release(dataBuffer);
    
                            String originalbody = new String(content, Charset.forName("UTF-8"));
                            String finalBody = originalbody;
    
                            ApiResult apiResult = JSON.parseObject(originalbody,ApiResult.class);
    
                            GatewayResult result = new GatewayResult();
                            result.setCode(GatewayResultEnums.SUCC.getCode());
                            result.setMsg(GatewayResultEnums.SUCC.getMsg());
                            result.setReq_id(requestId);
                            if (apiResult.getCode() == null && apiResult.getMsg() == null) {
                                // 尝试解析body为网关的错误信息
                                GatewayError gatewayError = JSON.parseObject(originalbody,GatewayError.class);
                                result.setSub_code(gatewayError.getStatus());
                                result.setSub_msg(gatewayError.getMessage());
                            } else {
                                result.setSub_code(apiResult.getCode());
                                result.setSub_msg(apiResult.getMsg());
                            }
    
                            result.setData(apiResult.getData());
    
                            finalBody = JSON.toJSONString(result);
    
                            return bufferFactory.wrap(finalBody.getBytes());
                        }));
                    }
    
                    return super.writeWith(body);
                }
            };
            return chain.filter(exchange.mutate().response(decoratedResponse).build());
        }
    
        @Override
        public int getOrder() {
            return GatewayConstant.Order.MODIFY_RESPONSE_ORDER;
        }
    }

    4.测试

    10:25:54.961 [main] INFO  c.f.microservice.mock.util.SignUtil - StringToSign:
    GET
    
    
    1573093554201
    /v2/base/zuul/tag/getMostUsedTags?from=2017-11-25 00:00:00&plate_num=部A11110&to=2017-11-30 00:00:00
    10:25:54.979 [main] INFO  c.f.microservice.mock.util.HttpUtil - sign:Y+usbpHlwOw4F2sq4b0pNjgXGDAXoYgs1syOOPxPFAE=
    10:25:59.868 [main] INFO  com.fdzang.microservice.mock.Demo - {"code":0,"data":[{"tagPublishedRefCount":3,"tagTitle":"Solo","id":"1533101769023","tagReferenceCount":3},{"tagPublishedRefCount":1,"tagTitle":"tetet","id":"1559285894006","tagReferenceCount":1}],"msg":"succ","req_id":"2627469547766022144","sub_code":0,"sub_msg":"ok"}
    
    Process finished with exit code 0

     由返回结果,可知此次请求完成。

    5.注意事项

    转发的目标服务需要跟网关注册在同一个注册中心下,路由uri配置为 lb://service_name,则会转发到对应的服务下,并且gateway会自动采用负载均衡机制

    响应请求的顺序需要小于 NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER 该值为-1

    其他拦截器的顺序无固定要求,值越小越先执行

  • 相关阅读:
    springMVC工作原理
    关于VS调试Web 无法启动IIS Express Web 服务器的问题解决
    用泛型创建SqlServerHelper类实现增删改查(一)
    laytpl--前端数据绑定
    安装.NET Core 运行时和托管包后,.Net Core项目选择不到安装的.Net Core Sdk,导致项目加载失败
    .Net上传图片的一些问题
    微信退款参数格式错误
    Ajax设置自定义请求头的两种方法
    asp.net获取当前请求的url
    Windows服务器上使用phpstudy部署PHP程序
  • 原文地址:https://www.cnblogs.com/fdzang/p/11810932.html
Copyright © 2011-2022 走看看