Spring Cloud微服務學習筆記


Spring Cloud微服務學習筆記

SOA->Dubbo

微服務架構->Spring Cloud提供了一個一站式的微服務解決方案

第一部分 微服務架構

1 互聯網應用架構發展

那些迫使系統演進的因素:

業務量上去了后,負載能力滿足不了,對高負載能力的需求,以及高性能高可用、自動化運維管理、等的需求。

1、單體應用架構

優點:

缺點:

2、垂直應用架構

優點:

缺點:

3、SOA應用架構

優點:

缺點

4、微服務架構

2 微服務架構體現的思想及優缺點

優點:

  • 微服務很小,便於特定業務功能的聚焦
  • 微服務很小,每個微服務都可以被一個團隊單獨實施(開發、測試、部署上線、運維),團隊合作一定程度解耦,便於實施敏捷開發
  • 微服務很小,便於重用和模塊之間的組裝
  • 微服務很獨立,那么不同的微服務可以使用不同的語言開發,松耦合
  • 更容易引入新技術
  • 可以更好的實現DevOps開發運維一體化

缺點:

  • 服務數量越多越難管理
  • 鏈路難跟蹤

3 微服務架構中的一些概念

服務注冊與發現

服務注冊:服務提供者將所提供的服務信息注冊/登記到注冊中心

服務發現:服務消費者能夠從注冊中心獲取到較為實時的服務列表,然后根據一定的策略選擇一個服務訪問

負載均衡

將請求壓力分配到多個服務器(應用服務器、數據庫服務器等),以此來提高服務等性能、可靠性

熔斷

即斷路保護。如果下游服務因訪問壓力過大而響應變慢或失敗,上游服務為了保護系統整體可用性,可以暫時切斷對下游服務等調用。這種犧牲局部,保全整體的措施就叫做熔斷。

熔斷的本質是為了保護服務的整體可用性

鏈路追蹤

對一次請求涉及的很多個服務鏈路進行日志記錄、性能監控

API網關

如果沒有網關,客戶端直接與各個微服務通信的問題:

1、客戶端調用不同的url地址,增加了維護調用難度

2、在一定的場景下,也存在跨域請求的問題(可以使用Nginx做反向代理服務器)

3、每個微服務都需要進行單獨的身份認證

網關除了轉發請求,更專注安全、路由、流量問題的處理,它的功能有:

1、統一接入(路由)

2、安全防護(統一鑒權,負責網關訪問身份驗證,與“訪問認證中心”通信,實際認證業務邏輯交移“訪問認證中心”處理)

3、黑白名單(實現通過IP地址控制禁止訪問網關功能,控制訪問)

4、協議適配(實現通信協議校驗、適配轉換的功能)

5、流量管控(限流)

6、長短鏈接支持

7、容錯能力(負載均衡)

第二部分 Spring Cloud概述

Spring Cloud是什么

Spring cloud是一套規范,間commons下的接口

實現有SCN和SCA

Spring Cloud解決什么問題

解決微服務架構實施過程中存在的問題,比如服務注冊發現、網絡問題(熔斷場景)、統一認證安全授權、負載均衡問題、鏈路追蹤問題等。

Spring Cloud架構

Spring Cloud組件

第一代SCN 第二代SCA
注冊中心 Eureka Nacos
客戶端負載均衡 Ribbon Dubbo LB、Spring Cloud Loadbalancer
熔斷器 Hystrix Sentinel
網關 Zuul Spring Cloud Gateway
配置中心 Spring Cloud Config Nacos、Apollo
服務調用 Feign Dubbo RPC
消息驅動 Spring Cloud Stream
鏈路追蹤 Spring Cloud Sleuth/Zipkin
Seata分布式事務解決方案

Spring Cloud 體系結構(組件協同工作機制)

image-20200620212327068

  • 注冊中心負責服務的注冊和發現
  • 網關負責轉發所有外來的請求
  • 斷路器負責監控服務之間的調用情況,連續多次失敗進行熔斷保護
  • 配置中心提供了統一的配置信息管理服務,可以實時的通知各個服務獲取最新的配置信息

Spring Cloud與Dubbo對比

Dubbo定位於高性能RPC框架,不是一站式的微服務解決方案

Spring Cloud與Spring Boot的關系

Spring Boot是實現Spring Cloud的基礎,提供依賴版本管理、自動配置、快速啟動

第三部分 案例准備

案例說明

數據庫環境准備

工程環境准備

使用Maven聚合工程

案例核心微服務開發及通信調用

案例代碼問題分析

存在的問題:

  • 在服務消費者中,我們把url地址硬編碼到代碼中,不方便后期維護
  • 服務提供者只有一個服務,即便服務提供者形成集群,服務消費者還需要自己實現負載均衡
  • 在服務消費者中,不清楚服務提供者的狀態
  • 服務消費者調用服務提供者的時候,如果出現故障能否及時發現不向用戶拋出異常頁面
  • RestTemplate這種請求調用方式是否還要優化空間?能不能類似於Dubbo那樣玩
  • 這么多微服務統一認證如何實現
  • 配置文件每次都修改好多個很麻煩

微服務架構面臨的共有的問題:

  • 服務管理:自動注冊與發現、狀態監管
  • 服務負載均衡
  • 熔斷
  • 遠程過程調用
  • 網關攔截、路由轉發
  • 統一認證
  • 集中式配置管理,配置信息實時自動更新

第四部分 第一代Spring Cloud核心組件

image-20200620214721693

從形式上說,Feign一個頂三,Feign=RestTemplate+Ribbon+Hystrix

Eureka注冊中心

關於服務注冊中心

解耦服務提供者和服務調用者

服務注冊中心一般原理

image-20200620214915243

注冊中心做哪些事

主流服務注冊中心對比

服務注冊中心Eureka

Eureka基礎架構

image-20200620215145893

Eureka交互流程及原理

image-20200620215217312

Eureka應用及高可用集群

Eureka細節詳解

Eureka元數據

Eureka客戶端詳解

Eureka服務端詳解

Eureka核心源碼剖析

第五部分 常見問題及解決方案

第六部分 Spring Cloud高級進階

