OAuth2在分佈式微服務架構下基於角色的權限設計(RBAC)

在前兩節的基礎上,對權限控制作進一步的分析與設計。

RBAC(Role-Base Access Control,基於角色的訪問控制)

本篇內容基於個人理解,不當之處,歡迎批評指正。

前兩篇內容:

1、OAuth2中用戶訪問的基本流程

在這裏插入圖片描述

  • 用戶經過認證/授權後,進入客戶端(認證中心給客戶端發放令牌),客戶端攜帶令牌訪問對應的資源。
  • 客戶端是用戶和資源之外的第三方,要想訪問資源必須得到用戶的允許。
  • 用戶擁有資源,通過客戶端去訪問,把訪問權限賦於給了客戶端。

2、SCOPE、ROLE、AUTH 區別

  • SCOPE:範圍;指用戶授權客戶端可以訪問的範圍。客戶端只能在這個範圍內去訪問。是針對客戶端來說的。
  • ROLE:角色;是用戶的身份。是針對用戶來說的。
  • AUTH:權限;是角色所擁有的。角色與權限是多對多的關係;一個角色可以有多個權限,一個權限也可以同時被多個角色所擁有。權限也可以直接針對於用戶,如果用戶不指定角色,可以直接把權限賦於用戶。
區別 含義 面向對象
SCOPE 範圍 客戶端
ROLE 角色 用戶
AUTH 權限 角色 或 用戶

在這裏插入圖片描述

在這裏插入圖片描述

在這裏插入圖片描述

3、server、resource、client 中訪問主體的區別

在這裏插入圖片描述

從圖中可以看出,在每個系統中的訪問主體及權限是不同的(這裏的權限是統稱,包括SCOPE、ROLE、AUTH,不僅僅指AUTH)

  • 當用戶登錄後,在認證中心內,訪問主體就是 第三方用戶它的權限是他在認證中心中的權限,和我方系統無關

  • 在客戶端中,訪問主體還是 第三方用戶,權限包括:用戶授於客戶端的 SCOPE,以及 ROLE_USER

    ROLE_USER 表示這是一個經過認證的用戶,不管第三方用戶在第三方系統中是什麼身份,只要進入到我方系統中,就是 ROLE_USER 身份;對應於 ROLE_ANONYMOUS(未認證用戶)

  • 客戶端攜帶令牌訪問資源,在資源服務器中訪問主體就是 客戶端,權限只有:SCOPE;因爲客戶端是在用戶授權下去訪問的,所以在認證中心生成令牌時,只包括了用戶授於的 SCOPE,不可能把用戶的身份ROLE也賦於客戶端。

在這裏插入圖片描述

在這裏插入圖片描述

在這裏插入圖片描述

4、訪問控制分析

通過上面的分析可以發現,資源端只有 SCOPE,不可能用 ROLE 或 AUTH 去控制用戶的訪問。認證中心不負責訪問資源,要想通過 ROLE 或 AUTH 去控制用戶訪問資源,只能在 客戶端 去操作。資源API在客戶端有對應的接口,要想控制資源API,就控制客戶端的對應接口就可以了。只要用戶能訪問客戶端的某個API接口,它就能訪問與之對應的資源API。

  • 資源API 面向 SCOPE 開放
  • 客戶端API 面向 ROLE 或 AUTH 開放

但是,所有第三方用戶,進入我方系統後,都具有 ROLE_USER 身份,身份是一樣的,如何在客戶端中通過 ROLE 或 AUTH 去控制用戶訪問資源呢?

解決方案添加本地用戶,賦於不同的 ROLE 或 AUTH ;第三方用戶與本地用戶實現綁定;通過本地用戶的 ROLE 或 AUTH 去控制用戶訪問資源。這是三方登錄的一個通用做法。

那第三方用戶進入我方系統後,如何改變他的身份?把本地的 ROLE 或 AUTH 賦給他呢?辦法就是權限提升

在這裏插入圖片描述

5、客戶端權限提升

  • 第三方用戶進入我方系統後,從 SecurityContextHolder 中獲取第三方用戶的 name 和 authorities
  • 根據第三方用戶的 name ,查詢綁定的本地用戶,進而得到本地用戶的 authorities
  • 把本地 authorities 加入到 第三方用戶的 authorities 中
  • 重新生成新的 Authentication
  • 注入 SecurityContextHolder中,替換原來的 authorities,完成權限提升
public class IndexController {
    @Autowired
    UserDetailsService userDetailsService;

    @GetMapping("/")
    public String user(Model model) {
        // 從安全上下文中獲取登錄信息,返回給model
        Map<String, Object> map = new HashMap<>(5);

        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        String username = auth.getName();
        map.put("當前用戶", username);
        map.put("原來權限", auth.getAuthorities());

        // 使用Set,不使用List;List可以存重複元素;登錄後,在首頁刷新,List會重複添加
        //List<GrantedAuthority> authorities = new ArrayList<>(auth.getAuthorities());
        Set<GrantedAuthority> authorities = new HashSet<>(auth.getAuthorities());

        // 根據三方用戶查綁定的本地用戶
        String localUser = getLocalUser(username);
        UserDetails userDetails = userDetailsService.loadUserByUsername(localUser);
        map.put("本地用戶", localUser);
        // 本地用戶權限
        //List<GrantedAuthority> authorities1 = new ArrayList<>(userDetails.getAuthorities());
        Set<GrantedAuthority> authorities1 = new HashSet<>(userDetails.getAuthorities());
        map.put("本地用戶權限", authorities1);
        // 把本地用戶權限加入原來權限集中
        authorities.addAll(authorities1);
        map.put("新的權限", authorities);
        // 生成新的認證信息
        Authentication newAuth = new OAuth2AuthenticationToken((OAuth2User) auth.getPrincipal(),authorities,"myClient");
        // 重置認證信息
        SecurityContextHolder.getContext().setAuthentication(newAuth);
        model.addAttribute("user", map);
        return "index";
    }

    /**
     * 模擬通過第三方用戶,得到本地用戶
     * @param remoteUsername
     * @return
     */
    private String getLocalUser(String remoteUsername){
        String u = "";
        // 模擬通過三方用戶查本地用戶
        if(StringUtils.isNotEmpty(remoteUsername)){
            u = "local_admin";
        }
        return u;
    }
}
@Configuration
public class SecurityConfiguration {
    /**
     * 虛擬一個本地用戶
     *
     * @return UserDetailsService
     */
    @Bean
    UserDetailsService userDetailsService() {
        return username -> User.withUsername("local_admin")
                .password("123456")
                .roles("TEST","ABC")
                //.authorities("ROLE_ADMIN", "ROLE_USER")
                .build();
    }
}

在這裏插入圖片描述

在這裏插入圖片描述

在這裏插入圖片描述

  • 訪問測試

在這裏插入圖片描述

在這裏插入圖片描述

在這裏插入圖片描述

在這裏插入圖片描述

6、權限設計

  • 客戶端
客戶端分類 被授於的 SCOPE
電腦端 SCOPE_1
手機端 SCOPE_2
內部資源服務 SCOPE_0
  • 資源端
資源分類 允許訪問的 SCOPE 說明
r1/res1 SCOPE_0、SCOPE_1、SCOPE_2 資源服務器1 中的 資源1,可以被三個客戶端訪問
r1/res2 SCOPE_0、SCOPE_1 資源服務器1 中的 資源2,只可以被電腦端、內部資源訪問
r2/res1 SCOPE_0、SCOPE_2 資源服務器2 中的 資源1,只可以被手機端、內部資源訪問
r2/res2 SCOPE_1、SCOPE_2 資源服務器2 中的 資源2,只可以被電腦端、手機端訪問
r3/res1 SCOPE_2 資源服務器3 中的 資源1,只可以被手機端訪問
r3/res2 SCOPE_0 資源服務器3 中的 資源2,只可以被內部資源訪問
  • 用戶與角色
用戶 角色
張三 ROLE_1
李四 ROLE_2
  • 角色與權限
權限 角色
AUTH_1 ROLE_1
AUTH_2 ROLE_2
AUTH_3 ROLE_1、ROLE_2
AUTH_4 ROLE_2

ROLE_1:包含 AUTH_1、AUTH_3

ROLE_2:包含 AUTH_2、AUTH_3、AUTH_4

在這裏插入圖片描述

