# Spring Cloud 高级进阶


<!--more-->


## 一. 服务监控之Turbine聚合监控

已在Hystrix中示例过

## 二. 分布式链路追踪技术Sleuth+Zipkin

### 2.1 适用场景

- 场景描述

  为了⽀撑⽇益增⻓的庞⼤业务量，我们会使⽤微服务架构设计我们的系统，使得我们的系统不仅能够通过集群部署抵挡流量的冲击，⼜能根据业务进⾏灵活的扩展。

  那么，在微服务架构下，⼀次请求少则经过三四次服务调⽤完成，多则跨越⼏⼗个甚⾄是上百个服务节点。那么问题接踵⽽来：

  > 1）如何动态展示服务的调⽤链路？（⽐如A服务调⽤了哪些其他的服务---依赖关系）
  >
  > 2）如何分析服务调⽤链路中的瓶颈节点并对其进⾏调优？(⽐如A—>B—>C，C 服务处理时间特别⻓)
  >
  > 3）如何快速进⾏服务链路的故障发现？

  这就是分布式链路追踪技术存在的⽬的和意义

- 分布式链路追踪技术

  如果我们在⼀个请求的调⽤处理过程中，在各个链路节点都能够记录下⽇志，并最终将⽇志进⾏集中可视化展示，那么我们想监控调⽤链路中的⼀些指标就有希望了~~~⽐如，请求到达哪个服务实例？请求被处理的状态怎样？处理耗时怎样？这些都能够分析出来了...

  分布式环境下基于这种想法实现的监控技术就是就是分布式链路追踪（全链路追踪）。

- 追踪方案

  - Spring Cloud Sleuth+Twitter Zipkin
  - Alibaba 鹰眼
  - 大众点评 CAT
  - 美团 Mtrace
  - 京东 Hydra
  - 新浪 Watchman
  - Alache Skywalking

### 2.2 核心思想

本质：记录⽇志，作为⼀个完整的技术，分布式链路追踪也有⾃⼰的理论和概念

微服务架构中针对请求处理的调⽤链可以展现为⼀棵树，示意如下