Turbine聚合監控

參照Hustrix部分

微服務監控之分布式鏈路追蹤技術Sleuth+Zipkin

分布式鏈路追蹤技術適用場景(問題場景)

場景描述

  1. 如何動態展示服務的調用鏈路
  2. 如何分析服務調用鏈路中的瓶頸節點並對其進行調優?
  3. 如何快速進行服務鏈路的故障發現

分布式鏈路追蹤技術

如果我們在一個請求的調用處理過程中,在各個鏈路節點都能夠記錄下日志,並最終將日志進行集中可視化展示,那么我們想監控調用鏈路中一些指標就有希望了。

比如,請求到達哪個服務實例?請求倍處理的狀態怎樣?這些都能夠分析出來了

分布式環境下基於這種想法實現的監控技術就是分布式鏈路追蹤(全鏈路追蹤)。

市場上的分布式鏈路追蹤方案

分布式鏈路追蹤技術方案:

  • Spring Cloud Sleuth+Twitter Zipkin
  • 阿里巴巴的"鷹眼"
  • 大眾點評的"CAT"
  • 美團的"Mtrace"
  • 京東的"Hydra"
  • 新浪的"Watchman"
  • Apache Skywalking

分布式鏈路追蹤技術的核心思想

本質:記錄日志,作為一個完整的技術,分布式鏈路追蹤技術有自己的理論和概念

微服務架構中,針對請求處理的調用鏈路可以展示為一棵樹:

image-20200705213247275

上圖描述了一個常見的調用場景,一個請求通過網關服務路由到下游的微服務1,然后微服務1調用微服務2,拿到結果再調用微服務3,最后組合微服務2和微服務3的結果,通過網關返回給用戶

追蹤調用鏈路就要記錄日志,Google的論文,《Dapper, a Large-Scale Distributed Systems Tracing Infrastructure》

image-20200705213543752

上圖標識一個請求鏈路,一條鏈路通過TraceId唯一標識,span標識發起的請求信息,各span通過parrentId關聯起來。

Trace:服務追蹤的追蹤單元是從客戶發起請求抵達被追蹤系統的邊界開始,到被追蹤系統向客戶返回響應為止的過程

Trace ID:為了實現請求追蹤,當請求發送到分布式系統的入口端點時,只需要服務跟蹤框架為該請求創建一個唯一的跟蹤標識Trace ID,同時在分布式系統內部流轉的時候,框架始終保持該唯一標識,直到返回給請求方

一個Trace由一個或多個Span組成,每一個Span都有一個SpanId,Span中會記錄TraceId,同時還有一個叫做parentId,指向了另一個Span的SpanId,表明父子關系,其實本質表達了依賴關系。

Span ID:為了統計各處理單元的時間延遲,當請求達到各個組件時,也是通過一個唯一標識Span ID來標記他的開始,具體過程以及結束。對每個Span來說,它必須有開始和結束兩個節點,通過記錄開始Span和結束Span時間戳,就能統計該Span的時間延遲,每個Span還包含了時間名稱、請求信息等元數據。

每個Span都會有一個唯一跟蹤標識SpanId,若干個有序的span就組成一個trace。

Span可以認為是一個日志數據結構,在一些特殊時機點記錄了一些日志信息,比如有時間戳、spanid、traceId、parentId等。Span中也抽象出另外一個概念,叫做事件,核心事件如下:

  • CS:client send/start 客戶端/消費者發出一個請求,描述的是一個span開始
  • SR:server received/start 服務端/生產者接收請求SR-CS屬於請求發送的網絡延遲
  • SS:server send/finish 服務端/生產者發送應答SS-SR屬於服務端消耗時間
  • CR:client received/finished 客戶端/消費者接受應答CR-SS表示回復需要的時間(響應的網絡延遲)

Spring Cloud Sleuth(追蹤服務框架)可以追蹤服務之間的調用,Sleuth可以記錄一個服務請求經過哪些服務、服務處理時長,根據這些信息,我們能夠理清各微服務間的調用關系及進行問題追蹤分析

  • 耗時分析:通過Sleuth了解采樣請求的耗時,分析服務性能問題(哪些服務調用比較耗時)
  • 鏈路優化:發現頻繁調用的服務,針對性優化等

Sleuth就是通過記錄日志的方式來記錄追蹤鏈路的

注意:我們往往把Spring Cloud Sleuth和Zipkin一起使用,把Sleuth的數據信息發送給Zipkin進行聚合,利用Zipkin存儲並展示數據。

image-20200705215456677

image-20200705215633659

Sleuth + Zipkin

1、每一個需要被追蹤蹤跡的微服務工程都需要引入依賴坐標

<!--鏈路追蹤-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-sleuth</artifactId>
        </dependency>

2、每一個微服務都修改application.yml配置文件,添加日志級別

#分布式鏈路追蹤
logging:
  level:
    org.springframework.web.servlet.DispatcherServlet: debug
    org.springframework.cloud.sleuth: debug

請求到來時,我們可以看到控制台Sleuth輸出的日志(全局TraceId、SpanId等)

image-20200705220510153

這樣的日志不易觀察,另外日志分散在各個微服務服務器上,接下來我們使用Zipkin統一聚合軌跡日志並進行存儲展示

3、結合Zipkin展示追蹤數據

Zipkin server構建

pom.xml

<!--zipkin-server的依賴坐標-->
        <dependency>
            <groupId>io.zipkin.java</groupId>
            <artifactId>zipkin-server</artifactId>
            <version>2.12.3</version>
            <exclusions>
                <!--排除掉log4j2的傳遞依賴,避免和springboot依賴的日志組件沖突-->
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-log4j2</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <!--zipkin-server ui界面依賴坐標-->
        <dependency>
            <groupId>io.zipkin.java</groupId>
            <artifactId>zipkin-autoconfigure-ui</artifactId>
            <version>2.12.3</version>
        </dependency>

入口啟動類

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import zipkin2.server.internal.EnableZipkinServer;

import javax.sql.DataSource;

@SpringBootApplication
@EnableZipkinServer // 開啟Zipkin 服務器功能
public class ZipkinServerApplication9411 {

