Spring Security OAuth2.0分布式認證和授權方案


1 OAuth 2.0

1.1 OAuth2.0介紹

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

  • OAuth 2.0是OAuth 協議的延續版本,但是不兼容OAuth 1.0即完全廢除了OAuth 1.0。很多大公司如Google、Microsoft等都提供了OAuth 認證服務,這些都足以說明OAuth 標准逐漸成為開放資源授權的標准。

  • 下邊分析一個OAuth 2.0認證的例子,通過這個例子可以很好的理解OAuth 2.0協議的認證過程,本例子是借助ProcessOn網站使用QQ認證的過程,過程簡要描述如下:

  • 用戶借助QQ認證登錄ProcessOn網站,用戶就不需要在ProcessOn網站上單獨注冊用戶,那么怎么才算認證成功呢?ProcessOn網站需要成功從QQ獲取用戶的身份信息則認為用戶認證成功,那如何從QQ獲取用戶的身份信息?用戶信息的擁有者是用戶本人,QQ需要經過用戶的同意方可為ProcessOn網站生成令牌,ProcessOn拿到此令牌才可以從QQ獲取用戶的信息。

  • 1️⃣客戶端請求第三方授權:用戶進入ProcessOn的登錄頁面,點擊QQ的圖標以QQ的賬號和密碼登錄系統,用戶是自己在QQ里信息的資源擁有者。

客戶端請求第三方授權

  • 2️⃣資源擁有者同意給客戶端授權:資源擁有者輸入QQ的賬號和密碼表示資源擁有者同意給客戶端授權,QQ會對資源擁有者的身份進行驗證,驗證通過后,QQ會詢問用戶是否給ProcessOn訪問自己的QQ數據,用戶打開QQ手機版,點擊“確認登錄”表示同意授權,QQ認證服務器會頒發一個授權碼,並重定向到ProcessOn的網站。

資源擁有者同意給客戶端授權1

資源擁有者同意給客戶端授權

  • 3️⃣客戶端獲取到授權碼,請求認證服務器申請令牌:此過程用戶看不到,客戶端應用程序請求認證服務器,請求攜帶授權碼。
  • 4️⃣認證服務器向客戶端響應令牌:QQ認證服務器驗證了客戶端請求的授權碼,如果合法則給客戶端頒發令牌,令牌是客戶端訪問資源的通行證。此交互過程用戶是看不到的,當客戶端拿到令牌后,用戶在ProcessOn網站上看到已經登錄成功。
  • 5️⃣客戶端請求資源服務器的資源:客戶端攜帶令牌請求訪問QQ服務器獲取用戶的基本信息。
  • 6️⃣資源服務器返回受保護資源:資源服務器校驗令牌的合法性,如果合法則向用戶響應資源信息內容。

1.2 OAuth 2.0角色

Oauth2.0角色

  • 1️⃣客戶端:本身不存儲資源,需要通過資源擁有者的授權去請求資源服務器的資源,比如:Android客戶端、Web客戶端、微信客戶端等。
  • 2️⃣資源擁有者:通常為用戶,也可以是應用程序,即該資源的擁有者。
  • 3️⃣授權服務器(也稱為認證服務器):用於服務提供商對資源擁有的身份進行認證,對訪問資源進行授權,認證成功后會給客戶端發放令牌(access_token),作為客戶端訪問資源服務器的憑證。
  • 4️⃣資源服務器:存儲資源的服務器,比如QQ服務器。

服務提供商不會隨便允許任意一個客戶端接入到它的授權服務器的,服務提供商會給准入的接入方一個身份,用於接入的憑證:client_id(客戶端標識)和client_secret(客戶端密鑰)。

1.3 OAuth 2.0的四種授權方式

1.3.1 概述

  • OAuth 2.0規定了四種授權模式。
  • 1️⃣授權碼(authorization code)。
  • 2️⃣隱藏式(implicit)。
  • 3️⃣密碼(password)。
  • 4️⃣客戶端憑證(client credentials)。

1.3.2 授權碼(authorization code)

  • 授權碼方式,指的是第三方應用先申請一個授權碼,然后再用這個授權碼去獲取令牌。

  • 授權碼方式是最常用的方式,安全性也最高,它適用於那些有后端的Web應用。授權碼通過前端發送,令牌是存儲到后端的,而且所有和資源服務器的通信都是在后端完成。這樣的前后端分離,可以避免令牌泄露。

  • 1️⃣A網站提供一個鏈接,用戶點擊后就會跳轉到B網站,授權用戶數據給A網站使用。下面就是A網站跳轉到B網站的一個示例連接。

https://b.com/oauth/authorize?
  response_type=code&
  client_id=CLIENT_ID&
  redirect_uri=CALLBACK_URL&
  scope=read

上面的URL中,response_type參數表示要求返回的授權碼codeclient_id參數是讓B網站知道是誰在請求,redirect_uri參數是B接受或拒絕請求后的跳轉網址,scope參數表示要求的授權范圍(這里是只讀)。

請求授權碼

  • 2️⃣用戶點擊后,B網站會要求用戶登錄,然后詢問是否同意給A網站授權。用戶表示同意,這時B網站就會調回redirect_uri參數指定的網址。跳轉的時候,會傳一個授權碼。
https://a.com/callback?code=AUTHORIZATION_CODE

上面的URL中,code參數就是授權碼。

返回授權碼

  • 3️⃣A網站拿到授權碼以后,可以在后端向B網站請求令牌。
https://b.com/oauth/token?
 client_id=CLIENT_ID&
 client_secret=CLIENT_SECRET&
 grant_type=AUTHORIZATION_CODE&
 code=authorization_code&
 redirect_uri=CALLBACK_URL

上面的URL中,client_id參數和client_secret參數用來讓B網站確認A網站的身份(client_secret參數是保密的,所以只能由后端發送請求)。grant_type參數的值是AUTHORIZATION_CODE,表示采用的授權方式是授權碼,code參數是上一步拿到的授權碼,redirect_uri參數是令牌頒發后的回調網址。

請求令牌

  • 4️⃣B網站收到請求以后,就會頒發令牌。具體的做法是向redirect_uri指定的網址,發送一段JSON數據,
{    
  "access_token":"ACCESS_TOKEN",
  "token_type":"bearer",
  "expires_in":2592000,
  "refresh_token":"REFRESH_TOKEN",
  "scope":"read",
  "uid":100101,
  "info":{...}
}

上面的JSON數據中,access_token字段就是令牌,A網站在后端可以拿到。

返回令牌

1.3.3 隱藏式(implicit)

  • 有些web應用是純前端應用,沒有后端。這時就不能使用上面的方式了,必須將令牌存儲在前端。這種方式沒有授權碼這個步驟,所以稱為授權碼的隱藏式。

  • 1️⃣A網站提供一個連接,要求用戶跳轉到B網站,授權用戶數據給A網站使用。

https://b.com/oauth/authorize?
  response_type=token&
  client_id=CLIENT_ID&
  redirect_uri=CALLBACK_URL&
  scope=read

上面的URL中,response_type參數為token,表示要求直接返回令牌。

  • 2️⃣用戶點擊后,跳轉到B網站,登錄后同意給A網站授權。這時,B網站就會跳轉到redirect_uri參數指定的跳轉網址,並且把令牌作為URL參數,傳給A網站。
https://a.com/callback#token=ACCESS_TOKEN

上面的URL中,token的參數就是令牌,A網站因此直接在前端拿到令牌。

注意:令牌的位置是URL錨點,而不是查詢字符串,這是因為OAuth 2.0允許跳轉網址是HTTP協議,因為存在“中間人攻擊”的風險,而瀏覽器跳轉時,錨點不會發到服務器,就減少了泄露令牌的風險。

隱藏式請求令牌和返回令牌

隱藏式是把令牌直接傳給前端,很不安全。因此,只能用於一些安全性要求不高的場景,並且令牌的有效期必須非常短,通常就是會話期間有效,瀏覽器關閉,令牌就失效了。

1.3.4 密碼式(password)

  • 如果你高度信任某個應用,OAuth 2.0允許用戶把用戶名和密碼,直接告訴該應用。該應用就使用你的密碼,申請令牌,這種方式稱為密碼式。

  • 1️⃣A網站要求用戶提供B網站的用戶名和密碼。拿到以后,A直接向B請求令牌。

https://oauth.b.com/token?
  grant_type=password&
  username=USERNAME&
  password=PASSWORD&
  client_id=CLIENT_ID&
  client_secret=CLIENT_SECRET

上面的URL中,grant_type參數是授權方式,這里的password表示密碼式,usernamepassword是B的用戶名和密碼。

  • 2️⃣B網站驗證身份通過后,直接給出令牌。注意,此時不需要跳轉,而是把令牌放在JSON里面,作為HTTP回應,A網站因此拿到令牌。

