寫了一個 SSO 單點登錄的代碼示例給胖友!

發危~

摘要: 原創出處 http://www.iocoder.cn/Spring-Security/OAuth2-learning-sso/ 「芋道源碼」歡迎轉載,保留摘要,謝謝!

  • 1. 概述

  • 2. 搭建統一登錄系統

  • 3. 搭建 XXX 系統

  • 666. 彩蛋


本文在提供完整代碼示例,可見 https://github.com/YunaiV/SpringBoot-Labs 的 lab-68-spring-security-oauth 目錄。

原創不易,給點個 Star 嘿,一起衝鴨!

1. 概述

在前面的文章中,我們學習了 Spring Security OAuth 的簡單使用。

  • 《芋道 Spring Security OAuth2 入門》

  • 《芋道 Spring Security OAuth2 存儲器》

今天我們來搞波“大”的,通過 Spring Security OAuth 實現一個單點登錄的功能。

可能會有粉絲不太瞭解單點登錄是什麼?單點登錄,英文是 Single Sign On,簡稱爲 SSO,指的是當有多個系統需要登錄時,用戶只需要登錄一個統一的登錄系統,而無需在多個系統重複登錄。

舉個最常見的例子,我們在瀏覽器中使用阿里“全家桶”:

求助信:麻煩有認識阿里的胖友,讓他們給打下錢。。。

  • 淘寶:https://www.taobao.com

  • 天貓:https://www.tmall.com

  • 飛豬:https://www.fliggy.com

  • ...