    public static void main(String[] args) {
        SpringApplication.run(ZipkinServerApplication9411.class,args);
    }

}

application.yml

server:
  port: 9411
management:
  metrics:
    web:
      server:
        auto-time-requests: false # 關閉自動檢測
Zipkin Client構建(在具體微服務中修改)

pom.xml文件中添加zipkin依賴

				<!--鏈路追蹤-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-sleuth</artifactId>
        </dependency>
				<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-zipkin</artifactId>
        </dependency>

application.yml中添加對zipkin server的引用

spring:
  application:
    name: lagou-service-autodeliver
  zipkin:
    base-url: http://127.0.0.1:9411 # zipkin server的請求地址
    sender:
      # web 客戶端將蹤跡日志數據通過網絡請求的方式傳送到服務端,另外還有配置
      # kafka/rabbit 客戶端將蹤跡日志數據傳遞到mq進行中轉
      type: web
    sleuth:
      sampler:
        # 采樣率 1 代表100%全部采集 ,默認0.1 代表10% 的請求蹤跡數據會被采集
        # 生產環境下,請求量非常大,沒有必要所有請求的蹤跡數據都采集分析,對於網絡包括server端壓力都是比較大的,可以配置采樣率采集一定比例的請求的蹤跡數據進行分析即可
        probability: 1

對於log日志,依然保持開啟debug狀態,在各個微服務中配置開啟debug日志

logging:
  level:
    # Feign日志只會對日志級別為debug的做出響應
    com.lagou.edu.controller.service.ResumeServiceFeignClient: debug
    org.springframework.web.servlet.DispatcherServlet: debug
    org.springframework.cloud.sleuth: debug
Zipkin server頁面解讀

會方便我們查看服務調用依賴關系及一些性能指標和異常信息

追蹤數據Zipkin持久化到mysql

mysql中創建名為zipkin的數據庫,並執行官方提供的sql語句

zipkin server的pom文件引入依賴

<!--zipkin針對mysql持久化的依賴-->
        <dependency>
            <groupId>io.zipkin.java</groupId>
            <artifactId>zipkin-autoconfigure-storage-mysql</artifactId>
            <version>2.12.3</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>
        <!--操作數據庫需要事務控制-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
        </dependency>

修改配置文件,添加如下內容

spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/zipkin?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true
    username: root
    password: 123456
    druid:
      initialSize: 10
      minIdle: 10
      maxActive: 30
      maxWait: 50000
# 指定zipkin持久化介質為mysql
zipkin:
  storage:
    type: mysql

啟動類中注入事務管理器

// 注入事務控制器
    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

微服務統一認證方案Spring Cloud OAuth2+JWT

認證:驗證用戶的合法身份,比如輸入用戶名和密碼,系統會在后台驗證用戶名和密碼是否合法,合法的前提下,才能夠進行后續的操作,訪問受保護的資源。

微服務架構下統一認證場景

每個服務實現一套認證邏輯不現實,需要由獨立的認證服務處理系統認證請求

image-20200705222447169

微服務架構下統一認證思路

1、基於session的認證方式

用session存儲用戶信息。解決分布式場景下session不一致,要使用Session共享方案、Session黏貼等方案

session方案的缺點:基於cookie,移動端不能有效使用

2、基於token的認證方式

服務端不用存儲認證數據,易維護擴展性強,客戶端可以把token存在任意地方,並且可以實現web和app統一認證機制。其缺點也很明顯,token由於自身包含信息,因此一般數據量較大,而且每次請求都需要傳遞,因此比較占帶寬。另外,token的簽名驗簽操作也會給cpu帶來額外的負擔

OAuth2開放授權協議/標准

OAuth(開放授權)是一個開放協議/標准,允許用戶授權第三方應用訪問他們存儲在另外的服務提供者上的信息,而不需要將用戶名和密碼提供給第三方應用或分享他們數據的所有內容。

允許用戶授權第三方應用訪問他們存儲在另外的服務提供者上的信息,而不需要將用戶名和密碼提供給第三方應用或分析他們數據的所有內容。

結合“使用QQ登錄拉鈎”這個場景拆分理解上述那句話

用戶:我們自己

第三方應用:拉勾網

另外的服務提供者:QQ

OAuth2是OAuth協議的延續版本,但不向后兼容OAuth1,即完全廢止了OAuth1。

OAuth2協議角色和流程

拉勾網要開發使用QQ登錄這個功能的話,那么拉勾網要提前到QQ平台進行登記(否則QQ憑什么陪着拉勾網玩授權登錄這件事)

1)拉勾網——登記——QQ平台

2)QQ平台會頒發一些參數給拉勾網,后續上線進行授權登錄的時候(打開授權頁面)需要攜帶這些參數

client_id:客戶端id(QQ最終相當於一個認證授權服務服務器,拉勾網就相當於一個客戶端了,所以會給一個客戶端ID),相當於賬號

secret:相當於密碼

image-20200709200309449

  • 資源擁有者:可以理解為用戶自己
  • 客戶端:我們想登陸的網站或應用,比如拉勾網
  • 認證服務器:可以理解為微信或QQ
  • 資源服務器:可以理解為微信或QQ

什么情況下需要使用OAuth2

第三方授權登錄的場景:比如,微信授權登錄、QQ授權登錄、微博授權登錄、抖音授權登錄、釘釘授權登錄。

單點登錄場景:如果項目中有很多微服務或公司的內部有很多服務,可以專門做一個認證中心(充當認證平台的角色),所有的服務都要到這個認證中心做認證,只做一次登錄,就可以在多個授權范圍內的服務中自由串行。

OAuth2的頒發Token授權方式

1)授權碼

2)密碼 提供用戶名+密碼換取token令牌

3)隱藏式

4)客戶端憑證

授權碼模式使用到了回調地址,是最復雜的授權方式,微博、微信、QQ等第三方登錄就是這種模式。我們重點講解接口對接中常用的密碼模式,也就是第二種。

Spring Cloud OAuth2+JWT實現

Spring Cloud OAuth2介紹

