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("\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

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

posted @ 2019-11-21 16:52  星朝  阅读(911)  评论(0)    收藏  举报