微服务网关

1 微服务网关的概述

不同的微服务一般会有不同的网络地址,而外部客户端可能需要调用多个服务的接口才能完成一个业务需求,如果让客户端直接与各个微服务通信,会有以下的问题:

  • 客户端会多次请求不同的微服务,增加了客户端的复杂性
  • 存在跨域请求,在一定场景下处理相对复杂
  • 认证复杂,每个服务都需要独立认证
  • 难以重构,随着项目的迭代,可能需要重新划分微服务。例如,可能将多个服务合并成一个或者将一个服务拆分成多个。如果客户端直接与微服务通信,那么重构将会很难实施
  • 某些微服务可能使用了防火墙 / 浏览器不友好的协议,直接访问会有一定的困难

以上这些问题可以借助网关解决。

网关是介于客户端和服务器端之间的中间层,所有的外部请求都会先经过 网关这一层。也就是说,API 的实现方面更多的考虑业务逻辑,而安全、性能、监控可以交由 网关来做,这样既提高业务灵活性又不缺安全性,典型的架构图如图所示:
微服务网关_第1张图片优点如下:

  • 安全 ,只有网关系统对外进行暴露,微服务可以隐藏在内网,通过防火墙保护。
  • 易于监控。可以在网关收集监控数据并将其推送到外部系统进行分析。
  • 易于认证。可以在网关上进行认证,然后再将请求转发到后端的微服务,而无须在每个微服务中进行认证。
  • 减少了客户端与各个微服务之间的交互次数
  • 易于统一授权。

总结:微服务网关就是一个系统,通过暴露该微服务网关系统,方便我们进行相关的鉴权,安全控制,日志统一处理,易于监控的相关功能。

微服务网关技术

实现微服务网关的技术有很多,

  • nginx Nginx (tengine x) 是一个高性能的HTTP和反向代理web服务器,同时也提供了IMAP/POP3/SMTP服务
  • zuul ,Zuul 是 Netflix 出品的一个基于 JVM 路由和服务端的负载均衡器。
  • spring-cloud-gateway, 是spring 出品的 基于spring 的网关项目,集成断路器,路径重写,性能比Zuul好。

我们使用gateway这个网关技术,无缝衔接到基于spring cloud的微服务开发中来。

gateway官网:

https://spring.io/projects/spring-cloud-gateway

2、网关系统使用

2.3 跨域配置

有时候,我们需要对所有微服务跨域请求进行处理,则可以在gateway中进行跨域支持。修改application.yml,添加如下代码:

spring:
  cloud:
    gateway:
      globalcors:
        cors-configurations:
          '[/**]': # 匹配所有请求
              allowedOrigins: "*" #跨域处理 允许所有的域
              allowedMethods: # 支持的方法
                - GET
                - POST
                - PUT
                - DELETE
  application:
    name: gateway-web
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

跨域的说明:

哪些场景是跨域:不同的系统进行AJAX的请求的时候属于跨域的。 跨域的请求一般是不被允许的。
1.www.jd.com---->www.taobao.com 跨域
2.localhost:8001 --->localhost:8002 跨域
3.www.jd.com:80--->www.jd.com:81 跨域
4.https --->http 跨域。

协议相同、域名相同、端口相同, 就不是跨域。

解决跨域的问题的解决方案:
+ CORS的协议 (高版本的浏览器至少 IE10  google)
	+ 在服务端进行注解配置(相当配置了CORS的配置项)springmvc的注解 @CrossOrigin
	+ 通过在网关(spring cloud gateway)进行统一的配置  
	+ 通过在网关(nginx里面进行配置CORS)
+ JSONP的方式(了解) 利用JS的漏洞的实现跨域,而且只支持GET请求 不支持其他的请求。

CORS “跨域资源共享”(Cross-origin resource sharing)

@CrossOrigin

微服务网关_第2张图片微服务网关_第3张图片

2.4 网关过滤配置

微服务网关_第4张图片
路由过滤器允许以某种方式修改传入的HTTP请求或传出的HTTP响应。 路径过滤器的范围限定为特定路径。 Spring Cloud Gateway包含许多内置的GatewayFilter工厂。如上图,根据请求路径路由到不同微服务去,这块可以使用Gateway的路由过滤功能实现。

过滤器 有 20 多个 实现 类, 包括 头部 过滤器、 路径 类 过滤器、 Hystrix 过滤器 和 变更 请求 URL 的 过滤器, 还有 参数 和 状态 码 等 其他 类型 的 过滤器。

内置的过滤器工厂有22个实现类,包括 头部过滤器、路径过滤器、Hystrix 过滤器 、请求URL 变更过滤器,还有参数和状态码等其他类型的过滤器。根据过滤器工厂的用途来划分,可以分为以下几种:Header、Parameter、Path、Body、Status、Session、Redirect、Retry、RateLimiter和Hystrix。

1.路由 转发的规则
2.断言 转发请求的条件
3.过滤 转发请求额外处理

2.4.1 Host 路由

比如用户请求cloud.123.com的时候,可以将请求路由给http://localhost:18081服务处理,如下配置:

spring:
  application:
    name: gateway-web
  cloud:
    gateway:
      globalcors:
        cors-configurations:
          '[/**]': # 匹配所有请求
            allowedOrigins: "*" #跨域处理 允许所有的域
            allowedMethods: # 支持的方法
              - GET
              - POST
              - PUT
              - DELETE
      routes:
        - id: changgou_goods_route
          uri: http://localhost:18081
          predicates:
            - Host=cloud.123.com** #请求是cloud.123.com的请求都转发到http://localhost:18081处理

注意:此时要想让cloud.123.com访问本地计算机,要配置C:\Windows\System32\drivers\etc\hosts文件,映射配置如下:

127.0.0.1 cloud.123.com

域名访问原理:
微服务网关_第5张图片

utools的使用

2.4.2 路径匹配过滤配置

我们还可以根据请求路径实现对应的路由过滤操作,例如请求中以/brand/路径开始的请求,都直接交给http://localhost:180801服务处理,如下配置:

spring:
  application:
    name: gateway-web
  cloud:
    gateway:
      globalcors:
        cors-configurations:
          '[/**]': # 匹配所有请求
            allowedOrigins: "*" #跨域处理 允许所有的域
            allowedMethods: # 支持的方法
              - GET
              - POST`在这里插入代码片`
              - PUT
              - DELETE
      routes:
        - id: changgou_goods_route
          uri: http://localhost:18081
          predicates:
            - Path=/brand**

2.4.3 PrefixPath 过滤配置

用户每次请求路径的时候,我们可以给真实请求加一个统一前缀,例如用户请求http://localhost:8001的时候我们让它请求真实地址http://localhost:8001/brand,如下配置:

      routes:
            - id: changgou_goods_route
              uri: http://localhost:18081
              predicates:
              #- Host=cloud.123.com**
              - Path=/**
              filters:
              - PrefixPath=/brand #用户所有请求都加一个前缀

2.4.4 StripPrefix 过滤配置

很多时候也会有这么一种请求,用户请求路径是/api/brand,而真实路径是/brand,这时候我们需要去掉/api才是真实路径,此时可以使用SttripPrefix功能来实现路径的过滤操作,如下配置:

      routes:
            - id: changgou_goods_route
              uri: http://localhost:18081
              predicates:
              #- Host=cloud.123.com**
              - Path=/**
              filters:
              #- PrefixPath=/brand
              - StripPrefix=1

2.4.5 LoadBalancerClient 路由过滤器(客户端负载均衡)

上面的路由配置每次都会将请求给指定的URL处理,但如果在以后生产环境,并发量较大的时候,我们需要根据服务的名称判断来做负载均衡操作,可以使用LoadBalancerClientFilter来实现负载均衡调用。LoadBalancerClientFilter会作用在url以lb开头的路由,然后利用loadBalancer来获取服务实例,构造目标requestUrl,设置到GATEWAY_REQUEST_URL_ATTR属性中,供NettyRoutingFilter使用。

修改application.yml配置文件,代码如下:

      routes:
            - id: changgou_goods_route
              #uri: http://localhost:18081
              uri: lb://goods
              predicates:
              #- Host=cloud.itheima.com**
              - Path=/**
              filters:
              #- PrefixPath=/brand
              - StripPrefix=1

2.5 网关限流

网关可以做很多的事情,比如,限流,当我们的系统 被频繁的请求的时候,就有可能 将系统压垮,所以 为了解决这个问题,需要在每一个微服务中做限流操作,但是如果有了网关,那么就可以在网关系统做限流,因为所有的请求都需要先通过网关系统才能路由到微服务中。

微服务网关_第6张图片2.5.2 令牌桶算法

令牌桶算法是比较常见的限流算法之一,大概描述如下:

1)所有的请求在处理之前都需要拿到一个可用的令牌才会被处理;

2)根据限流大小,设置按照一定的速率往桶里添加令牌;

3)桶设置最大的放置令牌限制,当桶满时、新添加的令牌就被丢弃或者拒绝;

4)请求达到后首先要获取令牌桶中的令牌,拿着令牌才可以进行其他的业务逻辑,处理完业务逻辑之后,将令牌直接删除;

5)令牌桶有最低限额,当桶中的令牌达到最低限额的时候,请求处理完之后将不会删除令牌,以此保证足够的限流

如下图:
微服务网关_第7张图片这个算法的实现,有很多技术,Guaua是其中之一,redis客户端也有其实现。

2.5.3 使用令牌桶进行请求次数限流

spring cloud gateway 默认使用redis的RateLimter限流算法来实现。所以我们要使用首先需要引入redis的依赖

(1)引入redis依赖

在changgou-gateway的pom.xml中引入redis的依赖


<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-data-redis-reactiveartifactId>
    <version>2.1.3.RELEASEversion>
dependency>

(2)定义KeyResolver

在Applicatioin引导类中添加如下代码,KeyResolver用于计算某一个类型的限流的KEY也就是说,可以通过KeyResolver来指定限流的Key。

我们可以根据IP来限流,比如每个IP每秒钟只能请求一次,在GatewayWebApplication定义key的获取,获取客户端IP,将IP作为key,如下代码:

/***
 * IP限流
 * @return
 */
@Bean(name="ipKeyResolver")
public KeyResolver userKeyResolver() {
    return new KeyResolver() {
        @Override
        public Mono<String> resolve(ServerWebExchange exchange) {
            //获取远程客户端IP
            String hostName = exchange.getRequest().getRemoteAddress().getAddress().getHostAddress();
            System.out.println("hostName:"+hostName);
            return Mono.just(hostName);
        }
    };
}

4 JWT

在网关系统中比较适合进行权限校验。
微服务网关_第8张图片
我们可以采用JWT的方式来实现鉴权校验

4.1 什么是JWT

JSON Web Token (JWT) rfc