Spring Cloud OAuth2是Spring Cloud體系對OAuth2協議的實現,可以用來做多個微服務的統一認證(驗證身份合法性)授權(驗證權限)。通過向OAuth2服務發送某個類型的grant_type進行集中認證和授權,從而獲得access_token(訪問令牌),而這個token是受其他微服務信任的。

注意:使用OAuth2解決問題的本質是,引入一個認證授權層,認證授權連接了資源的擁有者,在授權層里面,資源的擁有者可以給第三方應用授權去訪問某些受保護的資源。

Spring Cloud OAuth2構建微服務統一認證服務思路

image-20200709224323241

注意:在我們統一認證的場景中,Resource Server其實就是我們的各種受保護的微服務,微服務中的各種API訪問接口就是資源,發起http請求的瀏覽器就是Client客戶端(對應為第三方應用)

搭建認證服務器

新建項目lagou-cloud-oauth-server-9999

pom.xml

<?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>lagou-parent</artifactId>
        <groupId>com.lagou.edu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>lagou-cloud-oauth-server-9999</artifactId>



    <dependencies>
        <!--導入Eureka Client依賴-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>


        <!--導入spring cloud oauth2依賴-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.security.oauth.boot</groupId>
                    <artifactId>spring-security-oauth2-autoconfigure</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.security.oauth.boot</groupId>
            <artifactId>spring-security-oauth2-autoconfigure</artifactId>
            <version>2.1.11.RELEASE</version>
        </dependency>
        <!--引入security對oauth2的支持-->
        <dependency>
            <groupId>org.springframework.security.oauth</groupId>
            <artifactId>spring-security-oauth2</artifactId>
            <version>2.3.4.RELEASE</version>
        </dependency>




        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>
        <!--操作數據庫需要事務控制-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
        </dependency>


        <dependency>
            <groupId>com.lagou.edu</groupId>
            <artifactId>lagou-service-common</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

    </dependencies>

</project>

application.yml(配置文件誤特別之處)

server:
  port: 9999
Spring:
  application:
    name: lagou-cloud-oauth-server
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/oauth2?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true
    username: root
    password: 123456
    druid:
      initialSize: 10
      minIdle: 10
      maxActive: 30
      maxWait: 50000
eureka:
  client:
    serviceUrl: # eureka server的路徑
      defaultZone: http://lagoucloudeurekaservera:8761/eureka/,http://lagoucloudeurekaserverb:8762/eureka/ #把 eureka 集群中的所有 url 都填寫了進來,也可以只寫一台,因為各個 eureka server 可以同步注冊表
  instance:
    #使用ip注冊,否則會使用主機名注冊了(此處考慮到對老版本的兼容,新版本經過實驗都是ip)
    prefer-ip-address: true
    #自定義實例顯示格式,加上版本號,便於多版本管理,注意是ip-address,早期版本是ipAddress
    instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@

入口類無特別之處

認證服務器配置類

package com.lagou.edu.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.jwt.crypto.sign.MacSigner;
import org.springframework.security.jwt.crypto.sign.SignatureVerifier;
import org.springframework.security.jwt.crypto.sign.Signer;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.token.*;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;

import javax.sql.DataSource;
import java.util.ArrayList;
import java.util.List;


/**
 * 當前類為Oauth2 server的配置類(需要繼承特定的父類 AuthorizationServerConfigurerAdapter)
 */
@Configuration
@EnableAuthorizationServer  // 開啟認證服務器功能
public class OauthServerConfiger extends AuthorizationServerConfigurerAdapter {


    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private LagouAccessTokenConvertor lagouAccessTokenConvertor;


    private String sign_key = "lagou123"; // jwt簽名密鑰


    /**
     * 認證服務器最終是以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()// 客戶端信息存儲在什么地方,可以在內存中,可以在數據庫里
                .withClient("client_lagou")  // 添加一個client配置,指定其client_id
                .secret("abcxyz")                   // 指定客戶端的密碼/安全碼
                .resourceIds("autodeliver")         // 指定客戶端所能訪問資源id清單,此處的資源id是需要在具體的資源服務器上也配置一樣
                // 認證類型/令牌頒發模式,可以配置多個在這里,但是不一定都用,具體使用哪種方式頒發token,需要客戶端調用的時候傳遞參數指定
                .authorizedGrantTypes("password","refresh_token")
                // 客戶端的權限范圍,此處配置為all全部即可
                .scopes("all");*/

        // 從數據庫中加載客戶端詳情
        clients.withClientDetails(createJdbcClientDetailsService());

    }

    @Autowired
    private DataSource dataSource;

    @Bean
    public JdbcClientDetailsService createJdbcClientDetailsService() {
        JdbcClientDetailsService jdbcClientDetailsService = new JdbcClientDetailsService(dataSource);
        return jdbcClientDetailsService;
    }


    /**
     * 認證服務器是玩轉token的,那么這里配置token令牌管理相關(token此時就是一個字符串,當下的token需要在服務器端存儲,
     * 那么存儲在哪里呢?都是在這里配置)
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        super.configure(endpoints);
        endpoints
                .tokenStore(tokenStore())  // 指定token的存儲方法
                .tokenServices(authorizationServerTokenServices())   // token服務的一個描述,可以認為是token生成細節的描述,比如有效時間多少等
                .authenticationManager(authenticationManager) // 指定認證管理器,隨后注入一個到當前類使用即可
                .allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST);
    }


    /*
        該方法用於創建tokenStore對象(令牌存儲對象)
        token以什么形式存儲
     */
    public TokenStore tokenStore(){
        //return new InMemoryTokenStore();
        // 使用jwt令牌
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    /**
     * 返回jwt令牌轉換器(幫助我們生成jwt令牌的)
     * 在這里,我們可以把簽名密鑰傳遞進去給轉換器對象
     * @return
     */
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        jwtAccessTokenConverter.setSigningKey(sign_key);  // 簽名密鑰
        jwtAccessTokenConverter.setVerifier(new MacSigner(sign_key));  // 驗證時使用的密鑰,和簽名密鑰保持一致
        jwtAccessTokenConverter.setAccessTokenConverter(lagouAccessTokenConvertor);

        return jwtAccessTokenConverter;
    }




    /**
     * 該方法用戶獲取一個token服務對象(該對象描述了token有效期等信息)
     */
    public AuthorizationServerTokenServices authorizationServerTokenServices() {
        // 使用默認實現
        DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
        defaultTokenServices.setSupportRefreshToken(true); // 是否開啟令牌刷新
        defaultTokenServices.setTokenStore(tokenStore());

        // 針對jwt令牌的添加
        defaultTokenServices.setTokenEnhancer(jwtAccessTokenConverter());

        // 設置令牌有效時間(一般設置為2個小時)
        defaultTokenServices.setAccessTokenValiditySeconds(20); // access_token就是我們請求資源需要攜帶的令牌
        // 設置刷新令牌的有效時間
        defaultTokenServices.setRefreshTokenValiditySeconds(259200); // 3天

        return defaultTokenServices;
    }
}

