Zuul 1.x 是一个基于阻塞 IO 的 API Gateway 以及 Servlet;直到 2018 年 5 月,Zuul 2.x(基于Netty,也是非阻塞的,支持长连接)才发布,但 Spring Cloud 暂时还没有整合计划。Spring Cloud Gateway 比 Zuul 1.x 系列的性能和功能整体要好。
Spring Cloud Gateway 是 Spring 官方基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技术开的网关,旨在为微服务架构提供一种简单而有效的统一的 API 路由管理方式,统一访问接口。SpringCloud Gateway 作为 Spring Cloud 生态系中的网关,目标是替代 Netflflix ZUUL,其不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控/埋点,和限流等。它是基于Nttey的响应式开发模式。
上表为Spring Cloud Gateway与Zuul的性能对比,从结果可知,Spring Cloud Gateway的RPS是Zuul的1.6倍
创建工程导入依赖
在项目中添加新的模块 gateway_server ,并导入依赖
>
>org.springframework.cloud >
>spring-cloud-starter-gateway >
>
启动时和spring-boot-startet-web有冲突, 现在将父工程模块写的jar转移到,那个模块用拷贝的那个模块下。
配置启动类
@SpringBootApplication
public class GatewayServerApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayServerApplication.class, args);
}
}
编写配置文件
创建 application.yml 配置文件
server:
port: 8080
eureka:
client:
service-url:
defaultZone: http://localhost:9000/eureka/
instance:
prefer-ip-address: true
spring:
application:
name: gateway-notzull-server
cloud:
gateway:
routes:
- id: user-server
uri: http://127.0.0.1:9004
predicates:
- Path=/user/**
id:我们自定义的路由 ID,保持唯一
uri:目标服务地址
predicates:路由条件,Predicate 接受一个输入参数,返回一个布尔值结果。该接口包含多种默
认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非)。
filters:过滤规则,暂时没用。
上面这段配置的意思是,配置了一个 id 为 user-server的路由规则,当访问网关请求地址以
user开头时,会自动转发到地址: http://127.0.0.1:9002/ 。配置完成启动项目即可在浏览器
访问进行测试,当我们访问地址 http://localhost:8080/user/show?uid=2 时会展示页面展示如下:
原始页面
网关请求
路由规则
Spring Cloud Gateway 的功能很强大,前面我们只是使用了 predicates 进行了简单的条件匹配,其实 Spring Cloud Gataway 帮我们内置了很多 Predicates 功能。在 Spring Cloud Gateway 中 Spring 利用Predicate 的特性实现了各种路由匹配规则,有通过 Header、请求参数等不同的条件来进行作为条件匹配到对应的路由。
示例
#路由断言之后匹配
spring:
cloud:
gateway:
routes:
- id: after_route
uri: https://xxxx.com
#路由断言之前匹配
predicates:
- After=xxxxx
#路由断言之前匹配
- id: before_route
uri: https://xxxxxx.com
predicates:
- Before=xxxxxxx
#路由断言之间
- id: between_route
uri: https://xxxx.com
predicates:
- Between=xxxx,xxxx
#路由断言Cookie匹配,此predicate匹配给定名称(chocolate)和正则表达式(ch.p)
- id: cookie_route
uri: https://xxxx.com
predicates:
- Cookie=chocolate, ch.p
#路由断言Header匹配,header名称匹配X-Request-Id,且正则表达式匹配\d+
- id: header_route
uri: https://xxxx.com
predicates:
- Header=X-Request-Id, \d+
#路由断言匹配Host匹配,匹配下面Host主机列表,**代表可变参数
- id: host_route
uri: https://xxxx.com
predicates:
- Host=**.somehost.org,**.anotherhost.org
#路由断言Method匹配,匹配的是请求的HTTP方法
- id: method_route
uri: https://xxxx.com
predicates:
- Method=GET
#路由断言匹配,{segment}为可变参数
- id: host_route
uri: https://xxxx.com
predicates:
- Path=/foo/{segment},/bar/{segment}
#路由断言Query匹配,将请求的参数param(baz)进行匹配,也可以进行regexp正则表达式匹配 (参数包含 foo,并且foo的值匹配ba.)
- id: query_route
uri: https://xxxx.com
predicates:
- Query=baz 或 Query=foo,ba.
#路由断言RemoteAddr匹配,将匹配192.168.1.1~192.168.1.254之间的ip地址,其中24为子网掩码位数即255.255.255.0
- id: remoteaddr_route
uri: https://example.org
predicates:
- RemoteAddr=192.168.1.1/24
动态路由
和zuul网关类似,在SpringCloud GateWay中也支持动态路由:即自动的从注册中心中获取服务列表并访问。
添加注册中心依赖
在工程的pom文件中添加注册中心的客户端依赖(这里以Eureka为例)
>
>org.springframework.cloud >
>spring-cloud-starter-netflix-eureka-client >
>
配置动态路由
修改 application.yml 配置文件,添加eureka注册中心的相关配置,并修改访问映射的URL为服务名称
server:
port: 8080
eureka:
client:
service-url:
defaultZone: http://localhost:9000/eureka/
instance:
prefer-ip-address: true #显示浏览器中的状态栏显示ip
spring:
application:
name: gateway-notzull-server
cloud:
gateway:
routes:
- id: user-server
# uri: http://127.0.0.1:9004
uri: lb://user-consumer-fegin
predicates:
- Path=/user/**
重写转发路径
在SpringCloud Gateway中,路由转发是直接将匹配的路由path直接拼接到映射路径(URI)之后,那么在微服务开发中往往没有那么便利。这里就可以通过RewritePath机制来进行路径重写。
案例改造
修改 application.yml ,将匹配路径改为 /user-service/**
添加RewritePath重写转发路径
修改 application.yml ,添加重写规则
spring:
application:
name: gateway-notzull-server
cloud:
gateway:
routes:
- id: user-server
# uri: http://127.0.0.1:9004
uri: lb://user-consumer-fegin
predicates:
- Path=/user-server/**
filters:
- RewritePath=/user-server/(?.*), /$\{segment}
通过RewritePath配置重写转发的url,将/product-service/(?.*),重写为{segment},然后转发到订单微服务。比如在网页上请求http://localhost:8080/user-service/user,此时会将请求转发到http://127.0.0.1:9002/user/1( 值得注意的是在yml文档中 $ 要写成 $\ )
Spring Cloud Gateway除了具备请求路由功能之外,也支持对请求的过滤。通过Zuul网关类似,也是通过过滤器的形式来实现的。那么接下来我们一起来研究一下Gateway中的过滤器
过滤器基础
过滤器的生命周期
Spring Cloud Gateway 的 Filter 的生命周期不像 Zuul 的那么丰富,它只有两个:“pre” 和 “post”。
PRE: 这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择
请求的微服务、记录调试信息等。
POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的 HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等
(2) 过滤器类型
Spring Cloud Gateway 的 Filter 从作用范围可分为另外两种GatewayFilter 与 GlobalFilter。
GatewayFilter:应用到单个路由或者一个分组的路由上。
GlobalFilter:应用到所有的路由上。
局部过滤器
局部过滤器(GatewayFilter),是针对单个路由的过滤器。可以对访问的URL过滤,进行切面处理。在Spring Cloud Gateway中通过GatewayFilter的形式内置了很多不同类型的局部过滤器。这里简单将Spring Cloud Gateway内置的所有过滤器工厂整理成了一张表格,虽然不是很详细,但能作为速览使用。如下:
每个过滤器工厂都对应一个实现类,并且这些类的名称必须以 GatewayFilterFactory 结尾,这是
Spring Cloud Gateway的一个约定,例如 AddRequestHeader 对应的实现类为
AddRequestHeaderGatewayFilterFactory 。对于这些过滤器的使用方式可以参考官方文档
全局过滤器
全局过滤器(GlobalFilter)作用于所有路由,Spring Cloud Gateway 定义了Global Filter接口,用户可以自定义实现自己的Global Filter。通过全局过滤器可以实现对权限的统一校验,安全性验证等功能,并且全局过滤器也是程序员使用比较多的过滤器。
Spring Cloud Gateway内部也是通过一系列的内置全局过滤器对整个路由转发进行处理如下:
统一鉴权
内置的过滤器已经可以完成大部分的功能,但是对于企业开发的一些业务功能处理,还是需要我们自己编写过滤器来实现的,那么我们一起通过代码的形式自定义一个过滤器,去完成统一的权限校验。
鉴权逻辑
开发中的鉴权逻辑:
当客户端第一次请求服务时,服务端对用户进行信息认证(登录)认证通过,将用户信息进行加密形成token,返回给客户端,作为登录凭证 以后每次请求,客户端都携带认证的token 服务端对token进行解密,判断是否有效。
如上图,对于验证用户是否已经登录鉴权的过程可以在网关层统一检验。检验的标准就是请求中是否携带token凭证以及token的正确性。
代码实现
下面的我们自定义一个GlobalFilter,去校验所有请求的请求参数中是否包含“token”,如何不包含请求参数“token”则不转发路由,否则执行正常的逻辑。
@Component
public class LoginFilter implements GlobalFilter , Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = exchange.getRequest().getQueryParams().getFirst("token");
if(token==null || "".equals(token)){
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}
自定义全局过滤器需要实现GlobalFilter和Ordered接口。
在fifilter方法中完成过滤器的逻辑判断处理
在getOrder方法指定此过滤器的优先级,返回值越大级别越低
ServerWebExchange 就相当于当前请求和响应的上下文,存放着重要的请求-响应属性、请求实
例和响应实例等等。一个请求中的request,response都可以通过 ServerWebExchange 获取
调用 chain.filter 继续向下游执行
基于Filter的限流
SpringCloudGateway官方就提供了基于令牌桶的限流支持。基于其内置的过滤器工厂
RequestRateLimiterGatewayFilterFactory 实现。在过滤器工厂中是通过Redis和lua脚本结合的方式进行流量控制。
环境搭建
导入redis的依赖
首先在工程的pom文件中引入gateway的起步依赖和redis的reactive依赖,代码如下:
<!--监控依赖-->
>
>org.springframework.boot >
>spring-boot-starter-actuator >
>
<!--redis的依赖-->
>
>org.springframework.boot >
>spring-boot-starter-data-redis-reactive >
>
准备redis
修改application.yml配置文件
在application.yml配置文件中加入限流的配置,代码如下:
server:
port: 8080
eureka:
client:
service-url:
defaultZone: http://localhost:9000/eureka/
instance:
prefer-ip-address: true #显示浏览器中的状态栏显示ip
spring:
application:
name: gateway-notzull-server
cloud:
gateway:
routes:
- id: user-server
# uri: http://127.0.0.1:9004
uri: lb://user-consumer-fegin
predicates:
- Path=/user-server/**
filters:
- RewritePath=/user-server/(?.*), /$\{segment}
- name: RequestRateLimiter
args:
key-resolver: '#{@pathKeyResolver}' # 使用SpEL从容器中获取对象
redis-rate-limiter.replenishRate: 1 # 令牌桶每秒填充平均速率
redis-rate-limiter.burstCapacity: 3 # 令牌桶的总容量
redis:
host: 127.0.0.1
port: 6379
database: 0
在 application.yml 中添加了redis的信息,并配置了RequestRateLimiter的限流过滤器:
burstCapacity,令牌桶总容量。
replenishRate,令牌桶每秒填充平均速率。
key-resolver,用于限流的键的解析器的 Bean 对象的名字。它使用 SpEL 表达式根据#
{@beanName}从 Spring 容器中获取 Bean 对象。
配置KeyResolver
为了达到不同的限流效果和规则,可以通过实现 KeyResolver 接口,定义不同请求类型的限流键。
@Configuration
public class LimitFilter {
@Bean
public KeyResolver pathKeyResolver(){
return new KeyResolver() {
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
return Mono.just(exchange.getRequest().getPath().toString());
}
};
}
}
根据用户限流
@Bean
public KeyResolver userKeyResolver(){
return new KeyResolver() {
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
return Mono.just(exchange.getRequest().getQueryParams().getFirst("uid"));
}
};
}
Yml配置
spring:
application:
name: gateway-notzull-server
cloud:
gateway:
routes:
- id: user-server
# uri: http://127.0.0.1:9004
uri: lb://user-consumer-fegin
predicates:
- Path=/user-server/**
filters:
- RewritePath=/user-server/(?>.*), /$\{segment}
- name: RequestRateLimiter
args:
# key-resolver: '#{@pathKeyResolver}' # 使用SpEL从容器中获取对象
key-resolver: '#{@userKeyResolver}' # 使用SpEL从容器中获取对象
redis-rate-limiter.replenishRate: 1 # 令牌桶每秒填充平均速率
redis-rate-limiter.burstCapacity: 3 # 令牌桶的总容量
用户登录
项目中有2个重要角色,分别为管理员和用户,我们将实现购物下单和支付,用户如果没登录是没法下单和支付的,所以我们这里需要实现一个登录功能。
网关关联
在我们平时工作中,并不会直接将微服务暴露出去,一般都会使用网关对接,实现对微服务的一个保护作用,如上图,当用户访问/api/user/的时候我们再根据用户请求调用用户微服务的指定方法。当然,除了/api/user/还有/api/address/、/api/areas/、/api/cities/、/api/provinces/都需要由user微服务处理,修改网关工程gateway-web的application.yml配置文件,如下代码:
spring:
cloud:
gateway:
globalcors:
corsConfigurations:
'[/**]': # 匹配所有请求
allowedOrigins: "*" #跨域处理 允许所有的域
allowedMethods: # 支持的方法
- GET
- POST
- PUT
- DELETE
routes:
- id: changgou_goods_route
uri: lb://goods
predicates:
- Path=/api/goods/**
filters:
- name: RequestRateLimiter
args:
key-resolver: "#{@ipKeyResolver}"
redis-rate-limiter.replenishRate: 1
redis-rate-limiter.burstCapacity: 1
#用户微服务
- id: changgou_user_route
uri: lb://user
predicates:
- Path=/api/user/**,/api/address/**
application:
name: gateway-web
#Redis配置
redis:
host: 192.168.211.132
port: 6379
server:
port: 8001
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:7001/eureka
instance:
prefer-ip-address: true
management:
endpoint:
gateway:
enabled: true
web:
exposure:
include: true
使用Postman访问
http://localhost:8001/api/user/login?username=username&password=password
需求分析
我们之前已经搭建过了网关,使用网关在网关系统中比较适合进行权限校验。
那么我们可以采用JWT的方式来实现鉴权校验。
什么是JWT
JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。
JWT的构成
一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。
头部(Header)
头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个JSON对象。
{"typ":"JWT","alg":"HS256"}
在头部指明了签名算法是HS256算法。 我们进行BASE64编码http://base64.xpcha.com/,编码后的字符串如下:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
小知识:Base64是一种基于64个可打印字符来表示二进制数据的表示方法。由于2的6次方等于64,所以每6个比特为一个单元,对应某个可打印字符。三个字节有24个比特,对应于4个Base64单元,即3个字节需要用4个可打印字符来表示。JDK 中提供了非常方便的 BASE64Encoder 和 BASE64Decoder,用它们可以非常方便的完成基于 BASE64 的编码和解码
载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分
(1)标准中注册的声明(建议但不强制使用)
iss: jwt签发者
sub: jwt所面向的用户
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
(2)公共的声明
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
(3)私有的声明
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
这个指的就是自定义的claim。比如下面面结构举例中的admin和name都属于自定的claim。这些claim跟JWT标准规定的claim区别在于:JWT规定的claim,JWT的接收方在拿到JWT之后,都知道怎么对这些标准的claim进行验证(还不知道是否能够验证);而private claims不会验证,除非明确告诉接收方要对这些claim进行验证以及规则才行。
定义一个payload:
{“sub”:“1234567890”,“name”:“John Doe”,“admin”:true}
然后将其进行base64加密,得到Jwt的第二部分。
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
header (base64后的)
payload (base64后的)
secret
这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
将这三部分用.连接成一个完整的字符串,构成了最终的jwt:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
JJWT是一个提供端到端的JWT创建和验证的Java库。永远免费和开源(Apache License,版本2.0),JJWT很容易使用和理解。它被设计成一个以建筑为中心的流畅界面,隐藏了它的大部分复杂性。
创建TOKEN
依赖引入
在common-api项目中的pom.xml中添加依赖:
>
>io.jsonwebtoken >
>jjwt >
>0.9.0 >
>
创建测试
在common-api的/test/java下创建测试类,并设置测试方法
public class JwtTest {
/****
* 创建Jwt令牌
*/
@Test
public void testCreateJwt(){
JwtBuilder builder= Jwts.builder()
.setId("888") //设置唯一编号
.setSubject("小白") //设置主题 可以是JSON数据
.setIssuedAt(new Date()) //设置签发日期
.signWith(SignatureAlgorithm.HS256,"xzzb");//设置签名 使用HS256算法,并设置SecretKey(字符串)
//构建 并返回一个字符串
System.out.println( builder.compact() );
}
}
运行打印结果:
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NjIwNjIyODd9.RBLpZ79USMplQyfJCZFD2muHV_KLks7M1ZsjTu6Aez4
再次运行,会发现每次运行的结果是不一样的,因为我们的载荷中包含了时间。
我们刚才已经创建了token ,在web应用中这个操作是由服务端进行然后发给客户端,客户端在下次向服务端发送请求时需要携带这个token(这就好像是拿着一张门票一样),那服务端接到这个token 应该解析出token中的信息(例如用户id),根据这些信息查询数据库返回相应的结果。
/***
* 解析Jwt令牌数据
*/@Test
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMTAiLCJpYXQiOjE1OTIyNzcxNTAsInN1YiI6IntpZDoxLG5hbWU6J2FkbWluJ30ifQ.2dhrzJFH2hSeDVgTSqUWiC6V-riUBtqTz8LzVFL-gFA
public void testParseJwt(){
String compactJwt="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NjIwNjIyODd9.RBLpZ79USMplQyfJCZFD2muHV_KLks7M1ZsjTu6Aez4";
Claims claims = Jwts.parser().
setSigningKey("xzzb").
parseClaimsJws(compactJwt).
getBody();
System.out.println(claims);
}
运行打印效果:
{jti=888, sub=小白, iat=1562062287}
试着将token或签名秘钥篡改一下,会发现运行时就会报错,所以解析token也就是验证token.
设置过期时间
有很多时候,我们并不希望签发的token是永久生效的,所以我们可以为token添加一个过期时间。
解释:
.setExpiration(date)//用于设置过期时间 ,参数为Date类型数据
当前时间超过过期时间,则会报错
自定义claims
我们刚才的例子只是存储了id和subject两个信息,如果你想存储更多的信息(例如角色)可以定义自定义claims。
创建测试类,并设置测试方法:
创建token:
JwtBuilder jwtBuilder = Jwts.builder();
jwtBuilder.setId("888");
jwtBuilder.setSubject("测试JWT");
jwtBuilder.setIssuedAt(new Date());
// jwtBuilder.setExpiration(new Date());
jwtBuilder.signWith(SignatureAlgorithm.HS256,"xzzb");
Map<String,Object> userInfo = new HashMap<>();
userInfo.put("name","张三");
userInfo.put("age",20);
userInfo.put("address","徐州解放路");
jwtBuilder.addClaims(userInfo);
String key = jwtBuilder.compact();
System.out.println(key);
// String key = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLmtYvor5VKV1QiLCJpYXQiOjE1OTIyMDQ0MTksImV4cCI6MTU5MjIwNDQxOX0.yW-ZtZ7I6czQNMaP9Khf7oVU5M6HidSadxZ-z9DQZUI";
Claims claims =Jwts.parser().setSigningKey("xzzb").parseClaimsJws(key).getBody();
System.out.println(claims);
鉴权处理
思路分析
1.用户通过访问微服务网关调用微服务,同时携带头文件信息
2.在微服务网关这里进行拦截,拦截后获取用户要访问的路径
3.识别用户访问的路径是否需要登录,如果需要,识别用户的身份是否能访问该路径[这里可以基于数据库设计一套权限]
4.如果需要权限访问,用户已经登录,则放行
5.如果需要权限访问,且用户未登录,则提示用户需要登录
6.用户通过网关访问用户微服务,进行登录验证
7.验证通过后,用户微服务会颁发一个令牌给网关,网关会将用户信息封装到头文件中,并响应用户
8.用户下次访问,携带头文件中的令牌信息即可识别是否登录
用户登录签发TOKEN
生成令牌工具类
common-api下JwtUtil,主要辅助生成Jwt令牌信息,代码如下:
public class JwtUtil {
//有效期为
public static final Long JWT_TTL = 3600000L;// 60 * 60 *1000 一个小时
//Jwt令牌信息
public static final String JWT_KEY = "xzzb";
public static String createJWT(String id, String subject, Long ttlMillis) {
//指定算法
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
//当前系统时间
long nowMillis = System.currentTimeMillis();
//令牌签发时间
Date now = new Date(nowMillis);
//如果令牌有效期为null,则默认设置有效期1小时
if(ttlMillis==null){
ttlMillis=JwtUtil.JWT_TTL;
}
//令牌过期时间设置
long expMillis = nowMillis + ttlMillis;
Date expDate = new Date(expMillis);
//生成秘钥
SecretKey secretKey = generalKey();
//封装Jwt令牌信息
JwtBuilder builder = Jwts.builder()
.setId(id) //唯一的ID
.setSubject(subject) // 主题 可以是JSON数据
.setIssuer("admin") // 签发者
.setIssuedAt(now) // 签发时间
.signWith(signatureAlgorithm, secretKey) // 签名算法以及密匙
.setExpiration(expDate); // 设置过期时间
return builder.compact();
}
/**
* 生成加密 secretKey
* @return
*/
public static SecretKey generalKey() {
byte[] encodedKey = Base64.getEncoder().encode(JwtUtil.JWT_KEY.getBytes());
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return key;
}
/**
* 解析令牌数据
* @param jwt
* @return
* @throws Exception
*/
public static Claims parseJWT(String jwt) throws Exception {
SecretKey secretKey = generalKey();
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();
}
}
用户登录成功 则 签发TOKEN,修改登录的方法:
@RequestMapping(value = "/login")
public Dto login(String username,String password){
//查询用户信息
User user = userService.findById(username);
if(user!=null){
//设置令牌信息
Map<String,Object> info = new HashMap<String,Object>();
info.put("role","USER");
info.put("success","SUCCESS");
info.put("username",username);
//生成令牌
String jwt = JwtUtil.createJWT(UUID.randomUUID().toString(), JSON.toJSONString(info),null);
return DtoUtil.returnSuccess(true,StatusCode.OK,"登录成功!",jwt);
}
return DtoUtil.returnSuccess(false,StatusCode.LOGINERROR,"账号或者密码错误!");
}
网关过滤器拦截请求处理
gateway-web
AuthorizeFilter代码如下:
@Component
public class AuthorizeFilter implements GlobalFilter, Ordered {
//令牌头名字
private static final String AUTHORIZE_TOKEN = "Authorization";
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//获取Request、Response对象
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
//获取请求的URI
String path = request.getURI().getPath();
//如果是登录、goods等开放的微服务[这里的goods部分开放],则直接放行,这里不做完整演示,完整演示需要设计一套权限系统
if (path.startsWith("/api/user/login") || path.startsWith("/api/brand/search/")) {
//放行
Mono<Void> filter = chain.filter(exchange);
return filter;
}
//获取头文件中的令牌信息
String tokent = request.getHeaders().getFirst(AUTHORIZE_TOKEN);
//如果头文件中没有,则从请求参数中获取
if (StringUtils.isEmpty(tokent)) {
tokent = request.getQueryParams().getFirst(AUTHORIZE_TOKEN);
}
//如果为空,则输出错误代码
if (StringUtils.isEmpty(tokent)) {
//设置方法不允许被访问,405错误代码
response.setStatusCode(HttpStatus.METHOD_NOT_ALLOWED);
return response.setComplete();
}
//解析令牌数据
try {
Claims claims = JwtUtil.parseJWT(tokent);
} catch (Exception e) {
e.printStackTrace();
//解析失败,响应401错误
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
//放行
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}
配置过滤规则
修改网关系统的yml文件:
测试访问
http://localhost:8080/api/user/login,效果如下:
没有带token记录的
带token信息的