6. 合並服務器
在項目比較小時,考慮到節省服務器資源,會考慮將授權服務器和資源服務器合並到一個項目中,避免啟動多個 Java 進程。良心的艿艿,編寫了四種授權模式的示例,如下圖所示:
- 基於授權碼模式的示例:lab-68-demo01-authorization-code-server
- 基於簡化模式的示例:lab-68-demo01-implicit-server
- 基於客戶端模式的示例:lab-68-demo01-client-credentials-server
具體的代碼實現,實際和上述每個授權模式對應的小節是基本一致的,只是說將代碼“放”在了一個項目中。嘿嘿~
7. 刷新令牌
在 OAuth2.0 中,一共有兩類令牌:
- 訪問令牌(Access Token)
- 刷新令牌(Refresh Token)
在訪問令牌過期時,我們可以使用刷新令牌向授權服務器獲取一個新的訪問令牌。
可能會胖友有疑惑,為什么會有刷新令牌呢?每次請求資源服務器時,都會在請求上帶上訪問令牌,這樣它的泄露風險是相對高的。
因此,出於安全性的考慮,訪問令牌的過期時間比較短,刷新令牌的過期時間比較長。這樣,如果訪問令牌即使被盜用走,那么在一定的時間后,訪問令牌也能在較短的時間吼過期。當然,安全也是相對的,如果使用刷新令牌后,獲取到新的訪問令牌,訪問令牌后續又可能被盜用。
艿艿整理了下,大家常用開放平台的令牌過期時間,讓大家更好的理解:
開放平台 | Access Token 有效期 | Refresh Token 有效期 |
---|---|---|
微信開放平台 | 2 小時 | 未知 |
騰訊開放平台 | 90 天 | 未知 |
小米開放平台 | 90 天 | 10 年 |
7.1 示例項目
下面,復制出 lab-68-demo03-authorization-server-with-client-credentials
項目,搭建提供訪問令牌的授權服務器。改動點如下圖所示:
@Configuration @EnableAuthorizationServer public class OAuth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { /** * 用戶認證 Manager */ @Autowired private AuthenticationManager authenticationManager; @Autowired private UserDetailsService userDetailsService; //配置使用的 AuthenticationManager 實現用戶認證的功能 @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception{ endpoints.authenticationManager(authenticationManager) .userDetailsService(userDetailsService); } // @Override // public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { // endpoints.authenticationManager(authenticationManager); // } //設置 /oauth/check_token 端點,通過認證后可訪問。 //這里的認證,指的是使用 client-id + client-secret 進行的客戶端認證,不要和用戶認證混淆。 //其中,/oauth/check_token 端點對應 CheckTokenEndpoint 類,用於校驗訪問令牌的有效性。 //在客戶端訪問資源服務器時,會在請求中帶上訪問令牌。 //在資源服務器收到客戶端的請求時,會使用請求中的訪問令牌,找授權服務器確認該訪問令牌的有效性。 @Override public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception { oauthServer.checkTokenAccess("isAuthenticated()"); } //進行 Client 客戶端的配置。 //設置使用基於內存的 Client 存儲器。實際情況下,最好放入數據庫中,方便管理。 /* * * 創建一個 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,也不能和授權服務器發生有效的交互。 */ @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() // <4.1> .withClient("clientapp").secret("112233") // <4.2> Client 賬號、密碼。 .authorizedGrantTypes("password","refresh_token") // .scopes("read_userinfo", "read_contacts") // <4.2> 可授權的 Scope .accessTokenValiditySeconds(3000) .refreshTokenValiditySeconds(864000) // .and().withClient() // <4.3> 可以繼續配置新的 Client ; } }
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override @Bean(name = BeanIds.AUTHENTICATION_MANAGER) public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override @Bean(name = BeanIds.USER_DETAILS_SERVICE) public UserDetailsService userDetailsServiceBean() throws Exception { return super.userDetailsServiceBean(); } @Bean public static NoOpPasswordEncoder passwordEncoder() { return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth. // 使用內存中的 InMemoryUserDetailsManager inMemoryAuthentication() // 不使用 PasswordEncoder 密碼編碼器 .passwordEncoder(passwordEncoder()) // 配置 yunai 用戶 .withUser("yunai").password("1024").roles("USER"); } }
① 在 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. 刪除令牌
在用戶登出系統時,我們會有刪除令牌的需求。雖然說,可以通過客戶端本地刪除令牌的方式實現。但是,考慮到真正的徹底的實現刪除令牌,必然服務端自身需要刪除令牌。
友情提示:客戶端本地刪除令牌的方式實現,指的是清楚本地 Cookie、localStorage 的令牌緩存。
在 Spring Security OAuth2 中,並沒有提供內置的接口,所以需要自己去實現。筆者參看 《Spring Security OAuth2 – Simple Token Revocation》 文檔,實現刪除令牌的 API 接口。
具體的實現,通過調用 ConsumerTokenServices 的 #revokeToken(String tokenValue)
方法,刪除訪問令牌和刷新令牌。如下圖所示:
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 { @Autowired private 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 團隊正在實現新的 Spring Authorization Server 授權服務器,目前還處於 Experimental 實驗狀態。
實際項目中,根據艿艿了解到的情況,很少項目會直接采用 Spring Security OAuth 框架,而是自己參考它進行 OAuth2.0 的實現。並且,一般只會實現密碼授權模式。
在本文中,我們采用基於內存的 InMemoryTokenStore,實現訪問令牌和刷新令牌的存儲。它會存在兩個明顯的缺點:
- 重啟授權服務器時,令牌信息會丟失,導致用戶需要重新授權。
- 多個授權服務器時,令牌信息無法共享,導致用戶一會授權成功,一會授權失敗。
因此,下一篇《芋道 Spring Security OAuth2 存儲器》文章,我們來學習 Spring Security OAuth 提供的基於數據庫和 Redis的存儲器。走起~