OAuth是一種用來規范令牌(Token)發放的授權機制,主要包含了四種授權模式:授權碼模式、簡化模式、密碼模式和客戶端模式。Spring Security OAuth2對這四種授權模式進行了實現。這節主要記錄下什么是OAuth2以及Spring Security OAuth2的基本使用。
四種授權模式
在了解這四種授權模式之前,我們需要先學習一些和OAuth相關的名詞。舉個社交登錄的例子吧,比如在瀏覽器上使用QQ賬號登錄虎牙直播,這個過程可以提取出以下幾個名詞:
-
Third-party application 第三方應用程序,比如這里的虎牙直播;
-
HTTP service HTTP服務提供商,比如這里的QQ(騰訊);
-
Resource Owner 資源所有者,就是QQ的所有人,你;
-
User Agent 用戶代理,這里指瀏覽器;
-
Authorization server 認證服務器,這里指QQ提供的第三方登錄服務;
-
Resource server 資源服務器,這里指虎牙直播提供的服務,比如高清直播,彈幕發送等(需要認證后才能使用)。
認證服務器和資源服務器可以在同一台服務器上,比如前后端分離的服務后台,它即供認證服務(認證服務器,提供令牌),客戶端通過令牌來從后台獲取服務(資源服務器);它們也可以不在同一台服務器上,比如上面第三方登錄的例子。
大致了解了這幾個名詞后,我們開始了解四種授權模式。
授權碼模式
授權碼模式是最能體現OAuth2協議,最嚴格,流程最完整的授權模式,流程如下所示:
A. 客戶端將用戶導向認證服務器;
B. 用戶決定是否給客戶端授權;
C. 同意授權后,認證服務器將用戶導向客戶端提供的URL,並附上授權碼;
D. 客戶端通過重定向URL和授權碼到認證服務器換取令牌;
E. 校驗無誤后發放令牌。
其中A步驟,客戶端申請認證的URI,包含以下參數:
-
response_type:表示授權類型,必選項,此處的值固定為”code”,標識授權碼模式
-
client_id:表示客戶端的ID,必選項
-
redirect_uri:表示重定向URI,可選項
-
scope:表示申請的權限范圍,可選項
-
state:表示客戶端的當前狀態,可以指定任意值,認證服務器會原封不動地返回這個值。
D步驟中,客戶端向認證服務器申請令牌的HTTP請求,包含以下參數:
-
grant_type:表示使用的授權模式,必選項,此處的值固定為”authorization_code”。
-
code:表示上一步獲得的授權碼,必選項。
-
redirect_uri:表示重定向URI,必選項,且必須與A步驟中的該參數值保持一致。
-
client_id:表示客戶端ID,必選項。
密碼模式
在密碼模式中,用戶像客戶端提供用戶名和密碼,客戶端通過用戶名和密碼到認證服務器獲取令牌。流程如下所示:
A. 用戶向客戶端提供用戶名和密碼;
B. 客戶端向認證服務器換取令牌;
C. 發放令牌。
B步驟中,客戶端發出的HTTP請求,包含以下參數:
-
grant_type:表示授權類型,此處的值固定為”password”,必選項。
-
username:表示用戶名,必選項。
-
password:表示用戶的密碼,必選項。
-
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