Spring Cloud Gateway 用"Netty + Webflux"实现,不需要导入Web依赖。
在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
在子模块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
再添加一个服务提供者,用以实现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,估计是轮询的策略。
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+
Host路由断言
1)Host 路由断言 Factory包括一个参数:host name列表。使用Ant路径匹配规则, .作为分隔符。访问的主机匹配http或者https, baidu.com 默认80端口, 就可以通过路由。 多个参数使用,号隔开。
predicates:
- Path=/payment/*
- Host=127.0.0.1,localhost
Method路由断言
1)即Request请求的方式,例如GET或POST请求,不匹配则无法进行请求
predicates:
- Path=/payment/*
- Method=GET,POST
2)可以使用postman,也可以使用浏览器直接访问,因为不需要加任何参数
Query路由断言
1)请求断言也是在业务中经常使用的,它会从ServerHttpRequest中的Parameters列表中查询指定的属性,例如验证参数的类型等
predicates:
- Path=/payment/*
- Query=age,\d+
https://docs.spring.io/spring-cloud-gateway/docs/4.0.4/reference/html/#gatewayfilter-factories
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
2)在浏览器测试:http://localhost:9527/payment/lb
在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)
在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;
}
}
https://docs.spring.io/spring-cloud-gateway/docs/4.0.4/reference/html/#global-filters
跨域
即当一个请求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
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字段里面。
在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进行测试
即在网关过滤器中加入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时(即未登录时)