在這裏插入圖片描述

  • 客戶端與資源的訪問綁定關係是一一對應的,應該相應穩定。客戶端能訪問某個資源就提供一個接口。不能隨時修改。

  • 用戶通過角色訪問客戶端中的服務API,這個關係比較靈活, 相對鬆散。客戶端中只需指定某個接口可以被哪些AUTH訪問即可。

  • 角色ROLE與權限AUTH的關係相對穩定,但可以比客戶端和資源的關係靈活,可以修改編輯。

  • 在項目設計階段,應該首先確定客戶端的種類,再基本確認項目中所涉及的角色。根據資源API功能,決定需要哪些權限,應該把權限賦於哪種角色。

1、項目概述

1.1、概述

  • Server + Resource + Client

  • 功能完善:

    • 授權中心Server: 進行認證、授權,併發放token、刷新token,不負責token鑑權(由資源服務器自行鑑權);
    • 資源服務器Resource:提供資源,需要攜帶token請求,可以自行鑑權;
    • 客戶端Client:面向用戶的操作入口;向Server請求token,攜帶token訪問Resource;
  • 實現單點登錄;讓授權和鑑權解耦;所有授權操作統一由授權中心完成,資源服務(各微服務)只需要鑑別請求的權限,不需要關心它的權限哪裏獲取。

  • 獲取token的模式:授權碼模式(用於用戶訪問資源)、客戶端模式(用於微服務間相互訪問)。

  • 項目只關注核心流程,儘可能剝離無關的實現;如:數據庫操作僅在授權中心中實現(jdbc),其餘地方採用模擬數據。

  • 在一些細節的實現上有不錯的地方,也有不少拙的地方,歡迎批評指正。

  • 各模塊可以分別部署;本項目爲了測試方便,採用單機部署。

  • 由於oauth2底層實現錯綜複雜,想完全搞懂太難。本項目側重於需求實現,並儘可能剖析原理。在demo的前提下,再深入理解領會底層。沒有demo,一開始就想深入底層,這個路很難。

  • 本人對學習oauth2總結的一點拙見:

    1、先對oauth2有個總體上的認識,能說出個123來

    2、再着力實現一個相對完整的demo

    3、然後再結合實際需求,不斷地debug,不斷地優化,在此過程中學習的深度也得到了加強

1.2、整體架構圖

整體架構圖;具體實現中會有細節圖

在這裏插入圖片描述

  • 授權碼模式:適用於用戶訪問;需要登錄/授權,發放授權碼,申請令牌,刷新令牌等等
  • 客戶端模式:適用於微服務(資源)間的相互訪問;請求時只需要提供客戶端ID、密鑰,直接發放令牌

1.3、搭建環境

  • Spring Security 5.6.3 (Client/Resource)
  • Spring Authorization Server 0.2.3
  • Spring Boot 2.6.7
  • jdk 1.8
  • mysql 5.7
  • lombok、log4j、fastjson2 …

2、項目結構搭建

模塊 端口 說明
oauth2-server-resource-client 父工程
oauth2-client-8000 8000 項目首頁(oauth2客戶端)
oauth2-server-9000 9000 認證授權中心(oauth2服務端)
oauth2-resource-a-8001 8001 微服務A(oauth2資源服務器),受保護對象
oauth2-resource-b-8002 8002 微服務B(oauth2資源服務器),受保護對象

2.1、父工程