關於三個configure方法

  • configure(ClientDetailServiceConfigurer clients)

用來配置客戶端詳情服務(ClientDetailService),客戶端詳情信息在這里進行初始化,你能夠把客戶端詳情信息寫死在這里或者通過數據庫來存儲調取詳情信息

  • configure(AuthorizationServerEndpointsConfigurer endpoints)

用來配置令牌(token)的訪問端點和令牌服務(token services)

  • configure(AuthorizationServerSecurityConfigurer oauthServer)

用來配置令牌端點的安全約束

關於TokenStore

  • InMemoryTokenStore

默認采用,可以在開發階段使用

  • JdbcTokenStore

這是一個基於JDBC的實現版本,令牌會被保存進關系型數據庫。可以在不同的服務器之間共享令牌信息,使用這個版本的時候請注意把spring-jdbc依賴加入到classpath中

  • JwtTokenStore

JSON Web Token(JWT),它可以把令牌相關的數據進行編碼(因此對於后端服務來說,它不需要進行存儲,這將是一個重大的優勢),缺點就是這個令牌占用的空間會比較大,如果你加入了比較多的用戶憑證信息,JwtTokenStore不會保存任何數據。

認證服務器安全配置類

package com.lagou.edu.config;

import com.lagou.edu.service.JdbcUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;


/**
 * 該配置類,主要處理用戶名和密碼的校驗等事宜
 */
@Configuration
public class SecurityConfiger extends WebSecurityConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private JdbcUserDetailsService jdbcUserDetailsService;

    /**
     * 注冊一個認證管理器對象到容器
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }


    /**
     * 密碼編碼對象(密碼不進行加密處理)
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    /**
     * 處理用戶名和密碼驗證事宜
     * 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);*/

        auth.userDetailsService(jdbcUserDetailsService).passwordEncoder(passwordEncoder);
    }
}

測試

獲取token:http://localhost:9999/oauth?client_secret=abcxyz&grant_type=password&username=admin&password=123456&client_id=client_lagou

  • endpoint:/oauth/token
  • 獲取token攜帶的參數
    • client_id:
    • client_secret:
    • grant_type:指定使用哪種頒發類型,password
    • username:
    • password:

校驗token:http://localhost:9999/oauth/check_token=

刷新token:http://localhost:9999/oauth/token?grant_type=refresh_token&client_id=client_lagou&client_secret=abcxyz&refresh_token=

資源服務器(希望訪問被認證的微服務)Resource Server配置

  • 資源服務配置類
package com.lagou.edu.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.jwt.crypto.sign.MacSigner;
import org.springframework.security.jwt.crypto.sign.RsaVerifier;
import org.springframework.security.jwt.crypto.sign.SignatureVerifier;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.RemoteTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;

@Configuration
@EnableResourceServer  // 開啟資源服務器功能
@EnableWebSecurity  // 開啟web訪問安全
public class ResourceServerConfiger extends ResourceServerConfigurerAdapter {

    private String sign_key = "lagou123"; // jwt簽名密鑰

    @Autowired
    private LagouAccessTokenConvertor lagouAccessTokenConvertor;

    /**
     * 該方法用於定義資源服務器向遠程認證服務器發起請求,進行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("client_lagou");
        remoteTokenServices.setClientSecret("abcxyz");

        // 別忘了這一步
        resources.tokenServices(remoteTokenServices);*/


        // jwt令牌改造
        resources.resourceId("autodeliver").tokenStore(tokenStore()).stateless(true);// 無狀態設置
    }


    /**
     * 場景:一個服務中可能有很多資源(API接口)
     *    某一些API接口,需要先認證,才能訪問
     *    某一些API接口,壓根就不需要認證,本來就是對外開放的接口
     *    我們就需要對不同特點的接口區分對待(在當前configure方法中完成),設置是否需要經過認證
     *
     * @param http
     * @throws Exception
     */
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http    // 設置session的創建策略(根據需要創建即可)
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
                .and()
                .authorizeRequests()
                .antMatchers("/autodeliver/**").authenticated() // autodeliver為前綴的請求需要認證
                .antMatchers("/demo/**").authenticated()  // demo為前綴的請求需要認證
                .anyRequest().permitAll();  //  其他請求不認證
    }




    /*
       該方法用於創建tokenStore對象(令牌存儲對象)
       token以什么形式存儲
    */
    public TokenStore tokenStore(){
        //return new InMemoryTokenStore();

        // 使用jwt令牌
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    /**
     * 返回jwt令牌轉換器(幫助我們生成jwt令牌的)
     * 在這里,我們可以把簽名密鑰傳遞進去給轉換器對象
     * @return
     */
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        jwtAccessTokenConverter.setSigningKey(sign_key);  // 簽名密鑰
        jwtAccessTokenConverter.setVerifier(new MacSigner(sign_key));  // 驗證時使用的密鑰,和簽名密鑰保持一致
        jwtAccessTokenConverter.setAccessTokenConverter(lagouAccessTokenConvertor);
        return jwtAccessTokenConverter;
    }

}

思考:當我們第一次登錄之后,認證服務器頒發token並將其存儲在認證服務器中,后期我們訪問資源服務器時會攜帶token,資源服務器會請求認證服務器驗證token有效性,如果資源服務器很多,那么認證服務器壓力會很大。。。。

