![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLicmbw5yYkZDNmVWO5ITOiFTYkNjM5YmN1gjM5ImMlZzYwQWNw8CX0JXZ252bj91Ztl2Lc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
摘要: 原創出處http://www.iocoder.cn/Spring-Security/OAuth2-learning-sso/「芋道源碼」歡迎轉載,保留摘要,謝謝!
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
。
“旁白君:機智的胖友,是不是發現這個項目和
授權 伺服器非常相似!!! - 建立
項目,模拟需要登入的 XXX 系統lab-68-demo21-resource-server-on-sso
。
“旁白君:機智的胖友,是不是發現這個項目和
資源 伺服器非常相似!!!
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 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;
結果如下圖所示:
表結構
② 執行
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);
}
}
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());
}
}
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