原地址:http://www.iocoder.cn/Spring-Security/OAuth2-learning/
1.1 OAuth2.0 是什么?
OAuth(Open Authorization)是一個開放標准,允許用戶讓第三方應用訪問該用戶在某一網站上存儲的私密的資源(如照片,視頻,聯系人列表),而無需將用戶名和密碼提供給第三方應用。
OAuth 允許用戶提供一個令牌,而不是用戶名和密碼來訪問他們存放在特定服務提供者的數據。
每一個令牌授權一個特定的網站(例如,視頻編輯網站)在特定的時段(例如,接下來的 2 小時內)內訪問特定的資源(例如僅僅是某一相冊中的視頻)。這樣,OAuth 讓用戶可以授權第三方網站訪問他們存儲在另外服務提供者的某些特定信息,而非所有內容。
OAuth2.0 是用於授權的行業標准協議。OAuth2.0 為簡化客戶端開發提供了特定的授權流,包括 Web 應用、桌面應用、移動端應用等。
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 的授權碼模式的運行流程:

(A)用戶打開客戶端以后,客戶端要求用戶給予授權。 (B)用戶同意給予客戶端授權。 (C)客戶端使用上一步獲得的授權,向認證服務器申請令牌。 (D)認證服務器對客戶端進行認證以后,確認無誤,同意發放令牌。 (E)客戶端使用令牌,向資源服務器申請獲取資源。 (F)資源服務器確認令牌無誤,同意向客戶端開放資源。
上述的六個步驟,B 是關鍵,即用戶如何給客戶端進行授權。有了授權之,客戶端就可以獲取令牌,進而憑令牌獲取資源。
這個時候的資源,資源主要指的是三方開放平台的用戶資料等等。
1.4 OAuth 2.0 授權模式
客戶端必須得到用戶的授權(Authorization Grant),才能獲得訪問令牌(Access Token)。
OAuth2.0 定義了四種授權方式:
- 授權碼模式(Authorization Code)
- 密碼模式(Resource Owner Password Credentials)
- 簡化模式(Implicit)
- 客戶端模式(Client Credentials)
其中,密碼模式和授權碼模式比較常用。

當然,對於黃框部分,對於筆者還是比較困惑的。筆者認為,第三方的單頁應用 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. 密碼模式
示例代碼對應倉庫:
本小節,我們來學習密碼模式(Resource Owner Password Credentials Grant)。
密碼模式,用戶向客戶端提供自己的用戶名和密碼。客戶端使用這些信息,向授權服務器索要授權。
在這種模式中,用戶必須把自己的密碼給客戶端,但是客戶端不得儲存密碼。這通常用在用戶對客戶端高度信任的情況下,比如客戶端是操作系統的一部分,或者由一個著名公司出品。而授權服務器只有在其他授權模式無法執行的情況下,才能考慮使用這種模式。

- (A)用戶向客戶端提供用戶名和密碼。
- (B)客戶端將用戶名和密碼發給授權服務器,向后者請求令牌。
- (C)授權服務器確認無誤后,向客戶端提供訪問令牌。
下面,我們來新建兩個項目,搭建一個密碼模式的使用示例。如下圖所示:(使用Spring_Book創建兩個模塊)


2.1 搭建授權服務器
創建 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 依賴。如下圖所示:

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();
}
@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");
}
}
我們通過 Spring Security 提供認證功能,所以這里需要配置一個用戶。
友情提示:看不懂這個配置的胖友,后續可回《芋道 Spring Boot 安全框架 Spring Security 入門》重造下。
2.1.3 OAuth2AuthorizationServerConfig
創建 OAuth2AuthorizationServerConfig 配置類,進行授權服務器。代碼如下:
@Configuration
@EnableAuthorizationServer
public class OAuth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
/**
* 用戶認證 Manager
*/
@Autowired
private AuthenticationManager authenticationManager;
//配置使用的 AuthenticationManager 實現用戶認證的功能
@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") // <4.2> 密碼模式
.scopes("read_userinfo", "read_contacts") // <4.2> 可授權的 Scope
// .and().withClient() // <4.3> 可以繼續配置新的 Client
;
}
}


2.1.4 AuthorizationServerApplication
創建 AuthorizationServerApplication 類,授權服務器的啟動類。代碼如下:
//@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) 不調用數據庫啟動
@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 地址,使用密碼模式進行授權。如下圖所示:


請求說明:
- 通過 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 地址,校驗訪問令牌的有效性。如下圖所示:


請求和響應比較簡單,胖友自己瞅瞅即可。
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: 9090
security:
oauth2:
# OAuth2 Client 配置,對應 OAuth2ClientProperties 類
client:
client-id: clientapp
client-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 {
@Override
public 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 {
@Autowired
private 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 接口,使用用戶的用戶名與密碼進行登錄,獲得訪問令牌。如下圖所示:

響應結果和授權服務器的 /oauth/token 接口是一致的,因為就是調用它,嘿嘿~
② 然后,請求 <127.0.0.1:9090/api/example/hello> 接口,帶剛剛的訪問令牌,則請求會被通過。如下圖所示:

