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
其他拦截器的顺序无固定要求,值越小越先执行