首页 资讯 社群 我的社区 搜索

SpringCloud:搭建基于Gateway的微服务网关(二)

LM123
2019-11-12 17:46:31

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("n");
        }
        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

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

用户评论