文章目录
- 一、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 {@Beanpublic 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: 9527eureka:instance:# 注册名instance-id: cloud-gateway-gateway9527client:service-url:# Eureka server的地址#集群defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka#单机#defaultZone: http://localhost:7001/eureka/ spring:application:#设置应用名name: cloud-gatewaycloud: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: 9527eureka:instance:# 注册名instance-id: cloud-gateway-gateway9527client:service-url:# Eureka server的地址#集群defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka#单机#defaultZone: http://localhost:7001/eureka/ spring:application:#设置应用名name: cloud-gatewaycloud: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*/@Overridepublic List<String> shortcutFieldOrder() {return Arrays.asList("consoleLog");}/*** 执行过滤的逻辑* @param config* @return*/@Overridepublic GatewayFilter apply(Config config) {return ((exchange, chain) -> {if(config.consoleLog){log.info("********* consoleLog日志 开启 ********");}return chain.filter(exchange);});}/*** 过滤器使用的配置内容**/@Datapublic 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*/@Overridepublic 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*/@Overridepublic 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: trueallowedOriginPatterns: "*"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.创建JWTString 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: 6500eureka:instance:# 注册名instance-id: cloud-auth-user6500client: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.创建JWTString 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: 9527eureka:instance:# 注册名instance-id: cloud-gateway-gateway9527client:service-url:# Eureka server的地址#集群defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka#单机#defaultZone: http://localhost:7001/eureka/org:my:jwt:# 跳过认证路由skipAuthUrls:- /user/loginspring:application:#设置应用名name: cloud-gatewaycloud: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=trueglobalcors:cors-configurations:'[/**]':allowCredentials: trueallowedOriginPatterns: "*"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*/@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {// 获取请求url地址String path = exchange.getRequest().getURI().getPath();// 跳过不需要验证的路径if(skipAuthUrls!=null && isSKip(path)){return chain.filter(exchange);}// 1.从请求头中获取tokenString token = exchange.getRequest().getHeaders().getFirst("token");// 2.判断tokenif(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));}// 验证tokenboolean 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);}@Overridepublic 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是否有效的操作。