創建普通meven工程 oauth2-server-resource-client;打包格式pom,刪除 src

  • 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">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.tuwer</groupId>
    <artifactId>oauth2-server-resource-client</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>pom</packaging>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <mysql-connector-java.version>8.0.29</mysql-connector-java.version>
        <lombok.version>1.18.22</lombok.version>
        <log4j.version>1.2.17</log4j.version>
        <fastjson2.version>2.0.3</fastjson.version>
        <commons-lang.version>2.6</commons-lang.version>
    </properties>
    <dependencyManagement>
        <dependencies>
            <!--spring-cloud-dependencies-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>2021.0.0</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!-- spring-boot-dependencies-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.6.7</version>
                <type>pom</type>
                <!--<scope>provided</scope>-->
                <scope>import</scope>
            </dependency>
            <!-- Spring Security OAuth2 依賴 -->
            <!-- 授權服務器 Spring Authorization Server-->
            <dependency>
                <groupId>org.springframework.security</groupId>
                <artifactId>spring-security-oauth2-authorization-server</artifactId>
                <version>0.2.3</version>
            </dependency>
            <!-- mysql-connector-java -->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>${mysql-connector-java.version}</version>
            </dependency>
            <!--fastjson-->
            <dependency>
                <groupId>com.alibaba.fastjson2</groupId>
                <artifactId>fastjson2</artifactId>
                <version>${fastjson2.version}</version>
            </dependency>
            <!--lombok-->
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>${lombok.version}</version>
            </dependency>
            <!--日誌-->
            <dependency>
                <groupId>log4j</groupId>
                <artifactId>log4j</artifactId>
                <version>${log4j.version}</version>
            </dependency>
            <!-- StringUtils -->
            <dependency>
                <groupId>commons-lang</groupId>
                <artifactId>commons-lang</artifactId>
                <version>${commons-lang.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

</project>

2.2、子模塊

全部在父工程下創建,maven普通模塊

在這裏插入圖片描述

3、資源服務初步實現

初步實現就是不包括安全策略的實現。

該部分不詳細說明。可參考:SpringCloud_土味兒~的博客-CSDN博客

3.1、微服務A(資源服務器)

3.1.1、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>oauth2-server-resource-client</artifactId>
        <groupId>com.tuwer</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>oauth2-resource-a-8001</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
                <groupId>com.alibaba.fastjson2</groupId>
                <artifactId>fastjson2</artifactId>
            </dependency>
        <dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>
</project>

3.1.2、application.yml

server:
  port: 8001

spring:
  application:
    # 應用名稱
    name: oauth2-resource-a-8001

3.1.3、啓動類

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

3.1.4、工具類Result.java

package com.tuwer.util;

import lombok.AccessLevel;
import lombok.Data;
import lombok.Setter;

import java.time.LocalDateTime;

/**
 * <p>結果對象</p>
 *
 * @author 土味兒
 * Date 2022/5/18
 * @version 1.0
 * -----------
 * //@Setter(AccessLevel.NONE) 表示禁用set方法,防止篡改結果
 */
@Data
@Setter(AccessLevel.NONE)
public class Result {
    /**
     * 返回碼
     */
    private Integer code;
    /**
     * 數據
     */
    private Object data;
    /**
     * 時間
     */
    private LocalDateTime time;

    public Result(Integer code,Object data){
        this.code = code;
        this.data = data;
        this.time = LocalDateTime.now();
    }
}

3.1.5、服務接口Controller

package com.tuwer.api;

import com.alibaba.fastjson2.JSON;
import com.tuwer.util.Result;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDateTime;

/**
 * @author 土味兒
 * Date 2022/5/18
 * @version 1.0
 */
@RestController
public class ResourceController {
    @GetMapping("/res1")
    public String getRes1(){
        return JSON.toJSONString(new Result(200, "服務A -> 資源1"));
    }

    @GetMapping("/res2")
    public String getRes2(){
        return JSON.toJSONString(new Result(200, "服務A -> 資源2"));
    }
}

在這裏插入圖片描述

3.1.6、測試

在這裏插入圖片描述

3.2、微服務B(資源服務器)

類似服務A;省略

4、搭建授權服務器

4.1、hosts中映射IP

這是前期自已遇到的一個坑!爲查找原因,頭都大了…

由於客戶端向授權服務器申請授權過程中,需要有多次的重定向操作,但是同一域名下多端口網站共享cookie,會造成授權失敗!

解決方案:在hosts文件指定授權服務器的IP映射(需要對hosts有操作權限)

在這裏插入圖片描述

# 在文件中添加; os.com 就是自已的授權服務器域名
127.0.0.1 os.com

4.2、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>oauth2-server-resource-client</artifactId>
        <groupId>com.tuwer</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>oauth2-server-9000</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>
    <dependencies>
        <!-- 授權服務 -->
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-oauth2-authorization-server</artifactId>
        </dependency>
        <!-- 資源服務可以省略;因爲oauth2-authorization-server中已經存在 -->
<!--        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-oauth2-resource-server</artifactId>
        </dependency>-->
        <!-- web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--  數據庫 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>
</project>
  • 如果 jks、cer文件在編譯時出錯,可以嘗試在pom.xml中加入下面代碼。這也是一個坑,之前遇到過,排查花了很多時間。現在採用新的JWT實現(Nimbus),這個問題好像不存在了…
    <build>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <filtering>true</filtering>
                <excludes>
                    <exclude>**/*.jks</exclude>
                    <exclude>**/*.cer</exclude>
                </excludes>
            </resource>
            <resource>
                <directory>src/main/resources</directory>
                <filtering>false</filtering>
                <includes>
                    <include>**/*.jks</include>
                    <include>**/*.cer</include>
                </includes>
            </resource>
        </resources>
        <plugins>
            <plugin>
                <!-- 打包插件 -->
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

4.3、建數據庫表

數據庫:oauth2-server-resource-client

這些建表語句由官方提供

在這裏插入圖片描述

在這裏插入圖片描述

-- Spring Authorization Server Mysql DDL
-- 保存註冊的客戶端
CREATE TABLE oauth2_registered_client
(
    id                            varchar(100)                            NOT NULL,
    client_id                     varchar(100)                            NOT NULL,
    client_id_issued_at           timestamp     DEFAULT CURRENT_TIMESTAMP NOT NULL,
    client_secret                 varchar(200)  DEFAULT NULL,
    client_secret_expires_at      timestamp     DEFAULT NULL,
    client_name                   varchar(200)                            NOT NULL,
    client_authentication_methods varchar(1000)                           NOT NULL,
    authorization_grant_types     varchar(1000)                           NOT NULL,
    redirect_uris                 varchar(1000) DEFAULT NULL,
    scopes                        varchar(1000)                           NOT NULL,
    client_settings               varchar(2000)                           NOT NULL,
    token_settings                varchar(2000)                           NOT NULL,
    PRIMARY KEY (id)
);

-- 記錄用戶確認授權記錄
CREATE TABLE oauth2_authorization_consent
(
    registered_client_id varchar(100)  NOT NULL,
    principal_name       varchar(200)  NOT NULL,
    authorities          varchar(1000) NOT NULL,
    PRIMARY KEY (registered_client_id, principal_name)
);

-- 記錄發放令牌記錄
CREATE TABLE oauth2_authorization
(
    id                            varchar(100) NOT NULL,
    registered_client_id          varchar(100) NOT NULL,
    principal_name                varchar(200) NOT NULL,
    authorization_grant_type      varchar(100) NOT NULL,
    attributes                    blob          DEFAULT NULL,
    state                         varchar(500)  DEFAULT NULL,
    authorization_code_value      blob          DEFAULT NULL,
    authorization_code_issued_at  timestamp     DEFAULT NULL,
    authorization_code_expires_at timestamp     DEFAULT NULL,
    authorization_code_metadata   blob          DEFAULT NULL,
    access_token_value            blob          DEFAULT NULL,
    access_token_issued_at        timestamp     DEFAULT NULL,
    access_token_expires_at       timestamp     DEFAULT NULL,
    access_token_metadata         blob          DEFAULT NULL,
    access_token_type             varchar(100)  DEFAULT NULL,
    access_token_scopes           varchar(1000) DEFAULT NULL,
    oidc_id_token_value           blob          DEFAULT NULL,
    oidc_id_token_issued_at       timestamp     DEFAULT NULL,
    oidc_id_token_expires_at      timestamp     DEFAULT NULL,
    oidc_id_token_metadata        blob          DEFAULT NULL,
    refresh_token_value           blob          DEFAULT NULL,
    refresh_token_issued_at       timestamp     DEFAULT NULL,
    refresh_token_expires_at      timestamp     DEFAULT NULL,
    refresh_token_metadata        blob          DEFAULT NULL,
    PRIMARY KEY (id)
);

4.4、application.yml

server:
  port: 9000
spring:
  application:
    # 應用名稱
    name: oauth2-server-9000

  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/oauth2-server-resource-client?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
    username: root
    password: 123456

4.5、生成jks和cer

參考:Java Keytool生成數字證書/.cer/.p12文件

  • 打開cmd,切換至目標目錄
  • 創建密鑰庫
# keystore格式
# 密碼統一爲:123456
# 別名:mykey
keytool -genkeypair -alias mykey -keyalg RSA -keysize 2048 -validity 365 -keystore mykey.keystore
# 參數解釋:
# storepass  keystore文件存儲密碼,不加這個參數會在後面要求你輸入密碼
# keypass  私鑰加解密密碼
# alias  實體別名(包括證書私鑰)
# dname  證書個人信息
# keyalg  採用公鑰算法,默認是DSA,這裏採用RSA
# keysize  密鑰長度(DSA算法對應的默認算法是sha1withDSA,不支持2048長度,此時需指定RSA)
# validity  有效期
# keystore  指定keystore文件儲存位置
# jks格式
# 密碼統一爲:123456
# 別名:myjks
keytool -genkeypair -alias myjks -keyalg RSA -validity 365 -keystore myjks.jks

在這裏插入圖片描述

在這裏插入圖片描述

  • 查看密鑰庫
# keystore格式
keytool -v -list -keystore myjks.keystore
# jks格式
keytool -v -list -keystore myjks.jks
  • 導出本地證書cer
# keystore格式導出
keytool -exportcert -keystore  myjks.keystore -file myjks.cer -alias myjks
# 參數解釋:
# -export  表示證書導出操作
# -keystore  指定祕鑰庫文件
# -file  指定導出文件路徑
# -storepass  輸入密碼
# -rfc  指定以Base64編碼格式輸出
# jks格式導出
keytool -exportcert -keystore  myjks.jks -file myjks.cer -alias myjks

在這裏插入圖片描述

在這裏插入圖片描述

  • 打印cer證書
Keytool -printcert -file myjks.cer

複製生成的 myjks.jksmyjks.cer 到授權服務器的資源路徑下;jks 用於生成token時加密,cer用於解析token時解密

在這裏插入圖片描述

4.6、主啓動類

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

4.7、安全策略配置

訪問認證服務器的一些安全措施

package com.tuwer.config;

import lombok.SneakyThrows;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.interfaces.RSAPublicKey;

/**
 * <p>授權服務器安全策略</p>
 *
 * @author 土味兒
 * Date 2022/5/10
 * @version 1.0
 */
@EnableWebSecurity(debug = true)
public class DefaultSecurityConfig {
    /**
     * 配置 請求授權
     *
     * @param http
     * @return
     * @throws Exception
     */
    @Bean
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        // 配置 請求授權
        http.authorizeRequests(authorizeRequests ->
                // 任何請求都需要認證(不對未登錄用戶開放)
                authorizeRequests.anyRequest().authenticated()
            )
                // 表單登錄
                .formLogin()
            .and()
                .logout()
            .and()
                .oauth2ResourceServer().jwt();
        return http.build();
    }

    /**
     * 模擬用戶
     *
     * @return
     */
    @Bean
    UserDetailsService users() {
        UserDetails user = User.builder()
                .username("admin")
                .password("123456")
                .passwordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder()::encode)
                .roles("USER")
                //.authorities("SCOPE_userinfo")
                .build();
        return new InMemoryUserDetailsManager(user);
    }

    /**
     * jwt解碼器
     * 客戶端認證授權後,需要訪問user信息,解碼器可以從令牌中解析出user信息
     *
     * @return
     */
    @SneakyThrows
    @Bean
    JwtDecoder jwtDecoder() {
        CertificateFactory certificateFactory = CertificateFactory.getInstance("x.509");
        // 讀取cer公鑰證書來配置解碼器
        ClassPathResource resource = new ClassPathResource("myjks.cer");
        Certificate certificate = certificateFactory.generateCertificate(resource.getInputStream());
        RSAPublicKey publicKey = (RSAPublicKey) certificate.getPublicKey();
        return NimbusJwtDecoder.withPublicKey(publicKey).build();
    }

    /**
     * 開放一些端點的訪問控制
     * 不需要認證就可以訪問的端口
     * @return
     */
    //@Bean
/*    WebSecurityCustomizer webSecurityCustomizer() {
        return web -> web.ignoring().antMatchers("/actuator/health", "/actuator/info");
    }*/
}

4.8、授權策略配置

核心類:用於授權、生成令牌;註冊客戶端,向數據庫保存操作記錄

package com.tuwer.config;

import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import lombok.SneakyThrows;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.ClassPathResource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.ClientSettings;
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
import org.springframework.security.oauth2.server.authorization.config.TokenSettings;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.RequestMatcher;