一起重新全面认识JWT-Json Web Token
JSON Web Token(JWT)是一个非常轻巧的规范。 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准(RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

JWS和JWE是JWT的两种实现

JWS : Json Web Signature

JWE : JSON Web Encryption

4.2 JWT的构成

一个JWT实际上就是一个字符串,JWT通常由三部分组成,按照顺序: 头信息(header), 载荷(payload)和 签名(signature)。

头信息(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 的编码和解码

载荷(playload)

载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分

(1)标准中注册的声明(建议但不强制使用)

iss (issuer): jwt签发者
sub (subject): 主题  可以是JSON数据
aud (audience): 受众,接收jwt的一方
exp (expiration time): jwt的过期时间,这个过期时间必须要大于签发时间
nbf (Not Before): 生效时间。定义在什么时间之前,该jwt都是不可用的.
iat (Issued At): jwt的签发时间
jti (JWT ID): 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

签证(signature)

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了。

用户登录的解决方案:
微服务网关_第9张图片

4.3 JJWT的介绍和使用

JJWT是一个提供端到端的JWT创建和验证的Java库。永远免费和开源(Apache License,版本2.0),JJWT很容易使用和理解。它被设计成一个以建筑为中心的流畅界面,隐藏了它的大部分复杂性。

官方文档:

https://github.com/jwtk/jjwt

4.3.1 创建TOKEN

(1)依赖引入


<dependency>
    <groupId>io.jsonwebtokengroupId>
    <artifactId>jjwtartifactId>
    <version>0.9.0version>
dependency>

(2)创建测试

public class JwtTest {

    /****
     * 创建Jwt令牌
     */
    @Test
    public void testCreateJwt(){
        JwtBuilder builder= Jwts.builder()
                .setId("888")             //设置唯一编号
                .setSubject("小白")       //设置主题  可以是JSON数据
                .setIssuedAt(new Date())  //设置签发日期
                .signWith(SignatureAlgorithm.HS256,"itmean");//设置签名 使用HS256算法,并设置SecretKey(字符串)
        //构建 并返回一个字符串
        System.out.println( builder.compact() );
    }
}

4.3.2 TOKEN解析

我们刚才已经创建了token ,在web应用中这个操作是由服务端进行然后发给客户端,客户端在下次向服务端发送请求时需要携带这个token(这就好像是拿着一张门票一样),那服务端接到这个token 应该解析出token中的信息(例如用户id),根据这些信息查询数据库返回相应的结果。

 /***
 * 解析Jwt令牌数据
 */
@Test
public void testParseJwt(){
    String compactJwt="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NjIwNjIyODd9.RBLpZ79USMplQyfJCZFD2muHV_KLks7M1ZsjTu6Aez4";
    Claims claims = Jwts.parser().
            setSigningKey("itmean").
            parseClaimsJws(compactJwt).
            getBody();
    System.out.println(claims);
}

Jwts 类

用于创建JWT接口实例的工厂类

Jwts 的常用方法

//返回一个新的JwtBuilder实例,该实例可以配置,然后用于创建JWT紧凑序列化字符串。
1public static JwtBuilder builder()

///返回一个用作JWT主体的新的Claims实例
2public static Claims claims()
//返回一个由指定的名称/值对填充的新的Claims实例。
public static Claims claims(Map<String, Object> claims)

//返回一个新的JwtParser实例,该实例可以配置,然后用于解析JWT字符串
3public static JwtParser parser()

Claims 接口

A JWT Claims set .
JWT Claims Set表示一个JSON对象,其成员是JWT传达的声明。

这最终是一个JSON映射,任何值都可以添加到其中,但是为了方便起见,JWT标准名称作为类型安全的getter和setter提供。
因为这个接口扩展了Map,如果你想添加自己的属性,你只需要使用Map方法,例如:

claims.put("someKey", "someValue");

最简单的创建Claims实例, 通过调用 JWTs.claims() 工厂方法

JwtBuilder 接口

用于构建 JWT 的构建器

JwtBuilder 的常用方法

//设置JWT 声明 jti (JWT ID)的值,null 值将从Claim中删除该属性
//大小写敏感
1JwtBuilder setId(String jti)
例如:setId(UUID.randomUUID().toString())

//设置JWT Claims sub(subject)值 null 值将从Claim中删除该属性

2JwtBuilder setSubject(String sub)
例如:String jwt = Jwts.builder().setSubject("Me").compact();

//将JWT有效负载设置为JSON Claims实例。如果您不希望JWT主体是JSON,而是希望
//它是一个纯文本字符串,那么可以使用setPayload(string)方法。
//将JWT有效负载设置为由指定的名称/值对填充的JSON Claims实例
3JwtBuilder setClaims(Claims claims);
//payload* and claims*属性是互斥的——只能使用两者中的一个
JwtBuilder setClaims(Map<String, Object> claims);

//将JWT的有效负载设置为一个纯文本(非json)字符串。
4JwtBuilder setPayload(String payload)

//设置JWT Claims iat(issued at)的值
5JwtBuilder setIssuedAt(Date iat)
例如:String jwt = Jwts.builder().setIssuedAt(new Date()).compact();

//使用指定的算法和指定的密钥对构造的JWT进行签名,生成JWS
//首先将string参数以base64解码为字节数组
6JwtBuilder signWith(SignatureAlgorithm alg, String base64EncodedSecretKey)
JwtBuilder signWith(SignatureAlgorithm alg, byte[] secretKey)
//key:用于对JWT进行数字签名的特定于算法的签名密钥
JwtBuilder signWith(SignatureAlgorithm alg, Key key)

//实际上构建JWT并根据JWT紧凑序列化规则将其序列化为一个紧凑的、url安全的字符串
7String compact()

//将所有给定的名称/值对添加到有效负载中的the JSON Claims中
//自定义声明
8JwtBuilder addClaims(Map<String, Object> claims)


JwtParser

读取JWT字符串,转换为表示扩展的JWT的JWT对象

JwtParser 常用方法

//设置用于验证任何发现的JWS数字签名的签名密钥。
//参数首先被base64解码为字节数组
//注意,该密钥必须是在JWT报头(作为alg报头参数)中找到的签名算法的有效密钥
1JwtParser setSigningKey(String base64EncodedKeyBytes)
JwtParser setSigningKey(byte[] key)

//基于构建器的当前配置状态解析指定的紧凑序列化JWS字符串,并返回结果Claims JWS实例。
2Jws<Claims> parseClaimsJws(String claimsJws)

Jws<Claims>:
public B getBody()

UUID

( universally unique identifier)
public final class UUID implements java.io.Serializable, Comparable
表示不可变的全局唯一标识符(UUID)的类。UUID表示一个128位的值。

全局标识符存在不同的变体。这个类的方法用于操作Leach-Salz变体,尽管构造函数允许创建UUID的任何变体(如下所述)。
变体2 (Leach-Salz) UUID的布局如下:

The most significant long consists of the following unsigned fields:
   0xFFFFFFFF00000000 time_low
   0x00000000FFFF0000 time_mid
   0x000000000000F000 version
   0x0000000000000FFF time_hi
   
The least significant long consists of the following unsigned fields:
   0xC000000000000000 variant
   0x3FFF000000000000 clock_seq
   0x0000FFFFFFFFFFFF node

version字段:描述了这个UUID的类型。uuid有四种不同的基本类型:基于时间的、DCE安全的、基于名称的和随机生成的uuid。这些类型的版本值分别为1、2、3和4

variant字段:标识UUID的布局

UUID 常用方法

//静态工厂来生成类型4(伪随机生成的(pseudo randomly generated))UUID
//这个UUID是使用密码学强的伪随机数生成器生成的。
public static UUID randomUUID()

SignatureAlgorithm

JSON Web算法规范中定义的标准 JWT 签名算法名称的类型安全表示

4.3.3 自定义claims

我们刚才的例子只是存储了id和subject两个信息,如果你想存储更多的信息(例如角色)可以定义自定义claims。

创建token:

4.4 鉴权处理

微服务网关_第10张图片4.5.2用户登录签发TOKEN

(1)生成令牌工具类

主要辅助生成Jwt令牌信息,代码如下:

public class JwtUtil {

    //有效期为
    public static final Long JWT_TTL = 3600000L;// 60 * 60 *1000  一个小时

    //Jwt令牌信息
    public static final String JWT_KEY = "itmean";

    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();
    }
}

Key

java.security
Key接口是所有 key 的顶级接口。所有的key 都有三个特点:
1、算法
算法通常是一个加密或不对称算法(如DSA或RSA等),它将与这些算法以及相关算法(如MD5与RSA、SHA-1与RSA、Raw DSA等)一起工作。
2、一种编码格式
当在Java虚拟机外部需要密钥的标准表示时(比如在将密钥传输给其他方时),这是密钥的外部编码格式。密钥根据标准格式(例如X.509 SubjectPublicKeyInfo或PKCS#8)进行编码,并使用getEncoded方法返回。

3、格式
这是编码key的格式的名称(如:RAW)。它由getFormat方法返回

密钥通常通过密钥生成器、证书或用于管理密钥的各种Identity类获得。密钥也可以通过使用密钥工厂(参见KeyFactory)从密钥规范(底层密钥透明表示)中获得

Key常用方法

//返回此键的标准算法名称。例如,“DSA”表示这个密钥是一个DSA密钥。有关标准算法名称的信息,
//请参阅Java密码体系结构API规范和参考中的附录A。
public String getAlgorithm()

public byte[] getEncoded()

public String getFormat()

SecretKeySpec

javax.crypto.spec

网关过滤器拦截请求处理

自定义全局过滤器

AuthorizeFilter代码:

import io.jsonwebtoken.Claims;
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.HttpCookie;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Component
public class AuthorizeFilter implements GlobalFilter, Ordered {

    //令牌头名字
    private static final String AUTHORIZE_TOKEN = "Authorization";

    /***
     * 全局过滤器
     * @param exchange
     * @param chain
     * @return
     */
    @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();
        //System.out.println("path:"+path);
        //System.out.println("host:" + request.getURI().getHost());

        //如果是登录、goods等开放的微服务[这里的goods部分开放],则直接放行,这里不做完整演示,完整演示需要设计一套权限系统
        if (path.startsWith("/api/user/login")) {
            //放行
            Mono<Void> filter = chain.filter(exchange);
            return filter;
        }

        //1、获取头文件中的令牌信息
        String token = request.getHeaders().getFirst(AUTHORIZE_TOKEN);

        //2、如果头文件中没有,则从请求参数中获取
        if (StringUtils.isEmpty(token)) {
            token = request.getQueryParams().getFirst(AUTHORIZE_TOKEN);
        }
        //3、如果请求参数中没有,则从Cookie中获取
        if (StringUtils.isEmpty(token)){
            HttpCookie cookie = request.getCookies().getFirst(AUTHORIZE_TOKEN);
            if(cookie!=null){
                token= cookie.getValue();
                System.out.println(token);
            }
        }

        //4、如果为空,则输出错误代码
        if (StringUtils.isEmpty(token)) {
            //设置方法不允许被访问,401错误代码
           response.setStatusCode(HttpStatus.UNAUTHORIZED);
           //return response.setComplete();
        }


        //解析令牌数据
        try {
            Claims claims = JwtUtil.parseJWT(token);
            request.mutate().header(AUTHORIZE_TOKEN, claims.toString());
        } catch (Exception e) {
            e.printStackTrace();
            //解析失败,响应401错误
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }

        //放行
        return chain.filter(exchange);
    }


    /***
     * 过滤器执行顺序
     * @return
     */
    @Override
    public int getOrder() {
        return 0;
    }
}

Ordered

org.springframework.core包中

rdered是一个接口,可以由应该是可排序的对象实现,例如在Collection中。
实际的顺序可以解释为优先级,第一个对象(具有最低的顺序值)具有最高的优先级
注意,这个接口还有一个子接口: PriorityOrdered。由PriorityOrdered表示的顺序值总是适用于由普通Ordered对象表示的相同顺序值。

Ordered 常用方法

//获取该对象的顺序值
//较高的值被解释为较低的优先级。因此,值最低的对象具有最高的优先级
//有点类似于Servlet load-on-startup值
int getOrder()

4.5 会话保持

用户每次请求的时候,我们都需要获取令牌数据,方法有多重,可以在每次提交的时候,将数据提交到头文件中,也可以将数据存储到Cookie中,每次从Cookie中校验数据,还可以每次将令牌数据以参数的方式提交到网关,这里面采用Cookie的方式比较容易实现。

修改user微服务,每次登录的时候,添加令牌信息到Cookie中:

@RequestMapping("/login")
    public Result login(String username, String password, HttpServletResponse response){
        //1.判断是否为空
        if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)){
            return new Result(false, StatusCode.LOGINERROR,"用户名和密码不能为空");
        }

        //2.判断是否数据库中有该用户名对应的数据
        User user = userService.selectByPrimaryKey(username);

        if (user == null){
            return new Result(false, StatusCode.LOGINERROR, "用户名或者密码错误");
        }

        if (!new BCryptPasswordEncoder().matches(password, user.getPassword())){
            return new Result(false, StatusCode.LOGINERROR, "用户名或者密码错误");
        }

        //4.返回

        //4.1 生成令牌
        Map<String,Object> subObject = new HashMap<>();
        subObject.put("username",username);
        subObject.put("role","ROLE_ADMIN");
        String token = JwtUtil.createJWT(UUID.randomUUID().toString(), JSON.toJSONString(subObject), null);
        //4.2 将令牌信息存储到cookie中
        Cookie cookie = new Cookie("Authorization", token);
        cookie.setPath("/");
        response.addCookie(cookie);
        return new Result(true, StatusCode.OK, "登录成功");
    }

你可能感兴趣的:(spring,cloud,微服务,java,spring,cloud)