另外,資源服務器向認證服務器check_token,獲取的也是用戶信息UserInfo,能否把用戶信息存儲到令牌中,讓客戶端一值持有這個令牌,令牌的驗證也在資源服務器進行,這樣避免和認證服務器頻繁的交互。。。

我們可以考慮使用JWT進行改造,使用JWT機制之后,資源服務器不需要訪問認證服務器。。。

JWT改造統一認證授權中心的令牌存儲機制

JWT令牌介紹

通過上邊的測試我們發現,當資源服務和授權服務不在一起時資源服務使用RemoteTokenServices遠程請求授權服務驗證token,如果訪問量較大將會影響系統的性能。

解決上邊的問題:令牌采用JWT格式即可解決上邊的問題,用戶認證通過會得到一個JWT令牌,JWT令牌中已經包括了用戶相關的信息,客戶端只需要攜帶JWT訪問資源服務,資源服務根據事先約定的算法自行完成令牌校驗,無需每次請求認證服務完成授權。

資源服務接收到請求,需要請求認證服務器驗證token,當資源服務器很多時,這個可能存在瓶頸,使用JWT令牌,令牌本身包含用戶信息,可以通過事先約定的算法在資源服務器內部完成令牌校驗。

1)什么是JWT?

JSON Web Token(JWT)是一個開放的行業標准(RFC 7519),它定義了一種簡潔的、自包含的協議格式,用於在通信雙方傳遞json對象,傳遞的信息經過數字簽名可以被驗證和信任。JWT可以使用HMAC算法或使用RSA的公鑰/私鑰來對簽名驗證,防止被篡改。

2)JWT令牌結構

JWT令牌由三部分組成,每部分中間使用點(.)分割,比如:xxx.yyy.zzz

  • Header

頭部包括令牌的類型(即JWT)及使用的哈希算法(如HMAC SHA256或RSA),例如

{
  "alg":"HS256",
  "typ":"JWT"
}

將上邊的內容使用Base64Url編碼,得到一個字符串就是JWT令牌的第一部分。

  • Payload

第二部分是負載,內容也是一個json對象,它是存放有效信息的地方,它可以存放jwt提供的現成字段,比如:iss(簽發者),ex(過期時間戳),sub(面向的用戶)等,也可以自定義字段。此部分不建議存放敏感信息,因為此部分可以解碼還原原始內容。最后將第二部分負載使用Base64Url編碼,得到一個字符串就是JWT令牌的第二部分,一個例子:

{
  "sub":"1234567890",
  "name":"Johh Doe",
  "iat":1516239022
}
  • Signature

第三部分是簽名,此部分用於防止jwt內容被篡改。這個部分使用base64url將前兩部分進行編碼,編碼后使用點(.)連接組成字符串,最后使用header中聲明簽名算法進行簽名。

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

base64UrlEncode(header):jwt令牌的第一部分

base64UrlEncode(payload):jwt令牌的第二部分

secret:簽名所使用的密鑰

認證服務器端JWT改造(改造主配置類)

/*
        該方法用於創建tokenStore對象(令牌存儲對象)
        token以什么形式存儲
     */
    public TokenStore tokenStore(){
        //return new InMemoryTokenStore();
        // 使用jwt令牌
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    /**
     * 返回jwt令牌轉換器(幫助我們生成jwt令牌的)
     * 在這里,我們可以把簽名密鑰傳遞進去給轉換器對象
     * @return
     */
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        jwtAccessTokenConverter.setSigningKey(sign_key);  // 簽名密鑰
        jwtAccessTokenConverter.setVerifier(new MacSigner(sign_key));  // 驗證時使用的密鑰,和簽名密鑰保持一致
        jwtAccessTokenConverter.setAccessTokenConverter(lagouAccessTokenConvertor);

        return jwtAccessTokenConverter;
    }

修改jwt令牌服務方法

image-20200712131804267

資源服務器校驗JWT令牌

不需要和遠程認證服務器交互,添加本地tokenStore

package com.lagou.edu.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.jwt.crypto.sign.MacSigner;
import org.springframework.security.jwt.crypto.sign.RsaVerifier;
import org.springframework.security.jwt.crypto.sign.SignatureVerifier;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.RemoteTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;

@Configuration
@EnableResourceServer  // 開啟資源服務器功能
@EnableWebSecurity  // 開啟web訪問安全
public class ResourceServerConfiger extends ResourceServerConfigurerAdapter {

    private String sign_key = "lagou123"; // jwt簽名密鑰

    @Autowired
    private LagouAccessTokenConvertor lagouAccessTokenConvertor;

    /**
     * 該方法用於定義資源服務器向遠程認證服務器發起請求,進行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("client_lagou");
        remoteTokenServices.setClientSecret("abcxyz");

        // 別忘了這一步
        resources.tokenServices(remoteTokenServices);*/


        // jwt令牌改造
        resources.resourceId("autodeliver").tokenStore(tokenStore()).stateless(true);// 無狀態設置
    }


    /**
     * 場景:一個服務中可能有很多資源(API接口)
     *    某一些API接口,需要先認證,才能訪問
     *    某一些API接口,壓根就不需要認證,本來就是對外開放的接口
     *    我們就需要對不同特點的接口區分對待(在當前configure方法中完成),設置是否需要經過認證
     *
     * @param http
     * @throws Exception
     */
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http    // 設置session的創建策略(根據需要創建即可)
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
                .and()
                .authorizeRequests()
                .antMatchers("/autodeliver/**").authenticated() // autodeliver為前綴的請求需要認證
                .antMatchers("/demo/**").authenticated()  // demo為前綴的請求需要認證
                .anyRequest().permitAll();  //  其他請求不認證
    }




    /*
       該方法用於創建tokenStore對象(令牌存儲對象)
       token以什么形式存儲
    */
    public TokenStore tokenStore(){
        //return new InMemoryTokenStore();

        // 使用jwt令牌
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    /**
     * 返回jwt令牌轉換器(幫助我們生成jwt令牌的)
     * 在這里,我們可以把簽名密鑰傳遞進去給轉換器對象
     * @return
     */
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        jwtAccessTokenConverter.setSigningKey(sign_key);  // 簽名密鑰
        jwtAccessTokenConverter.setVerifier(new MacSigner(sign_key));  // 驗證時使用的密鑰,和簽名密鑰保持一致
        jwtAccessTokenConverter.setAccessTokenConverter(lagouAccessTokenConvertor);
        return jwtAccessTokenConverter;
    }

}

