文章目录
- 一、SpringCloudGateway服务网关概论
- 1、SpringCloudGateway服务网关概论
- 2、SpringCloudGateway的三大核心概念
- 二、SpringCloudGateway的路由及断言
- 1、子模块项目SpringCloudGateway的搭建
- 2、SpringCloudGateway_Java API构建路由
- 3、SpringCloudGateway的动态路由功能
- 4、SpringCloudGateway的路由断言
- 三、SpringCloudGateway的过滤器及跨域
- 1、SpringCloudGateway的过滤器
- 2、网关过滤器GatewayFilter
- 3、自定义网关过滤器GatewayFilter
- 4、自定义全局过滤器GlobalFilter
- 5、内置全局过滤器
- 6、服务网关Gateway实现跨域
- 四、SpringCloudGateway实现用户鉴权
- 1、JsonWebToken概论
- 2、创建用户的微服务及登录操作
- 3、服务网关Gateway实现用户鉴权
- 总结
一、SpringCloudGateway服务网关概论
1、SpringCloudGateway服务网关概论
Spring Cloud Gateway 用"Netty + Webflux"实现,不需要导入Web依赖。
- Webflux模式替换了旧的Servlet线程模型。用少量的线程处理request和response io操作,这些线程称为Loop线程,而业务交给响应式编程框架处理,响应式编程是非常灵活的,用户可以将业务中阻塞的操作提交到响应式框架的work线程中执行,而不阻塞的操作依然可以在Loop线程中进行处理,大大提高了Loop线程的利用率。
即Webflux中的Loop线程不仅可以处理请求和响应请求,还可以对业务中不阻塞的操作进行处理,从而提高它的利用率。阻塞的操作由work线程进行处理。 - Webflux虽然可以兼容多个底层的通信框架,但是一般情况下,底层使用的还是Netty,毕竟,Netty是目前业界认可的最高性能的通信框架。
Netty 是一个基于NIO的客户、服务器端的编程框架。提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。 - Spring Cloud Gateway特点
1)易于编写谓词( Predicates )和过滤器( Filters ) 。其Predicates和Filters
可作用于特定路由。
2)支持路径重写。
3)支持动态路由。
4)集成了Spring Cloud DiscoveryClient。
2、SpringCloudGateway的三大核心概念
- 路由(Route)
这是网关的基本构建块。它由一个ID,一个目标URI,一组断言和一组过滤器定义。如果断言为真,则路由匹配。
即根据URL请求去匹配路由。 - 断言(predicate)
输入类型是一个ServerWebExchange。我们可以使用它来匹配来自HTTP请求的任何内容,例如headers或参数。匹配请求内容。
匹配完路由后,每个路由上面都会有断言,然后根据断言来判断是否可以进行路由。 - 过滤(filter)
在匹配完路由和断言为真后,可以在请求被路由前或者之后对请求进行修改。
即根据业务对其进行监控,限流,日志输出等等。
二、SpringCloudGateway的路由及断言
1、子模块项目SpringCloudGateway的搭建
-
在cloud父项目中新建一个模块Module,创建子模块网关cloud-gateway-gateway9527
-
在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>cloud</artifactId> <groupId>com.zzx</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>cloud-gateway-gateway9527</artifactId> <properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> </properties> <dependencies> <!-- 引入网关Gateway依赖 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.22</version> </dependency> <!-- 引入Eureka client依赖 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <!-- actuator监控信息完善 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> </dependencies> </project>
-
在gateway子模块中创建包com.zzx,在包下创建主启动类GatewayMain9527
package com.zzx; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @Slf4j public class GatewayMain9527 { public static void main(String[] args) { SpringApplication.run(GatewayMain9527.class,args); log.info("************ GatewayMain9527服务 启动成功 *************"); } }
-
在resources目录下创建application.yml文件,配置如下
server: port: 9527 spring: cloud: gateway: routes: # 路由ID,没有固定规则但要求唯一,建议配合服务名 - id: cloud-payment-provider # 匹配后提供服务的路由地址 (即目标服务地址) uri: http://localhost:8001 # 断言会接收一个输入参数,返回一个布尔值结果 predicates: # 路径相匹配的进行路由 - Path=/payment/*
-
测试
1)先开启7001和7002的Eureka服务,payment8001服务提供者和gateway9527服务。
2)在浏览器使用9527端口,也就是网关进行访问payment8001服务即可。
在浏览器输入:http://localhost:9527/payment/index
2、SpringCloudGateway_Java API构建路由
-
在子模块cloud-gateway-gateway9527中的com.zzx包下,创建包config,并在包下创建GatewayConfig
package com.zzx.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 routeLocator(RouteLocatorBuilder builder){ //获取路由 RouteLocatorBuilder.Builder routes = builder.routes(); /** * 设置路由 * 1.路由id * 2.路由匹配规则 * 3.目标地址 */ routes.route("path_route",r->r.path("/payment/*").uri("http://localhost:8001/")).build(); return routes.build(); } }
-
测试
1)将yml文件中的gateway配置注释掉,然后重启该服务。
2)在浏览器上访问:http://localhost:9527/payment/index
3、SpringCloudGateway的动态路由功能
-
再添加一个服务提供者,用以实现Gateway网关的动态路由的功能。
1)复制payment8001服务,然后点击cloud父工程,ctrl+v进行粘贴,修改名字为8002
2)修改POM文件:<artifactId>cloud-provider-payment8002</artifactId>
3)将POM右键,选择添加为Maven项目Add as Maven Project
4)修改com.zzx包下的启动类的名字以及类中的名字package com.zzx; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * 主启动类 */ @SpringBootApplication @Slf4j public class PaymentMain8002 { public static void main(String[] args) { SpringApplication.run(PaymentMain8002.class,args); log.info("****** PaymentMain8002服务启动成功 *****"); } }
5)将yml文件的端口号port和instance-id的名字有8001部分都修改为8002
然后在启动类中运行该payment8002服务。 -
修改gateway9527项目的yml文件
server: port: 9527 eureka: instance: # 注册名 instance-id: cloud-gateway-gateway9527 client: service-url: # Eureka server的地址 #集群 defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka #单机 #defaultZone: http://localhost:7001/eureka/ spring: application: #设置应用名 name: cloud-gateway cloud: gateway: routes: # 路由ID,没有固定规则但要求唯一,建议配合服务名 - id: cloud-payment-provider # 匹配后提供服务的路由地址 (即目标服务地址) lb后跟提供服务的微服务的名字 uri: lb://CLOUD-PAYMENT-PROVIDER # 断言会接收一个输入参数,返回一个布尔值结果 predicates: # 路径相匹配的进行路由 - Path=/payment/*
-
注释之前的配置文件GatewayConfig中的方法。
-
在服务提供者payment8001和payment8002中的com.zzx.controller的PaymentController类中添加如下代码
@Value("${server.port}") private String port; @GetMapping("lb") public String lb(){ return port; }
即通过该lb的url请求来测试动态路由是否配置生效。
-
测试动态路由是否配置生效。
1)重启payment8001和payment8002以及gateway9527服务
2)浏览器中访问:http://localhost:9527/payment/lb
此时刷新后随即出现8001或8002,估计是轮询的策略。
4、SpringCloudGateway的路由断言
-
UTC时间格式的时间参数时间生成方法
package demo; import java.time.ZonedDateTime; public class Test1 { public static void main(String[] args) { ZonedDateTime now = ZonedDateTime.now(); System.out.println(now); } }
-
Postman的下载地址:
https://dl.pstmn.io/download/latest/win64
Postman即用来URL请求测试的软件,可以很方便的添加任何请求参数。
点击+号即可创建新的请求窗口,用来发送URL请求
-
After路由断言
predicates: - Path=/payment/* # 在这个时间点之后才能访问 - After=2030-04-28T11:50:49.213572400+08:00[Asia/Shanghai]
即使用生成的UTC时间格式的时间,在该时间之后才允许访问。
-
Before路由断言
predicates: - Path=/payment/* # 在这个时间点之前才能访问 - Before=2030-04-28T11:50:49.213572400+08:00[Asia/Shanghai]
即使用生成的UTC时间格式的时间,在该时间之前才允许访问。
-
Between路由断言
predicates: - Path=/payment/* # 在两个时间内才能访问 - Between=2030-04-28T11:50:49.213572400+08:00[Asia/Shanghai],2030-04-28T11:50:49.213572400+08:00[Asia/Shanghai]
即使用生成的UTC时间格式的时间,在两个时间内才允许访问。
-
Cookie路由断言
1)Cookie验证的是Cookie中保存的信息,Cookie断言和上面介绍的两种断言使用方式大同小异,唯一的不同是它必须连同属性值一同验证,不能单独只验证属性是否存在。predicates: - Path=/payment/* - Cookie=username,zzx
即Cookie的username的值为zzx才允许访问
2)使用postman进行测试,在headers添加Cookie即可
此时如果不带Cookie,则报404错误 -
Header路由断言
1)这个断言会检查Header中是否包含了响应的属性,通常可以用来验证请求是否携带了访问令牌。predicates: - Path=/payment/* - Header=X-Request-Id,\d+
2)使用postman进行测试,在headers添加X-Request-Id即可
-
Host路由断言
1)Host 路由断言 Factory包括一个参数:host name列表。使用Ant路径匹配规则, .作为分隔符。访问的主机匹配http或者https, baidu.com 默认80端口, 就可以通过路由。 多个参数使用,号隔开。predicates: - Path=/payment/* - Host=127.0.0.1,localhost
2)使用postman进行测试,在headers添加Host即可
-
Method路由断言
1)即Request请求的方式,例如GET或POST请求,不匹配则无法进行请求predicates: - Path=/payment/* - Method=GET,POST
2)可以使用postman,也可以使用浏览器直接访问,因为不需要加任何参数
-
Query路由断言
1)请求断言也是在业务中经常使用的,它会从ServerHttpRequest中的Parameters列表中查询指定的属性,例如验证参数的类型等predicates: - Path=/payment/* - Query=age,\d+
2)在参数Params中添加age属性,值为正整数即可访问
三、SpringCloudGateway的过滤器及跨域
1、SpringCloudGateway的过滤器
- 过滤器Filter
在用户访问各个服务前,应在网关层统一做好鉴权、限流等工作。
1)Filter的生命周期
根据生命周期可以将Spring Cloud Gateway中的Filter分为"PRE"和"POST"两种。
PRE:代表在请求被路由之前执行该过滤器,此种过滤器可用来实现参数校验、权限校验、流量监控、日志输出、协议转换等功能。
POST:代表在请求被路由到微服务之后执行该过滤器。此种过滤器可用来实现响应头的修改(如添加标准的HTTP Header )、收集统计信息和指标、将响应发送给客户端、输出日志、流量监控等功能。
即PRE是路由之前,POST是路由之后。
2)Filter分类
根据作用范围,Filter可以分为以下两种。
GatewayFilter:网关过滤器,此种过滤器只应用在单个路由或者一个分组的路由上。
GlobalFilter:全局过滤器,此种过滤器会应用在所有的路由上。
2、网关过滤器GatewayFilter
- 官方的配置文档:
https://docs.spring.io/spring-cloud-gateway/docs/4.0.4/reference/html/#gatewayfilter-factories
- 使用内置过滤器SetStatus
1)在yml文件中的filters下添加过滤器
2)在浏览器测试:server: port: 9527 eureka: instance: # 注册名 instance-id: cloud-gateway-gateway9527 client: service-url: # Eureka server的地址 #集群 defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka #单机 #defaultZone: http://localhost:7001/eureka/ spring: application: #设置应用名 name: cloud-gateway cloud: gateway: routes: # 路由ID,没有固定规则但要求唯一,建议配合服务名 - id: cloud-payment-provider # 匹配后提供服务的路由地址 (即目标服务地址) lb后跟提供服务的微服务的名字 uri: lb://CLOUD-PAYMENT-PROVIDER # 断言会接收一个输入参数,返回一个布尔值结果 predicates: # 路径相匹配的进行路由 - Path=/payment/* # 在这个时间点之后才能访问 # - After=2030-04-28T11:50:49.213572400+08:00[Asia/Shanghai] # 在这个时间点之前才能访问 # - Before=2030-04-28T11:50:49.213572400+08:00[Asia/Shanghai] # 在两个时间内才能访问 # - Between=2030-04-28T11:50:49.213572400+08:00[Asia/Shanghai],2030-04-28T11:50:49.213572400+08:00[Asia/Shanghai] # - Cookie=username,zzx # - Header=X-Request-Id,\d+ # - Host=127.0.0.1,localhost # - Method=GET,POST # - Query=age,\d+ #过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改 filters: # 修改原始响应的状态码 - SetStatus=250
http://localhost:9527/payment/lb
此时响应码成功修改为250。
3、自定义网关过滤器GatewayFilter
-
在gateway9527服务的com.zzx.config包下,创建日志网关过滤器类LogGatewayFilterFactory
package com.zzx.config; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; import org.springframework.stereotype.Component; import java.util.Arrays; import java.util.List; /** * 日志网关过滤器 */ @Component @Slf4j public class LogGatewayFilterFactory extends AbstractGatewayFilterFactory<LogGatewayFilterFactory.Config> { public LogGatewayFilterFactory() { super(Config.class); } /** * 表示配置填写顺序 * @return */ @Override public List<String> shortcutFieldOrder() { return Arrays.asList("consoleLog"); } /** * 执行过滤的逻辑 * @param config * @return */ @Override public GatewayFilter apply(Config config) { return ((exchange, chain) -> { if(config.consoleLog){ log.info("********* consoleLog日志 开启 ********"); } return chain.filter(exchange); }); } /** * 过滤器使用的配置内容 * */ @Data public static class Config{ private boolean consoleLog; } }
-
在YML文件中,添加如下
filters: # 控制日志是否开启 - Log=true
即开启日志,该true会被consoleLog获取到。 然后即可打印对应的日志。
-
测试
1)重启Gateway9527服务
2)在浏览器中访问:http://localhost:9527/payment/lb
步骤:
1、类名必须叫做XxxGatewayFilterFactory,注入到Spring容器后使用时的名称就叫做Xxx。
2、创建一个静态内部类Config, 里面的属性为配置文件中配置的参数, - 过滤器名称=参数1,参数2…
2、类必须继承 AbstractGatewayFilterFactory,让父类帮实现配置参数的处理。
3、重写shortcutFieldOrder()方法,返回List参数列表为Config中属性集合
return Arrays.asList(“参数1”,参数2…)
4、无参构造方法中super(Config.class)
5、编写过滤逻辑 public GatewayFilter apply(Config config)
4、自定义全局过滤器GlobalFilter
-
在gateway9527服务的com.zzx.config包下,创建用户鉴权全局过滤器类AuthGlobalFilter
package com.zzx.config; 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.HttpStatus; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; /** * 用户鉴权全局过滤器 */ @Component public class AuthGlobalFilter implements GlobalFilter, Ordered { /** * 自定义全局过滤器逻辑 * @param exchange * @param chain * @return */ @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { //1。请求中获取Token令牌 String token = exchange.getRequest().getQueryParams().getFirst("token"); //2.判断token是否为空 if(StringUtils.isEmpty(token)){ System.out.println("鉴权失败,令牌为空"); //将状态码设置为未授权 exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } //3。判断token是否有效 if(!token.equals("zzx")){ System.out.println("token令牌无效"); exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } return chain.filter(exchange); } /** * 全局过滤器执行顺序 数值越小,优先级越高 * @return */ @Override public int getOrder() { return 0; } }
-
使用postman测试,在params中添加一个token进行测试
5、内置全局过滤器
- 官方的配置文档:
https://docs.spring.io/spring-cloud-gateway/docs/4.0.4/reference/html/#global-filters
SpringCloud Gateway内部也是通过一系列的内置全局过滤器对整个路由转发进行处理的。 - 路由过滤器(Forward)
- 路由过滤器(LoadBalancerClient)
- Netty路由过滤器
- Netty写响应过滤器(Netty Write Response F)
- RouteToRequestUrl 过滤器
- 路由过滤器 (Websocket Routing Filter)
- 网关指标过滤器(Gateway Metrics Filter)
- 组合式全局过滤器和网关过滤器排序(Combined Global Filter and GatewayFilter Ordering)
- 路由(Marking An Exchange As Routed)
6、服务网关Gateway实现跨域
-
跨域
即当一个请求url的协议、域名、端口三者之间任意一个与当前页面url不同即为跨域 -
在resources目录下创建index.html文件
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> </body> <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script> <script> $.get("http://localhost:9527/payment/lb?token=zzx",function(data,status){ alert("Data: " + data + "\nStatus: " + status); }); </script> </html>
-
配置允许跨域
1)在未配置允许跨域之前,打开该index.html文件时,如图
2)在yml文件中配置允许跨域spring: cloud: gateway: globalcors: cors-configurations: '[/**]': allowCredentials: true allowedOriginPatterns: "*" allowedMethods: "*" allowedHeaders: "*" add-to-simple-url-handler-mapping: true
3)配置后,打开该index.html文件时,如图
四、SpringCloudGateway实现用户鉴权
1、JsonWebToken概论
-
JWT是一种用于双方之间传递安全信息的简洁的、URL安全的声明规范。定义了一种简洁的,自包含的方法用于通信双方之间以Json对象的形式安全的传递信息。特别适用于分布式站点的单点登录(SSO)场景。
-
JWT优点
1)无状态
2)适合移动端应用
3)单点登录友好 -
用户与服务端通信的时候,都要发回这个 JSON 对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候会加上签名,服务器就不保存任何 session 数据了,也就是说,服务器变成无状态了,从而比较容易实现扩展。
-
JWT 的三个部分依次如下:
1)头部(header)
JSON对象,描述 JWT 的元数据。其中 alg 属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ 属性表示这个令牌(token)的类型(type),统一写为 JWT。{ "alg": "HS256", "typ": "JWT" }
2)载荷(payload)
内容又可以分为3种标准
1.标准中注册的声明
iss: jwt签发者
sub: jwt所面向的用户
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
2.公共的声明
公共的声明可以添加任何的信息。一般这里我们会存放一下用户的基本信息(非敏感信息)。
3.私有的声明
私有声明是提供者和消费者所共同定义的声明。需要注意的是,不要存放敏感信息
base64编码,任何人获取到jwt之后都可以解码!!{ "sub": "1234567890", "name": "John Doe", "iat": 1516239022 }
3)签证(signature)
这部分就是 JWT 防篡改的精髓,其值是对前两部分base64UrlEncode 后使用指定算法签名生成,以默认 HS256 为例,指定一个密钥(secret),就会按照如下公式生成:HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret, )
-
客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。此后,客户端每次与服务器通信,都要带上这个 JWT。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求的头信息Authorization字段里面。
2、创建用户的微服务及登录操作
-
在cloud父工程下,创建子模块项目cloud-auth-user6500
-
在cloud-auth-user6500项目的pom文件中引入依赖
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- eureka client 依赖 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.22</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <!-- 引入JWT依赖 --> <!-- https://mvnrepository.com/artifact/com.alibaba/fastjson --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>2.0.23</version> </dependency> <!-- https://mvnrepository.com/artifact/com.auth0/java-jwt --> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>4.2.1</version> </dependency> </dependencies>
-
在com.zzx中创建一个包utils,创建工具类JWTUtils
package com.zzx.utils; import com.auth0.jwt.JWT; import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.JWTVerificationException; import com.auth0.jwt.interfaces.DecodedJWT; import java.util.Date; import java.util.concurrent.TimeUnit; public class JWTUtils { // 签发人 private static final String ISSUSER = "zzx"; // 过期时间 1分钟 private static final long TOKEN_EXPIRE_TIME = 60*1000; // 秘钥 public static final String SECRET_KEY = "zzx-13256"; /** * 生成令牌 * @return */ public static String token(){ Date now = new Date(); Algorithm hmac256 = Algorithm.HMAC256(SECRET_KEY); // 1.创建JWT String token = JWT.create(). // 签发人 withIssuer(ISSUSER) // 签发时间 .withIssuedAt(now) // 过期时间 .withExpiresAt(new Date(now.getTime()+TOKEN_EXPIRE_TIME)) // 加密算法 .sign(hmac256); return token; } /** * 验证令牌 * @return */ public static boolean verify(String token){ try { Algorithm hmac256 = Algorithm.HMAC256(SECRET_KEY); JWTVerifier verifier = JWT.require(hmac256) // 签发人 .withIssuer(ISSUSER) .build(); // 如果校验有问题则抛出异常 DecodedJWT verify = verifier.verify(token); return true; } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (JWTVerificationException e) { e.printStackTrace(); } return false; } public static void main(String[] args) throws InterruptedException { String token = token(); System.out.println(token); boolean verify = verify(token); System.out.println(verify); verify = verify(token+" 11"); System.out.println(verify); TimeUnit.SECONDS.sleep(61); verify = verify(token); System.out.println(verify); } }
在该工具类JWTUtils中创建main方法用来测试该工具类。后面需要删掉。
-
在com.zzx中创建一个包common,创建类Result
package com.zzx.common; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; /** * 返回实体类 */ @AllArgsConstructor @NoArgsConstructor @Data @Builder public class Result { // 状态码 private int code; // 描述信息 private String msg; // token令牌 private String token; }
即用该类来封装返回值信息。
-
在com.zzx中创建一个包controller,创建控制层类UserController
package com.zzx.controller; import com.zzx.common.Result; import com.zzx.utils.JWTUtils; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * 用户控制层 */ @RestController @RequestMapping("user") public class UserController { /** * 登录 * @param username * @param password */ @PostMapping("login") public Result login(String username, String password){ // 1.验证用户名和密码 // TODO 模拟数据库操作 if("zzx".equals(username)&&"123456".equals(password)){ // 2.生成令牌 String token = JWTUtils.token(); return Result.builder().code(200).msg("success").token(token).build(); }else{ return Result.builder().code(500).msg("用户名或密码不正确").build(); } } }
-
在resources目录下创建一个application.yml配置文件
server: port: 6500 eureka: instance: # 注册名 instance-id: cloud-auth-user6500 client: service-url: # Eureka server的地址 #集群 defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka #单机 #defaultZone: http://localhost:7001/eureka/ spring: application: #设置应用名 name: cloud-auth-user
-
在com.zzx中,修改主启动类Main,修改为UserMain6500
package com.zzx; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * 主启动类 */ @Slf4j @SpringBootApplication public class UserMain6500 { public static void main(String[] args) { SpringApplication.run(UserMain6500.class,args); log.info("************ UserMain6500服务 启动成功 ************"); } }
-
测试User控制层的login方法
1)启动eureka服务eureka7001和eureka7002以及user6500
2)在postman中,使用POST请求传入用户名和密码,对该url进行测试
3、服务网关Gateway实现用户鉴权
即在网关过滤器中加入JWT来鉴权
-
在gateway9527项目的POM文件中添加JWT依赖
<!-- 引入JWT依赖 --> <!-- https://mvnrepository.com/artifact/com.alibaba/fastjson --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>2.0.23</version> </dependency> <!-- https://mvnrepository.com/artifact/com.auth0/java-jwt --> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>4.2.1</version> </dependency>
-
将user6500项目中com.zzx.utils包下的JWTUtils复制到gateway9527项目的com.zzx.utils包下
package com.zzx.utils; import com.auth0.jwt.JWT; import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.JWTVerificationException; import com.auth0.jwt.interfaces.DecodedJWT; import java.util.Date; public class JWTUtils { // 签发人 private static final String ISSUSER = "zzx"; // 过期时间 1分钟 private static final long TOKEN_EXPIRE_TIME = 60*1000; // 秘钥 public static final String SECRET_KEY = "zzx-13256"; /** * 生成令牌 * @return */ public static String token(){ Date now = new Date(); Algorithm hmac256 = Algorithm.HMAC256(SECRET_KEY); // 1.创建JWT String token = JWT.create(). // 签发人 withIssuer(ISSUSER) // 签发时间 .withIssuedAt(now) // 过期时间 .withExpiresAt(new Date(now.getTime()+TOKEN_EXPIRE_TIME)) // 加密算法 .sign(hmac256); return token; } /** * 验证令牌 * @return */ public static boolean verify(String token){ try { Algorithm hmac256 = Algorithm.HMAC256(SECRET_KEY); JWTVerifier verifier = JWT.require(hmac256) // 签发人 .withIssuer(ISSUSER) .build(); // 如果校验有问题则抛出异常 DecodedJWT verify = verifier.verify(token); return true; } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (JWTVerificationException e) { e.printStackTrace(); } return false; } }
-
修改application.yml文件
server: port: 9527 eureka: instance: # 注册名 instance-id: cloud-gateway-gateway9527 client: service-url: # Eureka server的地址 #集群 defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka #单机 #defaultZone: http://localhost:7001/eureka/ org: my: jwt: # 跳过认证路由 skipAuthUrls: - /user/login spring: application: #设置应用名 name: cloud-gateway cloud: gateway: # 路由配置 routes: # 路由ID,没有固定规则但要求唯一,建议配合服务名 - id: cloud-auth-user # 匹配后提供服务的路由地址 (即目标服务地址) lb后跟提供服务的微服务的名字 uri: lb://CLOUD-AUTH-USER # 断言会接收一个输入参数,返回一个布尔值结果 predicates: # 路径相匹配的进行路由 - Path=/user/* # 路由ID,没有固定规则但要求唯一,建议配合服务名 - id: cloud-payment-provider # 匹配后提供服务的路由地址 (即目标服务地址) lb后跟提供服务的微服务的名字 uri: lb://CLOUD-PAYMENT-PROVIDER # 断言会接收一个输入参数,返回一个布尔值结果 predicates: # 路径相匹配的进行路由 - Path=/payment/* # 在这个时间点之后才能访问 # - After=2030-04-28T11:50:49.213572400+08:00[Asia/Shanghai] # 在这个时间点之前才能访问 # - Before=2030-04-28T11:50:49.213572400+08:00[Asia/Shanghai] # 在两个时间内才能访问 # - Between=2030-04-28T11:50:49.213572400+08:00[Asia/Shanghai],2030-04-28T11:50:49.213572400+08:00[Asia/Shanghai] # - Cookie=username,zzx # - Header=X-Request-Id,\d+ # - Host=127.0.0.1,localhost # - Method=GET,POST # - Query=age,\d+ #过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改 filters: # 修改原始响应的状态码 # - SetStatus=250 # 控制日志是否开启 - Log=true globalcors: cors-configurations: '[/**]': allowCredentials: true allowedOriginPatterns: "*" allowedMethods: "*" allowedHeaders: "*" add-to-simple-url-handler-mapping: true
即需要添加一个user微服务的路由,以及跳过权限验证的Path路径
-
将gateway9527项目的com.zzx.config包下原先的用户鉴权类AuthGlobalFilter上面的@Component注解注释掉,即不使用这个类来鉴权;创建使用另一个类UserAuthGlobalFilter来鉴权
package com.zzx.config; import com.alibaba.fastjson.JSONObject; import com.zzx.common.Response; import com.zzx.utils.JWTUtils; import io.micrometer.common.util.StringUtils; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.Ordered; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.ServerHttpResponse; 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.StandardCharsets; /** * 用户鉴权全局过滤器 */ @Data @ConfigurationProperties("org.my.jwt") @Component @Slf4j public class UserAuthGlobalFilter implements GlobalFilter, Ordered { private String[] skipAuthUrls; /** * 过滤器逻辑 * @param exchange * @param chain * @return */ @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { // 获取请求url地址 String path = exchange.getRequest().getURI().getPath(); // 跳过不需要验证的路径 if(skipAuthUrls!=null && isSKip(path)){ return chain.filter(exchange); } // 1.从请求头中获取token String token = exchange.getRequest().getHeaders().getFirst("token"); // 2.判断token if(StringUtils.isEmpty(token)){ // 3.设置响应 ServerHttpResponse response = exchange.getResponse(); // 4.设置响应状态码 response.setStatusCode(HttpStatus.OK); // 5.设置响应头 response.getHeaders().add("Content-Type","application/json;charset=UTF-8"); // 6.创建响应对象 Response res = new Response(200, "token 参数缺失"); // 7.对象转字符串 byte[] bytes = JSONObject.toJSONString(res).getBytes(StandardCharsets.UTF_8); // 8.数据流返回数据 DataBuffer wrap = response.bufferFactory().wrap(bytes); return response.writeWith(Flux.just(wrap)); } // 验证token boolean verify = JWTUtils.verify(token); if(!verify){ // 3.设置响应 ServerHttpResponse response = exchange.getResponse(); // 4.设置响应状态码 response.setStatusCode(HttpStatus.OK); // 5.设置响应头 response.getHeaders().add("Content-Type","application/json;charset=UTF-8"); // 6.创建响应对象 Response res = new Response(200, "token 失效"); // 7.对象转字符串 byte[] bytes = JSONObject.toJSONString(res).getBytes(StandardCharsets.UTF_8); // 8.数据流返回数据 DataBuffer wrap = response.bufferFactory().wrap(bytes); return response.writeWith(Flux.just(wrap)); } // token 令牌通过 return chain.filter(exchange); } @Override public int getOrder() { return 0; } private boolean isSKip(String url){ for (String skipAuthUrl :skipAuthUrls) { if(url.startsWith(skipAuthUrl)){ return true; } } return false; } }
-
测试
1)先启动eureka7001和eureka7002,还有Payment8001和Payment8002,以及user6500和gateway9527服务。
2)使用postman工具来测试,先进行登录,拿到用户的token
3)再切换到之前9527的url测试
token有效时
token过期失效时
没有token时(即未登录时)
总结
- Spring Cloud Gateway 用"Netty + Webflux"实现,不需要导入Web依赖。
1)Webflux模式替换了旧的Servlet线程模型。用少量的线程处理request和response io操作,这些线程称为Loop线程。Webflux中的Loop线程不仅可以处理请求和响应请求,还可以对业务中不阻塞的操作进行处理,从而提高它的利用率。阻塞的操作由work线程进行处理。
Webflux底层使用的还是Netty,Netty是目前业界认可的最高性能的通信框架。
2)SpringCloudGateway的三大核心概念,分别是路由、断言、过滤。
即根据url请求进行匹配到指定路由;每个路由上面都有断言,根据断言来判断是否可以进行路由;最后对该url请求进行一个过滤,例如监控、限流和日志输出等操作。 - 1)SpringCloudGateway的搭建,需要先引入依赖,然后创建主启动类,最后配置Gateway的配置文件。- id属性值需要唯一;uri的属性值即对应的服务器ip地址+端口号;predicates断言的属性值,例如OrderController第一层@RequestMapping注解的url属性值,即判断url是否跟该值一致。
2)服务网关Gateway通过Java API构建时需要实现RouteLocator接口构建路由规则。即先将yml文件中等价的gateway配置注释掉,然后创建一个config配置类,在配置类中,创建一个方法使用RouteLocator接口来构建路由。并在该方法上添加@Bean注解,即由SpringIOC容器进行管理。
3)SpringCloudGateway的动态路由功能,即在yml文件中将原本路由的uri改成lb://服务提供者的微服务的名字;然后需要引入EureakaClient和Gateway等依赖即可实现Gateway的动态路由功能。
也就是说需要配置和使用Eureka,但是可以设置不把自身注册到Eureka服务中。
4)SpringCloudGateway的路由断言,路由断言分别有After、Before、Between、Cookie、Header、Host、Method、Query等。其中After、Before、Between都是跟时间有关的;Cookie、Header、Host都是在头文件Headers中携带的参数;Method是匹配指定的请求方法;Query是在Params中检查参数的合法性。
断言是在YML文件的spring.cloud.gateway.routes.predicates下进行配置的。
5)SpringCloudGateway的过滤器,在用户访问各个服务前,应在网关层统一做好鉴权、限流等工作。过滤器Filter的生命周期分为PRE和POST,即PRE是路由之前,POST是路由之后。它作用范围分为GatewayFilter和GlobalFilter,即GatewayFilter是网关路由器,是应用在单个路由或一个分组的路由上的,而GlobalFilter是全局路由器,会应用在所有路由上的。 - 1)内置过滤器,即在YML文件中,在filters下添加内置过滤器。
2)自定义网关过滤器,即需要创建一个配置类,类名必须叫做XxxGatewayFilterFactory,在该类上使用@Component注解;该类需要创建一个静态内部类Config,里面的属性为配置文件中配置的参数;必须继承AbstractGatewayFilterFactory;重写shortcutFieldOrder()方法,返回List参数列表为Config中属性集合;创建无参构造方法,方法体为super(Config.class);编写过滤逻辑 public GatewayFilter apply(Config config)。
3)自定义全局过滤器,当客户端第一次请求服务时,服务端对用户进行信息认证(登录);认证通过,将用户信息进行加密形成token,返回给客户端,作为登录凭证;以后每次请求,客户端都携带认证的token;服务端对token进行解密,判断是否有效,有效则继续允许访问服务,无效则不允许访问服务。
此时需要实现GlobalFilter, Ordered接口,一个是全局过滤器接口,一个是全局过滤器执行顺序的接口。
即在Ordered接口的实现类中返回一个数值,该值越小,当前过滤器的优先级越高。
GlobalFilter接口的实现类,对请求参数进行一个过滤操作。 - 1)配置跨域,即在yml文件配置允许跨域即可。
2)JsonWebToken,是一种用于双方之间传递安全信息的简洁的、URL安全的声明规范。适用于分布式的单点登录(SSO)。
客户端收到服务器返回的 JWT,会把数据保存到loadStorage。JWT签证默认的算法是 HMAC SHA256(HS256)。
3)用户登录并生成token返回的业务流程,登录成功时JWT的Token通过JWTUtils工具类生成,状态码为200,消息为成功,以及返回值的类型需要封装为Result实体类;登录失败时不生成token,状态码500,消息为用户名或密码错误,同时返回值的类型也是需要封装为Result实体类。此时该方法为Post请求,因为涉及帐号信息。
并且需要引入JWT和fastjson依赖。 - 使用gateway网关进行用户鉴权,在application.yml文件中配置跳过login登录的鉴权,即其他的url请求都要进行用户鉴权;需要引入JWT和fastjson的依赖;使用JWTUtils的生成token方法以及验证token是否有效的方法;在用户鉴权时,需要先获取跳过的路径,进行匹配,匹配成功则跳过鉴权进行下一步的业务操作;匹配失败,则说明该请求需要验证token,首先需要从request请求的请求头中获取token,如果token为空,则返回一个response对象,包含状态码和字符串消息;如果token不为空,则进行下一步验证,即使用JWTUtils的token验证方法,如果返回false,则表示token无效或者失效,则返回一个response对象,包含状态码和字符串消息;如果token不为空且token有效,则进行下一步的业务操作。
即用户鉴权,实际上就判断该请求是否需要跳过鉴权,以及token是否为空和token是否有效的操作。