發危~
“摘要: 原創出處 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 相關的表。
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 系統會通過授權碼向統一登錄系統獲取訪問令牌。
通過這樣的方式,完成一次單點登錄的過程。
結果如下圖所示:
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');
結果如下圖所示:
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 的整體流程圖,胖友可以繼續深入理解下:
後續,想要深入的胖友,可以看看 Spring Security OAuth 提供的如下兩個過濾器:
-
OAuth2ClientContextFilter
-
OAuth2ClientAuthenticationProcessingFilter