javascript
可能是第二好的 Spring OAuth 2.0 文章,艿艿端午在家写了 3 天~
本文在提供完整代碼示例,可見 https://github.com/YunaiV/SpringBoot-Labs 的 lab-68-spring-security-oauth 目錄。
原創不易,給點個 Star 嘿,一起沖鴨!
1. 概述
在《芋道 Spring Boot 安全框架 Spring Security 入門》文章中,艿艿分享了如何使用 Spring Security 實現認證與授權的功能,獲得廣大女粉絲的好評。
于是乎,艿艿準備再來分享一波 Spring Security OAuth 框架,看看在 Spring Security 如何實現 OAuth2.0 實現授權的功能。
“旁白君:實際上艿艿很早寫了一篇關于 Spring Security OAuth 的文章,考慮到版本太老,提供的示例又過于簡單,所以本文也是該文章的升級版。
可能有胖友對 OAuth2.0 不是很了解,所以我們先來簡單介紹下它。可能胖友看 OAuth2.0 的概念會有點懵逼,不要擔心,后續看完艿艿提供的示例代碼,會突然清晰的哈。
另外,阮一峰提供了幾篇關于 OAuth2.0 非常不錯的文章,推薦胖友去從瞅瞅。同時,本文也會直接引用它的內容,方便胖友統一理解。
-
《理解 OAuth2.0》
-
《OAuth2.0 的一個簡單解釋》
-
《OAuth2.0 的四種方式》
-
《GitHub OAuth 第三方登錄示例教程》
1.1 OAuth2.0 是什么?
“FROM 《維基百科 —— 開放授權》
OAuth(Open Authorization)是一個開放標準,允許用戶讓第三方應用訪問該用戶在某一網站上存儲的私密的資源(如照片,視頻,聯系人列表),而無需將用戶名和密碼提供給第三方應用。
“旁白君:很多團隊,內部會采用 OAuth2.0 實現一個授權服務,避免每個上層應用或者服務重復開發。
OAuth 允許用戶提供一個令牌,而不是用戶名和密碼來訪問他們存放在特定服務提供者的數據。
每一個令牌授權一個特定的網站(例如,視頻編輯網站)在特定的時段(例如,接下來的 2 小時內)內訪問特定的資源(例如僅僅是某一相冊中的視頻)。這樣,OAuth 讓用戶可以授權第三方網站訪問他們存儲在另外服務提供者的某些特定信息,而非所有內容。
“旁白君:如果胖友對接過微信網頁授權功能,就會發現分成兩種方式:靜默授權、手動授權。前者只能獲取到用戶的?openid,而后者可以獲取到用戶的基本信息。
OAuth2.0 是用于授權的行業標準協議。OAuth2.0 為簡化客戶端開發提供了特定的授權流,包括 Web 應用、桌面應用、移動端應用等。
“旁白君:OAuth 1.0 協議體系本身存在一些問題,現已被各大開發平臺逐漸廢棄。
1.2 OAuth2.0 角色解釋
在 OAuth2.0 中,有如下角色:
①?Authorization?Server:認證服務器,用于認證用戶。如果客戶端認證通過,則發放訪問資源服務器的令牌。
②?Resource?Server:資源服務器,擁有受保護資源。如果請求包含正確的訪問令牌,則可以訪問資源。
“友情提示:提供管理后臺、客戶端 API 的服務,都可以認為是 Resource Server。
③?Client:客戶端。它請求資源服務器時,會帶上訪問令牌,從而成功訪問資源。
“友情提示:Client 可以是瀏覽器、客戶端,也可以是內部服務。
④ Resource?Owner:資源擁有者。最終用戶,他有訪問資源的賬號與密碼。
“友情提示:可以簡單把 Resource Owner 理解成人,她在使用 Client 訪問資源。
1.3 OAuth 2.0 運行流程
如下是 OAuth 2.0 的授權碼模式的運行流程:
OAuth 2.0 運行流程?
“-
(A)用戶打開客戶端以后,客戶端要求用戶給予授權。
-
(B)用戶同意給予客戶端授權。
-
(C)客戶端使用上一步獲得的授權,向認證服務器申請令牌。
-
(D)認證服務器對客戶端進行認證以后,確認無誤,同意發放令牌。
-
(E)客戶端使用令牌,向資源服務器申請獲取資源。
-
(F)資源服務器確認令牌無誤,同意向客戶端開放資源。
上述的六個步驟,B 是關鍵,即用戶如何給客戶端進行授權。有了授權之,客戶端就可以獲取令牌,進而憑令牌獲取資源。
“友情提示:如果胖友有對接過三方開放平臺,例如說微信、QQ、微博等三方登錄,就會很容易理解這個步驟過程。
這個時候的資源,資源主要指的是三方開放平臺的用戶資料等等。
1.4 OAuth 2.0 授權模式
客戶端必須得到用戶的授權(Authorization Grant),才能獲得訪問令牌(Access Token)。
OAuth2.0 定義了四種授權方式:
-
授權碼模式(Authorization Code)
-
密碼模式(Resource Owner Password Credentials)
-
簡化模式(Implicit)
-
客戶端模式(Client Credentials)
其中,密碼模式和授權碼模式比較常用。至于如何選擇,艿艿這里先提前劇透下,后續慢慢細品。
“FROM 《深度剖析 OAuth2 和微服務安全架構》
授權類型選擇?
當然,對于黃框部分,對于筆者還是比較困惑的。筆者認為,第三方的單頁應用 SPA ,也是適合采用 Authorization Code Grant 授權模式的。例如,《微信網頁授權》 :
“具體而言,網頁授權流程分為四步:
-
1、引導用戶進入授權頁面同意授權,獲取 code
-
2、通過 code 換取網頁授權 access_token(與基礎支持中的 access_toke n不同)
-
3、如果需要,開發者可以刷新網頁授權 access_token,避免過期
-
4、通過網頁授權 access_token 和 openid 獲取用戶基本信息(支持 UnionID 機制)
所以,艿艿猜測,之所以圖中畫的是 Implicit Grant 的原因是,受 Google 的 《OAuth 2.0 for Client-side Web Applications》 一文中,推薦使用了 Implicit Grant 。
當然,具體使用 Implicit Grant 還是 Authorization Code Grant 授權模式,沒有定論。筆者,偏向于使用?Authorization Code Grant,對于第三方客戶端的場景。
2. 密碼模式
“示例代碼對應倉庫:
-
授權服務器:lab-68-demo02-authorization-server-with-resource-owner-password-credentials
-
資源服務器:lab-68-demo02-resource-server
本小節,我們來學習密碼模式(Resource Owner Password Credentials Grant)。
密碼模式,用戶向客戶端提供自己的用戶名和密碼。客戶端使用這些信息,向授權服務器索要授權。
在這種模式中,用戶必須把自己的密碼給客戶端,但是客戶端不得儲存密碼。這通常用在用戶對客戶端高度信任的情況下,比如客戶端是操作系統的一部分,或者由一個著名公司出品。而授權服務器只有在其他授權模式無法執行的情況下,才能考慮使用這種模式。
“旁白君:如果客戶端和授權服務器都是自己公司的,顯然符合。
密碼模式?
“-
(A)用戶向客戶端提供用戶名和密碼。
-
(B)客戶端將用戶名和密碼發給授權服務器,向后者請求令牌。
-
(C)授權服務器確認無誤后,向客戶端提供訪問令牌。
下面,我們來新建兩個項目,搭建一個密碼模式的使用示例。如下圖所示:
項目結構?
-
lab-68-demo02-authorization-server-with-resource-owner-password-credentials:授權服務器。
-
lab-68-demo02-resource-server:資源服務器。
2.1 搭建授權服務器
創建?lab-68-demo02-authorization-server-with-resource-owner-password-credentials?項目,搭建授權服務器。
2.1.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-demo02-authorization-server-with-resource-owner-password-credentials</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>添加?spring-security-oauth2-autoconfigure?依賴,引入 Spring Security OAuth 并實現自動配置。同時,它也引入了 Spring Security 依賴。如下圖所示:
spring-security-oauth2-autoconfigure?
2.1.2 SecurityConfig
創建 SecurityConfig 配置類,提供一個賬號密碼為「yunai/1024」的用戶。代碼如下:
@Configuration @EnableWebSecurity public?class?SecurityConfig?extends?WebSecurityConfigurerAdapter?{@Override@Bean(name?=?BeanIds.AUTHENTICATION_MANAGER)public?AuthenticationManager?authenticationManagerBean()?throws?Exception?{return?super.authenticationManagerBean();}@Beanpublic?static?NoOpPasswordEncoder?passwordEncoder()?{return?(NoOpPasswordEncoder)?NoOpPasswordEncoder.getInstance();}@Overrideprotected?void?configure(AuthenticationManagerBuilder?auth)?throws?Exception?{auth.//?使用內存中的?InMemoryUserDetailsManagerinMemoryAuthentication()//?不使用?PasswordEncoder?密碼編碼器.passwordEncoder(passwordEncoder())//?配置?yunai?用戶.withUser("yunai").password("1024").roles("USER");}}我們通過?Spring Security 提供認證功能,所以這里需要配置一個用戶。
“友情提示:看不懂這個配置的胖友,后續可回《芋道 Spring Boot 安全框架 Spring Security 入門》重造下。
2.1.3 OAuth2AuthorizationServerConfig
創建 OAuth2AuthorizationServerConfig 配置類,進行授權服務器。代碼如下:
@Configuration @EnableAuthorizationServer public?class?OAuth2AuthorizationServerConfig?extends?AuthorizationServerConfigurerAdapter?{/***?用戶認證?Manager*/@Autowiredprivate?AuthenticationManager?authenticationManager;@Overridepublic?void?configure(AuthorizationServerEndpointsConfigurer?endpoints)?throws?Exception?{endpoints.authenticationManager(authenticationManager);}@Overridepublic?void?configure(AuthorizationServerSecurityConfigurer?oauthServer)?throws?Exception?{oauthServer.checkTokenAccess("isAuthenticated()");}@Overridepublic?void?configure(ClientDetailsServiceConfigurer?clients)?throws?Exception?{clients.inMemory()?//?<4.1>.withClient("clientapp").secret("112233")?//?<4.2> Client 賬號、密碼。.authorizedGrantTypes("password")?//?<4.2>?密碼模式.scopes("read_userinfo",?"read_contacts")?//?<4.2>?可授權的?Scope //????????????????.and().withClient()?//?<4.3>?可以繼續配置新的?Client;}}① 在類上添加?@EnableAuthorizationServer?注解,聲明開啟 OAuth?授權服務器的功能。
同時,繼承 AuthorizationServerConfigurerAdapter 類,進行 OAuth?授權服務器的配置。
②?#configure(AuthorizationServerEndpointsConfigurer endpoints)?方法,配置使用的 AuthenticationManager 實現用戶認證的功能。其中,authenticationManager?是由「2.1.2 SecurityConfig」創建,Spring Security 的配置類。
③?#configure(AuthorizationServerSecurityConfigurer oauthServer)?方法,設置?/oauth/check_token?端點,通過認證后可訪問。
“友情提示:這里的認證,指的是使用?client-id?+?client-secret?進行的客戶端認證,不要和用戶認證混淆。
其中,/oauth/check_token?端點對應 CheckTokenEndpoint 類,用于校驗訪問令牌的有效性。
-
在客戶端訪問資源服務器時,會在請求中帶上訪問令牌。
-
在資源服務器收到客戶端的請求時,會使用請求中的訪問令牌,找授權服務器確認該訪問令牌的有效性。
④?#configure(ClientDetailsServiceConfigurer clients)?方法,進行 Client 客戶端的配置。
<4.1>?處,設置使用基于內存的 Client 存儲器。實際情況下,最好放入數據庫中,方便管理。
ClientDetailsService 子類<4.2>?處,創建一個 Client 配置。如果要繼續添加另外的 Client 配置,可以在?<4.3>?處使用?#and()?方法繼續拼接。注意,這里的?.withClient("clientapp").secret("112233")?代碼段,就是?client-id?和?client-secret。
“補充知識:可能會有胖友會問,為什么要創建 Client 的?client-id?和?client-secret?呢?
通過?client-id?編號和?client-secret,授權服務器可以知道調用的來源以及正確性。這樣,即使“壞人”拿到 Access Token ,但是沒有?client-id?編號和?client-secret,也不能和授權服務器發生有效的交互。
2.1.4 AuthorizationServerApplication
創建 AuthorizationServerApplication 類,授權服務器的啟動類。代碼如下:
@SpringBootApplication public?class?AuthorizationServerApplication?{public?static?void?main(String[]?args)?{SpringApplication.run(AuthorizationServerApplication.class,?args);}}2.1.5 簡單測試
執行 AuthorizationServerApplication 啟動授權服務器。下面,我們使用?Postman 模擬一個 Client。
①?POST?請求 http://localhost:8080/oauth/token 地址,使用密碼模式進行授權。如下圖所示:
?
- client-id?+?client-secret?進行 Client 認證 密碼模式的認證
- 密碼模式的認證
請求說明:
-
通過 Basic Auth 的方式,填寫?client-id?+?client-secret?作為用戶名與密碼,實現 Client 客戶端有效性的認證。
-
請求參數?grant_type?為?"password",表示使用密碼模式。
-
請求參數?username?和?password,表示用戶的用戶名與密碼。
響應說明:
-
響應字段?access_token?為訪問令牌,后續客戶端在訪問資源服務器時,通過它作為身份的標識。
-
響應字段?token_type?為令牌類型,一般是?bearer?或是?mac?類型。
-
響應字段?expires_in?為訪問令牌的過期時間,單位為秒。
-
響應字段?scope?為權限范圍。
友情提示:/oauth/token?對應 TokenEndpoint 端點,提供 OAuth2.0 的四種授權模式。感興趣的胖友,可以后續去擼擼。
②?POST?請求 http://localhost:8080/oauth/check_token 地址,校驗訪問令牌的有效性。如下圖所示:
?
- client-id?+?client-secret?進行 Client 認證 密碼模式的認證
- 密碼模式的認證
請求和響應比較簡單,胖友自己瞅瞅即可。
2.2 搭建資源服務器
創建?lab-68-demo02-resource-server?項目,搭建資源服務器。
2.2.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-demo02-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> “友情提示:和「2.1.1 引入依賴」小節,是一致的哈。
2.2.2 配置文件
創建?application.yml?配置文件,添加 Spring Security OAuth 相關配置。
server:port:?9090security:oauth2:#?OAuth2?Client?配置,對應?OAuth2ClientProperties?類client:client-id:?clientappclient-secret:?112233#?OAuth2?Resource?配置,對應?ResourceServerProperties?類resource:token-info-uri:?http://127.0.0.1:8080/oauth/check_token?#?獲得?Token?信息的?URL#?訪問令牌獲取?URL,自定義的access-token-uri:?http://127.0.0.1:8080/oauth/token①?security.oauth2.client?配置項,OAuth2 Client 配置,對應 OAuth2ClientProperties 類。在這個配置項中,我們添加了客戶端的?client-id?和?client-secret。
為什么要添加這個配置項呢?因為資源服務器會調用授權服務器的?/oauth/check_token?接口,而考慮到安全性,我們配置了該接口需要進過客戶端認證。
“友情提示:這里艿艿偷懶了,其實單獨給資源服務器配置一個 Client 的?client-id?和?client-secret。我們可以把資源服務器理解成授權服務器的一個特殊的客戶端。
②?security.oauth2.resource?配置項,OAuth2 Resource 配置,對應 ResourceServerProperties 類。
這里,我們通過?token-info-uri?配置項,設置使用授權服務器的?/oauth/check_token?接口,校驗訪問令牌的有效性。
③?security.access-token-uri?配置項,是我們自定義的,設置授權服務器的?oauth/token?接口,獲取訪問令牌。因為稍后我們將在 LoginController 中,實現一個?/login?登錄接口。
2.2.3 OAuth2ResourceServerConfig
創建 OAuth2ResourceServerConfig 類,進行資源服務器。代碼如下:
@Configuration @EnableResourceServer public?class?OAuth2ResourceServerConfig?extends?ResourceServerConfigurerAdapter?{@Overridepublic?void?configure(HttpSecurity?http)?throws?Exception?{http.authorizeRequests()//?設置?/login?無需權限訪問.antMatchers("/login").permitAll()//?設置其它請求,需要認證后訪問.anyRequest().authenticated();}}① 在類上添加?@EnableResourceServer?注解,聲明開啟 OAuth?資源服務器的功能。
同時,繼承 ResourceServerConfigurerAdapter 類,進行 OAuth?資源服務器的配置。
②?#configure(HttpSecurity http)?方法,設置 HTTP 權限。這里,我們設置?/login?接口無需權限訪問,其它接口認證后可訪問。
這樣,客戶端在訪問資源服務器時,其請求中的訪問令牌會被資源服務器調用授權服務器的?/oauth/check_token?接口,進行校驗訪問令牌的正確性。
2.2.4 ExampleController
創建 ExampleController 類,提供?/api/example/hello?接口,表示一個資源。代碼如下:
@RestController @RequestMapping("/api/example") public?class?ExampleController?{@RequestMapping("/hello")public?String?hello()?{return?"world";}}2.2.5 ResourceServerApplication
創建 ResourceServerApplication 類,資源服務器的啟動類。代碼如下:
@SpringBootApplication public?class?ResourceServerApplication?{public?static?void?main(String[]?args)?{SpringApplication.run(ResourceServerApplication.class,?args);}}2.2.6 簡單測試(第一彈)
執行 ResourceServerApplication 啟動資源服務器。下面,我們來請求服務器的 <127.0.0.1:9090/api/example/hello> 接口,進行相應的測試。
① 首先,請求 <127.0.0.1:9090/api/example/hello> 接口,不帶訪問令牌,則請求會被攔截。如下圖所示:
不帶訪問令牌② 然后,請求 <127.0.0.1:9090/api/example/hello> 接口,帶上錯誤的訪問令牌,則請求會被攔截。如下圖所示:
錯誤的訪問令牌?
“友情提示:訪問令牌需要在請求頭?"Authorization"?上設置,并且以?"Bearer "?開頭。
③ 最后,請求 <127.0.0.1:9090/api/example/hello> 接口,帶上正確的訪問令牌,則請求會被通過。如下圖所示:
正確的訪問令牌?
2.2.7 LoginController
創建 LoginController 類,提供?/login?登錄接口。代碼如下:
@RestController @RequestMapping("/") public?class?LoginController?{@Autowiredprivate?OAuth2ClientProperties?oauth2ClientProperties;@Value("${security.oauth2.access-token-uri}")private?String?accessTokenUri;@PostMapping("/login")public?OAuth2AccessToken?login(@RequestParam("username")?String?username,@RequestParam("password")?String?password)?{//?<1>?創建?ResourceOwnerPasswordResourceDetails?對象ResourceOwnerPasswordResourceDetails?resourceDetails?=?new?ResourceOwnerPasswordResourceDetails();resourceDetails.setAccessTokenUri(accessTokenUri);resourceDetails.setClientId(oauth2ClientProperties.getClientId());resourceDetails.setClientSecret(oauth2ClientProperties.getClientSecret());resourceDetails.setUsername(username);resourceDetails.setPassword(password);//?<2>?創建?OAuth2RestTemplate?對象OAuth2RestTemplate?restTemplate?=?new?OAuth2RestTemplate(resourceDetails);restTemplate.setAccessTokenProvider(new?ResourceOwnerPasswordAccessTokenProvider());//?<3>?獲取訪問令牌return?restTemplate.getAccessToken();}}在?/login?接口中,資源服務器扮演的是一個 OAuth?客戶端的角色,調用授權服務器的?/oauth/token?接口,使用密碼模式進行授權,獲得訪問令牌。
①?<1>?處,創建 ResourceOwnerPasswordResourceDetails 對象,填寫密碼模式授權需要的請求參數。
②?<2>?處,創建 OAuth2RestTemplate 對象,它是 Spring Security OAuth 封裝的工具類,用于請求授權服務器。
同時,將 ResourceOwnerPasswordAccessTokenProvider 設置到其中,表示使用密碼模式授權。
“友情提示:這一步非常重要,艿艿在這里卡了非常非常非常久,一度自閉要放棄。
③?<3>?處,調用 OAuth2RestTemplate 的?#getAccessToken()?方法,調用授權服務器的?/oauth/token?接口,進行密碼模式的授權。
注意,OAuth2RestTemplate 是有狀態的工具類,所以需要每次都重新創建。
2.2.8 簡單測試(第二彈)
重新執行 ResourceServerApplication 啟動資源服務器。下面,我們來進行?/login?接口的測試。
① 首先,請求 http://127.0.0.1:9090/login 接口,使用用戶的用戶名與密碼進行登錄,獲得訪問令牌。如下圖所示:
測試?/login?接口響應結果和授權服務器的?/oauth/token?接口是一致的,因為就是調用它,嘿嘿~
② 然后,請求 <127.0.0.1:9090/api/example/hello> 接口,帶剛剛的訪問令牌,則請求會被通過。如下圖所示:
正確的訪問令牌?
3. 授權碼模式
“示例代碼對應倉庫:
-
授權服務器:lab-68-demo02-authorization-server-with-resource-owner-password-credentials
-
資源服務器:lab-68-demo02-resource-server
本小節,我們來學習授權碼模式(Authorization Code)。
授權碼模式,是功能最完整、流程最嚴密的授權模式。它的特點就是通過客戶端的后臺服務器,與授權務器進行互動。
“旁白君:一般情況下,在有客戶端的情況下,我們與第三方平臺常常采用這種方式。
授權碼模式?
“-
(A)用戶訪問客戶端,后者將前者跳轉到到授權服務器。
-
(B)用戶選擇是否給予客戶端授權。
-
(C)假設用戶給予授權,授權服務器將跳轉到客戶端事先指定的"重定向 URI"(Redirection URI),同時附上一個授權碼。
-
(D)客戶端收到授權碼,附上早先的"重定向 URI",向認證服務器申請令牌。這一步是在客戶端的后臺的服務器上完成的,對用戶不可見。
-
(E)認證服務器核對了授權碼和重定向 URI,確認無誤后,向客戶端發送訪問令牌。
下面,我們來新建兩個項目,搭建一個授權碼模式的使用示例。如下圖所示:
項目結構-
lab-68-demo02-authorization-server-with-resource-owner-password-credentials:授權服務器。
-
lab-68-demo02-resource-server:資源服務器。
3.1 搭建授權服務器
復制出?lab-68-demo02-authorization-server-with-resource-owner-password-credentials?項目,修改搭建授權服務器。改動點如下圖所示:
項目改動點僅僅需要修改 OAuth2AuthorizationServerConfig 類,設置使用?"authorization_code"?授權碼模式,并設置回調地址。
🙂 注意,這里設置的回調地址,稍后我們會在「3.2 搭建資源服務器」中實現。
3.1.1 簡單測試
執行 AuthorizationServerApplication 啟動授權服務器。
① 使用瀏覽器,訪問 http://127.0.0.1:8080/oauth/authorize?client_id=clientapp&redirect_uri=http://127.0.0.1:9090/callback&response_type=code&scope=read_userinfo 地址,獲取授權。請求參數說明如下:
-
client_id?參數,必傳,為我們在 OAuth2AuthorizationServer 中配置的 Client 的編號。
-
redirect_uri?參數,可選,回調地址。當然,如果?client_id?對應的 Client 未配置?redirectUris?屬性,會報錯。
-
response_type?參數,必傳,返回結果為?code?授權碼。
-
scope?參數,可選,申請授權的 Scope 。如果多個,使用逗號分隔。
-
state?參數,可選,表示客戶端的當前狀態,可以指定任意值,授權服務器會原封不動地返回這個值。
友情提示:state?參數,未在上述 URL 中體現出來。
因為我們并未登錄授權服務器,所以被攔截跳轉到登錄界面。如下圖所示:
登錄界面② 輸入用戶的賬號密碼「yunai/1024」進行登錄。登錄完成后,進入授權界面。如下圖所示:
“旁白君:和我們日常使用的騰訊 QQ、微信、微博等等三方登錄,是一模一樣的,除了丑了點,嘿嘿~
授權界面③ 選擇?scope.read_userinfo?為 Approve 允許,點擊「Authorize」按鈕,完成授權操作。瀏覽器自動重定向到 Redirection URI 地址,并且在 URI 上可以看到?code?授權碼。如下圖所示:
回調界面?
“友情提示:/oauth/authorize?對應 AuthorizationEndpoint 端點。
④ 因為我們暫時沒有啟動資源服務器,所以顯示無法訪問。這里,我們先使用 Postman 模擬請求 http://localhost:8080/oauth/token 地址,使用授權碼獲取到訪問令牌。如下圖所示:
?
- client-id?+?client-secret?進行 Client 認證 授權碼模式的認證
- 授權碼模式的認證
?
請求說明:
-
通過 Basic Auth 的方式,填寫?client-id?+?client-secret?作為用戶名與密碼,實現 Client 客戶端有效性的認證。
-
請求參數?grant_type?為?"authorization_code",表示使用授權碼模式。
-
請求參數?code,從授權服務器獲取到的授權碼。
-
請求參數?redirect_uri,Client 客戶端的?Redirection URI?地址。
注意,授權碼僅能使用一次,重復請求會報?Invalid authorization code:?錯誤。如下圖所示:
授權碼模式的認證 - 失敗3.2 搭建資源服務器
復用?lab-68-demo02-resource-server?項目,主要是提供回調地址。如下圖所示:
項目改動點① 新建 CallbackController 類,提供?/callback?回調地址。
② 在 OAuth2ResourceServerConfig 配置類中,設置?/callback?回調地址無需權限驗證,不然回調都跳轉不過來哈。
3.2.1 CallbackController
創建 CallbackController 類,提供?/callback?回調地址,在獲取到授權碼時,請求授權服務器,通過授權碼獲取訪問令牌。代碼如下:
@RestController @RequestMapping("/") public?class?CallbackController?{@Autowiredprivate?OAuth2ClientProperties?oauth2ClientProperties;@Value("${security.oauth2.access-token-uri}")private?String?accessTokenUri;@GetMapping("/callback")public?OAuth2AccessToken?login(@RequestParam("code")?String?code)?{//?創建?AuthorizationCodeResourceDetails?對象AuthorizationCodeResourceDetails?resourceDetails?=?new?AuthorizationCodeResourceDetails();resourceDetails.setAccessTokenUri(accessTokenUri);resourceDetails.setClientId(oauth2ClientProperties.getClientId());resourceDetails.setClientSecret(oauth2ClientProperties.getClientSecret());//?創建?OAuth2RestTemplate?對象OAuth2RestTemplate?restTemplate?=?new?OAuth2RestTemplate(resourceDetails);restTemplate.getOAuth2ClientContext().getAccessTokenRequest().setAuthorizationCode(code);?//?<1>?設置?coderestTemplate.getOAuth2ClientContext().getAccessTokenRequest().setPreservedState("http://127.0.0.1:9090/callback");?//?<2>?通過這個方式,設置?redirect_uri?參數restTemplate.setAccessTokenProvider(new?AuthorizationCodeAccessTokenProvider());//?獲取訪問令牌return?restTemplate.getAccessToken();}}代碼比較簡單,還是使用 OAuth2RestTemplate 進行請求授權服務器,胖友自己瞅瞅哈。
需要注意的是?<1>?和?<2>?處,設置請求授權服務器需要的?code?和?redirect_uri?參數。
3.2.2 簡單測試
執行 ResourceServerApplication 啟動資源服務器。
重復「3.2.1 簡單測試」的過程,成功獲取到訪問令牌。如下圖所示:
授權碼模式的認證 - 成功4. 簡化模式
“示例代碼對應倉庫:
-
授權服務器:lab-68-demo02-authorization-server-with-implicit
-
資源服務器:lab-68-demo02-resource-server
本小節,我們來學習簡化模式(Implicit)。
簡化模式,不通過第三方應用程序的服務器,直接在瀏覽器中向授權服務器申請令牌,跳過了“授權碼”這個步驟,因此得名。所有步驟在瀏覽器中完成,令牌對訪問者是可見的,且客戶端不需要授權。
簡化模式?
“-
(A)用戶訪問客戶端,后者將前者跳轉到到授權服務器。
-
(B)用戶選擇是否給予客戶端授權。
-
(C)假設用戶給予授權,授權服務器將用戶導向客戶端指定的"重定向URI",并在 URI 的?Hash 部分包含了訪問令牌。
-
(D)瀏覽器向資源服務器發出請求,其中不包括上一步收到的 Hash 值。
-
(E)資源服務器返回一個網頁,其中包含的代碼可以獲取 Hash 值中的令牌。
-
(F)瀏覽器執行上一步獲得的腳本,提取出令牌。
-
(G)瀏覽器將令牌發給客戶端。
?
-
lab-68-demo02-authorization-server-with-implicit:授權服務器。
-
lab-68-demo02-resource-server:資源服務器。
4.1 搭建授權服務器
復制出?lab-68-demo02-authorization-server-with-implicit?項目,修改搭建授權服務器。改動點如下圖所示:
項目改動點僅僅需要修改 OAuth2AuthorizationServerConfig 類,設置使用?"implicit"?簡化模式,并設置回調地址。
🙂 注意,這里設置的回調地址,稍后我們會在「4.2 搭建資源服務器」中實現。
4.2 搭建資源服務器
復用?lab-68-demo02-resource-server?項目,主要是提供回調地址。如下圖所示:
項目改動點① 新建 Callback02Controller 類,提供?/callback02?回調地址。代碼如下:
@RestController @RequestMapping("/") public?class?Callback02Controller?{@GetMapping("/callback02")public?String?login()?{return?"假裝這里有一個頁面";}} “友情提示:考慮到暫時不想做頁面,所以這里先假裝一下,嘿嘿。
② 在 OAuth2ResourceServerConfig 配置類中,設置?/callback02?回調地址無需權限驗證,不然回調都跳轉不過來哈。
4.3 簡單測試
執行 AuthorizationServerApplication 啟動授權服務器。
執行 ResourceServerApplication 啟動資源服務器。
① 使用瀏覽器,訪問 http://127.0.0.1:8080/oauth/authorize?client_id=clientapp&redirect_uri=http://127.0.0.1:9090/callback02&response_type=token&scope=read_userinfo 地址,獲取授權。請求參數說明如下:
-
client_id?參數,必傳,為我們在 OAuth2AuthorizationServer 中配置的 Client 的編號。
-
redirect_uri?參數,可選,回調地址。當然,如果?client_id?對應的 Client 未配置?redirectUris?屬性,會報錯。
-
response_type?參數,必傳,返回結果為?token?訪問令牌。
-
scope?參數,可選,申請授權的 Scope 。如果多個,使用逗號分隔。
-
state?參數,可選,表示客戶端的當前狀態,可以指定任意值,授權服務器會原封不動地返回這個值。
友情提示:state?參數,未在上述 URL 中體現出來。
因為我們并未登錄授權服務器,所以被攔截跳轉到登錄界面。如下圖所示:
登錄界面② 輸入用戶的賬號密碼「yunai/1024」進行登錄。登錄完成后,進入授權界面。如下圖所示:
“旁白君:和我們日常使用的騰訊 QQ、微信、微博等等三方登錄,是一模一樣的,除了丑了點,嘿嘿~
授權界面③ 選擇?scope.read_userinfo?為 Approve 允許,點擊「Authorize」按鈕,完成授權操作。瀏覽器自動重定向到 Redirection URI 地址,并且在 URI 上的?Hash 部分可以看到?access_token?訪問令牌。如下圖所示:
回調界面后續,可以通過編寫 Javascript 腳本的代碼,獲取 URI 上的?Hash 部分的訪問令牌。
5. 客戶端模式
“示例代碼對應倉庫:
-
授權服務器:lab-68-demo02-authorization-server-with-client-credentials
-
資源服務器:lab-68-demo02-resource-server
本小節,我們來學習客戶端模式(Client Credentials)。
客戶端模式,指客戶端以自己的名義,而不是以用戶的名義,向授權服務器進行認證。
嚴格地說,客戶端模式并不屬于 OAuth 框架所要解決的問題。在這種模式中,用戶直接向客戶端注冊,客戶端以自己的名義要求授權服務器提供服務,其實不存在授權問題。
“旁白君:我們對接微信公眾號時,就采用的客戶端模式。我們的后端服務器就扮演“客戶端”的角色,與微信公眾號的后端服務器進行交互。
客戶端模式?
“-
(A)客戶端向授權服務器進行身份認證,并要求一個訪問令牌。
-
(B)授權服務器確認無誤后,向客戶端提供訪問令牌。
下面,我們來新建兩個項目,搭建一個客戶端模式的使用示例。如下圖所示:
項目結構?
-
lab-68-demo02-authorization-server-with-client-credentials:授權服務器。
-
lab-68-demo02-resource-server:資源服務器。
5.1 搭建授權服務器
復制出?lab-68-demo02-authorization-server-with-client-credentials?項目,修改搭建授權服務器。改動點如下圖所示:
項目改動點① 刪除 SecurityConfig 配置類,因為客戶端模式下,無需 Spring Security 提供用戶的認證功能。
但是,Spring Security OAuth 需要一個 PasswordEncoder Bean,否則會報錯,因此我們在 OAuth2AuthorizationServerConfig 類的?#passwordEncoder()?方法進行創建。
② 修改 OAuth2AuthorizationServerConfig 類,設置使用?"client_credentials"?客戶端模式。
5.1.1 簡單測試
執行 AuthorizationServerApplication 啟動授權服務器。下面,我們使用?Postman 模擬一個 Client。
①?POST?請求 http://localhost:8080/oauth/token 地址,使用客戶端模式進行授權。如下圖所示:
- client-id?+?client-secret?進行 Client 認證 客戶端模式的認證
?
- 客戶端模式的認證
?
請求說明:
-
通過 Basic Auth 的方式,填寫?client-id?+?client-secret?作為用戶名與密碼,實現 Client 客戶端有效性的認證。
-
請求參數?grant_type?為?"client_credentials",表示使用客戶端模式。
響應就是訪問令牌,胖友自己瞅瞅即可。
5.2 搭建資源服務器
復用?lab-68-demo02-resource-server?項目,修改點如下圖所示:
項目改動點① 新建 ClientLoginController 類,提供?/client-login?接口,實現調用授權服務器,進行客戶端模式的授權,獲得訪問令牌。代碼如下:
@RestController @RequestMapping("/") public?class?ClientLoginController?{@Autowiredprivate?OAuth2ClientProperties?oauth2ClientProperties;@Value("${security.oauth2.access-token-uri}")private?String?accessTokenUri;@PostMapping("/client-login")public?OAuth2AccessToken?login()?{//?創建?ClientCredentialsResourceDetails?對象ClientCredentialsResourceDetails?resourceDetails?=?new?ClientCredentialsResourceDetails();resourceDetails.setAccessTokenUri(accessTokenUri);resourceDetails.setClientId(oauth2ClientProperties.getClientId());resourceDetails.setClientSecret(oauth2ClientProperties.getClientSecret());//?創建?OAuth2RestTemplate?對象OAuth2RestTemplate?restTemplate?=?new?OAuth2RestTemplate(resourceDetails);restTemplate.setAccessTokenProvider(new?ClientCredentialsAccessTokenProvider());//?獲取訪問令牌return?restTemplate.getAccessToken();}}代碼比較簡單,還是使用 OAuth2RestTemplate 進行請求授權服務器,胖友自己瞅瞅哈。
② 在 OAuth2ResourceServerConfig 配置類中,設置?/client-login?接口無需權限驗證,不然無法調用哈。
5.2.1 簡單測試
執行 ResourceServerApplication 啟動資源服務器。
① 使用「5.1.1 簡單測試」小節獲得的訪問令牌,請求 <127.0.0.1:9090/api/example/hello> 接口時帶上,則請求會被通過。如下圖所示:
正確的訪問令牌② 請求 http://127.0.0.1:9090/clientlogin 接口,使用客戶端模式進行授權,獲得訪問令牌。如下圖所示:
測試?client-login?接口響應結果和授權服務器的?/oauth/token?接口是一致的,因為就是調用它,嘿嘿~
6. 合并服務器
“旁白君:這個小節的標題,艿艿有點不知道怎么取了,就先叫合并服務器吧 = =!
在項目比較小時,考慮到節省服務器資源,會考慮將授權服務器和資源服務器合并到一個項目中,避免啟動多個 Java 進程。良心的艿艿,編寫了四種授權模式的示例,如下圖所示:
示例項目
-
基于密碼模式的示例:lab-68-demo01-resource-owner-password-credentials-server
-
基于授權碼模式的示例:lab-68-demo01-authorization-code-server
-
基于簡化模式的示例:lab-68-demo01-implicit-server
-
基于客戶端模式的示例:lab-68-demo01-client-credentials-server
具體的代碼實現,實際和上述每個授權模式對應的小節是基本一致的,只是說將代碼“放”在了一個項目中。嘿嘿~
7. 刷新令牌
“示例代碼對應倉庫:
-
授權服務器:lab-68-demo03-authorization-server-with-client-credentials
在 OAuth2.0 中,一共有兩類令牌:
-
訪問令牌(Access Token)
-
刷新令牌(Refresh Token)
在訪問令牌過期時,我們可以使用刷新令牌向授權服務器獲取一個新的訪問令牌。
可能會胖友有疑惑,為什么會有刷新令牌呢?每次請求資源服務器時,都會在請求上帶上訪問令牌,這樣它的泄露風險是相對高的。
因此,出于安全性的考慮,訪問令牌的過期時間比較短,刷新令牌的過期時間比較長。這樣,如果訪問令牌即使被盜用走,那么在一定的時間后,訪問令牌也能在較短的時間吼過期。當然,安全也是相對的,如果使用刷新令牌后,獲取到新的訪問令牌,訪問令牌后續又可能被盜用。
艿艿整理了下,大家常用開放平臺的令牌過期時間,讓大家更好的理解:
| 微信開放平臺 | 2 小時 | 未知 |
| 騰訊開放平臺 | 90 天 | 未知 |
| 小米開放平臺 | 90 天 | 10 年 |
7.1 示例項目
下面,復制出?lab-68-demo03-authorization-server-with-client-credentials?項目,搭建提供訪問令牌的授權服務器。改動點如下圖所示:
項目改動點① 在 OAuth2AuthorizationServerConfig 的?#configure(ClientDetailsServiceConfigurer clients)?方法中,在配置的 Client 的授權模式中,額外新增?"refresh_token"?刷新令牌。
通過?#accessTokenValiditySeconds(int accessTokenValiditySeconds)?方法,設置訪問令牌的有效期。
通過?#refreshTokenValiditySeconds(int refreshTokenValiditySeconds)?方法,設置刷新令牌的有效期。
② 在 OAuth2AuthorizationServerConfig 的?#configure(AuthorizationServerEndpointsConfigurer endpoints)?方法中,設置使用的?userDetailsService?用戶詳情 Service。
而該?userDetailsService?是在 SecurityConfig 的?#userDetailsServiceBean()?方法創建的 UserDetailsService Bean。
“友情提示:如果不進行 UserDetailsService 的設置,在使用刷新令牌獲取新的訪問令牌時,會拋出異常。
7.2 簡單測試
執行 AuthorizationServerApplication 啟動授權服務器。下面,我們使用?Postman 模擬一個 Client。
①?POST?請求 http://localhost:8080/oauth/token 地址,使用密碼模式進行授權。如下圖所示:
密碼模式的認證額外多返回了?refresh_token?刷新令牌。
②?POST?請求 http://localhost:8080/oauth/token 地址,使用刷新令牌模式進行授權。如下圖所示:
刷新令牌模式的認證請求說明:
-
通過 Basic Auth 的方式,填寫?client-id?+?client-secret?作為用戶名與密碼,實現 Client 客戶端有效性的認證。
-
請求參數?grant_type?為?"refresh_token",表示使用刷新令牌模式。
-
請求參數?refresh_token,表示刷新令牌。
在響應中,返回了新的?access_token?訪問令牌。注意,老的?access_token?訪問令牌會失效,無法繼續使用。
8. 刪除令牌
“示例代碼對應倉庫:
-
授權服務器:lab-68-demo03-authorization-server-with-client-credentials
在用戶登出系統時,我們會有刪除令牌的需求。雖然說,可以通過客戶端本地刪除令牌的方式實現。但是,考慮到真正的徹底的實現刪除令牌,必然服務端自身需要刪除令牌。
“友情提示:客戶端本地刪除令牌的方式實現,指的是清楚本地 Cookie、localStorage 的令牌緩存。
在 Spring Security OAuth2 中,并沒有提供內置的接口,所以需要自己去實現。筆者參看 《Spring Security OAuth2 – Simple Token Revocation》 文檔,實現刪除令牌的 API 接口。
具體的實現,通過調用 ConsumerTokenServices 的?#revokeToken(String tokenValue)?方法,刪除訪問令牌和刷新令牌。如下圖所示:
ConsumerTokenServices 實現類8.1 示例項目
下面,我們直接在授權服務器?lab-68-demo03-authorization-server-with-resource-owner-password-credentials?項目,修改接入刪除令牌的功能。改動點如下圖所示:
項目改動點① 創建 TokenDemoController 類,提供?/token/demo/revoke?接口,調用 ConsumerTokenServices 的?#revokeToken(String tokenValue)?方法,刪除訪問令牌和刷新令牌。代碼如下:
@RestController @RequestMapping("/token/demo") public?class?TokenDemoController?{@Autowiredprivate?ConsumerTokenServices?tokenServices;@PostMapping(value?=?"/revoke")public?boolean?revokeToken(@RequestParam("token")?String?token)?{return?tokenServices.revokeToken(token);}}② 在 SecurityConfig 配置類,設置?/token/demo/revoke?接口無需授權,方便測試。代碼如下:
//?SecurityConfig.java@Override protected?void?configure(HttpSecurity?http)?throws?Exception?{http.csrf().disable().authorizeRequests()//?設置?/token/demo/revoke?無需授權.mvcMatchers("/token/demo/revoke").permitAll()//?設置其它接口需要授權.anyRequest().authenticated(); }8.2 簡單測試
執行 AuthorizationServerApplication 啟動授權服務器。下面,我們使用?Postman 模擬一個 Client。
①?POST?請求 http://localhost:8080/oauth/token 地址,使用密碼模式進行授權。如下圖所示:
密碼模式的認證②?POST?請求 http://localhost:8080/token/demo/revoke 地址,刪除令牌。如下圖所示:
刪除令牌刪除成功。后續,胖友可以自己調用授權服務器的?oauth/check_token?接口,測試訪問令牌是否已經被刪除。
666. 彩蛋
至此,我們完整學習 Spring Security OAuth 框架。不過 Spring 團隊宣布該框架處于 Deprecation?廢棄狀態。如下圖所示:
Spring Security OAuth 被廢棄
同時,Spring 團隊正在實現新的 Spring Authorization Server?授權服務器,目前還處于 Experimental?實驗狀態。
實際項目中,根據艿艿了解到的情況,很少項目會直接采用 Spring Security OAuth 框架,而是自己參考它進行 OAuth2.0 的實現。并且,一般只會實現密碼授權模式。
在本文中,我們采用基于內存的 InMemoryTokenStore,實現訪問令牌和刷新令牌的存儲。它會存在兩個明顯的缺點:
-
重啟授權服務器時,令牌信息會丟失,導致用戶需要重新授權。
-
多個授權服務器時,令牌信息無法共享,導致用戶一會授權成功,一會授權失敗。
因此,下一篇《芋道 Spring Security OAuth2 存儲器》文章,我們來學習 Spring Security OAuth 提供的基于數據庫和?Redis的存儲器。走起~
總結
以上是生活随笔為你收集整理的可能是第二好的 Spring OAuth 2.0 文章,艿艿端午在家写了 3 天~的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Redis性能监控指标汇总
- 下一篇: 为什么 MySQL 使用 B+ 树,而不