目錄
本節大綱
一、OAuth2 簡介
二、OAuth2 授權總體流程
三、四種授權模式
授權碼模式
簡化模式
密碼模式
客戶端模式
四、OAuth2 標準接口
五、GitHub 授權登錄
1. 創建 OAuth 應用
2. 項目開發
六、Spring Security OAuth2
七、授權、資源服務器
1. 授權服務器搭建
1.1. 基于內存客戶端和令牌存儲
1.2. 基于數據庫客戶端和令牌存儲
2. 資源服務器搭建
八、使用 JWT
1. 授權服務器頒發 JWT 令牌
2. 使用 JWT 令牌資源服務器
本節大綱
- OAuth2 簡介
- 四種授權模式
- Spring Security OAuth2
- GitHub 授權登錄
- 授權服務器與資源服務器
- 使用 JWT
一、OAuth2 簡介
OAuth 是一個開放的非常重要的認證標準/協議,該標準允許用戶讓第三方應用訪問該用戶在某一網站上存
儲的私密資源(如頭像、照片、視頻等),并且在這個過程中無須將用戶名和密碼提供給第三方應用。通
過令牌(token)可以實現這一功能,每一個令牌授權一個特定的網站在特定的時段內允許可特定的資源。
OAuth 讓用戶可以授權第三方網站靈活訪問它們存儲在另外一些資源服務器上的特定信息,而非所有內
容。對于用戶而言,我們在互聯網應用中最常見的 OAuth 應用就是各種第三方登錄,例如QQ授權登錄、
微信授權登錄、微博授權登錄、GitHub 授權登錄等。例如用戶想登錄 Ruby China,傳統方式是使用用戶
名密碼但是這樣并不安全,因為網站會存儲你的用戶名密碼,這樣可能會導致密碼泄露。這種授權方式安
全隱患很大,如果使用 OAuth 協議就能很好地解決這一問題。
注意: OAuth2 是OAuth 協議的下一版本,但不兼容 OAuth 1.0。 OAuth2 關注客戶端開發者的簡易
性,同時為 Web 應用、桌面應用、移動設備、IoT 設備提供專門的認證流程。
二、OAuth2 授權總體流程
角色梳理: ? ?第三方應用 ? <----> ?存儲用戶私密信息應用 ?----> 授權服務器 ?----> 資源服務器
整體流程如下:(圖片來自 RFC6749文檔 https://tools.ietf.org/html/rfc6749)
- (A)用戶打開客戶端以后,客戶端要求用戶給予授權。
- (B)用戶同意給予客戶端授權。
- (C)客戶端使用上一步獲得的授權,向認證服務器申請令牌。
- (D)認證服務器對客戶端進行認證以后,確認無誤,同意發放令牌。
- (E)客戶端使用令牌,向資源服務器申請獲取資源。
- (F)資源服務器確認令牌無誤,同意向客戶端開放資源。
從上圖中我們可以看出六個步驟之中,B是關鍵,即用戶怎樣才能給于客戶端授權。同時會發現 OAuth2
中包含四種不同的角色:
- Client:第三方應用。
- Resource Owner:資源所有者。
- Authorization Server :授權服務器。
- Resource Server: 資源服務器。
三、四種授權模式
授權碼模式
授權碼模式(Authorization Code) 是功能最完整、流程最嚴密、最安全并且使用最廣泛的一種OAuth2授
權模式。同時也是最復雜的一種授權模式,它的特點就是通過客戶端的后臺服務器,與服務提供商
的認
證服務器進行互動。其具體的授權流程如圖所示(圖片來自RFC6749文檔 https://tools.ietf.org/html/rfc6749)
- Third-party application:第三方應用程序,簡稱"客戶端"(client);
- Resource Owner:資源所有者,簡稱"用戶"(user);
- User Agent:用戶代理,是指瀏覽器;
- Authorization Server:認證服務器,即服務端專門用來處理認證的服務器;
- Resource Server:資源服務器,即服務端存放用戶生成的資源的服務器。
它與認證服務器,可以是同一臺服務器,也可以是不同的服務器。
具體流程如下:
- (A)用戶訪問第三方應用,第三方應用通過瀏覽器導向認證服務器。
- (B)用戶選擇是否給予客戶端授權。
- (C)假設用戶給予授權,認證服務器將用戶導向客戶端事先指定的"重定向URI"(redirection URI),同時附上一個授權碼。
- (D)客戶端收到授權碼,附上早先的"重定向URI",向認證服務器申請令牌。
這一步是在客戶端的后臺的服務器上完成的,對用戶不可見。 - (E)認證服務器核對了授權碼和重定向URI,確認無誤后,向客戶端發送訪問令牌(access token)和更新令牌(refresh token)。
核心參數:
https://wx.com/oauth/authorize?response_type=code&client_id=CLIENT_ID&redirect_uri=http://www.baidu.com&scope=read
字段 | 描述 |
client_id | 授權服務器注冊應用后的唯一標識 |
response_type | 必須 固定值 ?在授權碼中必須為 code |
redirect_uri | 必須 通過客戶端注冊的重定向URL |
scope | 必須 令牌可以訪問資源權限 read 只讀 ? all 讀寫 |
state | 可選 存在原樣返回客戶端 用來防止 CSRF跨站攻擊 |
簡化模式
簡化模式(implicit grant type)不通過第三方應用程序的服務器,直接在瀏覽器中向認證服務器申請
令牌,跳過了"授權碼"這個步驟,因此得名。所有步驟在瀏覽器中完成,令牌對訪問者是可見的,且客戶端
不需要認證。其具體的授權流程如圖所示(圖片來自RFC6749文檔 https://tools.ietf.org/html/rfc6749)
具體步驟如下:
- (A)第三方應用將用戶導向認證服務器。
- (B)用戶決定是否給于客戶端授權。
- (C)假設用戶給予授權,認證服務器將用戶導向客戶端指定的"重定向URI",并在URI的Hash部分包含了訪問令牌。#token
- (D)瀏覽器向資源服務器發出請求,其中不包括上一步收到的Hash值。
- (E)資源服務器返回一個網頁,其中包含的代碼可以獲取Hash值中的令牌。
- (F)瀏覽器執行上一步獲得的腳本,提取出令牌。
- (G)瀏覽器將令牌發給客戶端。
核心參數:
https://wx.com/oauth/authorize?response_type=token&client_id=CLIENT_ID&redirect_uri=http://www.baidu.com&scope=read
字段 | 描述 |
client_id | 授權服務器注冊應用后的唯一標識 |
response_type | 必須 固定值 ?在授權碼中必須為 token |
redirect_uri | 必須 通過客戶端注冊的重定向URL |
scope | 必須 令牌可以訪問資源權限 |
state | 可選 存在原樣返回客戶端 用來防止 CSRF跨站攻擊 |
密碼模式
密碼模式(Resource Owner Password Credentials Grant)中,用戶向客戶端提供自己的用戶名和密
碼。客戶端使用這些信息,向"服務商提供商"索要授權。在這種模式中,用戶必須把自己的密碼給客戶端,
但是客戶端不得儲存密碼。這通常用在用戶對客戶端高度信任的情況下,比如客戶端是操作系統的一部
分,或者由一個相同公司出品。而認證服務器只有在其他授權模式無法執行的情況下,才能考慮使用這種
模式。其具體的授權流程如圖所示(圖片來自 RFC6749文檔 https://tools.ietf.org/html/rfc6749)
具體步驟如下:
- (A)用戶向客戶端提供用戶名和密碼。
- (B)客戶端將用戶名和密碼發給認證服務器,向后者請求令牌。
- (C)認證服務器確認無誤后,向客戶端提供訪問令牌。
核心參數:
https://wx.com/token?grant_type=password&username=USERNAME&password=PASSWORD&client_id=CLIENT_ID
客戶端模式
客戶端模式(Client Credentials Grant)指客戶端以自己的名義,而不是以用戶的名義,向"服務提供商"進
行認證。嚴格地說,客戶端模式并不屬于OAuth框架所要解決的問題。在這種模式中,用戶直接向客戶端
注冊,客戶端以自己的名義要求"服務提供商"提供服務,其實不存在授權問題。
具體步驟如下:
- (A)客戶端向認證服務器進行身份認證,并要求一個訪問令牌。
- (B)認證服務器確認無誤后,向客戶端提供訪問令牌。
https://wx.com/token?grant_type=client_credentials&client_id=CLIENT_ID&client_secret=CLIENT_SECRET
四、OAuth2 標準接口
- /oauth/authorize:授權端點
- /oauth/token:獲取令牌端點
- /oauth/confirm_access:用戶確認授權提交端點
- /oauth/error:授權服務錯誤信息端點
- /oauth/check_token:用于資源服務訪問的令牌解析端點
- /oauth/token_key:提供公有密匙的端點,如果使用JWT令牌的話
五、GitHub 授權登錄
1. 創建 OAuth 應用
訪問 github 并登錄,在 https://github.com/settings/profile 中找到 Developer Settings 選項
- 創建 OAuth App并輸入一下基本信息:
- 注冊成功后會獲取到對應的 Client ID 和 Client Secret。
2. 項目開發
- 創建 springboot 應用,并引入依賴
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency>
- 創建測試 controller
@RestController
public class HelloController {@GetMapping("/hello")public DefaultOAuth2User hello(){System.out.println("hello ");Authentication authentication = SecurityContextHolder.getContext().getAuthentication();return (DefaultOAuth2User) authentication.getPrincipal();}
}
- 配置 security
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().anyRequest().authenticated().and().oauth2Login();}
}
- 配置配置文件
server.port=8080spring.security.oauth2.client.registration.github.client-id=d6ea299b9ade3cd3b97d
spring.security.oauth2.client.registration.github.client-secret=aaa44b2675a7b636b1b43371e509e88ee9013816
# 一定要與重定向回調 URL 一致
spring.security.oauth2.client.registration.github.redirect-uri=http://localhost:8080/login/oauth2/code/github
- 啟動測試
- 點擊 github 登錄,點擊授權 訪問 hello 接口
六、Spring Security OAuth2
Spring Security 對 OAuth2 提供了很好的支持,這使得我們在 Spring Security中使用 OAuth2 非常地方
便。然而由于歷史原因,Spring Seaurity對 OAuth2 的支持比較混亂,這里簡單梳理一下。
大約十年前,Spring 引入了一個社區驅動的開源項目 Spring Security OAuth, 并將其納入 Spring 項目
組合中。
到今天為止,這個項目己經發展成為一個成熟的項目,可以支持大部分OAuth 規范,包括資源服務器、 客
戶端和授權服務器等。
然而早期的項目存在一些問題,例如:
- OAuth 是在早期完成的,開發者無法預料未來的變化以及這些代碼到底要被怎么使用,
這導致很多 Spring 項目提供了自己的 OAuth 支持,也就帶來了 OAuth 支持的碎片化。 - 最早的OAuth項目同時支特 OAuth1.0 和 OAuth2.0,而現在OAuth1.0 早已經不再使用,
可以放棄了。 - 現在我們有更多的庫可以選擇,可以在這些庫的基礎上去開發,以便更好地支持JWT等新技術。
基于以上這些原因,官方決定重寫 Spring Security OAuth, 以便更好地協調 Spring 和OAuth,并簡化
代碼庫,使Spring 的 OAuth 支持更加靈活。然而,在重寫的過程中,發生了不少波折。
2018年1月30日,Spring 官方發了一個通知,表示要逐漸停止現有的 OAuth2支持,然后在 Spring
Security 5中構建下一代 OAuth2.0 支持。
這么做的原因是因為當時 OAuth2 的落地方案比較混亂,在 Spring Security OAuth、 Spring Cloud
Security、Spring Boot 1.5.x 以及當時最新的Spring Security 5.x 中都提供了對 OAuth2 的實現。以至于
當開發者需要使用 OAuth2 時,不得不問,到底選哪一個依賴合適呢?
所以Spring 官方決定有必要將 OAuth2.0 的支持統一到一個項目中,以便為用戶提供明確的選擇,并避免
任何潛在的混亂,同時OAuth2.0 的開發文檔也要重新編寫,以方便開發人員學習。所有的決定將在
Spring Security 5 中開始,構建下一代 OAuth2.0的支持。
從那個時候起,Spring Security OAuth 項目就正式處于維護模式。官方將提供至少一年的錯識/安全修復
程序,并且會考慮添加次要功能,但不會添加主要功能。同時將 Spring Security OAuth中的所有功能重
構到 Spring Security 5.x 中。
到了2019年11月14日,Spring 官方又發布一個通知,這次的通知首先表示 Spring Security OAuth 在遷
往 Spring Security 5.x 的過程非常順利,大都分遷程工作已經完成了,剩下的將在5.3 版本中完成遷移,
在遷移的過程中還添加了許多新功能。包括對 OpenID Connect1.0 的支持。同時還宣布將不再支持授權
服務器,不支持的原因有兩個:
- 在2019年,已經有大量的商業和開源授權服務器可用。
- 授權服務器是使用一個庫來構建產品,而 Spring Security 作為框架,并不適合做這件事情。
一石激起千層浪,許多開發者表示對此難以接受。
這件事也在Spring 社區引發了激烈的討論,好在 Spring 官方愿意傾聽來自社區的聲音。
到了2020年4月15日,Spring 官方宣布啟動 Spring Authorization server 項目。這是一個由 Spring
Security 團隊領導的社區驅動的項目,致力于向 Spring 社區提供 Authorization Server支持,也就是
說,Spring 又重新支持授權服務器了。
2020年8月21日,Spring Authorization Server 0.0.1 正式發布!
這就是 OAuth2 在Spring 家族中的發展歷程了。在后面的學習中,客戶端和資源服務器都將采用最新的方
式來構建,授權服務器依然采用舊的方式來構建,因為目前的 Spring Authorization Server 0.0.1 功能較
少且 BUG 較多。
一般來說,當我們在項目中使用 OAuth2 時,都是開發客戶端,授權服務器和資源服務器都是由外部提
供。例如我們想在自己搭建網站上集成 GitHub 第三方登錄,只需要開發自己的客戶端即可,認證服務器
和授權服務器都是由 GitHub 提供的。
七、授權、資源服務器
前面的 GitHub 授權登錄主要向大家展示了 OAuth2 中客戶端的工作模式。對于大部分的開發者而言,日
常接觸到的 OAuth2 都是開發客戶端,例如接入 QQ 登錄、接入微信登錄等。不過也有少量場景,可能需
要開發者提供授權服務器與資源服務器,接下來我們就通過一個完整的案例演示如何搭建授權服務器與資
源服務器。
搭建授權服務器,我們可以選擇一些現成的開源項目,直接運行即可,例如:
- Keycloak: RedFat 公司提供的開源工具,提供了很多實用功能,倒如單點登錄、支持OpenID、可視化后臺管理等。
- Apache Oltu: Apache 上的開源項目,最近幾年沒怎么維護了。
接下來我們將搭建一個包含授權服務器、資源服務器以及客戶端在內的 OAuth2 案例。
項目規劃首先把項目分為三部分:
- 授權服務器:采用較早的 spring-cloud-starter-oauth2 來搭建授權服務器。
- 資源服務器:采用最新的 Spring Security 5.x 搭建資源服務器,
- 客戶端: 采用最新的 Spring Security5.x 搭建客戶端。
1. 授權服務器搭建
1.1. 基于內存客戶端和令牌存儲
創建 springboot 應用,并引入依賴
注意: 降低 springboot 版本為 2.2.5.RELEASE
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-oauth2</artifactId><version>2.2.5.RELEASE</version>
</dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId>
</dependency>
編寫配置類,添加 security 配置類以及 oauth 配置類
Spring Security 配置類:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}@Override@Beanprotected AuthenticationManager authenticationManager() throws Exception {return super.authenticationManager();}@Beanpublic UserDetailsService userDetailsService() {InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();UserDetails user = User.withUsername("root").password(passwordEncoder().encode("123")).roles("ADMIN").build();inMemoryUserDetailsManager.createUser(user);return inMemoryUserDetailsManager;}@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailsService());}@Overrideprotected void configure(HttpSecurity http) throws Exception {http.csrf().disable().formLogin();}
}
Authorization Server 配置類:
@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {private final PasswordEncoder passwordEncoder;private final UserDetailsService userDetailsService;@Autowiredpublic AuthorizationServer(PasswordEncoder passwordEncoder, UserDetailsService userDetailsService) {this.passwordEncoder = passwordEncoder;this.userDetailsService = userDetailsService;}/*** 配置客戶端細節 如 客戶端 id 秘鑰 重定向 url 等** @throws Exception*/@Overridepublic void configure(ClientDetailsServiceConfigurer clients) throws Exception {clients.inMemory().withClient("client").secret(passwordEncoder.encode("secret")).redirectUris("http://www.baidu.com").scopes("client:read,user:read").authorizedGrantTypes("authorization_code", "refresh_token","implicit","password","client_credentials");}@Overridepublic void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {endpoints.userDetailsService(userDetailsService);//開啟刷新令牌必須指定}
}
啟動服務,登錄之后進行授權碼獲取
http://localhost:8080/oauth/authorize?client_id=client&response_type=code&redirect_uri=http://www.baidu.com
點擊授權獲取授權碼
根據授權碼,申請令牌
curl -X POST -H "Content-Type: application/x-www-form-urlencoded" -d 'grant_type=authorization_code&code=IwvCtx&redirect_uri=http://www.baidu.com' "http://client:secret@localhost:8080/oauth/token"
刷新令牌
curl -X POST -H "Content-Type: application/x-www-form-urlencoded" -d 'grant_type=refresh_token&refresh_token=f6583d8a-598c-46bb-81d8-01fa6484cf05&client_id=client' "http://client:secret@localhost:8080/oauth/token"
1.2. 基于數據庫客戶端和令牌存儲
在上面的案例中,TokenStore 的默認實現為 InMemoryTokenStore 即內存存儲,對于 Client 信息,
ClientDetailsService 接口負責從存儲倉庫中讀取數據,在上面的案例中默認使用的也是
InMemoryClientDetailsService 實現類。
如果要想使用數據庫存儲,只要提供這些接口的實現類即可,而框架已經為我們寫好 JdbcTokenStore 和
JdbcClientDetailsService
建表:
https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql
# 注意: 并用 BLOB 替換語句中的 LONGVARBINARY 類型
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;-- ----------------------------
-- Table structure for clientdetails
-- ----------------------------
DROP TABLE IF EXISTS `clientdetails`;
CREATE TABLE `clientdetails` (`appId` varchar(256) NOT NULL,`resourceIds` varchar(256) DEFAULT NULL,`appSecret` varchar(256) DEFAULT NULL,`scope` varchar(256) DEFAULT NULL,`grantTypes` varchar(256) DEFAULT NULL,`redirectUrl` varchar(256) DEFAULT NULL,`authorities` varchar(256) DEFAULT NULL,`access_token_validity` int(11) DEFAULT NULL,`refresh_token_validity` int(11) DEFAULT NULL,`additionalInformation` varchar(4096) DEFAULT NULL,`autoApproveScopes` varchar(256) DEFAULT NULL,PRIMARY KEY (`appId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;-- ----------------------------
-- Table structure for oauth_access_token
-- ----------------------------
DROP TABLE IF EXISTS `oauth_access_token`;
CREATE TABLE `oauth_access_token` (`token_id` varchar(256) DEFAULT NULL,`token` blob,`authentication_id` varchar(256) NOT NULL,`user_name` varchar(256) DEFAULT NULL,`client_id` varchar(256) DEFAULT NULL,`authentication` blob,`refresh_token` varchar(256) DEFAULT NULL,PRIMARY KEY (`authentication_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;-- ----------------------------
-- Table structure for oauth_approvals
-- ----------------------------
DROP TABLE IF EXISTS `oauth_approvals`;
CREATE TABLE `oauth_approvals` (`userId` varchar(256) DEFAULT NULL,`clientId` varchar(256) DEFAULT NULL,`scope` varchar(256) DEFAULT NULL,`status` varchar(10) DEFAULT NULL,`expiresAt` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,`lastModifiedAt` date DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;-- ----------------------------
-- Table structure for oauth_client_details
-- ----------------------------
DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details` (`client_id` varchar(256) NOT NULL,`resource_ids` varchar(256) DEFAULT NULL,`client_secret` varchar(256) DEFAULT NULL,`scope` varchar(256) DEFAULT NULL,`authorized_grant_types` varchar(256) DEFAULT NULL,`web_server_redirect_uri` varchar(256) DEFAULT NULL,`authorities` varchar(256) DEFAULT NULL,`access_token_validity` int(11) DEFAULT NULL,`refresh_token_validity` int(11) DEFAULT NULL,`additional_information` varchar(4096) DEFAULT NULL,`autoapprove` varchar(256) DEFAULT NULL,PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;-- ----------------------------
-- Table structure for oauth_client_token
-- ----------------------------
DROP TABLE IF EXISTS `oauth_client_token`;
CREATE TABLE `oauth_client_token` (`token_id` varchar(256) DEFAULT NULL,`token` blob,`authentication_id` varchar(256) NOT NULL,`user_name` varchar(256) DEFAULT NULL,`client_id` varchar(256) DEFAULT NULL,PRIMARY KEY (`authentication_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;-- ----------------------------
-- Table structure for oauth_code
-- ----------------------------
DROP TABLE IF EXISTS `oauth_code`;
CREATE TABLE `oauth_code` (`code` varchar(256) DEFAULT NULL,`authentication` blob
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;-- ----------------------------
-- Table structure for oauth_refresh_token
-- ----------------------------
DROP TABLE IF EXISTS `oauth_refresh_token`;
CREATE TABLE `oauth_refresh_token` (`token_id` varchar(256) DEFAULT NULL,`token` blob,`authentication` blob
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;SET FOREIGN_KEY_CHECKS = 1;-- 寫入客戶端信息
INSERT INTO `oauth_client_details` VALUES ('client', NULL, '$2a$10$QCsINtuRfP8kM112xRVdvuI58MrefLlEP2mM0kzB5KZCPhnOf4392', 'read', 'authorization_code,refresh_token', 'http://www.baidu.com', NULL, NULL, NULL, NULL, NULL);
引入依賴
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope>
</dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
編寫配置文件
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/oauth?characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=root
編寫數據庫信息實現
@Configuration
@EnableAuthorizationServer
public class JdbcAuthorizationServer extends AuthorizationServerConfigurerAdapter {private final AuthenticationManager authenticationManager;private final PasswordEncoder passwordEncoder;private final DataSource dataSource;@Autowiredpublic JdbcAuthorizationServer(AuthenticationManager authenticationManager, PasswordEncoder passwordEncoder, DataSource dataSource) {this.authenticationManager = authenticationManager;this.passwordEncoder = passwordEncoder;this.dataSource = dataSource;}@Bean // 聲明TokenStore實現public TokenStore tokenStore() {return new JdbcTokenStore(dataSource);}@Bean // 聲明 ClientDetails實現public ClientDetailsService clientDetails() {JdbcClientDetailsService jdbcClientDetailsService = new JdbcClientDetailsService(dataSource);jdbcClientDetailsService.setPasswordEncoder(passwordEncoder);return jdbcClientDetailsService;}@Override //配置使用數據庫實現public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {endpoints.authenticationManager(authenticationManager);//認證管理器endpoints.tokenStore(tokenStore());//配置令牌存儲為數據庫存儲// 配置TokenServices參數DefaultTokenServices tokenServices = new DefaultTokenServices();//修改默認令牌生成服務tokenServices.setTokenStore(endpoints.getTokenStore());//基于數據庫令牌生成tokenServices.setSupportRefreshToken(true);//是否支持刷新令牌tokenServices.setReuseRefreshToken(true);//是否重復使用刷新令牌(直到過期)tokenServices.setClientDetailsService(endpoints.getClientDetailsService());//設置客戶端信息tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());//用來控制令牌存儲增強策略//訪問令牌的默認有效期(以秒為單位)。過期的令牌為零或負數。tokenServices.setAccessTokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(30)); // 30天//刷新令牌的有效性(以秒為單位)。如果小于或等于零,則令牌將不會過期tokenServices.setRefreshTokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(3)); //3天endpoints.tokenServices(tokenServices);//使用配置令牌服務}@Overridepublic void configure(ClientDetailsServiceConfigurer clients) throws Exception {clients.withClientDetails(clientDetails());//使用 jdbc存儲}
}
啟動測試,發現數據庫中已經存儲相關的令牌
2. 資源服務器搭建
引入依賴
<properties><java.version>1.8</java.version><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><spring-boot.version>2.2.5.RELEASE</spring-boot.version><spring-cloud.version>Hoxton.SR9</spring-cloud.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-oauth2</artifactId></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-oauth2-resource-server</artifactId></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId></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></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>org.springframework.boot</groupId><artifactId>spring-boot-dependencies</artifactId><version>${spring-boot.version}</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement>
創建資源
@RestController
public class HelloController {@GetMapping("/hello")public String hello(){return "hello!";}
}
編寫資源服務器配置類
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {private final DataSource dataSource;@Autowiredpublic ResourceServerConfig(DataSource dataSource) {this.dataSource = dataSource;}@Overridepublic void configure(ResourceServerSecurityConfigurer resources) throws Exception {resources.tokenStore(tokenStore());}@Beanpublic TokenStore tokenStore() {return new JdbcTokenStore(dataSource);}
}
編寫配置文件
# 應用服務 WEB 訪問端口
server.port=8081
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/security?characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=root
logging.level.org.springframework.jdbc.core=debug
啟動測試,生成令牌之后帶有令牌訪問:
curl -H "Authorization:Bearer dffa62d2-1078-457e-8a2b-4bd46fae0f47" http://localhost:8081/hello
八、使用 JWT
1. 授權服務器頒發 JWT 令牌
配置頒發 JWT 令牌
@Configuration
@EnableAuthorizationServer
public class JwtAuthServerConfig extends AuthorizationServerConfigurerAdapter {private final PasswordEncoder passwordEncoder;private final AuthenticationManager authenticationManager;private final DataSource dataSource;@Autowiredpublic JwtAuthServerConfig(PasswordEncoder passwordEncoder, AuthenticationManager authenticationManager, DataSource dataSource) {this.passwordEncoder = passwordEncoder;this.authenticationManager = authenticationManager;this.dataSource = dataSource;}@Override //配置使用 jwt 方式頒發令牌,同時配置 jwt 轉換器public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {endpoints.tokenStore(tokenStore()).accessTokenConverter(jwtAccessTokenConverter()).authenticationManager(authenticationManager);}@Bean//使用JWT方式生成令牌public TokenStore tokenStore() {return new JwtTokenStore(jwtAccessTokenConverter());}@Bean//使用同一個密鑰來編碼 JWT 中的 OAuth2 令牌public JwtAccessTokenConverter jwtAccessTokenConverter() {JwtAccessTokenConverter converter = new JwtAccessTokenConverter();converter.setSigningKey("123");//可以采用屬性注入方式 生產中建議加密return converter;}@Bean // 聲明 ClientDetails實現public ClientDetailsService clientDetails() {JdbcClientDetailsService jdbcClientDetailsService = new JdbcClientDetailsService(dataSource);jdbcClientDetailsService.setPasswordEncoder(passwordEncoder);return jdbcClientDetailsService;}@Override//使用數據庫方式客戶端存儲public void configure(ClientDetailsServiceConfigurer clients) throws Exception {clients.withClientDetails(clientDetails());}
}
啟動服務,根據授權碼獲取令牌
2. 使用 JWT 令牌資源服務器
配置資源服務器解析jwt
@Configuration
@EnableResourceServer
public class JwtResourceServerConfig extends ResourceServerConfigurerAdapter {@Overridepublic void configure(ResourceServerSecurityConfigurer resources) throws Exception {resources.tokenStore(tokenStore());}@Beanpublic TokenStore tokenStore() {return new JwtTokenStore(jwtAccessTokenConverter());}@Beanpublic JwtAccessTokenConverter jwtAccessTokenConverter() {JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();jwtAccessTokenConverter.setSigningKey("123");return jwtAccessTokenConverter;}
}
啟動測試,通過 jwt 令牌訪問資源
curl -H "Authorization:Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NjAzMzM4MjgsInVzZXJfbmFtZSI6InJvb3QiLCJhdXRob3JpdGllcyI6WyJST0xFX0FETUlOIl0sImp0aSI6ImJmZGVjMzg1LWQyYmYtNDc5Yi05YjhhLTgyZWE4YTRkNzgzMyIsImNsaWVudF9pZCI6ImNsaWVudCIsInNjb3BlIjpbImFwcDpyZWFkIl19.QlELW7LMLuD4OghbEFFzJpIxjW80hC3WHd3I0PiuI7Y" http://localhost:8081/hello