一、基本概念
1.网关基本概念
客户端和服务端中间的一堵墙,可以起到的作用有:请求转发、负载均衡、权限控制等等
个人理解:网关解决从客户端(如8080端口)进入服务端时调用哪个微服务的问题(请求转发功能),Feign解决各微服务之间的相互调用问题
API网关介绍
API 网关出现的原因是微服务架构的出现,不同的微服务一般会有不同的网络地址,而外部客户端可能需要调用多个服务的接口才能完成一个业务需求,如果让客户端直接与各个微服务通信,会有以下的问题:
- 客户端会多次请求不同的微服务,增加了客户端的复杂性。
- 存在跨域请求,在一定场景下处理相对复杂。
- 认证复杂,每个服务都需要独立认证。
- 难以重构,随着项目的迭代,可能需要重新划分微服务。例如,可能将多个服务合并成一个或者将一个服务拆分成多个。如果客户端直接与微服务通信,那么重构将会很难实施。
- 某些微服务可能使用了防火墙 / 浏览器不友好的协议,直接访问会有一定的困难。
以上这些问题可以借助 API 网关解决。
API 网关是介于客户端和服务器端之间的中间层,所有的外部请求都会先经过 API 网关这一层。
也就是说,API 的实现方面更多的考虑业务逻辑,而安全、性能、监控可以交由 API 网关来做,这样既提高业务灵活性又不缺安全性。
2.什么是Gateway网关
Spring Cloud Gateway核心概念
网关提供API全托管服务,丰富的API管理功能,辅助企业管理大规模的API,以降低管理成本和安全风险,包括协议适配、协议转发、安全策略、防刷、流量、监控日志等功能。
一般来说网关对外暴露的URL或者接口信息,我们统称为路由信息。
如果研发过网关中间件或者使用过Zuul的人,会知道网关的核心是Filter以及Filter Chain(Filter责任链)。Sprig Cloud Gateway也具有路由和Filter的概念。
Spring Cloud Gateway中几个重要的概念
路由:路由是网关最基础的部分,路由信息有一个ID、一个目的URL、一组断言和一组Filter组成。如果断言路由为真,则说明请求的URL和配置匹配断言:匹配的规则。Java8中的断言函数。Spring Cloud Gateway中的断言函数输入类型是Spring5.0框架中的ServerWebExchange。Spring Cloud Gateway中的断言函数允许开发者去定义匹配来自于httprequest中的任何信息,比如请求头和参数等。过滤器:一个标准的Spring webFilter。Spring cloud gateway中的filter分为两种类型的Filter,分别是Gateway Filter和Global Filter。过滤器Filter将会对请求和响应进行修改处理
3.网关实现流程
- 客户端发送请求先到网关
- 将网关服务和其它要访问的服务在注册中心进行注册
- 通过网关访问到注册的其它服务
GateWay Handler Mapping:访问之后做映射(路径)匹配
GateWay Web Handler:处理匹配
Filter:过滤器,实现权限管理、跨域等
二、项目整合
1.引入依赖
在api_gateway中引入依赖
<!--nacos注册中心--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency><!--Gateway网关依赖--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-gateway</artifactId></dependency><!--gson--><dependency><groupId>com.google.code.gson</groupId><artifactId>gson</artifactId></dependency><!--服务调用--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency>2.编写配置文件
properties写法
# nacos中注册的服务名spring.application.name=service-gateway# nacos服务地址spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848#使用服务发现路由,服务调用spring.cloud.gateway.discovery.locator.enabled=true#服务路由名小写#spring.cloud.gateway.discovery.locator.lower-case-service-id=true#设置路由id,下标从[0]开始#理论随便写,建议写服务的名字spring.cloud.gateway.routes[0].id=service-acl#设置路由的url#格式lb://+nacos中注册的服务名spring.cloud.gateway.routes[0].uri=lb://service-acl#设置路由断言(匹配规则),代理servicerId为auth-service的/auth/路径#格式Path=spring.cloud.gateway.routes[0].predicates=Path=/*/acl/** spring.cloud.gateway.routes[1].id=service-edu spring.cloud.gateway.routes[1].uri=lb://service-edu spring.cloud.gateway.routes[1].predicates=Path=/eduservice/**yml写法
server: port:8222spring: application: cloud: gateway: discovery: locator: enabled:trueroutes: - id: service-acl uri: lb://service-acl predicates: -Path=/*/acl/**# 路径匹配- id: service-edu uri: lb://service-edu predicates: -Path=/eduservice/**# 路径匹配nacos: discovery: server-addr:127.0.0.1:88483.启动类
importorg.springframework.boot.SpringApplication;importorg.springframework.boot.autoconfigure.SpringBootApplication;importorg.springframework.cloud.client.discovery.EnableDiscoveryClient;@SpringBootApplication @EnableDiscoveryClient public class ApiGatewayApplication{public static void main(String[]args){SpringApplication.run(ApiGatewayApplication.class, args);}}4.测试
- 启动nacos,启动Gateway,启动需要调用的服务
- 打开naocs,地址:http://localhost:8848/nacos,默认账号密码都为nacos
- 点开服务列表检查Gateway等服务是否注册到nacos
- 先通过原服务端口号访问,然后通过网关端口号访问
三、工具类
1.跨域配置
importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.web.cors.CorsConfiguration;importorg.springframework.web.cors.reactive.CorsWebFilter;importorg.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;importorg.springframework.web.util.pattern.PathPatternParser;@ConfigurationpublicclassCorsConfig{@BeanpublicCorsWebFiltercorsFilter(){CorsConfigurationconfig=newCorsConfiguration();config.addAllowedMethod("*");config.addAllowedOrigin("*");config.addAllowedHeader("*");UrlBasedCorsConfigurationSourcesource=newUrlBasedCorsConfigurationSource(newPathPatternParser());source.registerCorsConfiguration("/**",config);returnnewCorsWebFilter(source);}}2.全局Filter
统一处理会员登录与外部不允许访问的服务
importcom.google.gson.JsonObject;importorg.springframework.cloud.gateway.filter.GatewayFilterChain;importorg.springframework.cloud.gateway.filter.GlobalFilter;importorg.springframework.core.Ordered;importorg.springframework.core.io.buffer.DataBuffer;importorg.springframework.http.server.reactive.ServerHttpRequest;importorg.springframework.http.server.reactive.ServerHttpResponse;importorg.springframework.stereotype.Component;importorg.springframework.util.AntPathMatcher;importorg.springframework.web.server.ServerWebExchange;importreactor.core.publisher.Mono;importjava.nio.charset.StandardCharsets;importjava.util.List;@ComponentpublicclassAuthGlobalFilterimplementsGlobalFilter,Ordered{privateAntPathMatcherantPathMatcher=newAntPathMatcher();@OverridepublicMono<Void>filter(ServerWebExchangeexchange,GatewayFilterChainchain){ServerHttpRequestrequest=exchange.getRequest();Stringpath=request.getURI().getPath();//api接口,校验用户必须登录if(antPathMatcher.match("/api/**/auth/**",path)){List<String>tokenList=request.getHeaders().get("token");if(null==tokenList){ServerHttpResponseresponse=exchange.getResponse();returnout(response);}else{// Boolean isCheck = JwtUtils.checkToken(tokenList.get(0));// if(!isCheck) {ServerHttpResponseresponse=exchange.getResponse();returnout(response);// }}}//内部服务接口,不允许外部访问if(antPathMatcher.match("/**/inner/**",path)){ServerHttpResponseresponse=exchange.getResponse();returnout(response);}returnchain.filter(exchange);}@OverridepublicintgetOrder(){return0;}privateMono<Void>out(ServerHttpResponseresponse){JsonObjectmessage=newJsonObject();message.addProperty("success",false);message.addProperty("code",28004);message.addProperty("data","鉴权失败");byte[]bits=message.toString().getBytes(StandardCharsets.UTF_8);DataBufferbuffer=response.bufferFactory().wrap(bits);//response.setStatusCode(HttpStatus.UNAUTHORIZED);//指定编码,否则在浏览器中会中文乱码response.getHeaders().add("Content-Type","application/json;charset=UTF-8");returnresponse.writeWith(Mono.just(buffer));}}3.异常处理
importorg.springframework.beans.factory.ObjectProvider;importorg.springframework.boot.autoconfigure.web.ResourceProperties;importorg.springframework.boot.autoconfigure.web.ServerProperties;importorg.springframework.boot.context.properties.EnableConfigurationProperties;importorg.springframework.boot.web.reactive.error.ErrorAttributes;importorg.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;importorg.springframework.context.ApplicationContext;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.core.Ordered;importorg.springframework.core.annotation.Order;importorg.springframework.http.codec.ServerCodecConfigurer;importorg.springframework.web.reactive.result.view.ViewResolver;importjava.util.Collections;importjava.util.List;@Configuration@EnableConfigurationProperties({ServerProperties.class,ResourceProperties.class})publicclassErrorHandlerConfig{privatefinalServerPropertiesserverProperties;privatefinalApplicationContextapplicationContext;privatefinalResourcePropertiesresourceProperties;privatefinalList<ViewResolver>viewResolvers;privatefinalServerCodecConfigurerserverCodecConfigurer;publicErrorHandlerConfig(ServerPropertiesserverProperties,ResourcePropertiesresourceProperties,ObjectProvider<List<ViewResolver>>viewResolversProvider,ServerCodecConfigurerserverCodecConfigurer,ApplicationContextapplicationContext){this.serverProperties=serverProperties;this.applicationContext=applicationContext;this.resourceProperties=resourceProperties;this.viewResolvers=viewResolversProvider.getIfAvailable(Collections::emptyList);this.serverCodecConfigurer=serverCodecConfigurer;}@Bean@Order(Ordered.HIGHEST_PRECEDENCE)publicErrorWebExceptionHandlererrorWebExceptionHandler(ErrorAttributeserrorAttributes){JsonExceptionHandlerexceptionHandler=newJsonExceptionHandler(errorAttributes,this.resourceProperties,this.serverProperties.getError(),this.applicationContext);exceptionHandler.setViewResolvers(this.viewResolvers);exceptionHandler.setMessageWriters(this.serverCodecConfigurer.getWriters());exceptionHandler.setMessageReaders(this.serverCodecConfigurer.getReaders());returnexceptionHandler;}}4.返回
packagecom.athly.gateway.handler;importorg.springframework.boot.autoconfigure.web.ErrorProperties;importorg.springframework.boot.autoconfigure.web.ResourceProperties;importorg.springframework.boot.autoconfigure.web.reactive.error.DefaultErrorWebExceptionHandler;importorg.springframework.boot.web.reactive.error.ErrorAttributes;importorg.springframework.context.ApplicationContext;importorg.springframework.web.reactive.function.server.*;importjava.util.HashMap;importjava.util.Map;/** * 自定义异常处理 * <p>异常时用JSON代替HTML异常信息<p> */publicclassJsonExceptionHandlerextendsDefaultErrorWebExceptionHandler{publicJsonExceptionHandler(ErrorAttributeserrorAttributes,ResourcePropertiesresourceProperties,ErrorPropertieserrorProperties,ApplicationContextapplicationContext){super(errorAttributes,resourceProperties,errorProperties,applicationContext);}/** * 获取异常属性 */@OverrideprotectedMap<String,Object>getErrorAttributes(ServerRequestrequest,booleanincludeStackTrace){Map<String,Object>map=newHashMap<>();map.put("success",false);map.put("code",20005);map.put("message","网关失败");map.put("data",null);returnmap;}/** * 指定响应处理方法为JSON处理的方法 * @param errorAttributes */@OverrideprotectedRouterFunction<ServerResponse>getRoutingFunction(ErrorAttributeserrorAttributes){returnRouterFunctions.route(RequestPredicates.all(),this::renderErrorResponse);}/** * 根据code获取对应的HttpStatus * @param errorAttributes */@OverrideprotectedintgetHttpStatus(Map<String,Object>errorAttributes){return200;}}