可能是第二好的 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();
    }

    @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;

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authenticationManager(authenticationManager);
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
        oauthServer.checkTokenAccess("isAuthenticated()");
    }

    @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
                ;
    }

}

① 在類上添加 @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 類,用於校驗訪問令牌的有效性。

  • 在客戶端訪問資源服務器時,會在請求中帶上訪問令牌

  • 在資源服務器收到客戶端的請求時,會使用請求中的訪問令牌,找授權服務器確認該訪問令牌的有效性。

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: 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 接口,使用用戶用戶名密碼進行登錄,獲得訪問令牌。如下圖所示:

測試 /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 {

    @Autowired
    private 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> 設置 code
        restTemplate.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 {

    @Autowired
    private 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)

訪問令牌過期時,我們可以使用刷新令牌向授權服務器獲取一個的訪問令牌。

可能會胖友有疑惑,爲什麼會有刷新令牌呢?每次請求資源服務器時,都會在請求上帶上訪問令牌,這樣它的泄露風險是相對高的。

因此,出於安全性的考慮,訪問令牌的過期時間比較短,刷新令牌的過期時間比較長。這樣,如果訪問令牌即使被盜用走,那麼在一定的時間後,訪問令牌也能在較短的時間吼過期。當然,安全也是相對的,如果使用刷新令牌後,獲取到新的訪問令牌,訪問令牌後續可能被盜用。

艿艿整理了下,大家常用開放平臺的令牌過期時間,讓大家更好的理解:

開放平臺 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 {

    @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 Security OAuth 被廢棄

同時,Spring 團隊正在實現新的 Spring Authorization Server 授權服務器,目前還處於 Experimental 實驗狀態。

實際項目中,根據艿艿瞭解到的情況,很少項目會直接採用 Spring Security OAuth 框架,而是自己參考它進行 OAuth2.0 的實現。並且,一般只會實現密碼授權模式。


在本文中,我們採用基於內存的 InMemoryTokenStore,實現訪問令牌和刷新令牌的存儲。它會存在兩個明顯的缺點

  • 重啓授權服務器時,令牌信息會丟失,導致用戶需要重新授權。

  • 多個授權服務器時,令牌信息無法共享,導致用戶一會授權成功,一會授權失敗。

因此,下一篇《芋道 Spring Security OAuth2 存儲器》文章,我們來學習 Spring Security OAuth 提供的基於數據庫和 Redis的存儲器。走起~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章