import java.security.KeyStore;
import java.time.Duration;
import java.util.UUID;

/**
 * <p>授權服務配置</p>
 *
 * @author 土味兒
 * Date 2022/5/10
 * @version 1.0
 */
@Configuration(proxyBeanMethods = false)
public class AuthorizationServerConfiguration {
    /**
     * 授權配置
     * // @Order 表示加載優先級;HIGHEST_PRECEDENCE爲最高優先級
     *
     * @param http
     * @return
     * @throws Exception
     */
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
        // 定義授權服務配置器
        OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
                new OAuth2AuthorizationServerConfigurer<>();

        // 獲取授權服務器相關的請求端點
        RequestMatcher authorizationServerEndpointsMatcher =
                authorizationServerConfigurer.getEndpointsMatcher();

        http
                // 攔截對 授權服務器 相關端點的請求
                .requestMatcher(authorizationServerEndpointsMatcher)
                // 攔載到的請求需要認證確認(登錄)
                .authorizeRequests()
                // 其餘所有請求都要認證
                .anyRequest().authenticated()
             .and()
                // 忽略掉相關端點的csrf(跨站請求):對授權端點的訪問可以是跨站的
                .csrf(csrf -> csrf
                        .ignoringRequestMatchers(authorizationServerEndpointsMatcher))

                //.and()
                // 表單登錄
                .formLogin()
              .and()
                .logout()
              .and()
                // 應用 授權服務器的配置
                .apply(authorizationServerConfigurer);
        return http.build();
    }

    /**
     * 註冊客戶端
     *
     * @param jdbcTemplate 操作數據庫
     * @return 客戶端倉庫
     */
    @Bean
    public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
        // ---------- 1、檢查當前客戶端是否已註冊
        // 操作數據庫對象
        JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);

        /*
         客戶端在數據庫中的幾個記錄字段的說明
         ------------------------------------------
         id:僅表示客戶端在數據庫中的這個記錄
         client_id:唯一標示客戶端;請求token時,以此作爲客戶端的賬號
         client_name:客戶端的名稱,可以省略
         client_secret:密碼
         */
        String clientId_1 = "my_client";
        // 查詢客戶端是否存在
        RegisteredClient registeredClient_1 = registeredClientRepository.findByClientId(clientId_1);

        // ---------- 2、添加客戶端
        // 數據庫中沒有
        if (registeredClient_1 == null) {
            registeredClient_1 = this.createRegisteredClientAuthorizationCode(clientId_1);
            registeredClientRepository.save(registeredClient_1);
        }

        // ---------- 3、返回客戶端倉庫
        return registeredClientRepository;
    }

    /**
     * 定義客戶端(令牌申請方式:授權碼模式)
     *
     * @param clientId 客戶端ID
     * @return
     */
    private RegisteredClient createRegisteredClientAuthorizationCode(final String clientId) {
        // JWT(Json Web Token)的配置項:TTL、是否複用refrechToken等等
        TokenSettings tokenSettings = TokenSettings.builder()
                // 令牌存活時間:2小時
                .accessTokenTimeToLive(Duration.ofHours(2))
                // 令牌可以刷新,重新獲取
                .reuseRefreshTokens(true)
                // 刷新時間:30天(30天內當令牌過期時,可以用刷新令牌重新申請新令牌,不需要再認證)
                .refreshTokenTimeToLive(Duration.ofDays(30))
                .build();
        // 客戶端相關配置
        ClientSettings clientSettings = ClientSettings.builder()
                // 是否需要用戶授權確認
                .requireAuthorizationConsent(false)
                .build();

        return RegisteredClient
                // 客戶端ID和密碼
                .withId(UUID.randomUUID().toString())
                //.withId(id)
                .clientId(clientId)
                //.clientSecret("{noop}123456")
                .clientSecret(PasswordEncoderFactories.createDelegatingPasswordEncoder().encode("123456"))
                // 客戶端名稱:可省略
                .clientName("my_client_name")
                // 授權方法
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                // 授權模式
                // ---- 【授權碼模式】
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                // ---------- 刷新令牌(授權碼模式)
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                /* 回調地址:
                 * 授權服務器向當前客戶端響應時調用下面地址;
                 * 不在此列的地址將被拒絕;
                 * 只能使用IP或域名,不能使用localhost
                 */
                .redirectUri("http://127.0.0.1:8000/login/oauth2/code/myClient")
                .redirectUri("http://127.0.0.1:8000")
                // 授權範圍(當前客戶端的授權範圍)
                .scope("read")
                .scope("write")
                // JWT(Json Web Token)配置項
                .tokenSettings(tokenSettings)
                // 客戶端配置項
                .clientSettings(clientSettings)
                .build();
    }

    /**
     * 令牌的發放記錄
     *
     * @param jdbcTemplate               操作數據庫
     * @param registeredClientRepository 客戶端倉庫
     * @return 授權服務
     */
    @Bean
    public OAuth2AuthorizationService auth2AuthorizationService(
            JdbcTemplate jdbcTemplate,
            RegisteredClientRepository registeredClientRepository) {
        return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
    }

    /**
     * 把資源擁有者授權確認操作保存到數據庫
     * 資源擁有者(Resource Owner)對客戶端的授權記錄
     *
     * @param jdbcTemplate               操作數據庫
     * @param registeredClientRepository 客戶端倉庫
     * @return
     */
    @Bean
    public OAuth2AuthorizationConsentService auth2AuthorizationConsentService(
            JdbcTemplate jdbcTemplate,
            RegisteredClientRepository registeredClientRepository) {
        return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
    }


    /**
     * 加載jwk資源
     * 用於生成令牌
     * @return
     */
    @SneakyThrows
    @Bean
    public JWKSource<SecurityContext> jwkSource() {
        // 證書的路徑
        String path = "myjks.jks";
        // 證書別名
        String alias = "myjks";
        // keystore 密碼
        String pass = "123456";

        ClassPathResource resource = new ClassPathResource(path);
        KeyStore jks = KeyStore.getInstance("jks");
        char[] pin = pass.toCharArray();
        jks.load(resource.getInputStream(), pin);
        RSAKey rsaKey = RSAKey.load(jks, alias, pin);

        JWKSet jwkSet = new JWKSet(rsaKey);
        return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
    }

    /**
     * <p>授權服務器元信息配置</p>
     * <p>
     * 授權服務器本身也提供了一個配置工具來配置其元信息,大多數都使用默認配置即可,唯一需要配置的其實只有授權服務器的地址issuer
     * 在生產中這個地方應該配置爲域名
     *
     * @return
     */
    @Bean
    public ProviderSettings providerSettings() {
        return ProviderSettings.builder().issuer("http://os.com:9000").build();
    }
}
  • 客戶端在數據庫中的幾個記錄字段的說明
    • id:僅表示客戶端在數據庫中的這個記錄
    • client_id:唯一標示客戶端;請求token時,以此作爲客戶端的賬號
    • client_name:客戶端的名稱,可以省略
    • client_secret:密碼

在這裏插入圖片描述

4.9、user端口配置

就是客戶認證授權後,獲取user信息的接口

package com.tuwer.endpoint;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * <p>用戶信息接口</p>
 *
 * @author 土味兒
 * Date 2022/5/10
 * @version 1.0
 */
@RestController
@RequestMapping("/oauth2")
public class EndPointController {
    /**
     * 獲取用戶信息
     * @return
     */
    @GetMapping("/user")
    public Authentication oauth2UserInfo(){
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if(authentication == null){
            throw new RuntimeException("無有效認證用戶!");
        }
        return authentication;
    }
}

4.10、目錄結構

在這裏插入圖片描述

4.11、測試

藉助於postman

  • 請求授權碼

在這裏插入圖片描述

http://os.com:9000/oauth2/authorize?response_type=code&client_id=my_client&scope=read%20write&redirect_uri=http://127.0.0.1:8000 在瀏覽器地址欄中輸入

在這裏插入圖片描述

在這裏插入圖片描述

在這裏插入圖片描述

授權碼的有效期默認5分鐘,一次性的,在5分鐘內申請令牌,申請完令牌之後就失效,不管申請是否成功。由於註冊客戶端配置時,關閉了用戶確認授權,所以登錄後,直接返回了授權碼,跳過了授權確認頁面。授權確認頁面長這樣的:

在這裏插入圖片描述

  • 用授權碼請求令牌