這種方式需要用戶給出自己的用戶名和密碼,風險非常大,因此只適用於其他授權都無法采用的情況,而且必須是用戶高度信任的應用。

1.3.5 客戶端憑證式(client credentials)

  • 這種方式適用於沒有前端的命令行應用,即在命令行下請求令牌。

  • 1️⃣A應用在命令行向B發出請求。

https://oauth.b.com/token?
  grant_type=client_credentials&
  client_id=CLIENT_ID&
  client_secret=CLIENT_SECRET

上面的URL中,grant_type參數是授權方式,這里的client_credentials表示客戶端憑證式,client_idclient_secret用來讓B確認A的身份。

  • 2️⃣B網站驗證通過后,直接返回令牌。

這種方式給出的令牌,是針對第三方應用的,而不是針對用戶的,即有可能多個用戶共享同一個令牌。

1.4 OAuth 2.0中令牌的使用

  • A網站拿到令牌以后,就可以向B網站的API請求數據了。
  • 每個發送的API請求,都必須攜帶令牌。具體的做法就是在請求的頭信息,加上Authorization字段,令牌就放在這個字段里面。
curl -H "Authorization: Bearer ACCESS_TOKEN" \
"https://api.b.com"

上面命令中,Authorization就是拿到的令牌。

1.5 OAuth 2.0中更新令牌

  • 令牌的有效期到了,如果讓用戶重新走一遍上面的流程,再申請一個新的令牌,可能體驗不好,而且也沒有必要。OAuth 2.0允許用戶自動更新令牌。
  • B網站頒發令牌的時候,一次性頒發兩個令牌,一個用於獲取數據,另一個用於獲取新的令牌(refresh token)。令牌到期后,用戶使用refresh token發送一個請求,去更新令牌。
https://b.com/oauth/token?
  grant_type=refresh_token&
  client_id=CLIENT_ID&
  client_secret=CLIENT_SECRET&
  refresh_token=REFRESH_TOKEN

上面的URL中,grant_type參數為refresh_token表示要求更新令牌,client_id參數和client_secret參數用於確認身份,refresh_token參數就是用於更新令牌的令牌。

  • B網站驗證通過后,就會頒發新的令牌了。

2 Spring Security OAuth2

2.1 介紹

  • Spring Security OAuth2是對OAuth2的一種實現。

  • OAuth2的服務提供涵蓋了兩種服務:授權服務(Authorization Server,認證和授權服務)和資源服務(Resource Server)。使用Spring Security OAuth2的時候可以將授權服務和資源服務放在同一個應用程序中試下你,也可以選擇建立使用同一個授權服務的多個資源服務。

  • 授權服務(Authorization Server):包含對接入端以及登入用戶的合法性進行驗證並頒發token等功能,對令牌的請求端點由Spring MVC或Spring Webflux的控制器進行實現,下面是配置一個認證服務要實現的endpoints(端點):

    • AuthorizationEndpoint用於認證請求。默認的URL是/oauth/authorize。
    • TokenEndpoint用於訪問令牌的請求。默認的URL是/oauth/token。
  • 資源服務(Resource Server):包含對資源的保護功能,對非法請求進行攔截,對請求中token進行解析鑒權等,下面的過濾器用於實現OAuth2的資源服務。

    • OAuth2AuthenticationProcessingFilter用來對請求給出的身份令牌解析鑒權。

2.2 環境搭建

2.2.1 mysql數據庫的腳本

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for oauth_access_token
-- ----------------------------
DROP TABLE IF EXISTS `oauth_access_token`;
CREATE TABLE `oauth_access_token`  (
  `token_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `token` longblob NULL,
  `authentication_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `user_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `client_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `authentication` longblob NULL,
  `refresh_token` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Table structure for oauth_approvals
-- ----------------------------
DROP TABLE IF EXISTS `oauth_approvals`;
CREATE TABLE `oauth_approvals`  (
  `userId` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `clientId` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `scope` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `status` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `expiresAt` datetime(0) NULL DEFAULT NULL,
  `lastModifiedAt` datetime(0) NULL DEFAULT NULL
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of oauth_approvals
-- ----------------------------
INSERT INTO `oauth_approvals` VALUES ('admin', 'client_id', 'write', 'APPROVED', '2020-12-11 13:36:21', '2020-11-11 13:36:21');
INSERT INTO `oauth_approvals` VALUES ('admin', 'client_id', 'read', 'APPROVED', '2020-12-11 13:36:21', '2020-11-11 13:36:21');

-- ----------------------------
-- Table structure for oauth_client_details
-- ----------------------------
DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details`  (
  `client_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `resource_ids` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `client_secret` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `scope` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `authorized_grant_types` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `web_server_redirect_uri` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `authorities` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `access_token_validity` int(11) NULL DEFAULT NULL,
  `refresh_token_validity` int(11) NULL DEFAULT NULL,
  `additional_information` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `autoapprove` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of oauth_client_details
-- ----------------------------
INSERT INTO `oauth_client_details` VALUES ('client_id', 'product-api', '$2a$10$opFZ09sDtJ7yEA65V8cvnOcxgaWT9sqxy5TNzahQvvnhrsMgQQNVy', 'read,write', 'authorization_code,password,implicit,client_credentials,refresh_token', 'http://localhost:9001', NULL, NULL, NULL, NULL, 'false');

-- ----------------------------
-- Table structure for oauth_client_token
-- ----------------------------
DROP TABLE IF EXISTS `oauth_client_token`;
CREATE TABLE `oauth_client_token`  (
  `token_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `token` longblob NULL,
  `authentication_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `user_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `client_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Table structure for oauth_code
-- ----------------------------
DROP TABLE IF EXISTS `oauth_code`;
CREATE TABLE `oauth_code`  (
  `code` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `authentication` longblob NULL DEFAULT NULL
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Table structure for oauth_refresh_token
-- ----------------------------
DROP TABLE IF EXISTS `oauth_refresh_token`;
CREATE TABLE `oauth_refresh_token`  (
  `token_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `token` longblob NULL,
  `authentication` longblob NULL
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Table structure for sys_permission
-- ----------------------------
DROP TABLE IF EXISTS `sys_permission`;
CREATE TABLE `sys_permission`  (
  `ID` int(11) NOT NULL AUTO_INCREMENT COMMENT '編號',
  `permission_NAME` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '菜單名稱',
  `permission_url` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '菜單地址',
  `parent_id` int(11) NOT NULL DEFAULT 0 COMMENT '父菜單id',
  PRIMARY KEY (`ID`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role`  (
  `ID` int(11) NOT NULL AUTO_INCREMENT COMMENT '編號',
  `ROLE_NAME` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '角色名稱',
  `ROLE_DESC` varchar(60) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '角色描述',
  PRIMARY KEY (`ID`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_role
-- ----------------------------
INSERT INTO `sys_role` VALUES (1, 'ROLE_ADMIN', '管理員角色');

-- ----------------------------
-- Table structure for sys_role_permission
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_permission`;
CREATE TABLE `sys_role_permission`  (
  `RID` int(11) NOT NULL COMMENT '角色編號',
  `PID` int(11) NOT NULL COMMENT '權限編號',
  PRIMARY KEY (`RID`, `PID`) USING BTREE,
  INDEX `FK_Reference_12`(`PID`) USING BTREE,
  CONSTRAINT `FK_Reference_11` FOREIGN KEY (`RID`) REFERENCES `sys_role` (`ID`) ON DELETE RESTRICT ON UPDATE RESTRICT,
  CONSTRAINT `FK_Reference_12` FOREIGN KEY (`PID`) REFERENCES `sys_permission` (`ID`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用戶名稱',
  `password` varchar(120) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '密碼',
  `status` int(1) NULL DEFAULT 1 COMMENT '1開啟0關閉',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES (1, 'admin', '$2a$10$L0RfoLUKcx/a0HHW9p2/WOZQDa8NdTOv463TCXsaXT0wx7uJT0wjq', 1);

-- ----------------------------
-- Table structure for sys_user_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role`  (
  `UID` int(11) NOT NULL COMMENT '用戶編號',
  `RID` int(11) NOT NULL COMMENT '角色編號',
  PRIMARY KEY (`UID`, `RID`) USING BTREE,
  INDEX `FK_Reference_10`(`RID`) USING BTREE,
  CONSTRAINT `FK_Reference_10` FOREIGN KEY (`RID`) REFERENCES `sys_role` (`ID`) ON DELETE RESTRICT ON UPDATE RESTRICT,
  CONSTRAINT `FK_Reference_9` FOREIGN KEY (`UID`) REFERENCES `sys_user` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_user_role
-- ----------------------------
INSERT INTO `sys_user_role` VALUES (1, 1);

SET FOREIGN_KEY_CHECKS = 1;

2.2.2 父工程的pom

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <packaging>pom</packaging>
    <modules>
        <module>spring-security-oauth2-uaa</module>
        <module>spring-security-oauth2-product</module>
    </modules>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.11.RELEASE</version>
        <relativePath/>
    </parent>
    <groupId>com.sunxiaping</groupId>
    <artifactId>spring-security-oauth2</artifactId>
    <version>1.0</version>
    <name>spring-security-oauth2</name>


    <properties>
        <java.version>1.8</java.version>
        <spring-cloud-alibaba.version>2.2.1.RELEASE</spring-cloud-alibaba.version>
        <spring-cloud.version>Hoxton.SR8</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security.oauth.boot</groupId>
            <artifactId>spring-security-oauth2-autoconfigure</artifactId>
            <version>2.1.11.RELEASE</version>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${spring-cloud-alibaba.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.2.11.RELEASE</version>
            </plugin>
        </plugins>
    </build>

    <profiles>
        <profile>
            <id>default</id>
            <activation>
                <activeByDefault>true</activeByDefault>
            </activation>
            <build>
                <plugins>
                    <plugin>
                        <groupId>io.spring.javaformat</groupId>
                        <artifactId>spring-javaformat-maven-plugin</artifactId>
                        <version>0.0.25</version>
                    </plugin>
                    <plugin>
                        <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-surefire-plugin</artifactId>
                        <configuration>
                            <includes>
                                <include>**/*Tests.java</include>
                            </includes>
                            <excludes>
                                <exclude>**/Abstract*.java</exclude>
                            </excludes>
                            <systemPropertyVariables>
                                <java.security.egd>file:/dev/./urandom</java.security.egd>
                                <java.awt.headless>true</java.awt.headless>
                            </systemPropertyVariables>
                        </configuration>
                    </plugin>
                    <plugin>
                        <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-enforcer-plugin</artifactId>
                        <executions>
                            <execution>
                                <id>enforce-rules</id>
                                <goals>
                                    <goal>enforce</goal>
                                </goals>
                                <configuration>
                                    <rules>
                                        <bannedDependencies>
                                            <excludes>
                                                <exclude>commons-logging:*:*</exclude>
                                            </excludes>
                                            <searchTransitive>true</searchTransitive>
                                        </bannedDependencies>
                                    </rules>
                                    <fail>true</fail>
                                </configuration>
                            </execution>
                        </executions>
                    </plugin>
                    <plugin>
                        <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-install-plugin</artifactId>
                        <configuration>
                            <skip>true</skip>
                        </configuration>
                    </plugin>
                    <plugin>
                        <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-javadoc-plugin</artifactId>
                        <configuration>
                            <skip>true</skip>
                        </configuration>
                        <inherited>true</inherited>
                    </plugin>
                </plugins>
            </build>
        </profile>
    </profiles>

</project>

2.2.3 uua授權服務工程的pom

<?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>spring-security-oauth2</artifactId>
        <groupId>com.sunxiaping</groupId>
        <version>1.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>spring-security-oauth2-uaa</artifactId>
    <description>認證的微服務</description>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.0</version>
        </dependency>
    </dependencies>

</project>

2.2.4 product資源服務工程的pom

<?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>spring-security-oauth2</artifactId>
        <groupId>com.sunxiaping</groupId>
        <version>1.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>spring-security-oauth2-product</artifactId>
    <description>資源微服務</description>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
    </dependencies>

</project>

2.3 授權服務器配置

2.3.1 @EnableAuthorizationServer

  • 可以使用@EnableAuthorizationServer注解並繼承AuthorizationServerConfigurerAdapter來配置OAuth2.0授權服務器。
  • 在config包下創建Oauth2ServerConfig:
package com.sunxiaping.config;

import com.sunxiaping.service.UserService;
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.crypto.bcrypt.BCryptPasswordEncoder;
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.approval.ApprovalStore;
import org.springframework.security.oauth2.provider.approval.JdbcApprovalStore;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.JdbcAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;

import javax.sql.DataSource;

/**
 * @author 許大仙
 * @version 1.0
 * @since 2020-11-09 17:17
 */
@Configuration
@EnableAuthorizationServer // 開啟認證服務器
public class Oauth2ServerConfig extends AuthorizationServerConfigurerAdapter {
        
}    
  • AuthorizationServerConfigurerAdapter要求配置以下幾個類,這幾個類由Spring創建的獨立的配置對象,它們會被Spring傳入AuthorizationServerConfigurer中進行配置
package org.springframework.security.oauth2.config.annotation.web.configuration;

import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;

public class AuthorizationServerConfigurerAdapter implements AuthorizationServerConfigurer {
    public AuthorizationServerConfigurerAdapter() {
    }
	
    //用來配置令牌端點的安全性約束
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
    }
	
    //ClientDetailsServiceConfigurer:用來配置客戶端詳情服務(ClientDetailsService),客戶端詳情信息在這里進行初始化,我們可以把客戶端詳情信息寫死在這里,也可以通過數據庫來存儲調取詳情信息
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    }
    
    //用來配置令牌(token)的訪問端點和令牌服務(token services)
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    }
}

2.3.2 配置客戶端詳情服務

  • ClientDetailsServiceConfigurer能夠適用內存或者JDBC來實現客戶端詳情服務(ClientDetailsService),ClientDetailsService負責查找ClientDetails,而ClientDetails有以下幾個重要的屬性如下表所示:
屬性 描述
clientId 用來標識客戶端的id。
secret 客戶端安全碼,需要是信任的客戶端。
scope 用來限制客戶端的訪問范圍,如果為空(默認),那么客戶端將擁有全部的訪問范圍
authorizationGrantTypes 此客戶單可以使用的授權類型,默認為空。
authorities 此客戶端可以使用的權限(基於Spring Security authorities)
  • 客戶端詳情(ClientDetails)能夠在應用程序運行的時候進行更新,可以通過訪問底層的存儲服務(例如將客戶端詳情存儲在一個關系型數據庫的表中,就可以使用JdbcClientDetailsService)或者通過自己實現ClientRegistrationService接口(同時也可以使用ClientDetailsService接口)來進行管理。
  • 暫時使用內存方式存儲客戶端詳情信息,配置如下:
package com.sunxiaping.config;

import com.sunxiaping.service.UserService;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
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.approval.ApprovalStore;
import org.springframework.security.oauth2.provider.approval.JdbcApprovalStore;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.JdbcAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;

import javax.sql.DataSource;

/**
 * @author 許大仙
 * @version 1.0
 * @since 2020-11-09 17:17
 */
@Configuration
@EnableAuthorizationServer // 開啟認證服務器
public class Oauth2ServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder;

    /**
     * ClientDetailsServiceConfigurer:用來配置客戶端詳情服務(ClientDetaisService).
     * 客戶端詳情信息在這里進行初始化,我們可以把客戶端詳情信息寫死在這里,也可以通過數據庫來存儲調取詳情信息
     *
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        //使用內存方式存儲客戶端詳情
        clients.inMemory()
                .withClient("client_id") //client_id
                .secret(passwordEncoder.encode("secret")) //client_secret
                .resourceIds("product-id") //資源標識
                .authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token") //該client允許的授權類型
                .scopes("read,write")//允許的授權范圍
                .autoApprove(false)
                .resourceIds("https://www.baidu.com"); //回調地址
    }   
}

2.3.3 令牌服務

  • AuthorizationServerTokenServices接口定義了一些操作使得我們可以對令牌進行一些必要的管理,令牌可以被用來加載身份信息,里面包含了這個令牌的相關權限。

  • 我們自己可以創建AuthorizationServerTokenServices的實現類,但是我們也可以繼承DefaultTokenServices這個類,因為DefaultTokenServices里面包含了一些有用的實現,我們可以使用它來修改令牌的格式和令牌的存儲。默認情況下,當DefaultTokenServices創建一個令牌的時候,是使用隨機數來進行填充的,除了持久化令牌是委托給一個TokenStore接口來實現以外,這個類幾乎幫助我們做了所有的事情。並且TokenStore接口還有一個默認的實現,它就是InMemoryTokenStore,表明所有的令牌都保存在內存中。除了使用InMemoryTokenStore外,我們還可以使用其他的預定義實現,它們都實現了TokenStore接口:

    • 1️⃣InMemoryTokenStore:這個實現是默認采用的,它可以完美的工作的單服務器上(即訪問並發量壓力不大的情況下,並且它在失敗的時候不會進行備份),大多數的項目都可以使用這個類來進行嘗試,我們可以在開發的時候使用這個類進行管理,因為不會被保存到磁盤中,更易於調試。
    • 2️⃣JdbcTokenStore:這是一個基於JDBC的實現,令牌會被保存進關系型數據庫。使用這個類的時候,我們可以在不同的服務器之間共享令牌信息。
    • 3️⃣JwtTokenStore:這是一個基於Jwt的實現,它可以把令牌相關的數據進行編碼(對后端服務來說,它不需要進行存儲,有很大的優勢),但是有一個缺點,就是撤銷一個已經授權令牌會非常困難,所以通常用來處理一個生產周期較短的令牌以及撤銷刷新令牌(refresh token)。另一個缺點就是這個令牌的占用空間非常大。
  • 我們暫時先使用InMemoryTokenStore方式生成普通的令牌,並配置令牌服務:

package com.sunxiaping.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.password.PasswordEncoder;
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.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;

/**
 * @author 許大仙
 * @version 1.0
 * @since 2020-11-09 17:17
 */
@Configuration
@EnableAuthorizationServer // 開啟認證服務器
public class Oauth2ServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder;

    /**
     * Token存儲策略,使用內存方式
     *
     * @return
     */
    @Bean
    public TokenStore tokenStore() {
        return new InMemoryTokenStore();
    }

    @Autowired
    private ClientDetailsService clientDetailsService;

    /**
     * 令牌服務
     *
     * @return
     */
    @Bean
    public AuthorizationServerTokenServices authorizationServerTokenServices() {
        DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
        defaultTokenServices.setClientDetailsService(clientDetailsService); //客戶端詳情服務
        defaultTokenServices.setSupportRefreshToken(true);//支持刷新令牌
        defaultTokenServices.setTokenStore(tokenStore());//token存儲策略
        defaultTokenServices.setAccessTokenValiditySeconds(7200); //令牌默認的有效期是2小時
        defaultTokenServices.setRefreshTokenValiditySeconds(259200); //刷新令牌的有效期是3天
        return defaultTokenServices;
    }


    /**
     * ClientDetailsServiceConfigurer:用來配置客戶端詳情服務(ClientDetailsService).
     * 客戶端詳情信息在這里進行初始化,我們可以把客戶端詳情信息寫死在這里,也可以通過數據庫來存儲調取詳情信息
     *
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        //使用內存方式存儲客戶端詳情
        clients.inMemory()
                .withClient("client_id") //client_id
                .secret(passwordEncoder.encode("secret")) //client_secret
                .resourceIds("product-id") //資源標識
                .authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token") //該client允許的授權類型
                .scopes("read,write")//允許的授權范圍
                .autoApprove(false) //false表示跳轉到授權頁面
                .redirectUris("https://www.baidu.com"); //回調地址
    } 
}    

2.3.4 令牌訪問端點配置

  • AuthorizationServerEndpointsConfigurer這個對象的實例可以完成令牌服務以及令牌endpoint的配置。

  • 配置授權類型(Grant Types):AuthorizationServerEndpointsConfigurer通過設定如下的屬性來決定支持的授權支持的授權類型(Grant Types)。

    • authenticationManager:認證管理器,當我們選擇了資源所有者的密碼授權類型的時候,需要為這個屬性注入一個AuthenticationManager對象。
    • userDetailsService:如果我們設置了這個屬性的話,說明我們有一個自己的UserDetailsService接口的實現。
    • authorizationCodeServices:這個屬性是用來設置授權碼服務的(即AuthorizationCodeServices的實例對象),主要用於“authorization_code”授權碼類型模式。
    • implicitGrantService:這個屬性用於設置隱式授權模式,用來管理隱式授權模式的狀態。
    • tokenGranter:當我們設置了這個屬性(即tokenGranter接口實現),那么授權將完全由我們自己來掌控,並且會忽略上面的幾個屬性,這個屬性一般是用作拓展用途的,即標准的四種授權模式已經滿足不了我們的需求,才會考慮使用這個,實際中一般不用。
  • 配置授權端點的URL(Endpoint URLs)

    • AuthorizationServerEndpointsConfigurer有一個叫做pathMapping()的方法用來配置端點URL鏈接,它有兩個參數。

      • 第一個參數:String類型,這個端點URL的默認鏈接。
      • 第二個參數:String類型,我們可以進行替代的URL鏈接。
    • 以上的參數都將以"/"字符為開始的字符串,框架的默認URL鏈接如下所示,可以作為這個pathMapping()方法的第一個參數:

      URL鏈接 描述
      /oauth/authorize 授權端點
      /oauth/token 令牌端點
      /oauth/confirm_access 用戶確認授權提交端點
      /oauth/error 授權服務錯誤信息端點
      /oauth/check_token 用於資源服務訪問的令牌解析端點
      /oauth/token_key 提供公共密鑰的端點,如果我們使用JWT令牌的話
  • 下面配置SpringSecurity(這個和OAuth2沒有太多必然聯系):

    • application.yml
    server:
      port: 9001
    
    spring:
      datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://192.168.1.57:3306/test?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
        username: root
        password: 123456
        # Hikari 連接池配置
        hikari:
          # 最小空閑連接數量
          minimum-idle: 5
          # 空閑連接存活最大時間,默認600000(10分鍾)
          idle-timeout: 180000
          # 連接池最大連接數,默認是10
          maximum-pool-size: 1000
          # 此屬性控制從池返回的連接的默認自動提交行為,默認值:true
          auto-commit: true
          # 連接池名稱
          pool-name: HikariCP
          # 此屬性控制池中連接的最長生命周期,值0表示無限生命周期,默認1800000即30分鍾
          max-lifetime: 1800000
          # 數據庫連接超時時間,默認30秒,即30000
          connection-timeout: 30000
          connection-test-query: SELECT 1
          data-source-properties:
            useInformationSchema: true
      main:
        # 允許我們自己覆蓋Spring放入到IOC容器的對象
        allow-bean-definition-overriding: true
    
    mybatis:
      type-aliases-package: com.sunxiaping.domain
      configuration:
        map-underscore-to-camel-case: true
    
    logging:
      level:
        com.sunxiaping: debug
    
    
    • SysUser.java
    package com.sunxiaping.domain;
    
    import com.fasterxml.jackson.annotation.JsonIgnore;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.userdetails.UserDetails;
    
    import java.util.Collection;
    import java.util.List;
    
    public class SysUser implements UserDetails {
    
    	private Integer id;
    
    	private String username;
    
    	private String password;
    
    	private Integer status;
    
    	private List<SysRole> roles;
    
    	public List<SysRole> getRoles() {
    		return roles;
    	}
    
    	public void setRoles(List<SysRole> roles) {
    		this.roles = roles;
    	}
    
    	public Integer getId() {
    		return id;
    	}
    
    	public void setId(Integer id) {
    		this.id = id;
    	}
    
    	public void setUsername(String username) {
    		this.username = username;
    	}
    
    	public void setPassword(String password) {
    		this.password = password;
    	}
    
    	public Integer getStatus() {
    		return status;
    	}
    
    	public void setStatus(Integer status) {
    		this.status = status;
    	}
    
    	@JsonIgnore
    	@Override
    	public Collection<? extends GrantedAuthority> getAuthorities() {
    		return roles;
    	}
    
    	@Override
    	public String getPassword() {
    		return password;
    	}
    
    	@Override
    	public String getUsername() {
    		return username;
    	}
    
    	@JsonIgnore
    	@Override
    	public boolean isAccountNonExpired() {
    		return true;
    	}
    
    	@JsonIgnore
    	@Override
    	public boolean isAccountNonLocked() {
    		return true;
    	}
    
    	@JsonIgnore
    	@Override
    	public boolean isCredentialsNonExpired() {
    		return true;
    	}
    
    	@JsonIgnore
    	@Override
    	public boolean isEnabled() {
    		return true;
    	}
    
    }
    
    • SysRole.java
    package com.sunxiaping.domain;
    
    import com.fasterxml.jackson.annotation.JsonIgnore;
    import org.springframework.security.core.GrantedAuthority;
    
    public class SysRole implements GrantedAuthority {
    
    	private Integer id;
    
    	private String roleName;
    
    	private String roleDesc;
    
    	public Integer getId() {
    		return id;
    	}
    
    	public void setId(Integer id) {
    		this.id = id;
    	}
    
    	public String getRoleName() {
    		return roleName;
    	}
    
    	public void setRoleName(String roleName) {
    		this.roleName = roleName;
    	}
    
    	public String getRoleDesc() {
    		return roleDesc;
    	}
    
    	public void setRoleDesc(String roleDesc) {
    		this.roleDesc = roleDesc;
    	}
    
    	@JsonIgnore
    	@Override
    	public String getAuthority() {
    		return roleName;
    	}
    
    }
    
    • UserMapper.java
    package com.sunxiaping.mapper;
    
    import com.sunxiaping.domain.SysUser;
    import org.apache.ibatis.annotations.*;
    
    import java.util.List;
    
    @Mapper
    public interface UserMapper {
    
        @Select("select * from sys_user where username = #{username}")
        @Results(
                {@Result(id = true, property = "id", column = "id"), @Result(property = "roles", column = "id", javaType = List.class, many = @Many(select = "com.sunxiaping.mapper.RoleMapper.findByUid"))})
        SysUser findByName(String username);
    
    }
    
    • RoleMapper.java
    package com.sunxiaping.mapper;
    
    import com.sunxiaping.domain.SysRole;
    import org.apache.ibatis.annotations.Mapper;
    import org.apache.ibatis.annotations.Select;
    
    import java.util.List;
    
    @Mapper
    public interface RoleMapper {
    
        @Select("SELECT r.id, r.role_name roleName, r.role_desc roleDesc " + "FROM sys_role r, sys_user_role ur "
                + "WHERE r.id=ur.rid AND ur.uid=#{uid}")
        List<SysRole> findByUid(Integer uid);
    
    }
    
    • UserService.java
    package com.sunxiaping.service;
    
    import org.springframework.security.core.userdetails.UserDetailsService;
    
    public interface UserService extends UserDetailsService {
    
    }
    
    • UserServiceImpl.java
    package com.sunxiaping.service.impl;
    
    import com.sunxiaping.mapper.UserMapper;
    import com.sunxiaping.service.UserService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UsernameNotFoundException;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    
    @Service
    @Transactional
    public class UserServiceImpl implements UserService {
    
    	@Autowired
    	private UserMapper userMapper;
    
    	@Override
    	public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
    		return userMapper.findByName(s);
    	}
    
    }
    
    • 啟動類:
    package com.sunxiaping;
    
    import org.mybatis.spring.annotation.MapperScan;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    /**
     * @author 許大仙
     * @version 1.0
     * @since 2020-11-09 17:03
     */
    @SpringBootApplication
    @MapperScan(basePackages = "com.sunxiaping.mapper")
    public class UaaApplication {
    
    	public static void main(String[] args) {
    		SpringApplication.run(UaaApplication.class, args);
    	}
    
    }
    
    • WebSecurityConfig.java
    package com.sunxiaping.config;
    
    import com.sunxiaping.service.UserService;
    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.method.configuration.EnableGlobalMethodSecurity;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.builders.WebSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;
    
    /**
     * @author 許大仙
     * @version 1.0
     * @since 2020-11-09 17:25
     */
    @Configuration
    @EnableWebSecurity
    @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        private UserService userService;
    
        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                    // 所有資源必須授權后訪問
                    .anyRequest().authenticated()
                    .and()
                    .formLogin().loginProcessingUrl("/login").permitAll()// 指定認證頁面可以匿名訪問
                    // 關閉跨站請求防護
                    .and().csrf().disable();
        }
    
        @Override
        public void configure(WebSecurity web) throws Exception {
            web.ignoring().antMatchers( "/favicon.ico", "*/v2/api-docs", "*/v3/api-docs", "*/webjars/**", "/*/api-docs", "/swagger**/**", "/doc.html", "/v3/**");
        }
    
        /**
         * AuthenticationManager對象在Oauth2認證服務中要使用,提前放入到IOC容器中
         *
         * @return
         * @throws Exception
         */
        @Override
        @Bean
        public AuthenticationManager authenticationManagerBean() throws Exception {
            return super.authenticationManagerBean();
        }
    }
    
  • 配置令牌訪問和令牌服務部分:

package com.sunxiaping.config;

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.core.userdetails.UserDetailsService;
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.approval.ApprovalStore;
import org.springframework.security.oauth2.provider.approval.InMemoryApprovalStore;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.InMemoryAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;

/**
 * @author 許大仙
 * @version 1.0
 * @since 2020-11-09 17:17
 */
@Configuration
@EnableAuthorizationServer // 開啟認證服務器
public class Oauth2ServerConfig extends AuthorizationServerConfigurerAdapter {

    /**
     * Token存儲策略,使用內存方式
     *
     * @return
     */
    @Bean
    public TokenStore tokenStore() {
        return new InMemoryTokenStore();
    }

    @Autowired
    private ClientDetailsService clientDetailsService;

    /**
     * 令牌服務
     *
     * @return
     */
    @Bean
    public AuthorizationServerTokenServices authorizationServerTokenServices() {
        DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
        defaultTokenServices.setClientDetailsService(clientDetailsService); //客戶端詳情服務
        defaultTokenServices.setSupportRefreshToken(true);//支持刷新令牌
        defaultTokenServices.setTokenStore(tokenStore());//token存儲策略
        defaultTokenServices.setAccessTokenValiditySeconds(7200); //令牌默認的有效期是2小時
        defaultTokenServices.setRefreshTokenValiditySeconds(259200); //刷新令牌的有效期是3天
        return defaultTokenServices;
    }

    /**
     * 設置授權碼模式的授權服務,此處使用內存方式
     *
     * @return
     */
    @Bean
    public AuthorizationCodeServices authorizationCodeServices() {
        return new InMemoryAuthorizationCodeServices();
    }

    @Autowired
    private AuthenticationManager authenticationManager;


    @Autowired
    private UserDetailsService userDetailsService;


    /**
     * 授權信息保存策略
     *
     * @return
     */
    @Bean
    public ApprovalStore approvalStore() {
        return new InMemoryApprovalStore();
    }


    /**
     * 配置令牌(token)的訪問端點和令牌服務(token Services)
     *
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                .authenticationManager(authenticationManager) //認證管理器
                .userDetailsService(userDetailsService) // UserDetailsService
                .authorizationCodeServices(authorizationCodeServices()) //授權服務
                .tokenServices(authorizationServerTokenServices()) //令牌管理服務
                .approvalStore(approvalStore());
    }

}
  • 配置客戶端詳情服務和配置令牌訪問和令牌服務完整部分:
package com.sunxiaping.config;

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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
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.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.approval.ApprovalStore;
import org.springframework.security.oauth2.provider.approval.InMemoryApprovalStore;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.InMemoryAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;

/**
 * @author 許大仙
 * @version 1.0
 * @since 2020-11-09 17:17
 */
@Configuration
@EnableAuthorizationServer // 開啟認證服務器
public class Oauth2ServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder;

    /**
     * Token存儲策略,使用內存方式
     *
     * @return
     */
    @Bean
    public TokenStore tokenStore() {
        return new InMemoryTokenStore();
    }

    @Autowired
    private ClientDetailsService clientDetailsService;

    /**
     * 令牌服務
     *
     * @return
     */
    @Bean
    public AuthorizationServerTokenServices authorizationServerTokenServices() {
        DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
        defaultTokenServices.setClientDetailsService(clientDetailsService); //客戶端詳情服務
        defaultTokenServices.setSupportRefreshToken(true);//支持刷新令牌
        defaultTokenServices.setTokenStore(tokenStore());//token存儲策略
        defaultTokenServices.setAccessTokenValiditySeconds(7200); //令牌默認的有效期是2小時
        defaultTokenServices.setRefreshTokenValiditySeconds(259200); //刷新令牌的有效期是3天
        return defaultTokenServices;
    }

    /**
     * 設置授權碼模式的授權服務,此處使用內存方式
     *
     * @return
     */
    @Bean
    public AuthorizationCodeServices authorizationCodeServices() {
        return new InMemoryAuthorizationCodeServices();
    }

    @Autowired
    private AuthenticationManager authenticationManager;


    @Autowired
    private UserDetailsService userDetailsService;


    /**
     * 授權信息保存策略
     *
     * @return
     */
    @Bean
    public ApprovalStore approvalStore() {
        return new InMemoryApprovalStore();
    }


    /**
     * ClientDetailsServiceConfigurer:用來配置客戶端詳情服務(ClientDetailsService).
     * 客戶端詳情信息在這里進行初始化,我們可以把客戶端詳情信息寫死在這里,也可以通過數據庫來存儲調取詳情信息
     *
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        //使用內存方式存儲客戶端詳情
        clients.inMemory()
                .withClient("client_id") //client_id
                .secret(passwordEncoder.encode("secret")) //client_secret
                .resourceIds("product-id") //資源標識
                .authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token") //該client允許的授權類型
                .scopes("read,write")//允許的授權范圍
                .autoApprove(false) //false表示跳轉到授權頁面
                .redirectUris("https://www.baidu.com"); //回調地址
    }

    /**
     * 配置令牌(token)的訪問端點和令牌服務(token Services)
     *
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                .authenticationManager(authenticationManager) //認證管理器
                .userDetailsService(userDetailsService) // UserDetailsService
                .authorizationCodeServices(authorizationCodeServices()) //授權服務
                .tokenServices(authorizationServerTokenServices()) //令牌管理服務
                .approvalStore(approvalStore());
    }
}

2.3.5 令牌端點的安全性約束

  • AuthorizationServerSecurityConfigurer用來配置令牌端點的安全約束。
  • 配置令牌端點的安全性約束部分:
package com.sunxiaping.config;

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.core.userdetails.UserDetailsService;
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.approval.ApprovalStore;
import org.springframework.security.oauth2.provider.approval.InMemoryApprovalStore;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.InMemoryAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;

/**
 * @author 許大仙
 * @version 1.0
 * @since 2020-11-09 17:17
 */
@Configuration
@EnableAuthorizationServer // 開啟認證服務器
public class Oauth2ServerConfig extends AuthorizationServerConfigurerAdapter {

    /**
     * 用來配置令牌端點的安全約束
     *
     * @param security
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security
                .tokenKeyAccess("permitAll()") //當使用JwtToken且使用非對稱加密時,資源服務用語獲取公鑰而開放的,這里配置的是這個端點完全開放
                .checkTokenAccess("permitAll()") //checkToken這個端點完全公開
                .allowFormAuthenticationForClients(); //允許表單認證
    }
    
}    
  • 配置客戶端詳情服務和配置令牌訪問和令牌服務完整部分以及配置令牌端點的安全性約束完整部分:
package com.sunxiaping.config;

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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
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.approval.ApprovalStore;
import org.springframework.security.oauth2.provider.approval.InMemoryApprovalStore;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.InMemoryAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;

/**
 * @author 許大仙
 * @version 1.0
 * @since 2020-11-09 17:17
 */
@Configuration
@EnableAuthorizationServer // 開啟認證服務器
public class Oauth2ServerConfig extends AuthorizationServerConfigurerAdapter {


    /**
     * Token存儲策略,使用內存方式
     *
     * @return
     */
    @Bean
    public TokenStore tokenStore() {
        return new InMemoryTokenStore();
    }

    @Autowired
    private ClientDetailsService clientDetailsService;

    /**
     * 令牌服務
     *
     * @return
     */
    @Bean
    public AuthorizationServerTokenServices authorizationServerTokenServices() {
        DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
        defaultTokenServices.setClientDetailsService(clientDetailsService); //客戶端詳情服務
        defaultTokenServices.setSupportRefreshToken(true);//支持刷新令牌
        defaultTokenServices.setTokenStore(tokenStore());//token存儲策略
        defaultTokenServices.setAccessTokenValiditySeconds(7200); //令牌默認的有效期是2小時
        defaultTokenServices.setRefreshTokenValiditySeconds(259200); //刷新令牌的有效期是3天
        return defaultTokenServices;
    }

    /**
     * 設置授權碼模式的授權服務,此處使用內存方式
     *
     * @return
     */
    @Bean
    public AuthorizationCodeServices authorizationCodeServices() {
        return new InMemoryAuthorizationCodeServices();
    }

    @Autowired
    private AuthenticationManager authenticationManager;


    @Autowired
    private UserDetailsService userDetailsService;
    
    /**
     * 授權信息保存策略
     *
     * @return
     */
    @Bean
    public ApprovalStore approvalStore() {
        return new InMemoryApprovalStore();
    }

    @Autowired
    private PasswordEncoder passwordEncoder;

    /**
     * ClientDetailsServiceConfigurer:用來配置客戶端詳情服務(ClientDetailsService).
     * 客戶端詳情信息在這里進行初始化,我們可以把客戶端詳情信息寫死在這里,也可以通過數據庫來存儲調取詳情信息
     *
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        //使用內存方式存儲客戶端詳情
        clients.inMemory()
                .withClient("client_id") //client_id
                .secret(passwordEncoder.encode("secret")) //client_secret
                .resourceIds("product-id") //資源標識
                .authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token") //該client允許的授權類型
                .scopes("read,write")//允許的授權范圍
                .autoApprove(false) //false表示跳轉到授權頁面
                .redirectUris("https://www.baidu.com"); //回調地址
    }


    /**
     * 配置令牌(token)的訪問端點和令牌服務(token Services)
     *
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                .authenticationManager(authenticationManager) //認證管理器
                .userDetailsService(userDetailsService) // UserDetailsService
                .authorizationCodeServices(authorizationCodeServices()) //授權服務
                .tokenServices(authorizationServerTokenServices()) //令牌管理服務
                .approvalStore(approvalStore());
    }


    /**
     * 用來配置令牌端點的安全約束
     *
     * @param security
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security
                .tokenKeyAccess("permitAll()") //當使用JwtToken且使用非對稱加密時,資源服務用語獲取公鑰而開放的,這里配置的是這個端點完全開放
                .checkTokenAccess("permitAll()") //checkToken這個端點完全公開
                .allowFormAuthenticationForClients(); //允許表單認證
    }


}

2.3.6 授權服務配置總結

  • 1️⃣要完成認證,首先需要知道客戶端信息從哪里讀取,因此需要配置客戶端詳情服務。
  • 2️⃣要頒發token,必須定義token相關的端點以及token如何存儲,客戶端支持哪些類型的token。
  • 3️⃣因為要暴露一些端點,那么需要對這些端點進行一些安全上的約束。

2.4 資源服務器配置

  • 配置SpringSecurity的相關配置:

    • application.yml
    server:
      port: 9002
    
    spring:
      main:
        # 允許我們自己覆蓋Spring放入到IOC容器的對象
        allow-bean-definition-overriding: true
    
    logging:
      level:
        com.sunxiaping: debug
    
    • ProductController.java
    package com.sunxiaping.controller;
    
    import org.springframework.security.access.annotation.Secured;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    @RequestMapping(value = "/product")
    public class ProductController {
    
    	@Secured("ROLE_ADMIN")
    	@GetMapping(value = "/findAll")
    	public String findAll() {
    		return "產品列表查詢成功";
    	}
    
    }
    
    • ProductApplication.java
    package com.sunxiaping;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    @SpringBootApplication
    public class ProductApplication {
    
    	public static void main(String[] args) {
    		SpringApplication.run(ProductApplication.class, args);
    	}
    
    }
    
    • WebSecurityConfig.java
    package com.sunxiaping.config;
    
    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.annotation.web.configuration.WebSecurityConfigurerAdapter;
    
    /**
     * @author 許大仙
     * @version 1.0
     * @since 2020-11-12 16:25
     */
    @Configuration
    @EnableWebSecurity
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                    // 所有資源必須授權后訪問
                    .anyRequest().authenticated()
                    // 關閉跨站請求防護
                    .and().csrf().disable();
        }
    
    }
    
  • 配置資源服務器

    • Oauth2SourceConfig.java
    package com.sunxiaping.config;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.http.HttpMethod;
    import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    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.ResourceServerTokenServices;
    
    /**
     * 資源配置
     *
     * @author 許大仙
     * @version 1.0
     * @since 2020-11-09 16:42
     */
    @Configuration
    @EnableResourceServer // 開啟資源服務器
    @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
    public class Oauth2SourceConfig extends ResourceServerConfigurerAdapter {
        /**
         * 指定當前資源的id和存儲方案
         *
         * @param resources
         * @throws Exception
         */
        @Override
        public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
            resources.resourceId("product-id") // 指定當前資源的id
                    .tokenServices(tokenService());// 指定保存token的方式
        }
    
    
        // 資源服務令牌解析服務
        @Bean
        public ResourceServerTokenServices tokenService() {
            //使用遠程服務請求授權服務器校驗token,必須指定校驗token 的url、client_id,client_secret
            RemoteTokenServices service = new RemoteTokenServices();
            service.setCheckTokenEndpointUrl("http://localhost:9001/oauth/check_token");
            service.setClientId("client_id");
            service.setClientSecret("secret");
            return service;
        }
    
    
        @Override
        public void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                    // 指定不同請求方式訪問資源所需要的權限,一般查詢是read,其余是write。
                    .antMatchers(HttpMethod.GET, "/**").access("#oauth2.hasScope('read,write')")
                    .antMatchers(HttpMethod.POST, "/**").access("#oauth2.hasScope('write')")
                    .antMatchers(HttpMethod.PATCH, "/**").access("#oauth2.hasScope('write')")
                    .antMatchers(HttpMethod.PUT, "/**").access("#oauth2.hasScope('write')")
                    .antMatchers(HttpMethod.DELETE, "/**").access("#oauth2.hasScope('write')")
                    .and()
                    .headers()
                    .addHeaderWriter((request, response) -> {
                        response.addHeader("Access-Control-Allow-Origin", "*");// 允許跨域
                        if (request.getMethod().equals("OPTIONS")) {// 如果是跨域的預檢請求,則原封不動向下傳達請求頭信息
                            response.setHeader("Access-Control-Allow-Methods", request.getHeader("Access- Control-Request-Method"));
                            response.setHeader("Access-Control-Allow-Headers", request.getHeader("Access- Control-Request-Headers"));
                        }
                    });
        }
    
    }
    
  • 測試訪問資源:

資源服務器測試訪問資源

2.5 測試

2.5.1 授權碼模式測試

  • 通過瀏覽器訪問下面的地址:
http://localhost:9001/oauth/authorize?client_id=client_id&response_type=code
  • 第一次訪問會跳轉到登錄頁面:

授權碼模式測試登錄

  • 輸入用戶名和密碼驗證成功后,會詢問用戶是否授權:

輸入用戶名和密碼驗證成功后,會詢問用戶是否授權

  • 選擇授權后會跳轉到指定的地址(我在授權服務配置的是百度的網址),瀏覽器地址上會包含一個授權碼:

選擇授權后會到指定的地址(我在授權服務配置的是百度的網址),瀏覽器地址上會包含一個授權碼

  • 根據授權碼向服務器申請令牌:

根據授權碼向服務器申請令牌

  • 驗證token:

驗證token

2.5.2 隱藏式模式測試

  • 通過瀏覽器訪問下面的地址:
http://localhost:9001/oauth/authorize?client_id=client_id&response_type=token
  • 第一次訪問會跳轉到登錄頁面:

授權碼模式測試登錄

  • 輸入用戶名和密碼驗證成功后,會詢問用戶是否授權:

輸入用戶名和密碼驗證成功后,會詢問用戶是否授權

  • 選擇授權后會跳轉到指定的地址(我在授權服務配置的是百度的網址),瀏覽器地址上會包含一個令牌:

選擇授權后會跳轉到指定的地址(我在授權服務配置的是百度的網址),瀏覽器地址上會包含一個令牌

2.5.3 密碼模式測試

  • 通過client_id、client_secret、username、password以及grant_type=password向授權服務器申請令牌:
curl http://localhost:9001/oauth/token -X POST -d "grant_type=password&client_id=client_id&client_secret=secret&username=admin&password=123456"

密碼模式命令行測試

密碼模式postman測試

2.5.4 客戶端憑證式測試

  • 通過client_id、client_secret以及grant_type=client_credentials向授權服務器申請令牌:
curl http://localhost:9001/oauth/token -X POST -d "grant_type=client_credentials&client_id=client_id&client_secret=secret"

客戶端憑證式命令行測試

客戶端憑證式postman測試

2.6 JWT

2.6.1 前言

  • 通過上面的測試我們發現,當資源服務和授權服務不在一起的時候,資源服務使用RemoteTokenServices遠程請求授權服務驗證token,如果訪問量較大的時候會影響系統的性能。
  • 那么如何解決上面的問題,就是令牌采用JWT的格式。用戶認證通過會得到一個JWT令牌,JWT令牌中已經包括了用戶相關的信息,客戶端只需要攜帶JWT訪問資源服務,資源服務根據事先約定好的算法自動完成令牌校驗,無需每次都請求認證任務完成授權。

2.6.2 什么是JWT?

  • JWT(JSON Web Token)是一個開放的行業標准(RFC 7519),它定義了一種簡單的、自包含的協議格式,用於在通信雙方傳遞JSON對象,傳遞的信息經過數字簽名可以被驗證和信任。JWT可以使用HMAC算法或使用RSA的公鑰/私鑰對來簽名,防止被篡改。
  • JWT的官網標准
  • JWT的優點:
    • JWT基於JSON,非常方便解析。
    • 可以在令牌中自定義豐富的內容,易擴展。
    • 通過非對稱加密算法以及數字簽名技術,JWT防止篡改,安全性高。
    • 資源服務使用JWT可以不依賴認證服務即可完成授權。
  • JWT的缺點:
    • JWT的令牌較長,占存儲空間比較大。

2.6.3 JWT的令牌結構

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

    • Header:

      • 頭部包含令牌的類型(即JWT)以及使用的哈希算法(如RSA等)。
      • 例如:
      {"alg":"HS256","typ":"JWT"}
      
      • 將上面的內容使用Base64Url編碼,得到的一個字符串就是JWT令牌的第一個部分。
    • Payload:

      • 第二部分是有效載荷,內容也是一個JSON對象,它是存放有效信息的地方,它可以存放JWT提供的現成字段,比如:iss(簽發者)、exp(過期時間戳)、sub(面向的用戶)等,也可以自定義字段。
      • 此部分不建議存放敏感信息,因此此部分可以解碼還原原始內容。
      • 例如:
      {"sub":"1234567890","name":"456","admin":true}
      
      • 將上面的內容使用Base64Url編碼,得到的一個字符串就是JWT令牌的第二個部分。
    • Signature:

    • 第三部分是簽名,此部分用於防止JWT內容被篡改。

    • 這個部分使用Base64Url將前兩部分進行編碼,編號后使用點(.)連接組成字符串,最后使用header中聲明的算法進行簽名。

    • 例如:

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

2.6.4 配置JWT令牌服務

  • 在uaa中配置JWT令牌服務,即可實現生成jwt格式的令牌。
  • Oauth2ServerConfig.java的修改部分:
package com.sunxiaping.config;

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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
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.approval.ApprovalStore;
import org.springframework.security.oauth2.provider.approval.InMemoryApprovalStore;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.InMemoryAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.TokenStore;
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 java.util.Arrays;

/**
 * @author 許大仙
 * @version 1.0
 * @since 2020-11-09 17:17
 */
@Configuration
@EnableAuthorizationServer // 開啟認證服務器
public class Oauth2ServerConfig extends AuthorizationServerConfigurerAdapter {

    String SIGNING_KEY = "admin";

    /**
     * Token存儲策略,使用JWT的方式
     *
     * @return
     */
    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }


    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        jwtAccessTokenConverter.setSigningKey(SIGNING_KEY); //對稱密鑰,資源服務器使用該密鑰來驗證
        return jwtAccessTokenConverter;
    }


    @Autowired
    private ClientDetailsService clientDetailsService;

    /**
     * 令牌服務
     *
     * @return
     */
    @Bean
    public AuthorizationServerTokenServices authorizationServerTokenServices() {
        DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
        defaultTokenServices.setClientDetailsService(clientDetailsService); //客戶端詳情服務
        defaultTokenServices.setSupportRefreshToken(true);//支持刷新令牌
        defaultTokenServices.setTokenStore(tokenStore());//token存儲策略

        //對令牌服務進行增強
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtAccessTokenConverter()));
        defaultTokenServices.setTokenEnhancer(tokenEnhancerChain);

        defaultTokenServices.setAccessTokenValiditySeconds(7200); //令牌默認的有效期是2小時
        defaultTokenServices.setRefreshTokenValiditySeconds(259200); //刷新令牌的有效期是3天
        return defaultTokenServices;
    }
 	//其他略   
}    
  • Oauth2ServerConfig.java的完整部分:
package com.sunxiaping.config;

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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
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.approval.ApprovalStore;
import org.springframework.security.oauth2.provider.approval.InMemoryApprovalStore;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.InMemoryAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.TokenStore;
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 java.util.Arrays;

/**
 * @author 許大仙
 * @version 1.0
 * @since 2020-11-09 17:17
 */
@Configuration
@EnableAuthorizationServer // 開啟認證服務器
public class Oauth2ServerConfig extends AuthorizationServerConfigurerAdapter {

    String SIGNING_KEY = "admin";

    /**
     * Token存儲策略,使用JWT的方式
     *
     * @return
     */
    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }


    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        jwtAccessTokenConverter.setSigningKey(SIGNING_KEY); //對稱密鑰,資源服務器使用該密鑰來驗證
        return jwtAccessTokenConverter;
    }


    @Autowired
    private ClientDetailsService clientDetailsService;

    /**
     * 令牌服務
     *
     * @return
     */
    @Bean
    public AuthorizationServerTokenServices authorizationServerTokenServices() {
        DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
        defaultTokenServices.setClientDetailsService(clientDetailsService); //客戶端詳情服務
        defaultTokenServices.setSupportRefreshToken(true);//支持刷新令牌
        defaultTokenServices.setTokenStore(tokenStore());//token存儲策略

        //對令牌服務進行增強
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtAccessTokenConverter()));
        defaultTokenServices.setTokenEnhancer(tokenEnhancerChain);

        defaultTokenServices.setAccessTokenValiditySeconds(7200); //令牌默認的有效期是2小時
        defaultTokenServices.setRefreshTokenValiditySeconds(259200); //刷新令牌的有效期是3天
        return defaultTokenServices;
    }

    /**
     * 設置授權碼模式的授權服務,此處使用內存方式
     *
     * @return
     */
    @Bean
    public AuthorizationCodeServices authorizationCodeServices() {
        return new InMemoryAuthorizationCodeServices();
    }

    @Autowired
    private AuthenticationManager authenticationManager;


    @Autowired
    private UserDetailsService userDetailsService;

    /**
     * 授權信息保存策略
     *
     * @return
     */
    @Bean
    public ApprovalStore approvalStore() {
        return new InMemoryApprovalStore();
    }

    @Autowired
    private PasswordEncoder passwordEncoder;

    /**
     * ClientDetailsServiceConfigurer:用來配置客戶端詳情服務(ClientDetailsService).
     * 客戶端詳情信息在這里進行初始化,我們可以把客戶端詳情信息寫死在這里,也可以通過數據庫來存儲調取詳情信息
     *
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        //使用內存方式存儲客戶端詳情
        clients.inMemory()
                .withClient("client_id") //client_id
                .secret(passwordEncoder.encode("secret")) //client_secret
                .resourceIds("product-id") //資源標識
                .authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token") //該client允許的授權類型
                .scopes("read,write")//允許的授權范圍
                .autoApprove(false) //false表示跳轉到授權頁面
                .redirectUris("https://www.baidu.com"); //回調地址
    }


    /**
     * 配置令牌(token)的訪問端點和令牌服務(token Services)
     *
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                .authenticationManager(authenticationManager) //認證管理器
                .userDetailsService(userDetailsService) // UserDetailsService
                .authorizationCodeServices(authorizationCodeServices()) //授權服務
                .tokenServices(authorizationServerTokenServices()); //令牌管理服務
//                .approvalStore(approvalStore());
    }


    /**
     * 用來配置令牌端點的安全約束
     *
     * @param security
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security
                .tokenKeyAccess("permitAll()") //當使用JwtToken且使用非對稱加密時,資源服務用語獲取公鑰而開放的,這里配置的是這個端點完全開放
                .checkTokenAccess("permitAll()") //checkToken這個端點完全公開
                .allowFormAuthenticationForClients(); //允許表單認證
    }


}

2.6.5 生成JWT令牌

生成JWT令牌

2.6.6 校驗JWT令牌

  • 資源服務需要和授權服務擁有一致的簽名、令牌服務等。
  • Oauth2SourceConfig.java的修改部分:
package com.sunxiaping.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
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.ResourceServerTokenServices;
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;

/**
 * 資源配置
 *
 * @author 許大仙
 * @version 1.0
 * @since 2020-11-09 16:42
 */