從數據庫加載OAuth2客戶端信息

  • 創建表並初始化數據(表名和字段保持固定)
SET NAMES utf8mb4; 
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------

-- Table structure for oauth_client_details

-- ---------------------------
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;

-- ----------------------------

-- Records of oauth_client_details

-- ---------------------------
BEGIN; 
INSERT INTO `oauth_client_details` VALUES ('client_lagou123', 'autodeliver,resume', 'abcxyz', 'all', 'password,refresh_token', NULL, NULL, 7200, 259200, NULL, NULL); 
COMMIT;

SET FOREIGN_KEY_CHECKS = 1;
  • 配置數據源
server:
  port: 9999
Spring:
  application:
    name: lagou-cloud-oauth-server
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/oauth2?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true
    username: root
    password: 123456
    druid:
      initialSize: 10
      minIdle: 10
      maxActive: 30
      maxWait: 50000
eureka:
  client:
    serviceUrl: # eureka server的路徑
      defaultZone: http://lagoucloudeurekaservera:8761/eureka/,http://lagoucloudeurekaserverb:8762/eureka/ #把 eureka 集群中的所有 url 都填寫了進來,也可以只寫一台,因為各個 eureka server 可以同步注冊表
  instance:
    #使用ip注冊,否則會使用主機名注冊了(此處考慮到對老版本的兼容,新版本經過實驗都是ip)
    prefer-ip-address: true
    #自定義實例顯示格式,加上版本號,便於多版本管理,注意是ip-address,早期版本是ipAddress
    instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@

  • 認證服務器主配置類改造
 /**
     * 客戶端詳情配置,
     *  比如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()// 客戶端信息存儲在什么地方,可以在內存中,可以在數據庫里
                .withClient("client_lagou")  // 添加一個client配置,指定其client_id
                .secret("abcxyz")                   // 指定客戶端的密碼/安全碼
                .resourceIds("autodeliver")         // 指定客戶端所能訪問資源id清單,此處的資源id是需要在具體的資源服務器上也配置一樣
                // 認證類型/令牌頒發模式,可以配置多個在這里,但是不一定都用,具體使用哪種方式頒發token,需要客戶端調用的時候傳遞參數指定
                .authorizedGrantTypes("password","refresh_token")
                // 客戶端的權限范圍,此處配置為all全部即可
                .scopes("all");*/

        // 從數據庫中加載客戶端詳情
        clients.withClientDetails(createJdbcClientDetailsService());

    }

    @Autowired
    private DataSource dataSource;

    @Bean
    public JdbcClientDetailsService createJdbcClientDetailsService() {
        JdbcClientDetailsService jdbcClientDetailsService = new JdbcClientDetailsService(dataSource);
        return jdbcClientDetailsService;
    }

從數據庫驗證用戶合法性

  • 創建數據表users(表名不需固定),初始化數據
SET NAMES utf8mb4; 
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------

-- Table structure for users

-- ---------------------------
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;

-- ----------------------------

-- Records of users

-- ---------------------------
BEGIN; 
INSERT INTO `users` VALUES (4, 'lagou-user', 'iuxyzds'); 
COMMIT;

SET FOREIGN_KEY_CHECKS = 1;
  • 操作數據表的JPA配置及DAO接口
public interface UsersRepository extends JpaRepository<Users,Long> {
    Users findByUsername(String username);
}
  • 開發UserDetailsService接口的實現類,根據用戶名從數據庫加載用戶信息
@Service
public class JdbcUserDetailsService implements UserDetailsService {

    @Autowired
    private UsersRepository usersRepository;

    /**
     * 根據username查詢出該用戶的所有信息,封裝成UserDetails類型的對象返回,至於密碼,框架會自動匹配
     * @param username
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Users users = usersRepository.findByUsername(username);
        return new User(users.getUsername(),users.getPassword(),new ArrayList<>());
    }
}
  • 使用自定義的用戶詳情服務對象
@Autowired
    private JdbcUserDetailsService jdbcUserDetailsService;

/**
     * 處理用戶名和密碼驗證事宜
     * 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);*/

        auth.userDetailsService(jdbcUserDetailsService).passwordEncoder(passwordEncoder);
    }

基於OAuth2的JWT令牌信息擴展

OAuth2幫我們生產的JWT令牌載荷部分信息有限,關於用戶信息只有一個user_name,有些場景下我們希望放入一些擴展信息項,比如,之前我們經常向session中存入userid,或者現在我們希望在JWT的載荷部分存入當時請求令牌的客戶端IP,客戶端攜帶令牌訪問資源服務器時,可以對比當前請求的客戶端真是IP和令牌存放的客戶端IP是否匹配,不匹配拒絕請求,以此進一步提高安全性。那么如何在OAuth2環境下向JWT令牌中存入擴展信息呢?

  • 認證服務器生存JWT令牌時存入擴展信息(比如clientIp)

繼承DefaultAccessTokenConverter類,重寫convertAccessToken方法,存入擴展信息

@Component
public class LagouAccessTokenConvertor extends DefaultAccessTokenConverter {


    @Override
    public Map<String, ?> convertAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {
        // 獲取到request對象
        HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.getRequestAttributes())).getRequest();
        // 獲取客戶端ip(注意:如果是經過代理之后到達當前服務的話,那么這種方式獲取的並不是真實的瀏覽器客戶端ip)
        String remoteAddr = request.getRemoteAddr();
        Map<String, String> stringMap = (Map<String, String>) super.convertAccessToken(token, authentication);
        stringMap.put("clientIp",remoteAddr);
        return stringMap;
    }
}

