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的存儲器。走起~
