OAuth2 集成 - 構(gòu)造認(rèn)證授權(quán)服務(wù)器
1. 前言
上一節(jié)中,我們使用了 Spring Security 提供的社交化組件,實(shí)現(xiàn)了利用第三方認(rèn)證平臺(tái)完成用戶身份識(shí)別的過程。
雖然使用第三方平臺(tái)作為認(rèn)證中心十分的方便,但是如果我們的系統(tǒng)是在內(nèi)部環(huán)境下使用,或者我們的用戶沒有注冊(cè)過 Github、微信這類平臺(tái),又或者我們希望自己的平臺(tái)為其他應(yīng)用提供認(rèn)證服務(wù)時(shí),就需要考慮創(chuàng)建自己的認(rèn)證中心了。
本節(jié)將重點(diǎn)討論如何創(chuàng)建自己的 OAuth2 認(rèn)證中心。
本小節(jié)實(shí)例開發(fā)環(huán)境:
本小節(jié)所使用的實(shí)例代碼是基于 Spring 官網(wǎng)中提供的最小化 HelloWorld 模板創(chuàng)建,請(qǐng)點(diǎn)此下載完整的 HelloWorld 模板壓縮包。
- 編譯環(huán)境:JDK 1.8,點(diǎn)此下載;
- 構(gòu)建工具:Maven 3.5.3,點(diǎn)此下載;
- 開發(fā)工具:VS Code,點(diǎn)此下載、控制臺(tái);
- 其他依賴性:
spring-security-oauth2-autoconfigure
2. OAuth2 授權(quán)原理介紹
OAuth 的全稱為 Open Authorization 即「開放授權(quán)」。它被設(shè)計(jì)成為一個(gè)通用安全協(xié)議,用于實(shí)現(xiàn)桌面應(yīng)用(包括手機(jī)應(yīng)用)及 B / S 應(yīng)用的統(tǒng)一 API 鑒權(quán)服務(wù)。它通過頒發(fā)令牌的方式,允許第三方網(wǎng)站在特定時(shí)間、操作范圍內(nèi)訪問資源而避免了重新輸入密碼。
OAuth 授權(quán)有三個(gè)主要特點(diǎn):
- 簡單:易于理解和實(shí)現(xiàn);
- 安全:過程中不暴露敏感信息(如:用戶名、密碼等);
- 開放:誰都可以用。
OAuth 標(biāo)準(zhǔn)一共出現(xiàn)了兩代,1.0 在 2007 年底提出,只適用于瀏覽器 B / S 應(yīng)用,后來在 2011 年,OAuth 發(fā)布了協(xié)議 2.0,并開始支持終端應(yīng)用的認(rèn)證?,F(xiàn)在 OAuth 2.0 協(xié)議基本完全替代了 OAuth 1.0 協(xié)議。
OAuth 2.0 有四種授權(quán)方式,也就是四種獲得令牌的方式,分別是:授權(quán)碼式、隱蔽式、密碼式、客戶端憑證式。
2.1 授權(quán)碼式
這種方式下,APP(或網(wǎng)站)首先申請(qǐng)授權(quán)碼并保存在客戶端(或?yàn)g覽器)中,再用授權(quán)碼去換取令牌,并將令牌保存在服務(wù)器上。這樣就實(shí)現(xiàn)了即認(rèn)證客戶端,又認(rèn)證了服務(wù)器。
2.2 隱蔽式
有時(shí)會(huì)遇到應(yīng)用只有前端,沒有后端,上述方式就無法實(shí)現(xiàn)了,此時(shí)我們需要將令牌保存在前端,于是出現(xiàn)了第二種方式:隱蔽式。
這種方式下應(yīng)用客戶端直接向認(rèn)證服務(wù)器請(qǐng)求令牌。
注意:這種方式下,令牌被存儲(chǔ)在客戶端,容易被攻擊者攔截,所以用完后應(yīng)及時(shí)銷毀。
2.3 密碼式
加入客戶端應(yīng)用實(shí)可信的,既用戶允許客戶端知道自己的用戶名密碼,此時(shí)就可以使用密碼式換取令牌。
這種方式下,由客戶端認(rèn)證用戶,并攜帶用戶的認(rèn)證信息一并發(fā)送到認(rèn)證服務(wù)器換取令牌。
2.4 客戶端憑證式
有的應(yīng)用并沒有明確的前端應(yīng)用,比如控制臺(tái)程序或者是服務(wù)接口,這種情況下就需要用到客戶端憑證式獲得憑證了。
這種方式下,沒有「人」的參與,只有認(rèn)證服務(wù)對(duì)后臺(tái)服務(wù)的認(rèn)證。
3. 過程實(shí)現(xiàn)
在前面章節(jié),我們討論了如何快速建立一個(gè) Spring Security 的認(rèn)證服務(wù)器,此處我們將在前述實(shí)例上擴(kuò)展 OAuth2.0 認(rèn)證支持。
3.1 創(chuàng)建 Spring Boot web 服務(wù)端應(yīng)用
工程目錄結(jié)構(gòu)如下:
? OAuth2AuthorizationServer/
? src/
? main/
? java/imooc/springsecurity/oauth2/server/
? config/
OAuth2ServerConfiguration.java # OAuth2 相關(guān)配置類
UserConfiguration.java # 基礎(chǔ)認(rèn)證配置類,用于配置用戶信息
OAuth2AuthorizationServerApplication.java # 程序入口
? resources/
application.properties # 配置文件,本例中無特殊配置
? test/java/
pom.xml
在 pom.xml 文件中增加依賴項(xiàng),相比「用戶名密碼認(rèn)證實(shí)例」,此處注意添加了 OAuth2 自動(dòng)配置的相關(guān)依賴。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>
創(chuàng)建 SpringSecurity OAuth2 配置類: OAuth2ServerConfiguration.java。
src/
main/
java/
imooc/
springsecurity/
oauth2/
server/
OAuth2ServerConfiguration.java
- 使其繼承
org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter
類,并其增加@EnableAuthorizationServer
標(biāo)簽,以聲明此類作為 OAuth2 認(rèn)證服務(wù)器的配置依據(jù); - 在
configure(AuthorizationServerEndpointsConfigurer endpoints)
方法中配置其 TokenStore,為了便于演示,此例中 TokenStore 采用內(nèi)存形式,賬戶信息寫死在代碼中; - 在
configure(ClientDetailsServiceConfigurer clients)
方法中為 OAuth2 認(rèn)證服務(wù)器設(shè)置可用于認(rèn)證的客戶端信息。
完整代碼如下:
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() // 內(nèi)存模式
.withClient("reader") // 第一個(gè)客戶端用戶,其名稱為「reader」
.authorizedGrantTypes("password") // 授權(quán)模式為「password」
.secret("{noop}secret") // 認(rèn)證密碼為「secret」,加密方式為「NoOp」
.scopes("message:read") // 權(quán)限的使用范圍
.accessTokenValiditySeconds(600_000_000) // 票據(jù)有效期
.and() // 增加第二個(gè)授權(quán)客戶端,設(shè)置方法一致,但擁有不同的范圍權(quán)限
.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()); // 使用虛機(jī)內(nèi)存存儲(chǔ)票據(jù)信息,也可替換成 Mysql、Redis 等。
}
@Bean
public TokenStore tokenStore() {
return new InMemoryTokenStore();
}
}
除了設(shè)置授權(quán)客戶端之外,還要增加客戶端中被授權(quán)的用戶。
創(chuàng)建類 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() // 任何地址都受到保護(hù),需要首先認(rèn)證
.and()
.httpBasic() // 支持基本認(rèn)證,因?yàn)?OAuth2 認(rèn)證往往用于不同種類客戶端,所以基本認(rèn)證支持是必要的。
.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 運(yùn)行及測(cè)試
我們用 curl 工具測(cè)試 OAuth2.0 認(rèn)證服務(wù)器。
在 OAuth2.0 框架中,實(shí)現(xiàn) Password 認(rèn)證需要提供四個(gè)參數(shù):
- 客戶端標(biāo)識(shí):clientID;
- 客戶端認(rèn)證密碼:clientSecret;
- 授權(quán)類型:grant_type,該值固定為「password」;
- 認(rèn)證用戶的用戶名:username;
- 認(rèn)證用戶的密碼:password。
完整的請(qǐng)求表達(dá)式為:
curl [clientID]:[clientSecret]@ip:port/oauth/token -d grant_type=password -d username=[username] -d password=[password]
在本實(shí)例中,測(cè)試指令可定義為:
curl reader:secret@localhost:8080/oauth/token -d grant_type=password -d username=admin -d password=123456
如果認(rèn)證成功,服務(wù)端將返回以下內(nèi)容:
{
"access_token": "OOwNfgjvJKHItYnk4buWC8BMGtU=",
"token_type": "bearer",
"expires_in": 599995027,
"scope": "message:read"
}
其中,access_token 值在 OAuth2 體系中作為統(tǒng)一票據(jù),用于各個(gè)資源服務(wù)的認(rèn)證。
至此,OAuth2 認(rèn)證服務(wù)器的 Password 模式授權(quán)模式就已完成。Spring Security 對(duì) OAuth2.0 的其他幾種授權(quán)模式已有成熟支持,在使用時(shí)需要配置對(duì)應(yīng)的客戶端授權(quán)模式權(quán)限。
3.3 使用數(shù)據(jù)庫作為認(rèn)證源
如果使用數(shù)據(jù)庫(例如:mysql)作為數(shù)據(jù)源時(shí),需要?jiǎng)?chuàng)建 JdbcClientDetailsService
對(duì)象,并配置到 ClientDetailsServiceConfigurer
之中。具體代碼為
@Bean
public ClientDetailsService clientDetailsService() {
// 新增部分,用于從數(shù)據(jù)庫獲取客戶端信息
return new JdbcClientDetailsService(dataSource);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients)
throws Exception {
// 此處去掉內(nèi)存配置項(xiàng),改為 jdbc 數(shù)據(jù)源
clients.withClientDetails(clientDetailsService);
}
除此之外,還需要在書庫中插入相關(guān)數(shù)據(jù)表,表結(jié)構(gòu)定義如下,也可以從 spring 項(xiàng)目主頁 中獲取。
-- 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. 小結(jié)
本節(jié)我們討論了如何構(gòu)建自己的 OAuth2.0 認(rèn)證中心,主要知識(shí)點(diǎn)有:
- Spring Security 對(duì) OAuth2.0 認(rèn)證標(biāo)準(zhǔn)已經(jīng)有成熟的支持,僅需幾個(gè)注解就可以構(gòu)造出 OAuth2.0 認(rèn)證服務(wù)器;
- Spring Security 對(duì) OAuth2.0 認(rèn)證同樣提供了多種認(rèn)證支持,比如內(nèi)存認(rèn)證、數(shù)據(jù)庫認(rèn)證及 Redis 認(rèn)證等,可以根據(jù)需要進(jìn)行配置或調(diào)整。
下節(jié)我們繼續(xù) OAuth2.0 話題,討論如何在 OAuth2.0 協(xié)議下,如何保護(hù)私密資源被合理訪問。