Spring Security OAuth2入門


OAuth是一種用來規范令牌(Token)發放的授權機制,主要包含了四種授權模式:授權碼模式、簡化模式、密碼模式和客戶端模式。Spring Security OAuth2對這四種授權模式進行了實現。這節主要記錄下什么是OAuth2以及Spring Security OAuth2的基本使用。

四種授權模式

在了解這四種授權模式之前,我們需要先學習一些和OAuth相關的名詞。舉個社交登錄的例子吧,比如在瀏覽器上使用QQ賬號登錄虎牙直播,這個過程可以提取出以下幾個名詞:

  1. Third-party application 第三方應用程序,比如這里的虎牙直播;

  2. HTTP service HTTP服務提供商,比如這里的QQ(騰訊);

  3. Resource Owner 資源所有者,就是QQ的所有人,你;

  4. User Agent 用戶代理,這里指瀏覽器;

  5. Authorization server 認證服務器,這里指QQ提供的第三方登錄服務;

  6. Resource server 資源服務器,這里指虎牙直播提供的服務,比如高清直播,彈幕發送等(需要認證后才能使用)。

認證服務器和資源服務器可以在同一台服務器上,比如前后端分離的服務后台,它即供認證服務(認證服務器,提供令牌),客戶端通過令牌來從后台獲取服務(資源服務器);它們也可以不在同一台服務器上,比如上面第三方登錄的例子。

大致了解了這幾個名詞后,我們開始了解四種授權模式。

授權碼模式

授權碼模式是最能體現OAuth2協議,最嚴格,流程最完整的授權模式,流程如下所示:

A. 客戶端將用戶導向認證服務器;

B. 用戶決定是否給客戶端授權;

C. 同意授權后,認證服務器將用戶導向客戶端提供的URL,並附上授權碼;

D. 客戶端通過重定向URL和授權碼到認證服務器換取令牌;

E. 校驗無誤后發放令牌。

其中A步驟,客戶端申請認證的URI,包含以下參數:

  1. response_type:表示授權類型,必選項,此處的值固定為”code”,標識授權碼模式

  2. client_id:表示客戶端的ID,必選項

  3. redirect_uri:表示重定向URI,可選項

  4. scope:表示申請的權限范圍,可選項

  5. state:表示客戶端的當前狀態,可以指定任意值,認證服務器會原封不動地返回這個值。

D步驟中,客戶端向認證服務器申請令牌的HTTP請求,包含以下參數:

  1. grant_type:表示使用的授權模式,必選項,此處的值固定為”authorization_code”。

  2. code:表示上一步獲得的授權碼,必選項。

  3. redirect_uri:表示重定向URI,必選項,且必須與A步驟中的該參數值保持一致。

  4. client_id:表示客戶端ID,必選項。

密碼模式

在密碼模式中,用戶像客戶端提供用戶名和密碼,客戶端通過用戶名和密碼到認證服務器獲取令牌。流程如下所示:

A. 用戶向客戶端提供用戶名和密碼;

B. 客戶端向認證服務器換取令牌;

C. 發放令牌。

B步驟中,客戶端發出的HTTP請求,包含以下參數:

  1. grant_type:表示授權類型,此處的值固定為”password”,必選項。

  2. username:表示用戶名,必選項。

  3. password:表示用戶的密碼,必選項。

  4. scope:表示權限范圍,可選項。

剩下兩種授權模式可以參考下面的參考鏈接,這里就不介紹了。

Spring Security OAuth2

Spring框架對OAuth2協議進行了實現,下面學習下上面兩種模式在Spring Security OAuth2相關框架的使用。

Spring Security OAuth2主要包含認證服務器和資源服務器這兩大塊的實現:

認證服務器主要包含了四種授權模式的實現和Token的生成與存儲,我們也可以在認證服務器中自定義獲取Token的方式(后面會介紹到);資源服務器主要是在Spring Security的過濾器鏈上加了OAuth2AuthenticationProcessingFilter過濾器,即使用OAuth2協議發放令牌認證的方式來保護我們的資源。

配置認證服務器

新建一個Spring Boot項目,版本為2.1.6.RELEASE,並引入相關依賴,pom如下所示:

<?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">
  <modelVersion>4.0.0</modelVersion>
  <parent>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-parent</artifactId>
      <version>2.1.6.RELEASE</version>
      <relativePath/> <!-- lookup parent from repository -->
  </parent>
  <groupId>cc.mrbird</groupId>
  <artifactId>security</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>security</name>
  <description>Demo project for Spring Boot</description>

  <properties>
      <java.version>1.8</java.version>
      <spring-cloud.version>Greenwich.SR1</spring-cloud.version>
  </properties>

  <dependencies>
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-web</artifactId>
      </dependency>
      <dependency>
          <groupId>org.springframework.cloud</groupId>
          <artifactId>spring-cloud-starter</artifactId>
      </dependency>
      <dependency>
          <groupId>org.springframework.cloud</groupId>
          <artifactId>spring-cloud-starter-oauth2</artifactId>
      </dependency>
      <dependency>
          <groupId>org.springframework.cloud</groupId>
          <artifactId>spring-cloud-starter-security</artifactId>
      </dependency>
      <dependency>
          <groupId>org.apache.commons</groupId>
          <artifactId>commons-lang3</artifactId>
      </dependency>
  </dependencies>

  <dependencyManagement>
      <dependencies>
          <dependency>
              <groupId>org.springframework.cloud</groupId>
              <artifactId>spring-cloud-dependencies</artifactId>
              <version>${spring-cloud.version}</version>
              <type>pom</type>
              <scope>import</scope>
          </dependency>
      </dependencies>
  </dependencyManagement>

  <build>
      <plugins>
          <plugin>
              <groupId>org.springframework.boot</groupId>
              <artifactId>spring-boot-maven-plugin</artifactId>
          </plugin>
      </plugins>
  </build>
</project>

 

在創建認證服務器前,我們先定義一個MyUser對象:

public class MyUser implements Serializable {
  private static final long serialVersionUID = 3497935890426858541L;

  private String userName;
  private String password;
  private boolean accountNonExpired = true;
  private boolean accountNonLocked= true;
  private boolean credentialsNonExpired= true;
  private boolean enabled= true;
  // get set 略
}

 

接着定義UserDetailService實現org.springframework.security.core.userdetails.UserDetailsService接口:

@Service
public class UserDetailService implements UserDetailsService {
  @Autowired
  private PasswordEncoder passwordEncoder;

  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
      MyUser user = new MyUser();
      user.setUserName(username);
      user.setPassword(this.passwordEncoder.encode("123456"));
      return new User(username, user.getPassword(), user.isEnabled(),
              user.isAccountNonExpired(), user.isCredentialsNonExpired(),
              user.isAccountNonLocked(), AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
  }
}

 

這里的邏輯是用什么賬號登錄都可以,但是密碼必須為123456,並且擁有”admin”權限(這些都在前面的Security教程里說過了,就不再詳細說明了)。

接下來開始創建一個認證服務器,並且在里面定義UserDetailService需要用到的PasswordEncoder

創建認證服務器很簡單,只需要在Spring Security的配置類上使用@EnableAuthorizationServer注解標注即可。創建AuthorizationServerConfig,代碼如下所示:

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends WebSecurityConfigurerAdapter {

  @Bean
  public PasswordEncoder passwordEncoder() {
      return new BCryptPasswordEncoder();
  }
}

 

這時候啟動項目,會發現控制台打印出了隨機分配的client-id和client-secret:

為了方便后面的測試,我們可以手動指定這兩個值。在Spring Boot配置文件application.yml中添加如下配置:

security:
oauth2:
  client:
    client-id: test
    client-secret: test1234

 

重啟項目,發現控制台輸出:

說明替換成功。

授權碼模式獲取令牌

接下來開始往認證服務器請求授權碼。打開瀏覽器,訪問http://localhost:8080/oauth/authorize?response_type=code&client_id=test&redirect_uri=http://mrbird.cc&scope=all&state=hello

URL中的幾個參數在上面的授權碼模式的A步驟里都有詳細說明。這里response_type必須為code,表示授權碼模式,client_id就是剛剛在配置文件中手動指定的test,redirect_uri這里隨便指定一個地址即可,主要是用來重定向獲取授權碼的,scope指定為all,表示所有權限。

訪問這個鏈接后,頁面如下所示:

需要登錄認證,根據我們前面定義的UserDetailService邏輯,這里用戶名隨便輸,密碼為123456即可。輸入后,頁面跳轉如下所示:

原因是上面指定的redirect_uri必須同時在配置文件中指定,我們往application.yml添加配置:

security:
oauth2:
  client:
    client-id: test
    client-secret: test1234
    registered-redirect-uri: http://mrbird.cc

 

重啟項目,重新執行上面的步驟,登錄成功后頁面成功跳轉到了授權頁面:

選擇同意Approve,然后點擊Authorize按鈕后,頁面跳轉到了我們指定的redirect_uri,並且帶上了授權碼信息:

到這里我們就可以用這個授權碼從認證服務器獲取令牌Token了。

使用postman發送如下請求POST請求localhost:8080/oauth/token

這里要填的參數和上面介紹的授權碼模式D步驟介紹的一致。grant_type固定填authorization_code,code為上一步獲取到的授權碼,client_id和redirect_uri必須和我們上面定義的一致。

除了這幾個參數外,我們還需要在請求頭中填寫:

key為Authorization,value為Basic加上client_id:client_secret經過base64加密后的值(可以使用http://tool.chinaz.com/Tools/Base64.aspx):

參數填寫無誤后,點擊發送便可以獲取到令牌Token:

{
  "access_token": "950018df-0199-4936-aa80-a3a66183f634",
  "token_type": "bearer",
  "refresh_token": "cc22e8b2-e069-459d-8c24-cfda0bc72128",
  "expires_in": 42827,
  "scope": "all"
}

 

一個授權碼只能換一次令牌,如果再次點擊postman的發送按鈕,將返回:

{
  "error": "invalid_grant",
  "error_description": "Invalid authorization code: xw8x55"
}

 

密碼模式獲取令牌

和授權碼模式相比,使用密碼模式獲取令牌就顯得簡單多了。同樣使用postman發送POST請求localhost:8080/oauth/token

grant_type填password,表示密碼模式;然后填寫用戶名和密碼,頭部也需要填寫Authorization信息,內容和授權碼模式介紹的一致,這里就不截圖了。

點擊發送,也可以獲得令牌:

{
  "access_token": "d612cf50-6499-4a0c-9cd4-9c756839aa12",
  "token_type": "bearer",
  "refresh_token": "fdc6c77f-b910-46dc-a349-835dc0587919",
  "expires_in": 43090,
  "scope": "all"
}

 

配置資源服務器

為什么需要資源服務器呢?我們先來看下在沒有定義資源服務器的時候,使用Token去獲取資源時會發生什么。

定義一個REST接口:

@RestController
public class UserController {

  @GetMapping("index")
  public Object index(Authentication authentication){
      return authentication;
  }
}

 

啟動項目,為了方便我們使用密碼模式獲取令牌,然后使用該令牌獲取/index這個資源:

Authorization值為token_type access_token,發送請求后,返回:

{
  "timestamp": "2019-03-24T13:13:43.818+0000",
  "status": 401,
  "error": "Unauthorized",
  "message": "Unauthorized",
  "path": "/index"
}

 

雖然令牌是正確的,但是並無法訪問/index,所以我們必須配置資源服務器,讓客戶端可以通過合法的令牌來獲取資源。

資源服務器的配置也很簡單,只需要在配置類上使用@EnableResourceServer注解標注即可:

@Configuration
@EnableResourceServer
public class ResourceServerConfig {

}

 

重啟服務,重復上面的步驟,再次訪問/index便可以成功獲取到信息:

{
  "authorities": [
      {
          "authority": "admin"
      }
  ],
  "details": {
      "remoteAddress": "0:0:0:0:0:0:0:1",
      "sessionId": null,
      "tokenValue": "621f59ba-3161-4c9b-aff8-a8335ce6e3cc",
      "tokenType": "bearer",
      "decodedDetails": null
  },
  "authenticated": true,
  "userAuthentication": {
      "authorities": [
          {
              "authority": "admin"
          }
      ],
      "details": {
          "grant_type": "password",
          "username": "mrbird",
          "scope": "all"
      },
      "authenticated": true,
      "principal": {
          "password": null,
          "username": "mrbird",
          "authorities": [
              {
                  "authority": "admin"
              }
          ],
          "accountNonExpired": true,
          "accountNonLocked": true,
          "credentialsNonExpired": true,
          "enabled": true
      },
      "credentials": null,
      "name": "mrbird"
  },
  "credentials": "",
  "oauth2Request": {
      "clientId": "test",
      "scope": [
          "all"
      ],
      "requestParameters": {
          "grant_type": "password",
          "username": "mrbird",
          "scope": "all"
      },
      "resourceIds": [],
      "authorities": [
          {
              "authority": "ROLE_USER"
          }
      ],
      "approved": true,
      "refresh": false,
      "redirectUri": null,
      "responseTypes": [],
      "extensions": {},
      "refreshTokenRequest": null,
      "grantType": "password"
  },
  "clientOnly": false,
  "principal": {
      "password": null,
      "username": "mrbird",
      "authorities": [
          {
              "authority": "admin"
          }
      ],
      "accountNonExpired": true,
      "accountNonLocked": true,
      "credentialsNonExpired": true,
      "enabled": true
  },
  "name": "mrbird"
}

 

在同時定義了認證服務器和資源服務器后,再去使用授權碼模式獲取令牌可能會遇到 Full authentication is required to access this resource 的問題,這時候只要確保認證服務器先於資源服務器配置即可,比如在認證服務器的配置類上使用@Order(1)標注,在資源服務器的配置類上使用@Order(2)標注。

源碼鏈接:https://github.com/wuyouzhuguli/SpringAll/tree/master/63.Spring-Security-OAuth2-Guide

參考鏈接

  1. https://tools.ietf.org/html/rfc6749#section-4.1

  2. http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM