OAuth2 集成 - 構造認證授權服務器
1. 前言
上一節中,我們使用了 Spring Security 提供的社交化組件,實現了利用第三方認證平臺完成用戶身份識別的過程。
雖然使用第三方平臺作為認證中心十分的方便,但是如果我們的系統是在內部環境下使用,或者我們的用戶沒有注冊過 Github、微信這類平臺,又或者我們希望自己的平臺為其他應用提供認證服務時,就需要考慮創建自己的認證中心了。
本節將重點討論如何創建自己的 OAuth2 認證中心。
本小節實例開發環境:
本小節所使用的實例代碼是基于 Spring 官網中提供的最小化 HelloWorld 模板創建,請點此下載完整的 HelloWorld 模板壓縮包。
2. OAuth2 授權原理介紹
OAuth 的全稱為 Open Authorization 即「開放授權」。它被設計成為一個通用安全協議,用于實現桌面應用(包括手機應用)及 B / S 應用的統一 API 鑒權服務。它通過頒發令牌的方式,允許第三方網站在特定時間、操作范圍內訪問資源而避免了重新輸入密碼。
OAuth 授權有三個主要特點:
- 簡單:易于理解和實現;
- 安全:過程中不暴露敏感信息(如:用戶名、密碼等);
- 開放:誰都可以用。
OAuth 標準一共出現了兩代,1.0 在 2007 年底提出,只適用于瀏覽器 B / S 應用,后來在 2011 年,OAuth 發布了協議 2.0,并開始支持終端應用的認證。現在 OAuth 2.0 協議基本完全替代了 OAuth 1.0 協議。
OAuth 2.0 有四種授權方式,也就是四種獲得令牌的方式,分別是:授權碼式、隱蔽式、密碼式、客戶端憑證式。
2.1 授權碼式
這種方式下,APP(或網站)首先申請授權碼并保存在客戶端(或瀏覽器)中,再用授權碼去換取令牌,并將令牌保存在服務器上。這樣就實現了即認證客戶端,又認證了服務器。
2.2 隱蔽式
有時會遇到應用只有前端,沒有后端,上述方式就無法實現了,此時我們需要將令牌保存在前端,于是出現了第二種方式:隱蔽式。
這種方式下應用客戶端直接向認證服務器請求令牌。
注意:這種方式下,令牌被存儲在客戶端,容易被攻擊者攔截,所以用完后應及時銷毀。
2.3 密碼式
加入客戶端應用實可信的,既用戶允許客戶端知道自己的用戶名密碼,此時就可以使用密碼式換取令牌。
這種方式下,由客戶端認證用戶,并攜帶用戶的認證信息一并發送到認證服務器換取令牌。
2.4 客戶端憑證式
有的應用并沒有明確的前端應用,比如控制臺程序或者是服務接口,這種情況下就需要用到客戶端憑證式獲得憑證了。
這種方式下,沒有「人」的參與,只有認證服務對后臺服務的認證。
3. 過程實現
在前面章節,我們討論了如何快速建立一個 Spring Security 的認證服務器,此處我們將在前述實例上擴展 OAuth2.0 認證支持。
3.1 創建 Spring Boot web 服務端應用
工程目錄結構如下:
? OAuth2AuthorizationServer/
? src/
? main/
? java/imooc/springsecurity/oauth2/server/
? config/
OAuth2ServerConfiguration.java # OAuth2 相關配置類
UserConfiguration.java # 基礎認證配置類,用于配置用戶信息
OAuth2AuthorizationServerApplication.java # 程序入口
? resources/
application.properties # 配置文件,本例中無特殊配置
? test/java/
pom.xml
在 pom.xml 文件中增加依賴項,相比「用戶名密碼認證實例」,此處注意添加了 OAuth2 自動配置的相關依賴。spring-security-oauth2-autoconfigure
。完整 pom.xml 文件如下:
<?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.3.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>imooc.springsecurity</groupId>
<artifactId>OAuth2AuthorizationServerSample</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.3.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
創建 SpringSecurity OAuth2 配置類: OAuth2ServerConfiguration.java。
src/
main/
java/
imooc/
springsecurity/
oauth2/
server/
OAuth2ServerConfiguration.java
- 使其繼承
org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter
類,并其增加@EnableAuthorizationServer
標簽,以聲明此類作為 OAuth2 認證服務器的配置依據; - 在
configure(AuthorizationServerEndpointsConfigurer endpoints)
方法中配置其 TokenStore,為了便于演示,此例中 TokenStore 采用內存形式,賬戶信息寫死在代碼中; - 在
configure(ClientDetailsServiceConfigurer clients)
方法中為 OAuth2 認證服務器設置可用于認證的客戶端信息。
完整代碼如下:
package imooc.springsecurity.oauth2.server.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;
@EnableAuthorizationServer
@Configuration
public class OAuth2ServerConfiguration extends AuthorizationServerConfigurerAdapter {
private AuthenticationManager authenticationManager;
public OAuth2ServerConfiguration(
AuthenticationConfiguration authenticationConfiguration) throws Exception {
this.authenticationManager = authenticationConfiguration.getAuthenticationManager();
}
@Override
public void configure(ClientDetailsServiceConfigurer clients)
throws Exception {
// 配置授信客戶端信息
clients.inMemory() // 內存模式
.withClient("reader") // 第一個客戶端用戶,其名稱為「reader」
.authorizedGrantTypes("password") // 授權模式為「password」
.secret("{noop}secret") // 認證密碼為「secret」,加密方式為「NoOp」
.scopes("message:read") // 權限的使用范圍
.accessTokenValiditySeconds(600_000_000) // 票據有效期
.and() // 增加第二個授權客戶端,設置方法一致,但擁有不同的范圍權限
.withClient("writer")
.authorizedGrantTypes("password")
.secret("{noop}secret")
.scopes("message:write")
.accessTokenValiditySeconds(600_000_000);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.authenticationManager(this.authenticationManager)
.tokenStore(tokenStore()); // 使用虛機內存存儲票據信息,也可替換成 Mysql、Redis 等。
}
@Bean
public TokenStore tokenStore() {
return new InMemoryTokenStore();
}
}
除了設置授權客戶端之外,還要增加客戶端中被授權的用戶。
創建類 UserConfiguration.java
src/
main/
java/
imooc/
springsecurity/
oauth2/
server/
UserConfiguration.java
并在其中配置用戶信息。
package imooc.springsecurity.oauth2.server.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
@Configuration
public class UserConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated() // 任何地址都受到保護,需要首先認證
.and()
.httpBasic() // 支持基本認證,因為 OAuth2 認證往往用于不同種類客戶端,所以基本認證支持是必要的。
.and()
.csrf().disable();
}
@Bean
@Override
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
inMemoryUserDetailsManager.createUser(User.withUsername("admin").password("$2a$10$sR.KWdKOWYseh0KVHHnzMOveh/S7wvOkd.JrTyP2AzHhEcCSZfAmK").roles("USER").build()); // 用戶名: admin; 密碼: 123456
return inMemoryUserDetailsManager;
}
}
3.2 運行及測試
我們用 curl 工具測試 OAuth2.0 認證服務器。
在 OAuth2.0 框架中,實現 Password 認證需要提供四個參數:
- 客戶端標識:clientID;
- 客戶端認證密碼:clientSecret;
- 授權類型:grant_type,該值固定為「password」;
- 認證用戶的用戶名:username;
- 認證用戶的密碼:password。
完整的請求表達式為:
curl [clientID]:[clientSecret]@ip:port/oauth/token -d grant_type=password -d username=[username] -d password=[password]
在本實例中,測試指令可定義為:
curl reader:secret@localhost:8080/oauth/token -d grant_type=password -d username=admin -d password=123456
如果認證成功,服務端將返回以下內容:
{
"access_token": "OOwNfgjvJKHItYnk4buWC8BMGtU=",
"token_type": "bearer",
"expires_in": 599995027,
"scope": "message:read"
}
其中,access_token 值在 OAuth2 體系中作為統一票據,用于各個資源服務的認證。
至此,OAuth2 認證服務器的 Password 模式授權模式就已完成。Spring Security 對 OAuth2.0 的其他幾種授權模式已有成熟支持,在使用時需要配置對應的客戶端授權模式權限。
3.3 使用數據庫作為認證源
如果使用數據庫(例如:mysql)作為數據源時,需要創建 JdbcClientDetailsService
對象,并配置到 ClientDetailsServiceConfigurer
之中。具體代碼為
@Bean
public ClientDetailsService clientDetailsService() {
// 新增部分,用于從數據庫獲取客戶端信息
return new JdbcClientDetailsService(dataSource);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients)
throws Exception {
// 此處去掉內存配置項,改為 jdbc 數據源
clients.withClientDetails(clientDetailsService);
}
除此之外,還需要在書庫中插入相關數據表,表結構定義如下,也可以從 spring 項目主頁 中獲取。
-- used in tests that use HSQL
create table oauth_client_details (
client_id VARCHAR(256) PRIMARY KEY,
resource_ids VARCHAR(256),
client_secret VARCHAR(256),
scope VARCHAR(256),
authorized_grant_types VARCHAR(256),
web_server_redirect_uri VARCHAR(256),
authorities VARCHAR(256),
access_token_validity INTEGER,
refresh_token_validity INTEGER,
additional_information VARCHAR(4096),
autoapprove VARCHAR(256)
);
create table oauth_client_token (
token_id VARCHAR(256),
token LONGVARBINARY,
authentication_id VARCHAR(256) PRIMARY KEY,
user_name VARCHAR(256),
client_id VARCHAR(256)
);
create table oauth_access_token (
token_id VARCHAR(256),
token LONGVARBINARY,
authentication_id VARCHAR(256) PRIMARY KEY,
user_name VARCHAR(256),
client_id VARCHAR(256),
authentication LONGVARBINARY,
refresh_token VARCHAR(256)
);
create table oauth_refresh_token (
token_id VARCHAR(256),
token LONGVARBINARY,
authentication LONGVARBINARY
);
create table oauth_code (
code VARCHAR(256), authentication LONGVARBINARY
);
create table oauth_approvals (
userId VARCHAR(256),
clientId VARCHAR(256),
scope VARCHAR(256),
status VARCHAR(10),
expiresAt TIMESTAMP,
lastModifiedAt TIMESTAMP
);
-- customized oauth_client_details table
create table ClientDetails (
appId VARCHAR(256) PRIMARY KEY,
resourceIds VARCHAR(256),
appSecret VARCHAR(256),
scope VARCHAR(256),
grantTypes VARCHAR(256),
redirectUrl VARCHAR(256),
authorities VARCHAR(256),
access_token_validity INTEGER,
refresh_token_validity INTEGER,
additionalInformation VARCHAR(4096),
autoApproveScopes VARCHAR(256)
);
4. 小結
本節我們討論了如何構建自己的 OAuth2.0 認證中心,主要知識點有:
- Spring Security 對 OAuth2.0 認證標準已經有成熟的支持,僅需幾個注解就可以構造出 OAuth2.0 認證服務器;
- Spring Security 對 OAuth2.0 認證同樣提供了多種認證支持,比如內存認證、數據庫認證及 Redis 認證等,可以根據需要進行配置或調整。
下節我們繼續 OAuth2.0 話題,討論如何在 OAuth2.0 協議下,如何保護私密資源被合理訪問。