複製上一步中返回的授權碼,在postman中申請令牌;

請求地址:授權服務器:端口/oauth2/token,再加下圖中參數

在這裏插入圖片描述

在這裏插入圖片描述

在這裏插入圖片描述

令牌有3部分:頭部、載荷、校驗碼,以點號分隔;base64編碼;可以保證不被篡改,但不能保證信息不被泄露

  • 解碼令牌

進入 Base64 在線編碼解碼 | Base64 加密解密 - Base64.us

分別複製令牌中的前兩部分進行解碼

在這裏插入圖片描述

在這裏插入圖片描述

  • 訪問user信息

在這裏插入圖片描述

至此,授權服務器基本搭建完成!

4.12、疑惑解析

  • 授權服務中爲什麼也配有資源服務?

授權服務中也提供了資源服務;如:用戶信息 /oauth2/user ,在認證授權後,可以通過該接口,獲得用戶信息。如果把該資源服務剝離出去,就可以去掉與資源服務相關的內容:cer公鑰、解碼器方法、user端口API等;

  • 爲什麼要配置兩個 SecurityFilterChain ?

兩個 SecurityFilterChain 職責不一樣,且都是原型的。

一個是安全策略,訪問授權服務器時的安全檢查;

一個是授權策略,認證通過,進行授權、發放令牌等;

5、改造資源服務器

先只改造資源服務A oauth2-resource-a-8001

5.1、添加依賴

在pom.xml中添加

<!-- 資源服務器 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

在這裏插入圖片描述

5.2、複製cer公鑰到資源路徑

在這裏插入圖片描述

5.3、解碼器

5.3.1、自定義JWT屬性配置類

關於有效期expiresAt的設定:設爲0時,和令牌實際時間一致。 如果大於0,就是在原來過期時間的基礎再加上這個值。所以沒有必要配置這個值。

package com.tuwer.config.oauth2;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

/**
 * <p>屬性配置類</p>
 *
 * @author 土味兒
 * Date 2022/5/11
 * @version 1.0
 */
@Data
@ConfigurationProperties(prefix = "jwt")
public class JwtProperties {
    /*
    ======= 配置示例 ======
    # 自定義 jwt 配置
    jwt:
        cert-info:
            # 證書存放位置
            public-key-location: myKey.cer
        claims:
            # 令牌的鑑發方:即授權服務器的地址
            issuer: http://os:9000
    */
    /**
     * 證書信息(內部靜態類)
     * 證書存放位置...
     */
    private CertInfo certInfo;

    /**
     * 證書聲明(內部靜態類)
     * 發證方...
     */
    private Claims claims;

    @Data
    public static class Claims {
        /**
         * 發證方
         */
        private String issuer;
        /**
         * 有效期
         */
        //private Integer expiresAt;
    }

    @Data
    public static class CertInfo {
        /**
         * 證書存放位置
         */
        private String publicKeyLocation;
    }
}

5.3.2、自定義JWT解碼器

package com.tuwer.config.oauth2;

import com.nimbusds.jose.jwk.RSAKey;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.jwt.*;

import java.io.InputStream;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPublicKey;
import java.util.Collection;

/**
 * <p>自定義jwt解碼器</p>
 * proxyBeanMethods = false 每次調用都創建新的對象
 *
 * @author 土味兒
 * Date 2022/5/11
 * @version 1.0
 */
@EnableConfigurationProperties(JwtProperties.class)
@Configuration(proxyBeanMethods = false)
public class JwtDecoderConfiguration {
    /**
     * 注入 JwtProperties 屬性配置類
     */
    @Autowired
    private JwtProperties jwtProperties;

    /**
     *  校驗jwt發行者 issuer 是否合法
     *
     * @return the jwt issuer validator
     */
    @Bean
    JwtIssuerValidator jwtIssuerValidator() {
        return new JwtIssuerValidator(this.jwtProperties.getClaims().getIssuer());
    }

    /**
     *  校驗jwt是否過期
     *
     * @return the jwt timestamp validator
     */
/*    @Bean
    JwtTimestampValidator jwtTimestampValidator() {
        System.out.println("檢測令牌是否過期!"+ LocalDateTime.now());
        return new JwtTimestampValidator(Duration.ofSeconds((long) this.jwtProperties.getClaims().getExpiresAt()));
    }*/

    /**
     * jwt token 委託校驗器,集中校驗的策略{@link OAuth2TokenValidator}
     *
     * // @Primary:自動裝配時當出現多個Bean候選者時,被註解爲@Primary的Bean將作爲首選者,否則將拋出異常
     * @param tokenValidators the token validators
     * @return the delegating o auth 2 token validator
     */
    @Primary
    @Bean({"delegatingTokenValidator"})
    public DelegatingOAuth2TokenValidator<Jwt> delegatingTokenValidator(Collection<OAuth2TokenValidator<Jwt>> tokenValidators) {
        return new DelegatingOAuth2TokenValidator<>(tokenValidators);
    }

    /**
     * 基於Nimbus的jwt解碼器,並增加了一些自定義校驗策略
     *
     * // @Qualifier 當有多個相同類型的bean存在時,指定注入
     * @param validator DelegatingOAuth2TokenValidator<Jwt> 委託token校驗器
     * @return the jwt decoder
     */
    @SneakyThrows
    @Bean
    public JwtDecoder jwtDecoder(@Qualifier("delegatingTokenValidator")
                                         DelegatingOAuth2TokenValidator<Jwt> validator) {
        // 指定 X.509 類型的證書工廠
        CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
        // 讀取cer公鑰證書來配置解碼器
        String publicKeyLocation = this.jwtProperties.getCertInfo().getPublicKeyLocation();
        // 獲取證書文件輸入流
        ClassPathResource resource = new ClassPathResource(publicKeyLocation);
        InputStream inputStream = resource.getInputStream();
        // 得到證書
        X509Certificate certificate = (X509Certificate) certificateFactory.generateCertificate(inputStream);
        // 解析
        RSAKey rsaKey = RSAKey.parse(certificate);
        // 得到公鑰
        RSAPublicKey key = rsaKey.toRSAPublicKey();
        // 構造解碼器
        NimbusJwtDecoder nimbusJwtDecoder = NimbusJwtDecoder.withPublicKey(key).build();
        // 注入自定義JWT校驗邏輯
        nimbusJwtDecoder.setJwtValidator(validator);
        return nimbusJwtDecoder;
    }
}

5.4、異常處理器

5.4.1、認證失敗處理器

package com.tuwer.config.oauth2;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException;
import org.springframework.security.web.AuthenticationEntryPoint;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;

/**
 * <p>認證失敗處理器</p>
 *
 * @author 土味兒
 * Date 2022/5/11
 * @version 1.0
 */
public class SimpleAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @SneakyThrows
    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException authException
    ) throws IOException, ServletException {
        if (authException instanceof InvalidBearerTokenException) {
            System.out.println("token失效");
            //todo token處理邏輯
        }
        //todo your business
        HashMap<String, String> map = new HashMap<>(2);
        map.put("uri", request.getRequestURI());
        map.put("msg", "認證失敗");
        if (response.isCommitted()) {
            return;
        }
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.setStatus(HttpServletResponse.SC_ACCEPTED);
        response.setCharacterEncoding("utf-8");
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        ObjectMapper objectMapper = new ObjectMapper();
        String resBody = objectMapper.writeValueAsString(map);
        PrintWriter printWriter = response.getWriter();
        printWriter.print(resBody);
        printWriter.flush();
        printWriter.close();
    }
}

5.4.2、拒絕訪問處理器

package com.tuwer.config.oauth2;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.springframework.http.MediaType;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;

/**
 * <p>拒絕訪問處理器</p>
 *
 * @author 土味兒
 * Date 2022/5/11
 * @version 1.0
 */
public class SimpleAccessDeniedHandler implements AccessDeniedHandler {
    @SneakyThrows
    @Override
    public void handle(HttpServletRequest request,
                       HttpServletResponse response,
                       AccessDeniedException accessDeniedException
    ) throws IOException, ServletException {
        //todo your business
        HashMap<String, String> map = new HashMap<>(2);
        map.put("uri", request.getRequestURI());
        map.put("msg", "拒絕訪問");
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        response.setCharacterEncoding("utf-8");
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        ObjectMapper objectMapper = new ObjectMapper();
        String resBody = objectMapper.writeValueAsString(map);
        PrintWriter printWriter = response.getWriter();
        printWriter.print(resBody);
        printWriter.flush();
        printWriter.close();
    }
}