![img_37](https://img.zhaojq.top/20260727091923768.png "img_37")

> 上图描述了⼀个常⻅的调⽤场景，⼀个请求通过⽹关服务路由到下游的微服务-1，然后微服务-1调⽤微服务-2，拿到结果后再调⽤微服务-3，最后组合微服务-2和微服务-3的结果，通过⽹关返回给⽤户

为了追踪整个调⽤链路，肯定需要记录⽇志，⽇志记录是基础，在此之上肯定有⼀些理论概念，当下主流的的分布式链路追踪技术/系统所基于的理念都来⾃于Google 的⼀篇论⽂《Dapper,aLarge-ScaleDistributedSystemsTracing Infrastructure》，这⾥⾯涉及到的核⼼理念是什么，我们来看下

![img_38](https://img.zhaojq.top/20260727091923940.png "img_38")

上图标识⼀个请求链路，⼀条链路通过TraceId唯⼀标识，span标识发起的请求信息，各span通过parrentId关联起来

- Trace

  服务追踪的追踪单元是从客户发起请求（request）抵达被追踪系统的边界开始，到被追踪系统向客户返回响应（response）为⽌的过程

- TraceID

  为了实现请求跟踪，当请求发送到分布式系统的⼊⼝端点时，只需要服务跟踪框架为该请求创建⼀个唯⼀的跟踪标识TraceID，同时在分布式系统内部流转的时候，框架失踪保持该唯⼀标识，直到返回给请求⽅

  ⼀个Trace由⼀个或者多个Span组成，每⼀个Span都有⼀个SpanId，Span中会记录TraceId，同时还有⼀个叫做ParentId，指向了另外⼀个Span的SpanId，表明⽗⼦关系，其实本质表达了依赖关系

- SpanID

- 为了统计各处理单元的时间延迟，当请求到达各个服务组件时，也是通过⼀个唯⼀标识SpanID来标记它的开始，具体过程以及结束。对每⼀个Span来说，它必须有开始和结束两个节点，通过记录开始Span和结束Span的时间戳，就能统计出该Span的时间延迟，除了时间戳记录之外，它还可以包含⼀些其他元数据，⽐如时间名称、请求信息等。

  每⼀个Span都会有⼀个唯⼀跟踪标识SpanID,若⼲个有序的span就组成了⼀个trace。

  Span可以认为是⼀个⽇志数据结构，在⼀些特殊的时机点会记录了⼀些⽇志信息，⽐如有时间戳、spanId、TraceId，parentIde等，Span中也抽象出了另外⼀个概念，叫做事件，核⼼事件如下

  - CS：clientsend/start客户端/消费者发出⼀个请求，描述的是⼀个span开始
  - SR: serverreceived/start服务端/⽣产者接收请求SR-CS属于请求发送的⽹络延迟
  - SS: serversend/ﬁnish服务端/⽣产者发送应答SS-SR属于服务端消耗时间
  - CR：clientreceived/ﬁnished客户端/消费者接收应答CR-SS表示回复需要的时间(响应的⽹络延迟)

SpringCloudSleuth（追踪服务框架）可以追踪服务之间的调⽤，Sleuth可以记录⼀个服务请求经过哪些服务、服务处理时⻓等，根据这些，我们能够理清各微服务间的调⽤关系及进⾏问题追踪分析。

- 耗时分析：通过Sleuth了解采样请求的耗时，分析服务性能问题（哪些服务调⽤⽐较耗时）
- 链路优化：发现频繁调⽤的服务，针对性优化等,Sleuth就是通过记录⽇志的⽅式来记录踪迹数据的

**我们往往把SpringCloudSleuth和Zipkin⼀起使⽤，把Sleuth的数据信息发送给Zipkin进⾏聚合，利⽤Zipkin存储并展示数据**

![img_39](https://img.zhaojq.top/20260727091924117.png "img_39")

### 2.3 Sleuth + Zipkin

**Sleuth引入:**

- 每一个被追踪的微服务,引入依赖 spring-cloud-starter-sleuth, spring-cloud-starter-zipkin

  我们在cloud-gateway-server-9002, demo-service-autodeliver-8091, demo-service-resume-8081三个工程中引入

- 每⼀个微服务都修改application.yml配置⽂件，添加⽇志级别

  ```yml
  logging:
    level:
      org.springframework.web.servlet.DispatcherServlet: debug
      org.springframework.cloud.sleuth: debug
  ```

- 请求到来即可看到如下日志,但不好阅读

  ![img_40](https://img.zhaojq.top/20260727091924291.png "img_40")

**ZipkinServer构建:**

- 见示例工程: cloud-zipkin-server-9411

- 追踪数据Zipkin持久化到mysql

  初始化表结构sql: [zipkin.sql](https://github.com/openzipkin/zipkin/blob/master/zipkin-storage/mysql-v1/src/main/resources/mysql.sql)

- Zipkinserver⻚⾯⽅便我们查看服务调⽤依赖关系及⼀些性能指标和异常信息

**ZipkinClient构建**:

- 各微服务中添加依赖,前面我们已经添加过
- 各微服务配置中添加对zipkin server的引用,见示例代码配置文件

现在就可以看到调用情况和依赖关系

![img_41](https://img.zhaojq.top/20260727091924459.png "img_41")

![img_42](https://img.zhaojq.top/20260727091924687.png "img_42")



## 三. 微服务统⼀认证⽅案SpringCloudOAuth2 +JWT

### 3.1 微服务架构下统一认证

- 场景

  分布式系统的每个服务都会有认证需求，如果每个服务都实现⼀套认证逻辑会⾮常冗余，考虑分布式系统共享性的特点，需要由独⽴的认证服务处理系统认证的请求。

  ![img_43](https://img.zhaojq.top/20260727091924851.png "img_43")

- 思路

  - 基于Session的认证⽅式

    在分布式的环境下，基于session的认证会出现⼀个问题，每个应⽤服务都需要在session中存储⽤户身份信息，通过负载均衡将本地的请求分配到另⼀个应⽤服务需要将session信息带过去，否则会重新认证。我们可以使⽤Session共享、Session黏贴等⽅案。

    Session⽅案也有缺点，⽐如基于cookie，移动端不能有效使⽤等

  - 基于token的认证⽅式

    基于token的认证⽅式，服务端不⽤存储认证数据，易维护扩展性强，客户端可以把token存在任意地⽅，并且可以实现web和app统⼀认证机制。其缺点也很明显，token由于⾃包含信息，因此⼀般数据量较⼤，⽽且每次请求都需要传
    递，因此⽐较占带宽。另外，token的签名验签操作也会给cpu带来额外的处理负担。

### 3.2 OAuth2开放授权协议/标准

- OAuth2介绍

  OAuth（开放授权）是⼀个开放协议/标准，**允许⽤户授权第三⽅应⽤访问他们存储在另外的服务提供者上的信息，⽽不需要将⽤户名和密码提供给第三⽅应⽤或分享他们数据的所有内容**

- OAuth2协议⻆⾊和流程

  > 1. 如要开发qq授权登录功能, 则网站需先到qq平台进行登记注册
  > 2. 平台给网站一些授权参数信息,之后授权登录时传递过去 如client_id, secret
  >
  > ![img_44](https://img.zhaojq.top/20260727091925048.png "img_44")
  >
  > - 资源所有者（ResourceOwner）：可以理解为⽤户⾃⼰
  > - 客户端（Client）：我们想登陆的⽹站或应⽤，⽐如拉勾⽹
  > - 认证服务器（AuthorizationServer）：可以理解为微信或者QQ 
  > - 资源服务器（ResourceServer）：可以理解为微信或者QQ

- 什么情况下需要使⽤OAuth2

  第三⽅授权登录的场景：⽐如，我们经常登录⼀些⽹站或者应⽤的时候，可以选择使⽤第三⽅授权登录的⽅式，⽐如：微信授权登录、QQ授权登录、微博授权登录等，这是典型的OAuth2使⽤场景。

  单点登录的场景：如果项⽬中有很多微服务或者公司内部有很多服务，可以专⻔做⼀个认证中⼼（充当认证平台⻆⾊），所有的服务都要到这个认证中⼼做认证，只做⼀次登录，就可以在多个授权范围内的服务中⾃由串⾏。

- OAuth2的颁发Token授权⽅式

  - 授权码（authorization-code）

    > 授权码模式使⽤到了回调地址，是最复杂的授权⽅式，微博、微信、QQ等第三⽅登录就是这种模式。我们重点讲解接⼝对接中常使⽤的password密码模式（提供⽤户名+密码换取token）。

  - 密码式（password）提供⽤户名+密码换取token令牌

  - 隐藏式（implicit）

  - 客户端凭证（clientcredentials）

### 3.3 SpringCloudOAuth2+JWT实现

#### 3.3.1 SpringCloudOAuth2介绍

SpringCloudOAuth2是SpringCloud体系对OAuth2协议的实现，可以⽤来做多个微服务的统⼀认证（验证身份合法性）授权（验证权限）。通过向OAuth2服务（统⼀认证授权服务）发送某个类型的grant_type进⾏集中认证和授权，从⽽获得
access_token（访问令牌），⽽这个token是受其他微服务信任的

注意：使⽤OAuth2解决问题的本质是，引⼊了⼀个认证授权层，认证授权层连接了资源的拥有者，在授权层⾥⾯，资源的拥有者可以给第三⽅应⽤授权去访问我们的某些受保护资源。

#### 3.3.2 Spring CloudO Auth2构建微服务统⼀认证服务思路

![img_45](https://img.zhaojq.top/20260727091925248.png "img_45")

> 注意：在我们统⼀认证的场景中，ResourceServer其实就是我们的各种受保护的微服务，微服务中的各种API访问接⼝就是资源，发起http请求的浏览器就是Client 客户端（对应为第三⽅应⽤）

#### 3.3.3 搭建认证服务器（AuthorizationServer）

以下详情见示例工程

- 新建工程 cloud-oauth-server-9999

- 引入依赖

- 配置文件

  ```yml
  server:
    port: 9999
  
  #注册到Eureka服务中心
  eureka:
    client:
      service-url:
        # 注册到集群，就把多个Eurekaserver地址使用逗号连接起来即可；注册到单实例（非集群模式），那就写一个就ok
        defaultZone: http://CloudEurekaServerA:8761/eureka,http://CloudEurekaServerB:8762/eureka
    instance:
      prefer-ip-address: true  #服务实例中显示ip，而不是显示主机名（兼容老的eureka版本）
      # 实例名称： 192.168.1.103:lagou-service-resume:8080，我们可以自定义它
      instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@
  
  spring:
    application:
      name: cloud-oauth-server
  ```

- springboot启动类

- 认证服务器配置类

  ```java
  /**
   * 当前类为Oauth2 server的配置类（需要继承特定的父类 AuthorizationServerConfigurerAdapter）
   */
  @Configuration
  // 开启认证服务器功能
  @EnableAuthorizationServer
  public class OauthServerConfiger extends AuthorizationServerConfigurerAdapter {
  
      @Autowired
      private AuthenticationManager authenticationManager;
  
      /**
       * 认证服务器最终是以api接口的方式对外提供服务（校验合法性并生成令牌、校验令牌等）
       * 那么，以api接口方式对外的话，就涉及到接口的访问权限，我们需要在这里进行必要的配置
       *
       * @param security
       * @throws Exception
       */
      @Override
      public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
          super.configure(security);
          // 相当于打开endpoints 访问接口的开关，这样的话后期我们能够访问该接口
          security
              // 允许客户端表单认证
              .allowFormAuthenticationForClients()
              // 开启端口/oauth/token_key的访问权限（允许）
              .tokenKeyAccess("permitAll()")
              // 开启端口/oauth/check_token的访问权限（允许）
              .checkTokenAccess("permitAll()");
      }
  
      /**
       * 客户端详情配置，
       * 比如client_id，secret
       * 当前这个服务就如同QQ平台，拉勾网作为客户端需要qq平台进行登录授权认证等，提前需要到QQ平台注册，
       * QQ平台会给拉勾网, 颁发client_id等必要参数，表明客户端是谁
       *
       * @param clients
       * @throws Exception
       */
      @Override
      public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
          super.configure(clients);
  
          // 从内存中加载客户端详情
          // 客户端信息存储在什么地方，可以在内存中，可以在数据库里
          clients.inMemory()
              // 添加一个client配置,指定其client_id
              .withClient("demo_client_id")
              // 指定客户端的密码/安全码
              .secret("abcxyz")
              // 指定客户端所能访问资源id清单，此处的资源id是需要在具体的资源服务器上也配置一样
              .resourceIds("autodeliver")
              // 认证类型/令牌颁发模式，可以配置多个在这里，但是不一定都用，具体使用哪种方式颁发token，需要客户端调用的时候传递参数指定
              .authorizedGrantTypes("password", "refresh_token")
              // 客户端的权限范围，此处配置为all全部即可
              .scopes("all");
      }
  
  
      /**
       * 认证服务器是玩转token的，那么这里配置token令牌管理相关（token此时就是一个字符串，当下的token需要在服务器端存储，那么存储在哪里呢？都是在这里配置）
       *
       * @param endpoints
       * @throws Exception
       */
      @Override
      public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
          super.configure(endpoints);
          endpoints
              // 指定token的存储方法
              .tokenStore(tokenStore())
              // token服务的一个描述，可以认为是token生成细节的描述，比如有效时间多少等
              .tokenServices(authorizationServerTokenServices())
              // 指定认证管理器，随后注入一个到当前类使用即可
              .authenticationManager(authenticationManager)
              .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
      }
  
      /**
       * 该方法用于创建tokenStore对象（令牌存储对象）
       * token以什么形式存储
       */
      public TokenStore tokenStore() {
          return new InMemoryTokenStore();
      }
  
      /**
       * 该方法用户获取一个token服务对象（该对象描述了token有效期等信息）
       */
      public AuthorizationServerTokenServices authorizationServerTokenServices() {
          // 使用默认实现
          DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
          // 是否开启令牌刷新
          defaultTokenServices.setSupportRefreshToken(true);
          defaultTokenServices.setTokenStore(tokenStore());
  
          // 设置令牌有效时间（一般设置为2个小时）
          // access_token就是我们请求资源需要携带的令牌
          defaultTokenServices.setAccessTokenValiditySeconds(20);
          // 设置刷新令牌的有效时间
          defaultTokenServices.setRefreshTokenValiditySeconds(259200); // 3天
          return defaultTokenServices;
      }
  }
  ```

  >关于3个configure方法重写
  >
  >1. `conﬁgure(ClientDetailsServiceConﬁgurerclients)`
  >
  >   ⽤来配置客户端详情服务（ClientDetailsService），客户端详情信息在这⾥进⾏初始化，你能够把客户端详情信息写死在这⾥或者是通过数据库来存储调取详情信息
  >
  >2. `configure(AuthorizationServerEndpointsConfigurer endpoints)`
  >
  >   ⽤来配置令牌（token）的访问端点和令牌服务(tokenservices)
  >
  >3. `configure(AuthorizationServerSecurityConfigurer security)`
  >
  >   ⽤来配置令牌端点的安全约束

  > 关于TokenStore
  >
  > - InMemoryTokenStore 
  >   默认采⽤，它可以完美的⼯作在单服务器上（即访问并发量压⼒不⼤的情况下，并且它在失败的时候不会进⾏备份），⼤多数的项⽬都可以使⽤这个版本的实现来进⾏尝试，你可以在开发的时候使⽤它来进⾏管理，因为不会被保存到磁盘中，所以更易于调试。
  > - JdbcTokenStore 
  >   这是⼀个基于JDBC的实现版本，令牌会被保存进关系型数据库。使⽤这个版本的实现时，你可以在不同的服务器之间共享令牌信息，使⽤这个版本的时候请注意把"spring-jdbc"这个依赖加⼊到你的classpath 当中。
  > - JwtTokenStore这个版本的全称是JSONWebToken（JWT），它可以把令牌相关的数据进⾏编码（因此对于后端服务来说，它不需要进⾏存储，这将是⼀个重⼤优势），缺点就是这个令牌占⽤的空间会⽐较⼤，如果你加⼊了⽐较多⽤户凭证信息，JwtTokenStore不会保存任何数据。

- 认证服务器安全配置类

  ```java
  /**
   * 该配置类，主要处理用户名和密码的校验等事宜
   */
  @Configuration
  public class SecurityConfiger extends WebSecurityConfigurerAdapter {
  
      @Autowired
      private PasswordEncoder passwordEncoder;
  
      /**
       * 注册一个认证管理器对象到容器
       */
      @Bean
      @Override
      public AuthenticationManager authenticationManagerBean() throws Exception {
          return super.authenticationManagerBean();
      }
  
      /**
       * 密码编码对象（密码不进行加密处理）
       *
       * @return
       */
      @Bean
      public PasswordEncoder passwordEncoder() {
          return NoOpPasswordEncoder.getInstance();
      }
  
      /**
       * 处理用户名和密码验证事宜
       * <p>
       * 1）客户端传递username和password参数到认证服务器
       * 2）一般来说，username和password会存储在数据库中的用户表中
       * 3）根据用户表中数据，验证当前传递过来的用户信息的合法性
       */
      @Override
      protected void configure(AuthenticationManagerBuilder auth) throws Exception {
          // 在这个方法中就可以去关联数据库了，当前我们先把用户信息配置在内存中
          // 实例化一个用户对象(相当于数据表中的一条用户记录)
          UserDetails user = new User("admin", "123456", new ArrayList<>());
          auth.inMemoryAuthentication().withUser(user).passwordEncoder(passwordEncoder);
      }
  }
  ```

- 启动并测试

  获取token: 

  [http://localhost:9999/oauth/tokengrant_type=password&client_secret=abcxyz&client_id=demo_client_id&username=admin&password=123456]()

  > client_id：客户端id 
  >
  > client_secret：客户单密码
  >
  > grant_type：指定使⽤哪种颁发类型，
  >
  > password username：⽤户名
  >
  > password：密码

  校验token: [http://localhost:9999/oauth/check_token?token=]()

  刷新token: 

  [http://localhost:9999/oauth/tokengrant_type=refresh_token&client_secret=abcxyz&client_id=demo_client_id&refresh_token=]()

- 资源服务器（希望访问被认证的微服务）ResourceServer配置

  如我们在demo-service-autodeliver-8091下配置`ResourceServerConfiger`

  ```java
  @Configuration
  // 开启资源服务器功能
  @EnableResourceServer
  // 开启web访问安全
  @EnableWebSecurity
  public class ResourceServerConfiger extends ResourceServerConfigurerAdapter {
  
      private String sign_key = "demo123"; // jwt签名密钥
  
      /**
       * 该方法用于定义资源服务器向远程认证服务器发起请求，进行token校验等事宜
       *
       * @param resources
       * @throws Exception
       */
      @Override
      public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
          // 设置当前资源服务的资源id
          resources.resourceId("autodeliver");
          // 定义token服务对象（token校验就应该靠token服务对象）
          RemoteTokenServices remoteTokenServices = new RemoteTokenServices();
          // 校验端点/接口设置
          remoteTokenServices.setCheckTokenEndpointUrl("http://localhost:9999/oauth/check_token");
          // 携带客户端id和客户端安全码
          remoteTokenServices.setClientId("demo_client_id");
          remoteTokenServices.setClientSecret("abcxyz");
  
          // 别忘了这一步
          resources.tokenServices(remoteTokenServices);
      }
  
      /**
       * 场景：一个服务中可能有很多资源（API接口）
       * <p>
       * 某一些API接口，需要先认证，才能访问
       * 某一些API接口，压根就不需要认证，本来就是对外开放的接口
       * 我们就需要对不同特点的接口区分对待（在当前configure方法中完成），设置是否需要经过认证
       *
       * @param http
       * @throws Exception
       */
      @Override
      public void configure(HttpSecurity http) throws Exception {
          http    // 设置session的创建策略（根据需要创建即可）
              .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
              .and()
              .authorizeRequests()
              // autodeliver为前缀的请求需要认证
              .antMatchers("/autodeliver/**").authenticated()
              // demo为前缀的请求需要认证
              .antMatchers("/demo/**").authenticated()
              .anyRequest().permitAll();  //  其他请求不认证
      }
  }
  ```

  启动并验证,我们发现,只有带上正确token才能访问:

  http://localhost:8091/autodeliver/checkState/1545132?access_token=c8ddc18f-a6bc-4e7b-946d-0cd1ad3ca6a3



#### 3.3.4 JWT改造统⼀认证授权中⼼的令牌存储机制

**JWT令牌介绍**

> 通过上边的测试我们发现，当资源服务和授权服务不在⼀起时资源服务使⽤RemoteTokenServices远程请求授权服务验证token，如果访问量较⼤将会影响系统的性能。

> 解决上边问题：令牌采⽤JWT格式即可解决上边的问题，⽤户认证通过会得到⼀个JWT令牌，JWT令牌中已经包括了⽤户相关的信息，客户端只需要携带JWT访问资源服务，资源服务根据事先约定的算法⾃⾏完成令牌校验，⽆需每次都请求认证服务完成授权。

**JSON Web Token（JWT）**是⼀个开放的⾏业标准（RFC7519），它定义了⼀种简介的、⾃包含的协议格式，⽤于在通信双⽅传递json对象，传递的信息经过数字签名可以被验证和信任。JWT可以使⽤HMAC算法或使⽤RSA的公钥/私钥对来签名，防⽌被篡改。



**令牌结构**

JWT令牌由三部分组成，每部分中间使⽤点（.）分隔，⽐如：xxxxx.yyyyy.zzzzz

- Header

  头部包括令牌的类型（即JWT）及使⽤的哈希算法（如HMACSHA256或RSA）

  > 如{"alg":"HS256", "typ":"JWT"}
  >
  > 将上边的内容使⽤Base64Url编码，得到⼀个字符串就是JWT令牌的第⼀部分。

- Payload

  第⼆部分是负载，内容也是⼀个json对象，它是存放有效信息的地⽅，它可以存放jwt提供的现成字段，⽐如：iss（签发者）,exp（过期时间戳）,sub（⾯向的⽤户）等，也可⾃定义字段。此部分不建议存放敏感信息，因为此部分可以解码还原原始内容。最后将第⼆部分负载使⽤Base64Url编码，得到⼀个字符串就是JWT令牌的第⼆部分

  > 如 {"sub":"1234567890","name":"JohnDoe","iat":1516239022}

- Signature 
  第三部分是签名，此部分⽤于防⽌jwt内容被篡改。这个部分使⽤base64url将前两部分进⾏编码，编码后使⽤点（.）连接组成字符串，最后使⽤header中声明签名算法进⾏签名。

  > HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret))

**服务端改造**

- 添加签名

- tokenStore()存储方式改造

- 增加`public JwtAccessTokenConverter jwtAccessTokenConverter()`

- 添加 

  ```java
  // 针对jwt令牌的添加
          defaultTokenServices.setTokenEnhancer(jwtAccessTokenConverter());
  ```

**资源服务器校验jwt令牌**

- 添加签名

- `configure(ResourceServerSecurityConfigurer resources)`方法令牌改造

  ```java
  // jwt令牌改造
  resources.resourceId("autodeliver").tokenStore(tokenStore()).stateless(true);// 无状态设置
  ```

- 添加tokenStore()

- 添加jwtAccessTokenConverter()

- 测试获取的token如下所示

  ```tex
  eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiYXV0b2RlbGl2ZXIiXSwiZXhwIjoxNjQ4MjI1NTk3LCJ1c2VyX25hbWUiOiJhZG1pbiIsImp0aSI6IjNhNTFiODUzLTkyMWQtNDNlNy1iMjEwLTUxYTE5OGZmNjMxOSIsImNsaWVudF9pZCI6ImRlbW9fY2xpZW50X2lkIiwic2NvcGUiOlsiYWxsIl19.KyoQ5PIpUqUXcpcVfMVlndFquRRLL4WgSmdyqxTweRE
  ```

#### 3.3.5 从数据库加载Oauth2客户端信息

- 创建数据库(表名及字段保持一致)

  ```mysql
  SET NAMES utf8mb4;
  SET FOREIGN_KEY_CHECKS = 0;
  DROP TABLE IF EXISTS `oauth_client_details`;
  CREATE TABLE `oauth_client_details`
  (
      `client_id`               varchar(48) NOT NULL,
      `resource_ids`            varchar(256)  DEFAULT NULL,
      `client_secret`           varchar(256)  DEFAULT NULL,
      `scope`                   varchar(256)  DEFAULT NULL,
      `authorized_grant_types`  varchar(256)  DEFAULT NULL,
      `web_server_redirect_uri` varchar(256)  DEFAULT NULL,
      `authorities`             varchar(256)  DEFAULT NULL,
      `access_token_validity`   int(11)       DEFAULT NULL,
      `refresh_token_validity`  int(11)       DEFAULT NULL,
      `additional_information`  varchar(4096) DEFAULT NULL,
      `autoapprove`             varchar(256)  DEFAULT NULL,
      PRIMARY KEY (`client_id`)
  ) ENGINE = InnoDB
    DEFAULT CHARSET = utf8;
  BEGIN;
  INSERT INTO `oauth_client_details`
  VALUES ('demo_client_id','autodeliver,resume', 'abcxyz', 'all', 'password,refresh_token',NULL, NULL, 7200, 259200, NULL, NULL);
  COMMIT;
  SET FOREIGN_KEY_CHECKS = 1;
  ```

- 添加mysql驱动,spring 事务等依赖

- yml配置数据源

- 认证服务器主配置类改造



#### 3.3.6 从数据库验证用户合法性

- 创建数据表users（表名不需固定），初始化数据

  ```mysql
  DROP TABLE IF EXISTS `users`;
  CREATE TABLE `users` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `username` char(10) DEFAULT NULL,
    `password` char(100) DEFAULT NULL,
   PRIMARY KEY (`id`)
  ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
  
  INSERT INTO `users` VALUES (4, 'admin', '123456');
  ```

- 操作数据表的JPA配置以及针对该表的操作的Dao接⼝ 见示例代码

- 开发UserDetailsService接⼝的实现类，根据⽤户名从数据库加载⽤户信息 见示例代码

- 使⽤⾃定义的⽤户详情服务对象, 修改`SecurityConfiger ` 见示例代码 

#### 3.3.7 基于**Oauth2**的 **JWT** 令牌信息扩展

OAuth2帮我们⽣成的JWT令牌载荷部分信息有限，关于⽤户信息只有⼀个**user_name**，有些场景下我们希望放⼊⼀些扩展信息项，⽐如，之前我们经常向session中存⼊userId，或者现在我希望在JWT的载荷部分存⼊当时请求令牌的**客户端IP**，客户端携带令牌访问资源服务时，可以对⽐当前请求的客户端真实IP和令牌中存放的客户端IP是否匹配，不匹配拒绝请求，以此进⼀步提⾼安全性。那么如何在
OAuth2环境下向JWT令牌中存如扩展信息？

- 认证服务器⽣成JWT令牌时存⼊扩展信息（⽐如clientIp）

  继承DefaultAccessTokenConverter类，重写convertAccessToken⽅法存⼊扩展信息

- 将⾃定义的转换器对象注⼊

  `jwtAccessTokenConverter.setAccessTokenConverter(customAccessTokenConvertor);`

#### 3.3.8资源服务器取出JWT令牌扩展信息

资源服务器也需要⾃定义⼀个转换器类，继承DefaultAccessTokenConverter，重写extractAuthentication提取⽅法，把载荷信息设置到认证对象的details属性中

业务类⽐如Controller类中，可以通过SecurityContextHolder.getContext().getAuthentication()获取到认证对象，进⼀步获取到扩展信息

### 3.3.9 其他

tips:

- JWT令牌就是⼀种可以被验证的数据组织格式，它的玩法很灵活，我们这⾥是基于SpringCloudOauth2创建、校验JWT令牌
- 我们也可以⾃⼰写⼯具类⽣成、校验JWT令牌
- JWT令牌中不要存放过于敏感的信息，因为我们知道拿到令牌后，我们可以解码看到载荷部分的信息
- JWT令牌每次请求都会携带，内容过多，会增加⽹络带宽占⽤




