@Configuration
@EnableResourceServer // 開啟資源服務器
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class Oauth2SourceConfig extends ResourceServerConfigurerAdapter {


    String SIGNING_KEY = "admin";

    /**
     * Token存儲策略,使用JWT的方式
     *
     * @return
     */
    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }


    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        jwtAccessTokenConverter.setSigningKey(SIGNING_KEY); //對稱密鑰,資源服務器使用該密鑰來驗證
        return jwtAccessTokenConverter;
    }

    /**
     * 指定當前資源的id和存儲方案
     *
     * @param resources
     * @throws Exception
     */
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.resourceId("product-id") // 指定當前資源的id
                  .tokenStore(tokenStore());//
    }
	//略
}    
  • Oauth2SourceConfig.java的完整部分:
package com.sunxiaping.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
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.ResourceServerTokenServices;
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;

/**
 * 資源配置
 *
 * @author 許大仙
 * @version 1.0
 * @since 2020-11-09 16:42
 */
@Configuration
@EnableResourceServer // 開啟資源服務器
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class Oauth2SourceConfig extends ResourceServerConfigurerAdapter {


    String SIGNING_KEY = "admin";

    /**
     * Token存儲策略,使用JWT的方式
     *
     * @return
     */
    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }


    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        jwtAccessTokenConverter.setSigningKey(SIGNING_KEY); //對稱密鑰,資源服務器使用該密鑰來驗證
        return jwtAccessTokenConverter;
    }

    /**
     * 指定當前資源的id和存儲方案
     *
     * @param resources
     * @throws Exception
     */
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.resourceId("product-id") // 指定當前資源的id
                  .tokenStore(tokenStore());//
    }



    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                // 指定不同請求方式訪問資源所需要的權限,一般查詢是read,其余是write。
                .antMatchers(HttpMethod.GET, "/**").access("#oauth2.hasScope('read,write')")
                .antMatchers(HttpMethod.POST, "/**").access("#oauth2.hasScope('write')")
                .antMatchers(HttpMethod.PATCH, "/**").access("#oauth2.hasScope('write')")
                .antMatchers(HttpMethod.PUT, "/**").access("#oauth2.hasScope('write')")
                .antMatchers(HttpMethod.DELETE, "/**").access("#oauth2.hasScope('write')")
                .and()
                .headers()
                .addHeaderWriter((request, response) -> {
                    response.addHeader("Access-Control-Allow-Origin", "*");// 允許跨域
                    if (request.getMethod().equals("OPTIONS")) {// 如果是跨域的預檢請求,則原封不動向下傳達請求頭信息
                        response.setHeader("Access-Control-Allow-Methods", request.getHeader("Access- Control-Request-Method"));
                        response.setHeader("Access-Control-Allow-Headers", request.getHeader("Access- Control-Request-Headers"));
                    }
                });
    }

}
  • 測試:使用JWT令牌請求資源