我們只需要在統一登錄系統(https://login.taobao.com)進行登錄即可,而後就可以“愉快”的自由剁手,並且無需分別在淘寶、天貓、飛豬等等系統重新登錄。

登錄系統

 

友情提示:更多單點登錄的介紹,可見《維基百科 —— 單點登錄》。

下面,我們正式搭建 Spring Security OAuth 實現 SSO 的示例項目,如下圖所示:

項目結構

 

  • 創建 lab-68-demo21-authorization-server-on-sso 項目,作爲統一登錄系統

    旁白君:機智的胖友,是不是發現這個項目和授權服務器非常相似!!!

  • 創建 lab-68-demo21-resource-server-on-sso 項目,模擬需要登錄的 XXX 系統

    旁白君:機智的胖友,是不是發現這個項目和資源服務器非常相似!!!

2. 搭建統一登錄系統

示例代碼對應倉庫:

  • 統一登錄系統:lab-68-demo21-authorization-server-on-sso

創建 lab-68-demo21-authorization-server-on-sso 項目,作爲統一登錄系統

友情提示:整個實現代碼,和我們前文看到的授權服務器是基本一致的。

2.1 初始化數據庫

在 resources/db 目錄下,有四個 SQL 腳本,分別用於初始化 User 和 OAuth 相關的表。

SQL 腳本

 

2.1.1 初始化 OAuth 表

① 執行 oauth_schema.sql 腳本,創建數據庫表結構

drop table if exists oauth_client_details;
create table oauth_client_details (
  client_id VARCHAR(255) PRIMARY KEY,
  resource_ids VARCHAR(255),
  client_secret VARCHAR(255),
  scope VARCHAR(255),
  authorized_grant_types VARCHAR(255),
  web_server_redirect_uri VARCHAR(255),
  authorities VARCHAR(255),
  access_token_validity INTEGER,
  refresh_token_validity INTEGER,
  additional_information VARCHAR(4096),
  autoapprove VARCHAR(255)
);

create table if not exists oauth_client_token (
  token_id VARCHAR(255),
  token LONG VARBINARY,
  authentication_id VARCHAR(255) PRIMARY KEY,
  user_name VARCHAR(255),
  client_id VARCHAR(255)
);

create table if not exists oauth_access_token (
  token_id VARCHAR(255),
  token LONG VARBINARY,
  authentication_id VARCHAR(255) PRIMARY KEY,
  user_name VARCHAR(255),
  client_id VARCHAR(255),
  authentication LONG VARBINARY,
  refresh_token VARCHAR(255)
);

create table if not exists oauth_refresh_token (
  token_id VARCHAR(255),
  token LONG VARBINARY,
  authentication LONG VARBINARY
);

create table if not exists oauth_code (
  code VARCHAR(255), authentication LONG VARBINARY
);

create table if not exists oauth_approvals (
 userId VARCHAR(255),
 clientId VARCHAR(255),
 scope VARCHAR(255),
 status VARCHAR(10),
 expiresAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
 lastModifiedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

結果如下圖所示:

表結構

 

作用
oauth_access_token OAuth 2.0 訪問令牌
oauth_refresh_token OAuth 2.0 刷新令牌
oauth_code OAuth 2.0 授權碼
oauth_client_details OAuth 2.0 客戶端
oauth_client_token  
oauth_approvals  

旁白君:這裏的表結構設計,我們可以借鑑參考,實現自己的 OAuth 2.0 的功能。

② 執行 oauth_data.sql 腳本,插入一個客戶端記錄。

INSERT INTO oauth_client_details
 (client_id, client_secret, scope, authorized_grant_types,
 web_server_redirect_uri, authorities, access_token_validity,
 refresh_token_validity, additional_information, autoapprove)
VALUES
 ('clientapp', '112233', 'read_userinfo,read_contacts',
 'password,authorization_code,refresh_token', 'http://127.0.0.1:9090/login', null, 3600, 864000, null, true);

注意!這條記錄的 web_server_redirect_uri 字段,我們設置爲 http://127.0.0.1:9090/login,這是稍後我們搭建的 XXX 系統的回調地址。

  • 統一登錄系統採用 OAuth 2.0 的授權碼模式進行授權。

  • 授權成功後,瀏覽器會跳轉 http://127.0.0.1:9090/login 回調地址,然後 XXX 系統會通過授權碼向統一登錄系統獲取訪問令牌

通過這樣的方式,完成一次單點登錄的過程。

結果如下圖所示:

oauth_client_details 表記錄

 

2.1.2 初始化 User 表

① 執行 user_schema.sql 腳本,創建數據庫表結構

DROP TABLE IF EXISTS `authorities`;
CREATE TABLE `authorities` (
  `username` varchar(50) NOT NULL,
  `authority` varchar(50) NOT NULL,
  UNIQUE KEY `ix_auth_username` (`username`,`authority`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
  `username` varchar(50) NOT NULL,
  `password` varchar(500) NOT NULL,
  `enabled` tinyint(1) NOT NULL,
  PRIMARY KEY (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

結果如下圖所示:

表結構

 

作用
users 用戶
authorities 授權表,例如用戶擁有的角色

② 執行 user_data.sql 腳本,插入一個用戶記錄和一個授權記錄。

INSERT INTO `authorities` VALUES ('yunai', 'ROLE_USER');

INSERT INTO `users` VALUES ('yunai', '112233', '1');

結果如下圖所示:

users 和 authorities 表記錄

 

2.2 引入依賴

創建 pom.xml 文件,引入 Spring Security OAuth 依賴。

<?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>lab-68</artifactId>
        <groupId>cn.iocoder.springboot.labs</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>lab-68-demo21-authorization-server-on-sso</artifactId>

    <properties>
        <!-- 依賴相關配置 -->
        <spring.boot.version>2.2.4.RELEASE</spring.boot.version>
        <!-- 插件相關配置 -->
        <maven.compiler.target>1.8</maven.compiler.target>
        <maven.compiler.source>1.8</maven.compiler.source>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-parent</artifactId>
                <version>${spring.boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <!-- 實現對 Spring MVC 的自動配置 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- 實現對 Spring Security OAuth2 的自動配置 -->
        <dependency>
            <groupId>org.springframework.security.oauth.boot</groupId>
            <artifactId>spring-security-oauth2-autoconfigure</artifactId>
            <version>${spring.boot.version}</version>
        </dependency>

        <!-- 實現對數據庫連接池的自動化配置 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency> <!-- 本示例,我們使用 MySQL -->
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.48</version>
        </dependency>

    </dependencies>

</project>

2.3 配置文件

創建 application.yaml 配置文件,添加數據庫連接池的配置:

spring:
  # datasource 數據源配置內容,對應 DataSourceProperties 配置屬性類
  datasource:
    url: jdbc:mysql://127.0.0.1:43063/demo-68-authorization-server-sso?useSSL=false&useUnicode=true&characterEncoding=UTF-8
    driver-class-name: com.mysql.jdbc.Driver
    username: root # 數據庫賬號
    password: 123456 # 數據庫密碼

2.4 SecurityConfig

創建 SecurityConfig 配置類,通過 Spring Security 提供用戶認證的功能。代碼如下:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 數據源 DataSource
     */
    @Autowired
    private DataSource dataSource;

    @Override
    @Bean(name = BeanIds.AUTHENTICATION_MANAGER)
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    public static NoOpPasswordEncoder passwordEncoder() {
        return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.jdbcAuthentication()
                .dataSource(dataSource);
    }

}

友情提示:如果胖友想要自定義用戶的讀取,可以參考《芋道 Spring Boot 安全框架 Spring Security 入門》文章。

2.5 OAuth2AuthorizationServerConfig

創建 OAuth2AuthorizationServerConfig 配置類,通過 Spring Security OAuth 提供授權服務器的功能。代碼如下:

@Configuration
@EnableAuthorizationServer
public class OAuth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    /**
     * 用戶認證 Manager
     */
    @Autowired
    private AuthenticationManager authenticationManager;

    /**
     * 數據源 DataSource
     */
    @Autowired
    private DataSource dataSource;

    @Bean
    public TokenStore jdbcTokenStore() {
        return new JdbcTokenStore(dataSource);
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authenticationManager(authenticationManager)
                .tokenStore(jdbcTokenStore());
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
        oauthServer.checkTokenAccess("isAuthenticated()");
    }

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

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.withClientDetails(jdbcClientDetailsService());
    }

}

友情提示:如果胖友看不懂這個配置類,回到《芋道 Spring Security OAuth2 存儲器》文章複習下。

2.6 AuthorizationServerApplication

創建 AuthorizationServerApplication 類,統一登錄系統的啓動類。代碼如下:

@SpringBootApplication
public class AuthorizationServerApplication {

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

}

2.7 簡單測試

執行 AuthorizationServerApplication 啓動統一登錄系統。下面,我們使用 Postman 模擬一個 Client,測試我們是否搭建成功

POST 請求 http://localhost:8080/oauth/token 地址,使用密碼模式進行授權。如下圖所示:

密碼模式

成功獲取到訪問令牌,成功!

3. 搭建 XXX 系統

示例代碼對應倉庫:

  • XXX 系統:lab-68-demo21-resource-server-on-sso

創建 lab-68-demo21-resource-server-on-sso 項目,搭建 XXX 系統,接入統一登錄系統實現 SSO 功能。

友情提示:整個實現代碼,和我們前文看到的資源服務器是基本一致的。

3.1 引入依賴

創建 pom.xml 文件,引入 Spring Security OAuth 依賴。

<?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>lab-68</artifactId>
        <groupId>cn.iocoder.springboot.labs</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>lab-68-demo21-resource-server</artifactId>

    <properties>
        <!-- 依賴相關配置 -->
        <spring.boot.version>2.2.4.RELEASE</spring.boot.version>
        <!-- 插件相關配置 -->
        <maven.compiler.target>1.8</maven.compiler.target>
        <maven.compiler.source>1.8</maven.compiler.source>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-parent</artifactId>
                <version>${spring.boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <!-- 實現對 Spring MVC 的自動配置 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- 實現對 Spring Security OAuth2 的自動配置 -->
        <dependency>
            <groupId>org.springframework.security.oauth.boot</groupId>
            <artifactId>spring-security-oauth2-autoconfigure</artifactId>
            <version>${spring.boot.version}</version>
        </dependency>
    </dependencies>

</project>

3.2 配置文件

創建 application.yaml 配置文件,添加 SSO 相關配置:

server:
  port: 9090
  servlet:
    session:
      cookie:
        name: SSO-SESSIONID # 自定義 Session 的 Cookie 名字,防止衝突。衝突後,會導致 SSO 登錄失敗。

security:
  oauth2:
    # OAuth2 Client 配置,對應 OAuth2ClientProperties 類
    client:
      client-id: clientapp
      client-secret: 112233
      user-authorization-uri: http://127.0.0.1:8080/oauth/authorize # 獲取用戶的授權碼地址
      access-token-uri: http://127.0.0.1:8080/oauth/token # 獲取訪問令牌的地址
    # OAuth2 Resource 配置,對應 ResourceServerProperties 類
    resource:
      token-info-uri: http://127.0.0.1:8080/oauth/check_token # 校驗訪問令牌是否有效的地址

① server.servlet.session.cookie.name 配置項,自定義 Session 的 Cookie 名字,防止衝突。衝突後,會導致 SSO 登錄失敗。

友情提示:具體的值,胖友可以根據自己的喜歡設置。

② security.oauth2.client 配置項,OAuth2 Client 配置,對應 OAuth2ClientProperties 類。在這個配置項中,我們添加了客戶端的 client-id 和 client-secret

③ security.oauth2.client.user-authorization-uri 配置項,獲取用戶的授權碼地址。

在訪問 XXX 系統需要登錄的地址時,Spring Security OAuth 會自動跳轉到統一登錄系統,進行統一登錄獲取授權

而這裏配置的 security.oauth2.client.user-authorization-uri 地址,就是之前授權服務器的 oauth/authorize 接口,可以進行授權碼模式的授權。

友情提示:如果胖友忘記授權服務器的 oauth/authorize 接口,建議回看下《芋道 Spring Security OAuth2 入門》的「3. 授權碼模式」小節。

④ security.oauth2.client.access-token-uri 配置項,獲取訪問令牌的地址。

統一登錄系統完成統一登錄並授權後,瀏覽器會跳轉回 XXX 系統的回調地址。在該地址上,會調用統一登錄系統的 security.oauth2.client.user-authorization-uri 地址,通過授權碼獲取到訪問令牌

而這裏配置的 security.oauth2.client.user-authorization-uri 地址,就是之前授權服務器的 oauth/token 接口。

⑤ security.oauth2.resource.client.token-info-uri 配置項,校驗訪問令牌是否有效的地址。

在獲取到訪問令牌之後,每次請求 XXX 系統時,都會調用 統一登錄系統的 security.oauth2.resource.client.token-info-uri 地址,校驗訪問令牌的有效性,同時返回用戶的基本信息

而這裏配置的 security.oauth2.resource.client.token-info-uri 地址,就是之前授權服務器的 oauth/check_token 接口。


至此,我們可以發現,Spring Security OAuth 實現的 SSO 單點登錄功能,是基於其授權碼模式實現的。這一點,非常重要,稍後我們演示下會更加容易理解到。

3.3 OAuthSsoConfig

創建 OAuthSsoConfig 類,配置接入 SSO 功能。代碼如下:

@Configuration
@EnableOAuth2Sso // 開啓 Sso 功能
public class OAuthSsoConfig {

}

在類上添加 @EnableOAuth2Sso 註解,聲明基於 Spring Security OAuth 的方式接入 SSO 功能。

友情提示:想要深入的胖友,可以看看 SsoSecurityConfigurer 類。

3.4 UserController

創建 UserController 類,提供獲取當前用戶的 /user/info 接口。代碼如下:

@RestController
@RequestMapping("/user")
public class UserController {

    @RequestMapping("/info")
    public Authentication info(Authentication authentication) {
        return authentication;
    }

}

3.5 ResourceServerApplication

創建 ResourceServerApplication 類,XXX 系統的啓動類。代碼如下:

@SpringBootApplication
public class ResourceServerApplication {

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

}

3.6 簡單測試(第一彈)

執行 ResourceServerApplication 啓動 XXX 系統。下面,我們來演示下 SSO 單點登錄的過程。

① 使用瀏覽器,訪問 XXX 系統的 http://127.0.0.1:9090/user/info 地址。因爲暫未登錄,所以被重定向到統一登錄系統的 http://127.0.0.1:8080/oauth/authorize 授權地址。

又因爲在統一登錄系統暫未登錄,所以被重定向到統一登錄系統的 http://127.0.0.1:8080/login 登錄地址。如下圖所示:

登錄界面

② 輸入用戶的賬號密碼「yunai/1024」,進行統一登錄系統的登錄。登錄完成後,進入統一登錄系統的 http://127.0.0.1:8080/oauth/authorize 授權地址。如下圖所示:

授權界面

③ 點擊「Authorize」按鈕,完成用戶的授權。授權完成後,瀏覽器重定向到 XXX 系統的 http://127.0.0.1:9090/login 回調地址。

在 XX 系統的回調地址,拿到授權的授權碼後,會自動請求統一登錄系統,通過授權碼獲取到訪問令牌。如此,我們便完成了 XXX 系統 的登錄。

獲取授權碼完成後,自動跳轉到登錄前的 http://127.0.0.1:9090/user/info 地址,打印出當前登錄的用戶信息。如下圖所示:

用戶信息

 


如此,我們從統一登錄系統也拿到了用戶信息。下面,我們來進一步將 Spring Security 的權限控制功能來演示下。

3.7 SecurityConfig

創建 SecurityConfig 配置類,添加 Spring Security 的功能。代碼如下:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true) // 開啓對 Spring Security 註解的方法,進行權限驗證。
@Order(101) // OAuth2SsoDefaultConfiguration 使用了 Order(100),所以這裏設置爲 Order(101),防止相同順序導致報錯
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}

在類上,增加 @EnableGlobalMethodSecurity 註解,開啓對 Spring Security 註解的方法,進行權限驗證。

3.8 DemoController

創建 DemoController 類,提供測試權限的功能的接口。代碼如下:

@RestController
@RequestMapping("/demo")
public class DemoController {

    @GetMapping("/admin-list")
    @PreAuthorize("hasRole('ADMIN')") // 要求管理員 ROLE_ADMIN 角色
    public String adminList() {
        return "管理員列表";
    }

    @GetMapping("/user-list")
    @PreAuthorize("hasRole('USER')") // 要求普通用戶 ROLE_USER 角色
    public String userList() {
        return "用戶列表";
    }

}

因爲當前登錄的用戶只有 ROLE_USE 角色,所以可以訪問 /demo/user-list 接口,無法訪問 /demo/admin-list 接口。

3.9 簡單測試(第二彈)

執行 ResourceServerApplication 重啓 XXX 系統。下面,我們來演示下 Spring Security 的權限控制功能。

① 使用瀏覽器,訪問 http://127.0.0.1:9090/demo/user-list 地址,成功。如下圖所示:

成功訪問

② 使用瀏覽器,訪問 http://127.0.0.1:9090/demo/admin-list 地址,失敗。如下圖所示:

失敗訪問

 

666. 彩蛋

至此,我們成功使用 Spring Security OAuth 實現了一個 SSO 單點登錄的示例。下圖,是 SSO 的整體流程圖,胖友可以繼續深入理解下:

SSO 流程圖

後續,想要深入的胖友,可以看看 Spring Security OAuth 提供的如下兩個過濾器:

  • OAuth2ClientContextFilter

  • OAuth2ClientAuthenticationProcessingFilter

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