单体应用拆分成多个服务后,对外需要一个统一入口,解耦客户端与内部服务
Spring Cloud Gateway是由spring官方基于Spring5.0、Spring Boot2.0、Project Reactor等技术开发的网关,使用非阻塞API,Websockets得到支持,目的是代替原先版本中的Spring Cloud Netfilx Zuul,目前Netfilx已经开源了Zuul2.0,但Spring 没有考虑集成,而是推出了自己开发的Spring Cloud GateWay。 这里需要注意一下gateway使用的netty+webflux实现,不要加入web依赖(不要引用webmvc),否则初始化会报错 ,需要加入webflux依赖。
gateway与zuul的简单比较:gateway使用的是异步请求,zuul是同步请求,gateway的数据封装在ServerWebExchange里,zuul封装在RequestContext里,同步方便调式,可以把数据封装在ThreadLocal中传递。
Spring Cloud Gateway有三个核心概念:路由、断言、过滤器
过滤器:gateway有两种filter:GlobalFilter、GatewayFilter,全局过滤器默认对所有路由有效。
文档地址: cloud.spring.io/spring-clou…
网关作为所有请求流量的入口,在实际生产环境中为了保证高可靠和高可用,尽量避免重启,需要用到动态路由配置,在网关运行过程中更改路由配置
需要用到3个项目,eureka-server、gateway、consumer-service
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> 123456789101112复制代码
在主类上启用服务发现注册注解 @EnableDiscoveryClient
配置文件内容如下:
server: port: 9999 spring: profiles: active: dev application: name: gateway-service cloud: gateway: discovery: locator: enabled: true # 服务名小写 lower-case-service-id: true routes: - id: consumer-service # lb代表从注册中心获取服务,且已负载均衡方式转发 uri: lb://consumer-service predicates: - Path=/consumer/** # 加上StripPrefix=1,否则转发到后端服务时会带上consumer前缀 filters: - StripPrefix=1 # 注册中心 eureka: instance: prefer-ip-address: true client: service-url: defaultZone: http://zy:zy123@localhost:10025/eureka/ # 暴露监控端点 management: endpoints: web: exposure: include: '*' endpoint: health: show-details: always 123456789101112131415161718192021222324252627282930313233343536373839404142复制代码
上面就完成了网关代码部分,下面新建consumer-service
3.consumer-service 消费者服务 ,通过网关路由转发到消费者服务,并返回信息回去,因此是个mvc的项目
项目引用如下:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> 12345678复制代码
在主类上启用服务发现注册注解 @EnableDiscoveryClient
在配置文件中添加配置:
server.port=9700 spring.application.name=consumer-service eureka.instance.prefer-ip-address=true # 配置eureka-server security的账户信息 eureka.client.serviceUrl.defaultZone=http://zy:zy123@localhost:10025/eureka/ 12345复制代码
新建 IndexController ,添加一个 hello 方法,传入name参数,访问后返回 hi + name 字符串
@RestController public class IndexController { @RequestMapping("/hello") public String hello(String name){ return "hi " + name; } } 12345678复制代码
4.分别启动3个项目,访问 http://localhost:10025 看eureka上gateway与consumer-service实例是否注册了,可以看到已经注册了,分别在9700、9999端口
通过网关访问consumer-service的hello方法, http://localhost:9999/consumer/hello?name=zy ,效果如下,说明请求已经转发到consumer-service服务上了
以上完成了网关的基本代码,下面继续介绍一些常用的过滤器,通过过滤器实现统一认证鉴权、日志、安全等检验
/** * 全局过滤器 * 所有请求都会执行 * 可拦截get、post等请求做逻辑处理 */ @Component public class RequestGlobalFilter implements GlobalFilter,Ordered { //执行逻辑 @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest serverHttpRequest= exchange.getRequest(); String uri = serverHttpRequest.getURI().toString(); System.out.println(" uri : " + uri);//打印每次请求的url String method = serverHttpRequest.getMethodValue(); if ("POST".equals(method)) { //从请求里获取Post请求体 String bodyStr = resolveBodyFromRequest(serverHttpRequest); //TODO 得到Post请求的请求参数后,做你想做的事 //下面的将请求体再次封装写回到request里,传到下一级,否则,由于请求体已被消费,后续的服务将取不到值 URI uri = serverHttpRequest.getURI(); ServerHttpRequest request = serverHttpRequest.mutate().uri(uri).build(); DataBuffer bodyDataBuffer = stringBuffer(bodyStr); Flux<DataBuffer> bodyFlux = Flux.just(bodyDataBuffer); request = new ServerHttpRequestDecorator(request) { @Override public Flux<DataBuffer> getBody() { return bodyFlux; } }; //封装request,传给下一级 return chain.filter(exchange.mutate().request(request).build()); } else if ("GET".equals(method)) { Map requestQueryParams = serverHttpRequest.getQueryParams(); //TODO 得到Get请求的请求参数后,做你想做的事 return chain.filter(exchange); } return chain.filter(exchange); } /** * 从Flux<DataBuffer>中获取字符串的方法 * @return 请求体 */ private String resolveBodyFromRequest(ServerHttpRequest serverHttpRequest) { //获取请求体 Flux<DataBuffer> body = serverHttpRequest.getBody(); AtomicReference<String> bodyRef = new AtomicReference<>(); body.subscribe(buffer -> { CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer()); DataBufferUtils.release(buffer); bodyRef.set(charBuffer.toString()); }); //获取request body return bodyRef.get(); } private DataBuffer stringBuffer(String value) { byte[] bytes = value.getBytes(StandardCharsets.UTF_8); NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT); DataBuffer buffer = nettyDataBufferFactory.allocateBuffer(bytes.length); buffer.write(bytes); return buffer; } //执行顺序 @Override public int getOrder() { return 1; } } 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475复制代码
重新运行网关项目,并访问 http://localhost:9999/consumer/hello?name=zy ,查看控制台,可看到 uri 日志被打印出来了
/** * 可对客户端header 中的 Authorization 信息进行认证 */ @Component public class TokenAuthenticationFilter extends AbstractGatewayFilterFactory { private static final String Bearer_ = "Bearer "; @Override public GatewayFilter apply(Object config) { return (exchange, chain) -> { ServerHttpRequest request = exchange.getRequest(); ServerHttpRequest.Builder mutate = request.mutate(); ServerHttpResponse response = exchange.getResponse(); try { //String token = exchange.getRequest().getQueryParams().getFirst("authToken"); //1.获取header中的Authorization String header = request.getHeaders().getFirst("Authorization"); if (header == null || !header.startsWith(Bearer_)) { throw new RuntimeException("请求头中Authorization信息为空"); } //2.截取Authorization Bearer String token = header.substring(7); //可把token存到redis中,此时直接在redis中判断是否有此key,有则校验通过,否则校验失败 if(!StringUtils.isEmpty(token)){ System.out.println("验证通过"); //3.有token,把token设置到header中,传递给后端服务 mutate.header("userDetails",token).build(); }else{ //4.token无效 System.out.println("token无效"); DataBuffer bodyDataBuffer = responseErrorInfo(response , HttpStatus.UNAUTHORIZED.toString() ,"无效的请求"); return response.writeWith(Mono.just(bodyDataBuffer)); } }catch (Exception e){ //没有token DataBuffer bodyDataBuffer = responseErrorInfo(response , HttpStatus.UNAUTHORIZED.toString() ,e.getMessage()); return response.writeWith(Mono.just(bodyDataBuffer)); } ServerHttpRequest build = mutate.build(); return chain.filter(exchange.mutate().request(build).build()); }; } /** * 自定义返回错误信息 * @param response * @param status * @param message * @return */ public DataBuffer responseErrorInfo(ServerHttpResponse response , String status ,String message){ HttpHeaders httpHeaders = response.getHeaders(); httpHeaders.add("Content-Type", "application/json; charset=UTF-8"); httpHeaders.add("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0"); response.setStatusCode(HttpStatus.UNAUTHORIZED); Map<String,String> map = new HashMap<>(); map.put("status",status); map.put("message",message); DataBuffer bodyDataBuffer = response.bufferFactory().wrap(map.toString().getBytes()); return bodyDataBuffer; } } 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364复制代码
在配置文件中指定consumer-service服务使用 TokenAuthenticationFilter ,配置如下:
routes: - id: consumer-service uri: lb://consumer-service predicates: - Path=/consumer/** filters: # 进行token过滤 - TokenAuthenticationFilter - StripPrefix=1 123456789复制代码
运行项目,再次访问 http://localhost:9999/consumer/hello?name=zy
@Bean public WebFilter corsFilter() { return (ServerWebExchange ctx, WebFilterChain chain) -> { ServerHttpRequest request = ctx.getRequest(); if (!CorsUtils.isCorsRequest(request)) { return chain.filter(ctx); } HttpHeaders requestHeaders = request.getHeaders(); ServerHttpResponse response = ctx.getResponse(); HttpMethod requestMethod = requestHeaders.getAccessControlRequestMethod(); HttpHeaders headers = response.getHeaders(); headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, requestHeaders.getOrigin()); headers.addAll(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, requestHeaders.getAccessControlRequestHeaders()); if (requestMethod != null) { headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, requestMethod.name()); } headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); headers.add(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, "all"); headers.add(HttpHeaders.ACCESS_CONTROL_MAX_AGE, "3600"); if (request.getMethod() == HttpMethod.OPTIONS) { response.setStatusCode(HttpStatus.OK); return Mono.empty(); } return chain.filter(ctx); }; } 123456789101112131415161718192021222324252627复制代码
代码已上传至码云, 源码 ,项目使用的版本信息如下:
- SpringBoot 2.0.6.RELEASE - SpringCloud Finchley.SR2复制代码