使用JWT令牌請求資源

2.7 完善環境配置

  • 上面的客戶端信息和授權碼依然存儲在內存中,生產環境中通常會存儲在數據庫中。

  • 修改Oauth2ServerConfig.java中的ClientDetailsService和AuthorizationCodeServices使其從數據庫中讀取:

package com.sunxiaping.config;

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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
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.approval.ApprovalStore;
import org.springframework.security.oauth2.provider.approval.InMemoryApprovalStore;
import org.springframework.security.oauth2.provider.approval.JdbcApprovalStore;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.InMemoryAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.JdbcAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.TokenStore;
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.Arrays;

/**
 * @author 許大仙
 * @version 1.0
 * @since 2020-11-09 17:17
 */
@Configuration
@EnableAuthorizationServer // 開啟認證服務器
public class Oauth2ServerConfig extends AuthorizationServerConfigurerAdapter {

    String SIGNING_KEY = "admin";


    @Autowired
    private DataSource dataSource;

    @Autowired
    private PasswordEncoder passwordEncoder;


    @Autowired
    private AuthenticationManager authenticationManager;


    @Autowired
    private UserDetailsService userDetailsService;

    /**
     * Token存儲策略,使用JWT的方式
     *
     * @return
     */
    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }


    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        jwtAccessTokenConverter.setSigningKey(SIGNING_KEY); //對稱密鑰,資源服務器使用該密鑰來驗證
        return jwtAccessTokenConverter;
    }


    @Bean
    public ClientDetailsService clientDetailsService() {
        JdbcClientDetailsService clientDetailsService = new JdbcClientDetailsService(dataSource);
        clientDetailsService.setPasswordEncoder(passwordEncoder);
        return clientDetailsService;
    }

    /**
     * 設置授權碼模式的授權服務,此處使用數據庫方式
     *
     * @return
     */
    @Bean
    public AuthorizationCodeServices authorizationCodeServices() {
        return new JdbcAuthorizationCodeServices(dataSource);
    }


    /**
     * 授權信息保存策略
     *
     * @return
     */
    @Bean
    public ApprovalStore approvalStore() {
        return new JdbcApprovalStore(dataSource);
    }


    /**
     * 令牌服務
     *
     * @return
     */
    @Bean
    public AuthorizationServerTokenServices authorizationServerTokenServices() {
        DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
        defaultTokenServices.setClientDetailsService(clientDetailsService()); //客戶端詳情服務
        defaultTokenServices.setSupportRefreshToken(true);//支持刷新令牌
        defaultTokenServices.setTokenStore(tokenStore());//token存儲策略

        //對令牌服務進行增強
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtAccessTokenConverter()));
        defaultTokenServices.setTokenEnhancer(tokenEnhancerChain);

        defaultTokenServices.setAccessTokenValiditySeconds(7200); //令牌默認的有效期是2小時
        defaultTokenServices.setRefreshTokenValiditySeconds(259200); //刷新令牌的有效期是3天
        return defaultTokenServices;
    }


    /**
     * ClientDetailsServiceConfigurer:用來配置客戶端詳情服務(ClientDetailsService).
     * 客戶端詳情信息在這里進行初始化,我們可以把客戶端詳情信息寫死在這里,也可以通過數據庫來存儲調取詳情信息
     *
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.withClientDetails(clientDetailsService());
    }


    /**
     * 配置令牌(token)的訪問端點和令牌服務(token Services)
     *
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                .authenticationManager(authenticationManager) //認證管理器
                .userDetailsService(userDetailsService) // UserDetailsService
                .authorizationCodeServices(authorizationCodeServices()) //授權服務
                .tokenServices(authorizationServerTokenServices()) //令牌管理服務
                .approvalStore(approvalStore());
    }


    /**
     * 用來配置令牌端點的安全約束
     *
     * @param security
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security
                .tokenKeyAccess("permitAll()") //當使用JwtToken且使用非對稱加密時,資源服務用語獲取公鑰而開放的,這里配置的是這個端點完全開放
                .checkTokenAccess("permitAll()") //checkToken這個端點完全公開
                .allowFormAuthenticationForClients(); //允許表單認證
    }
}


免責聲明!

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



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