5.5、資源安全策略配置

package com.tuwer.config.oauth2;

import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.jwt.JwtClaimNames;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.web.SecurityFilterChain;

/**
 * <p>資源服務器配置</p>
 * 當解碼器JwtDecoder存在時生效
 * proxyBeanMethods = false 每次調用都創建新的對象
 *
 * @author 土味兒
 * Date 2022/5/11
 * @version 1.0
 */
@ConditionalOnBean(JwtDecoder.class)
@Configuration(proxyBeanMethods = false)
public class OAuth2ResourceServerConfiguration {
    /**
     * 資源管理器配置
     *
     * @param http the http
     * @return the security filter chain
     * @throws Exception the exception
     */
    @Bean
    SecurityFilterChain jwtSecurityFilterChain(HttpSecurity http) throws Exception {
        // 拒絕訪問處理器 401
        SimpleAccessDeniedHandler accessDeniedHandler = new SimpleAccessDeniedHandler();
        // 認證失敗處理器 403
        SimpleAuthenticationEntryPoint authenticationEntryPoint = new SimpleAuthenticationEntryPoint();

        return http
                // security的session生成策略改爲security不主動創建session即STALELESS
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
                // 對 /res1 的請求,需要 SCOPE_read 權限
                .authorizeRequests()
                .antMatchers("/res1").hasAnyAuthority("SCOPE_read","SCOPE_all")
                .antMatchers("/res2").hasAnyAuthority("SCOPE_write1","SCOPE_all")
                // 其餘請求都需要認證
                .anyRequest().authenticated()
             .and()
                // 異常處理
                .exceptionHandling(exceptionConfigurer -> exceptionConfigurer
                        // 拒絕訪問
                        .accessDeniedHandler(accessDeniedHandler)
                        // 認證失敗
                        .authenticationEntryPoint(authenticationEntryPoint)
                )
                // 資源服務
                .oauth2ResourceServer(resourceServer -> resourceServer
                        .accessDeniedHandler(accessDeniedHandler)
                        .authenticationEntryPoint(authenticationEntryPoint)
                        .jwt()
                )
                .build();
    }


    /**
     * JWT個性化解析
     *
     * @return
     */
    @Bean
    JwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
        JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
//        如果不按照規範  解析權限集合Authorities 就需要自定義key
//        jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("scopes");
//        OAuth2 默認前綴是 SCOPE_     Spring Security 是 ROLE_
//        jwtGrantedAuthoritiesConverter.setAuthorityPrefix("");
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
        // 用戶名 可以放sub
        jwtAuthenticationConverter.setPrincipalClaimName(JwtClaimNames.SUB);
        return jwtAuthenticationConverter;
    }
}

資源服務不涉及用戶登錄,僅靠token訪問,不需要seesion;

把session生成策略改爲不主動創建,即 STALELESS

http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)

5.6、yml中添加jwt配置

配置時注意命名規則;駝峯命名 與 短劃線;如:publicKeyLocation 對應 public-key-location

# 自定義 jwt 配置(校驗jwt)
jwt:
  cert-info:
    # 公鑰證書存放位置
    public-key-location: myjks.cer
  claims:
    # 令牌的鑑發方:即授權服務器的地址
    issuer: http://os.com:9000
    # 令牌有效時間(單位:秒);設爲0時,和令牌實際時間一致。
    # 如果大於0,就是在原來過期時間的基礎再加上這個值
    #expires-at: 0

在這裏插入圖片描述

2023-4-16:用starter實現Oauth2中資源服務的統一配置

5.7、測試

5.7.1、權限說明

資源權限說明:

  • 訪問資源 res1,需要有 read 或 all
  • 訪問資源 res2,需要有 write1 或 all

在這裏插入圖片描述

當前客戶端所擁有的權限範圍:

admin用戶通過當前客戶端進入後,只能在 read 或 write 範圍內訪問;所以可以看出,只能訪問res1,不能訪問res2,因爲沒有 write1 或 all權限。write 和 write1 是不同的。


SCOPE、ROLE、AUTH 簡單區別:

整個項目(包括多個微服務模塊)相當於一座大樓,每一樓層相當於一個微服務模塊,每一個微服務模塊內有多個資源。用戶進去大樓後,只可以訪問特定的樓層(這就是範圍SCOPE),到達樓層後,根據身份(ROLE),查看對應的權限(AUTH),再訪問對應的資源。

資源可以與SCOPE、ROLE、AUTH 綁定。如:

  • 綁定SCOPE:只要進入到樓層,就可以訪問
  • 綁定ROLE:先進入到樓層,再根據身份ROLE去訪問。只要這個ROLE能進入到樓層就可以。
  • 綁定AUTH:先進入到樓層,不看身份,只看有沒有與資源匹配的權限

三種綁定情況,對權限的要求粒度越來越細。

在這裏插入圖片描述

在這裏插入圖片描述

5.7.2、直接訪問

在這裏插入圖片描述

5.7.3、postman申請令牌

啓動授權服務,申請授權碼、令牌

在這裏插入圖片描述

在這裏插入圖片描述

5.7.4、攜帶令牌訪問資源1

在這裏插入圖片描述

在這裏插入圖片描述

在這裏插入圖片描述

5.7.5、攜帶令牌訪問資源2

因爲res2要求有 write1 或 all,當前用戶沒有這個權限,所以拒絕訪問。

在這裏插入圖片描述

5.7.6、資源服務器自行鑑權

在這裏插入圖片描述

5.8、疑惑解析

admin用戶的身份Role爲USER,如果把res2的訪問權限修改爲:hasAnyRole("USER"),即允許身份爲USER的用戶訪問,那麼是否可以成功訪問 res2?

在這裏插入圖片描述

重啓測試:

在這裏插入圖片描述

分析原因:

在這裏插入圖片描述

訪問的請求主體不同

當前測試的訪問主體是客戶端my_client,它在註冊時只有read、write權限範圍,用戶admin只會在這兩個範圍內給my_client授權,不會也不能把自已的身份USER賦於my_client,所以my_client是不具有USER身份的,也就不能訪問res2。

換言之,如果是admin用戶本人來訪問,它具有USER身份,當然就可以訪問了。但資源服務器不提供登錄認證的功能,所以用戶本人無法直接訪問。

在資源中指定ROLE,是針對當前訪問主體的身份,不是資源擁有者的身份。

6、搭建客戶端

6.1、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>oauth2-server-resource-client</artifactId>
        <groupId>com.tuwer</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>oauth2-client-8000</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
                <groupId>com.alibaba.fastjson2</groupId>
                <artifactId>fastjson2</artifactId>
            </dependency>
        <dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
        </dependency>
    </dependencies>
</project>

在這裏插入圖片描述

6.2、application.yml

server:
  port: 8000

spring:
  application:
    # 應用名稱
    name: oauth2-client-8000
  security:
    oauth2:
      client:
        registration:
          # 客戶端:與註冊時保持一致
          myClient:
            client-id: my_client
            client-secret: 123456
            #client-name: my_client_name
            scope: read,write
            authorization-grant-type: authorization_code
            provider: myOauth2
            redirect-uri: '{baseUrl}/{action}/oauth2/code/{registrationId}'
            # 認證方法
            client-authentication-method: client_secret_basic

        provider:
          # 服務提供地址
          myOauth2:
            #issuer-uri: http://os.com:9000
            # issuer-uri 可以簡化下面的配置
            # 請求授權碼地址
            authorization-uri: http://os.com:9000/oauth2/authorize
            # 請求令牌地址
            token-uri: http://os.com:9000/oauth2/token
            # 用戶資源地址
            user-info-uri: http://os.com:9000/oauth2/user
            # 用戶資源返回中的一個屬性名
            user-name-attribute: name
            user-info-authentication-method: GET

在這裏插入圖片描述

6.3、啓動類

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

6.4、首頁index.html

使用thymeleaf模版;放在 resources 下的 templates 中

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
登錄用戶:<span th:text="${user}"></span>
<hr/>
<ul>
    <li><a href="./server/a/res1">服務A —— 資源1</a></li>
    <li><a href="./server/a/res2">服務A —— 資源2</a></li>
    <li><a href="./server/b/res1">服務B —— 資源1</a></li>
    <li><a href="./server/b/res2">服務B —— 資源2</a></li>
</ul>
</body>
</html>

6.5、安全配置類

package com.tuwer.config;

import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.client.RestTemplate;

/**
 * @author 土味兒
 * Date 2022/5/13
 * @version 1.0
 */
@Configuration(proxyBeanMethods = false)
public class SecurityConfiguration {
    /***
     * 安全配置
     * @param http http
     * @return SecurityFilterChain
     * @throws Exception exception
     */
    @Bean
    SecurityFilterChain oauth2SecurityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeRequests(requests ->
                // 任何請求都需要認證
                requests.anyRequest().authenticated()
            )
                // oauth2三方登錄
                .oauth2Login(Customizer.withDefaults())
                .oauth2Client()
            .and()
                .logout();
        return http.build();
    }

    @Bean
    public RestTemplate oauth2ClientRestTemplate(RestTemplateBuilder restTemplateBuilder) {
        return restTemplateBuilder.build();
    }
}

6.6、Controller

6.6.1、IndexController.java

package com.tuwer.controller;

import com.alibaba.fastjson2.JSON;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.client.RestTemplate;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.*;

/**
 * @author 土味兒
 * Date 2022/5/16
 * @version 1.0
 */
@Controller
public class IndexController {
    @Autowired
    RestTemplate restTemplate;

    @GetMapping("/")
    public String index(Model model) {
        // 從安全上下文中獲取登錄信息,返回給model
        Map<String, Object> map = new HashMap<>(2);
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        map.put("name", auth.getName());
        Collection<? extends GrantedAuthority> authorities = auth.getAuthorities();
        Iterator<? extends GrantedAuthority> iterator = authorities.stream().iterator();
        ArrayList<Object> authList = new ArrayList<>();
        while (iterator.hasNext()) {
            authList.add(iterator.next().getAuthority());
        }

        map.put("authorities", authList);
        model.addAttribute("user", JSON.toJSONString(map));
        return "index";
    }
}

6.6.2、ResourceController

package com.tuwer.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;

/**
 * @author 土味兒
 * Date 2022/5/15
 * @version 1.0
 */
@Slf4j
@RestController
public class ResourceController {
    @Autowired
    RestTemplate restTemplate;

    @GetMapping("/server/a/res1")
    public String getServerARes1(@RegisteredOAuth2AuthorizedClient
                                         OAuth2AuthorizedClient oAuth2AuthorizedClient) {
        return getServer("http://127.0.0.1:8001/res1", oAuth2AuthorizedClient);
    }

    @GetMapping("/server/a/res2")
    public String getServerARes2(@RegisteredOAuth2AuthorizedClient
                                         OAuth2AuthorizedClient oAuth2AuthorizedClient) {
        return getServer("http://127.0.0.1:8001/res2", oAuth2AuthorizedClient);
    }

    @GetMapping("/server/b/res1")
    public String getServerBRes1(@RegisteredOAuth2AuthorizedClient
                                         OAuth2AuthorizedClient oAuth2AuthorizedClient) {
        return getServer("http://127.0.0.1:8002/res1", oAuth2AuthorizedClient);
    }

    @GetMapping("/server/b/res2")
    public String getServerBRes2(@RegisteredOAuth2AuthorizedClient
                                         OAuth2AuthorizedClient oAuth2AuthorizedClient) {
        return getServer("http://127.0.0.1:8002/res2", oAuth2AuthorizedClient);
    }

    /**
     * 綁定token,請求微服務
     *
     * @param url
     * @param oAuth2AuthorizedClient
     * @return
     */
    private String getServer(String url, OAuth2AuthorizedClient oAuth2AuthorizedClient) {
        // 獲取 token
        String tokenValue = oAuth2AuthorizedClient.getAccessToken().getTokenValue();

        // 請求頭
        HttpHeaders headers = new HttpHeaders();
        headers.add("Authorization", "Bearer " + tokenValue);
        // 請求體
        HttpEntity<Object> httpEntity = new HttpEntity<>(headers);
        // 發起請求
        ResponseEntity<String> responseEntity;
        try {
            responseEntity = restTemplate.exchange(url, HttpMethod.GET, httpEntity, String.class);
        } catch (RestClientException e) {
            // e.getMessage() 信息格式:
            // 403 : "{"msg":"拒絕訪問","uri":"/res2"}"
            // 解析,取出消息體 {"msg":"拒絕訪問","uri":"/res2"}
            String str = e.getMessage();
            // 取兩個括號中間的部分(包含兩個括號)
            return str.substring(str.indexOf("{"), str.indexOf("}") + 1);
        }
        // 返回
        return responseEntity.getBody();
    }
}

6.7、測試

  • 啓動服務

在這裏插入圖片描述

  • 資源訪問

在這裏插入圖片描述

在這裏插入圖片描述

6.8、註銷策略

用戶登錄後,會在認證服務器和客戶端都保存session信息。要註銷時,需要把兩個地方的都清除,包括安全上下文,僅清除客戶端或認證服務器是不徹底的。

security的退出操作是 /logout ,可以清除相關的登錄信息。

  • 客戶端首頁添加 退出 按鈕;先調用 /logout 測試
<a href="./logout">退出</a>

在這裏插入圖片描述

點擊退出後,出現確認退出頁面,確認後進入三方登錄列表頁,再點擊 myClient 登錄後,直接自動登錄了。這個過程沒有出現登錄/授權頁面。這樣的退出是不徹底的,僅僅是客戶端的退出。實際的需求應該是再次登錄時,需要用戶參與(登錄/授權)。

原因分析:這裏的退出,僅僅清除了客戶端的登錄信息。在認證服務器中,用戶還是登錄狀態。瀏覽器不關閉時,客戶端與認證服務器間的JSESSIONID是不變的。

用不變的JSESSIONID,向認證服務器發起請求,認證服務器中用戶是登錄狀態,保存有與JSESSIONID對應的信息,這時會直接返回用戶請求的信息,當然就不會再登錄/授權了。

在這裏插入圖片描述

解決思路:一次退出操作,同時清除客戶端和認證服務器的登錄信息

在這裏插入圖片描述

實現步驟:

1、客戶端添加自定義退出接口 /out

    @GetMapping("/out")
    public void logout(HttpServletRequest request,
                       HttpServletResponse response) {

        // ========== 清理客戶端 ===========
        // 清理客戶端session
        request.getSession().invalidate();
        // 清理客戶端安全上下文
        SecurityContextHolder.clearContext();

        // ========== 清理認證中心 ===========
        // 跳轉至認證中心退出頁面
        try {
            response.sendRedirect("http://os.com:9000/logout");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

2、修改客戶端退出鏈接

<a href="./out">退出</a>

3、認證服務器中配置 退出成功後跳轉頁面 logoutSuccessUsl()

// 在安全策略類、授權策略類中都添加上
// 退出成功後跳轉至客戶端
logoutSuccessUrl("http://127.0.0.1:8000")

在這裏插入圖片描述

演示

在這裏插入圖片描述

至此,本項目完結。

接下來,準備在此基礎上,實現資源服務間相互調用…

Git倉庫:https://gitee.com/tuwer/oauth2

 

  • 用戶通過客戶端訪問資源是 授權碼模式
  • 微服務(資源)間的訪問是 客戶端模式;客戶端模式下,只需要提供註冊客戶端的ID和密鑰,就可以向授權服務器申請令牌,授權服務器覈實ID和密鑰後,會直接發放令牌,無須再認證/授權,特別適合項目內部模塊間的調用。

在這裏插入圖片描述

2、授權服務器中註冊新客戶端

爲了讓請求資源的主體更加清晰,再註冊一個客戶端micro_service,專門供資源服務器之間的相互調用。也可以用原來客戶端 my_client ,不過要在授權模式GrantType中添加 CLIENT_CREDENTIALS

  • 客戶端模式直接返回token;不需要回調地址
  • 在授權服務器的授權服務配置類 AuthorizationServerConfiguration.java 中添加
    /**
     * 定義客戶端(令牌申請方式:客戶端模式)
     *
     * @param clientId 客戶端ID
     * @return
     */
    private RegisteredClient createRegisteredClient(final String clientId) {
        // JWT(Json Web Token)的配置項:TTL、是否複用refrechToken等等
        TokenSettings tokenSettings = TokenSettings.builder()
                // 令牌存活時間:1年
                .accessTokenTimeToLive(Duration.ofDays(365))
                // 令牌不可以刷新
                //.reuseRefreshTokens(false)
                .build();
        // 客戶端相關配置
        ClientSettings clientSettings = ClientSettings.builder()
                // 是否需要用戶授權確認
                .requireAuthorizationConsent(false)
                .build();

        return RegisteredClient
                // 客戶端ID和密碼
                .withId(UUID.randomUUID().toString())
                //.withId(id)
                .clientId(clientId)
                //.clientSecret("{noop}123456")
                .clientSecret(PasswordEncoderFactories.createDelegatingPasswordEncoder().encode("123456"))
                // 客戶端名稱:可省略
                .clientName("micro_service")
                // 授權方法
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                // 授權模式
                // ---- 【客戶端模式】
                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                // 客戶端模式直接返回token;不需要回調地址
                //.redirectUri("...")
                // 授權範圍(當前客戶端的角色)
                .scope("all")
                // JWT(Json Web Token)配置項
                .tokenSettings(tokenSettings)
                // 客戶端配置項
                .clientSettings(clientSettings)
                .build();
    }
  • 修改註冊方法:該方法僅注重功能,結構不夠優雅,可以自行修改
    /**
     * 註冊客戶端
     *
     * @param jdbcTemplate 操作數據庫
     * @return 客戶端倉庫
     */
    @Bean
    public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
        // ---------- 1、檢查當前客戶端是否已註冊
        // 操作數據庫對象
        JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);

        /*
         客戶端在數據庫中記錄的區別
         ------------------------------------------
         id:僅表示客戶端在數據庫中的這個記錄
         client_id:唯一標示客戶端;請求token時,以此作爲客戶端的賬號
         client_name:客戶端的名稱,可以省略
         client_secret:密碼
         */
        String clientId_1 = "my_client";
        String clientId_2 = "micro_service";
        // 查詢客戶端是否存在
        RegisteredClient registeredClient_1 = registeredClientRepository.findByClientId(clientId_1);
        RegisteredClient registeredClient_2 = registeredClientRepository.findByClientId(clientId_2);

        // ---------- 2、添加客戶端
        // 數據庫中沒有
        if (registeredClient_1 == null) {
            registeredClient_1 = this.createRegisteredClientAuthorizationCode(clientId_1);
            registeredClientRepository.save(registeredClient_1);
        }
        // 數據庫中沒有
        if (registeredClient_2 == null) {
            registeredClient_2 = this.createRegisteredClient(clientId_2);
            registeredClientRepository.save(registeredClient_2);
        }

        // ---------- 3、返回客戶端倉庫
        return registeredClientRepository;
    }

3、資源服務器之間訪問

3.1、案例說明

用 資源服務器B 調用 資源服務器A 中的資源;

具體:服務B/res1 --> 服務A/res2

服務A/res2 接口在前面用 my_client 是無法訪問的;

在這裏插入圖片描述

當前 資源服務器B 無安全策略,可以直接訪問

在這裏插入圖片描述

3.2、令牌申請與使用 處理邏輯

在這裏插入圖片描述

3.3、改造資源服務器B

  • 配置RestTemplat
@Configuration(proxyBeanMethods = false)
public class RestTemplateConfiguration {
    @Bean
    public RestTemplate oauth2ClientRestTemplate(RestTemplateBuilder restTemplateBuilder) {
        return restTemplateBuilder.build();
    }
}
  • 修改API接口類
@RestController
public class ResourceController {
    @Autowired
    RestTemplate restTemplate;

    @GetMapping("/res1")
    public String getRes1(HttpServletRequest request) {
        // 調用資源服務器A中的資源res2
        return getServer("http://127.0.0.1:8001/res2", request);
        //return JSON.toJSONString(new Result(200, "服務B -> 資源1"));
    }

    @GetMapping("/res2")
    public String getRes2() {
        return JSON.toJSONString(new Result(200, "服務B -> 資源2"));
    }

    /**
     * 請求資源
     *
     * @param url
     * @param request
     * @return
     */
    private String getServer(String url,
                             HttpServletRequest request) {
        // ======== 1、從session中取token ========
        HttpSession session = request.getSession();
        String token = (String) session.getAttribute("micro-token");

        // ======== 2、請求token ========
        // 先查session中是否有token;session中沒有
        if (StringUtils.isEmpty(token)) {
            // ===== 去認證中心申請 =====
            // 對id及密鑰加密
            byte[] userpass = Base64.encodeBase64(("micro_service:123456").getBytes());
            String str = "";
            try {
                str = new String(userpass, "UTF-8");
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }

            // 請求頭
            HttpHeaders headers1 = new HttpHeaders();
            // 組裝請求頭
            headers1.add("Authorization", "Basic " + str);
            // 請求體
            HttpEntity<Object> httpEntity1 = new HttpEntity<>(headers1);
            // 響應體
            ResponseEntity<String> responseEntity1 = null;
            try {
                // 發起申請令牌請求
                responseEntity1 = restTemplate.exchange("http://os.com:9000/oauth2/token?grant_type=client_credentials", HttpMethod.POST, httpEntity1, String.class);
            } catch (RestClientException e) {
                //
                System.out.println("令牌申請失敗");
            }

            // 令牌申請成功
            if (responseEntity1 != null) {
                // 解析令牌
                // String t = JSON.parseObject(responseEntity1.getBody(), MyAuth.class).getAccess_token();
                Map<String, String> resMap = JSON.parseObject(responseEntity1.getBody(), HashMap.class);
                String t = resMap.get("access_token");
                // 存入session
                session.setAttribute("micro-token", t);
                // 賦於token變量
                token = t;
            }
        }

        // ======== 3、請求資源 ========
        // 請求頭
        HttpHeaders headers2 = new HttpHeaders();
        // 組裝請求頭
        headers2.add("Authorization", "Bearer " + token);
        // 請求體
        HttpEntity<Object> httpEntity2 = new HttpEntity<>(headers2);
        // 響應體
        ResponseEntity<String> responseEntity2;
        try {
            // 發起訪問資源請求
            responseEntity2 = restTemplate.exchange(url, HttpMethod.GET, httpEntity2, String.class);
        } catch (RestClientException e) {
            // 令牌失效(認證失效401) --> 清除session
            // e.getMessage() 信息格式:
            // 401 : "{"msg":"認證失敗","uri":"/res2"}"   
            String str = e.getMessage();
            // 判斷是否含有 401
            if(StringUtils.contains(str, "401")){
                // 如果有401,把session中 micro-token 的值設爲空
                session.setAttribute("micro-token","");
            }            
            // 取兩個括號中間的部分(包含兩個括號)
            return str.substring(str.indexOf("{"), str.indexOf("}") + 1);
        }
        // 返回
        return responseEntity2.getBody();
    }
}

// 用於解析申請到的令牌數據
/*@Data
class MyAuth {
    private String access_token;
    private String scope;
    private String token_type;
    private long expires_in;
}*/

3.4、測試

  • 啓動server、resource,無須啓動client

在這裏插入圖片描述

  • 直接訪問resource_b

在這裏插入圖片描述

3.5、資源服務器B添加安全策略

  • 添加依賴
<!-- 資源服務器 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
  • 再次啓動測試,已經無法直接訪問;需要通過客戶端去訪問

在這裏插入圖片描述

3.6、繼續改造資源服務器B

複製 資源服務器A 中配置策略到 資源服務器B 中來

  • 複製cer公鑰文件
  • appliction.yml中添加jtw配置
# 自定義 jwt 配置(校驗jwt)
jwt:
  cert-info:
    # 公鑰證書存放位置
    public-key-location: myjks.cer
  claims:
    # 令牌的鑑發方:即授權服務器的地址
    issuer: http://os.com:9000
  • 複製 oauth2 配置包;如下圖

在這裏插入圖片描述

在這裏插入圖片描述

3.7、客戶端client訪問測試

  • 用 maven 的 clean 清理項目
  • 啓動 server、resource (a和b)、client

在這裏插入圖片描述

  • 登錄客戶端

在這裏插入圖片描述

  • 訪問資源服務A

在這裏插入圖片描述

在這裏插入圖片描述

  • 訪問資源服務B

在這裏插入圖片描述
在這裏插入圖片描述

如果需要 資源服務器A 調用 B 中資源;可以把 B 中的實現邏輯複製過去就行。

後期會把資源服務器中的公共部分抽離出來,製成starter

2023-4-16:用starter實現Oauth2中資源服務的統一配置

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章