介绍
资料
官网https://spring.io/projects/spring-cloud-gateway
https://jihulab.com/xuxiaowei-cloud/xuxiaowei-cloud/-/tree/main/gateway?ref_type=heads
网关缘由
单体变成微服务后,需要一个统一的入口,类似门窗(网关只对浏览器过来的请求拦截,feign之间的调用也需要添加feign的全局拦截器)
按照现在主流使用微服务架构的特点,假设现在有A、B、C三个服务,假如这三个服务都需要做一些请求过滤和权限校验,请问怎么实现?
- 每个服务自己实现登录
- 写在一个公共的服务,然后让A、B、C服务引入公共服务的Maven依赖,但是和第一种一样需要校验
- 使用服务网关,所有客户端请求服务网关进行请求过滤和权限校验,然后再路由转发到A、B、C服务
网关和nginx
微服务网关能够做的事情,Nginx也可以实现。
相同点:都是可以实现对api接口的拦截,负载均衡、反向代理、请求过滤等,可以实现和网关一样的效果。
不同点:
Nginx采用C语言编写的
在微服务领域中,都是自己语言编写的,比如我们使用java构建微服务项目,Gateway就是java语言编写的。
毕竟Gateway属于Java语言编写的, 能够更好对微服务实现扩展功能,相比Nginx如果想实现扩展功能需要结合Nginx+Lua语言等。
Nginx实现负载均衡的原理:属于服务器端负载均衡器。
Gateway实现负载均衡原理:采用本地负载均衡器的形式。
加载顺序--常常导致注入的依赖为null
可以通过实现ApplicationContextAware接口,然后通过上下文获取
demo(不加注册和发现服务版)
首先根据自己的springboot项目找对应的spring cloud版本,(单独引用不知道行不行)
https://spring.io/projects/spring-cloud#learn
https://start.spring.io/actuator/info
pom.xml
<properties> <project.version>0.0.1-SNAPSHOT</project.version> <java.version>17</java.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <spring-boot.version>3.1.0</spring-boot.version> <!--需要与springboot版本--> <!--https://spring.io/projects/spring-cloud#learn--> <!--https://start.spring.io/actuator/info--> <spring-cloud.version>2022.0.4</spring-cloud.version> </properties> <!-- 依赖声明 --> <dependencyManagement> <dependencies> <!-- Spring Boot 的依赖项管理,没有继承属性和插件--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring-boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>de.codecentric</groupId> <artifactId>spring-boot-admin-dependencies</artifactId> <version>${spring-boot-admin.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <!-- Spring Cloud Gateway --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> </dependencies> <build> <!--包名--> <!-- <finalName>${project.artifactId}</finalName>--> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>${spring-boot.version}</version> <!--替换为你的应用程序主类--> <configuration> <mainClass>com.cabin.gateway.GatewayApplication</mainClass> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> <executions> <execution> <id>repackage</id> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
启动类
com.cabin.gateway.GatewayApplication
package com.cabin.gateway; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class GatewayApplication { public static void main(String[] args) { SpringApplication.run(GatewayApplication.class, args); } }
方法一:yaml实现gateway
我的服务的接口是http://127.0.0.1:8080/util/test
那么gateway的yaml
server: port: 9090 spring: cloud: gateway: routes: - id: app # 路由的唯一标识 uri: http://localhost:8080 # 如果断言成功,将要转发去的地址 order: 0 # 优先级,越小优先级越高 predicates: # 断言,满足所有断言,才会进行转发 - Path=/app/** # 注意:这是使用= 不是: filters: - StripPrefix=1 # #转发请求时去掉1级前缀,去掉端口后的第一个节点,这里是client
开启gateway和test的服务后访问gateway的路由
http://127.0.0.1:9090/app/util/test
方法二:代码实现gateway
yaml只需要开启服务
server: port: 9090
配置类,如何自定义Filter
package com.cabin.gateway.config; import org.springframework.cloud.gateway.route.RouteLocator; import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class GatewayConfig { @Bean public RouteLocator customRouteLocator(RouteLocatorBuilder builder) { return builder.routes() .route("login_route", r -> r.path("/login") .uri("http://your-login-service/login")) .route("app_route", r -> r.path("/app/**") .filters(f -> f.stripPrefix(1) // f.filter(new AuthFilter()).stripPrefix(1) //如果自定义Filter ) .uri("http://localhost:8080")) .build(); } }
filter
如果你的gateway路由是代码注入的,不是yaml配置的,一般都会指定一个filter
GatewayFilter
package com.cabin.gateway.config; import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; @Component public class AuthFilter implements GatewayFilter { // 黑名单列表,也可以放在缓存数据库里 private static final String START_TIME = "startTime"; @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { Mono<Void> noLogin = Filter.loginLogic(exchange); if (noLogin != null) return noLogin; Mono<Void> blackUser = Filter.blackLoginLogic(exchange); if (blackUser != null) return blackUser; // 下面3行代码在前过滤器pre filter执行 String url = exchange.getRequest().getURI().getPath(); System.out.println("ip来源: " + "这里从nginx获取ip握手时的ip"); System.out.println("请求地址:" + url); System.out.println("入参:" + exchange.getRequest().getQueryParams().toString()); // exchange的getAttributes可以存放信息 exchange.getAttributes().put(START_TIME, System.currentTimeMillis()); // chain.filter里面的逻辑相当于后过滤器post filter return chain.filter(exchange).then( Mono.fromRunnable(() -> { Long startTime = exchange.getAttribute(START_TIME); if (startTime != null) { System.out.println(url + "耗时:" + (System.currentTimeMillis() - startTime) + "ms"); } }) ); // 如果放行不需要日志 // return chain.filter(exchange); } }
Filter的方法你可以自定义
package com.cabin.gateway.config; import org.springframework.http.HttpCookie; import org.springframework.http.HttpStatus; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import java.util.Arrays; import java.util.List; /** * @author 伍六七 * @date 2023/8/12 20:42 */ public class Filter { static List<String> blackList = Arrays.asList("123", "456"); /** * 黑名单 * * @return 为null表示非黑名单 */ public static Mono<Void> blackLoginLogic(ServerWebExchange exchange) { // 用户id String id = exchange.getRequest().getHeaders().getFirst("id"); // blackList可以做持久化 if (blackList.contains(id)) { // 如果是黑名单就直接返回,不再往目标服务器转发 exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE); return exchange.getResponse().setComplete(); } return null; } /** * 登录逻辑 <br/> * * @return 为null表示登录成功 */ public static Mono<Void> loginLogic(ServerWebExchange exchange) { // 在这里编写登录检测逻辑 // 如果登录有效,调用chain.filter(exchange)继续处理请求 // 如果登录无效,可以返回未授权的响应或重定向到登录页面 String token = null; HttpCookie tokenCookie = exchange.getRequest().getCookies().getFirst("token"); if (tokenCookie != null) { token = tokenCookie.getValue(); } // 功能1.拿到token判断是否已经登录 if (!isLogin(token)) { // 如果没有登录则重定向都登录页面 exchange.getResponse().setStatusCode(HttpStatus.FOUND); exchange.getResponse().getHeaders().set("Location", "/login"); return exchange.getResponse().setComplete(); } // 如果判断成功了放行 System.out.println("登录成功"); return null; } public static boolean isLogin(String token) { return true; } }
使用时
package com.cabin.gateway.config; import org.springframework.cloud.gateway.route.RouteLocator; import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class GatewayConfig { @Bean public RouteLocator customRouteLocator(RouteLocatorBuilder builder) { return builder.routes() .route("login_route", r -> r.path("/login") .uri("http://your-login-service/login")) .route("app_route", r -> r.path("/app/**") .filters(f -> f.filter(new AuthFilter()) ) .uri("http://localhost:8080")) .build(); } }
GlobalFilter
一般搭配Ordered,这个filter使用@Component注入即可,即可生效,对于所有路由
package com.cabin.gateway.config; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.Ordered; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; @Component public class MyGlobalFilter implements GlobalFilter, Ordered { private static final String START_TIME = "startTime"; @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { // 下面3行代码在前过滤器pre filter执行 String url = exchange.getRequest().getURI().getPath(); System.out.println("ip来源: " + "这里从nginx获取ip握手时的ip"); System.out.println("请求地址:" + url); System.out.println("入参:" + exchange.getRequest().getQueryParams().toString()); // exchange的getAttributes可以存放信息 exchange.getAttributes().put(START_TIME, System.currentTimeMillis()); // chain.filter里面的逻辑相当于后过滤器post filter return chain.filter(exchange).then( Mono.fromRunnable(() -> { Long startTime = exchange.getAttribute(START_TIME); if (startTime != null) { System.out.println(url + "耗时:" + (System.currentTimeMillis() - startTime) + "ms"); } }) ); } @Override public int getOrder() { return 0; } }