將自定義的轉換器對象注入

    /**
     * 返回jwt令牌轉換器(幫助我們生成jwt令牌的)
     * 在這里,我們可以把簽名密鑰傳遞進去給轉換器對象
     * @return
     */
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        jwtAccessTokenConverter.setSigningKey(sign_key);  // 簽名密鑰
        jwtAccessTokenConverter.setVerifier(new MacSigner(sign_key));  // 驗證時使用的密鑰,和簽名密鑰保持一致
        jwtAccessTokenConverter.setAccessTokenConverter(lagouAccessTokenConvertor);

        return jwtAccessTokenConverter;
    }

資源服務器取出JWT令牌擴展信息

資源服務器也需要自定義一個轉換器類,繼承DefaultAccessTokenConverter類,重寫extractAuthentication提取方法,把載荷信息設置到認證對象的details屬性中

@Component
public class LagouAccessTokenConvertor extends DefaultAccessTokenConverter {


    @Override
    public OAuth2Authentication extractAuthentication(Map<String, ?> map) {

        OAuth2Authentication oAuth2Authentication = super.extractAuthentication(map);
        oAuth2Authentication.setDetails(map);  // 將map放入認證對象中,認證對象在controller中可以拿到
        return oAuth2Authentication;
    }
}

業務類比如Controller類中,可以通過如下方法獲取認證對象,進一步獲取到擴展信息

 @GetMapping("/test")
    public String findResumeOpenState() {
        Object details = SecurityContextHolder.getContext().getAuthentication().getDetails();
        return "demo/test!";
    }

JWT注意事項

關於JWT令牌我們需要注意

  • JWT令牌就是一種可以被驗證的數據組織格式,玩法靈活,我們這里基於Spring Cloud OAuth2創建、校驗JWT令牌
  • 我們也可以自己寫工具類生成、校驗JWT令牌
  • JWT令牌中不要存放過於敏感的信息,因為我們拿到令牌后,可以解碼看到載荷部分的信息
  • JWT令牌每次請求都會攜帶,內容過多的話,會增加網絡寬帶占用

第七部分 第二代Spring Cloud核心組件(Spring Cloud Alibaba)

Nacos(服務注冊和配置中心)

Sentinel哨兵(服務熔斷、限流)

Dubbo RPC/LB

Seata分布式事務解決方案

Nacos服務注冊和配置中心

Nacos介紹

Nacos就是注冊中心+配置中心的組合(Nacos=Eureka+Config+Bus)

Nacos功能特性

  • 服務發現於健康檢查
  • 動態配置管理
  • 動態DNS服務
  • 服務和元數據管理,動態的服務權重調整、動態服務優雅下線

Nacos單實例部署

運行

unix:sh startup.sh -m standalone

訪問nacos管理控制台

http://127.0.0.1/8848/nacos

用戶名和密碼默認都是nacos

Nacos注冊中心

保護閾值

可以設置0-1之間的浮點數,是一個比值(當前服務健康實例數/當前服務總實例數)

當保護閾值觸發,nacos會把該服務的所有實例信息(健康的+不健康的)全部提供給消費者,消費者可能訪問到不健康的實例,請求失敗,但這樣也比造成雪崩好。

服務消費者從Nacos獲取服務

負載均衡

Nacos客戶端引入的時候,會關聯引入Ribbon的依賴包,我們使用OpenFiegn的時候也會引入Ribbon的依賴,Ribbon包括Hystrix都按原來方式進行配置即可。

Nacos會默認使用Ribbon客戶端負載均衡,如果想換成Dubbo LB呢

Nacos數據模型(領域模型)

Namespace命名空間、Group分組、集群這些都是為了進行歸類管理,把服務和配置文件進行歸類,歸類之后就可以實現一定的效果,比如隔離

比如,對於服務來說,不同命名空間中的服務不能互相訪問調用

image-20200712210310147

Namespace:命名空間,對不同環境進行隔離,比如開發環境、測試環境、生產環境

Group:分組,將若干個服務或若干個配置集歸為一組,通常習慣一個系統歸位一個組

Service:某一個服務,比如簡歷微服務

DataId:配置集或者可以認為是一個配置文件

Namespace+Group+Service如同Maven中的GAV坐標,GAV坐標是為了鎖定jar,而這里是為了鎖定服務

Namespace+Group+DataId如同Maven中的GAV坐標,GAV坐標是為了鎖定jar,而這里是為了鎖定配置文件

最佳實踐

Nacos抽象了Namespace、Group、Service、DataId等概念,具體代表什么取決於怎么用(非常靈活),推薦用法如下:

概念 描述
Namespace 代表不同的環境,如開發環境、測試環境、生產環境
Group 代表某項目,比如拉勾雲項目
Service 某個項目中具體的服務
DataId 某個項目中具體的配置文件
  • Nacos服務的分級模型

image-20200712211009939

Nacos Server數據持久化

Nacos默認使用嵌入式數據庫進行數據存儲,它支持改為外部MySQL存儲

  • 新建數據庫nacos_config,數據庫初始化腳本文件${nacoshome}/conf/nacos-mysql.sql
  • 修改${nacoshome}/conf/appcalition.properties
spring.datasource.platform=mysql

db.num=1

db.url.0=jdbc:mysql://127.0.0.1:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
db.user=root
db.password=123456

Nacos Server集群

  • 安裝3個或3個以上的Nacos

復制解壓后的nacos文件夾,分別命名為nacos-01、nacos-02、nacos-03

  • 修改配置文件

    • 同一台機器模擬,將上述三個文件夾中application.properties中的server.port分別改為8848、8849、8850,同時給當前實例節點綁定ip,因為服務器可能綁定多個ip。
    nacos.inetutils.ip-address=127.0.0.1
    
    • 復制一份conf/cluster.conf.example文件,命名為cluster.conf,在配置文件中設置集群中每一個節點的信息
    127.0.0.1:8848
    127.0.0.1:8849
    127.0.0.1:8850
    
  • 分別啟動每一個實例(可以批處理腳本完成)

sh startup.sh -m cluster

Nacos配置中心

在Nacos控制台添加配置,或者通過API添加配置

一個DataId表示一個配置文件


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM