1. 概述
本教程將探討 Spring 如何通過現有 DataSource 配置執行 JDBC 身份驗證的能力。
在我們的“基於數據庫的 UserDetailsService 身份驗證”一文中,我們分析了一種實現方法,即自行實現 UserDetailService 接口。
本次,我們將利用 AuthenticationManagerBuilder#jdbcAuthentication 指令來分析這種更簡單的方法的優缺點。
2. 使用嵌入式 H2 連接
首先,我們將分析如何使用嵌入式 H2 數據庫來實現身份驗證。
這很容易實現,因為大多數 Spring Boot 的自動配置都已針對此場景進行了準備。
2.1. 依賴項和數據庫配置
請按照我們之前關於 Spring Boot 與 H2 數據庫的文章的指示開始:
- 包含相應的 spring-boot-starter-data-jpa 和 h2 依賴項
- 使用應用程序屬性配置數據庫連接
- 啓用 H2 控制枱
2.2. 配置 JDBC 身份驗證
我們將使用 Spring Security 的 AuthenticationManagerBuilder 配置助手 來配置 JDBC 身份驗證:
@Autowired
private DataSource dataSource;
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth)
throws Exception {
auth.jdbcAuthentication()
.dataSource(dataSource)
.withDefaultSchema()
.withUser(User.withUsername("user")
.password(passwordEncoder().encode("pass"))
.roles("USER"));
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}如我們所見,我們正在使用自動配置的 DataSource。 withDefaultSchema 指令會添加一個數據庫腳本,用於填充默認模式,從而允許用户和權限存儲。
此基本的用户模式在 Spring Security 附錄 中進行了文檔記錄。
最後,我們通過程序方式在數據庫中創建了一個默認用户條目。
2.3. 驗證配置
讓我們創建一個簡單的端點來檢索已認證的 Principal 信息:
@RestController
@RequestMapping("/principal")
public class UserController {
@GetMapping
public Principal retrievePrincipal(Principal principal) {
return principal;
}
}此外,我們還將安全地保護該端點,同時允許訪問 H2 控制枱:
@Configuration
public class SecurityConfiguration {
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity.authorizeHttpRequests(authorizationManagerRequestMatcherRegistry ->
authorizationManagerRequestMatcherRegistry
.requestMatchers(PathRequest.toH2Console()).permitAll().anyRequest().authenticated())
.formLogin(AbstractAuthenticationFilterConfigurer::permitAll);
httpSecurity.csrf(httpSecurityCsrfConfigurer -> httpSecurityCsrfConfigurer.ignoringRequestMatchers(PathRequest.toH2Console()));
httpSecurity.headers(httpSecurityHeadersConfigurer ->
httpSecurityHeadersConfigurer.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin));
return httpSecurity.build();
}
}注意:這裏我們正在重現 Spring Boot 之前實施的 安全配置,但在實際場景中,我們可能根本不會啓用 H2 控制枱。
現在我們將運行應用程序並瀏覽 H2 控制枱。我們可以驗證 Spring 正在創建我們嵌入數據庫中的兩個表:users 和 authorities。
它們的結構與我們之前提到的 Spring Security 附錄中定義的結構相對應。
最後,我們將會進行身份驗證並請求 /principal 端點以查看相關信息,包括用户詳情。
2.4. 源碼解析
在本文開頭,我們提供了一個鏈接,其中解釋瞭如何自定義基於數據庫的身份驗證,並實現 UserDetailsService 接口;如果您想了解其工作原理,我們強烈建議您閲讀該文章。
在本例中,我們依賴於 Spring Security 提供的同一接口的實現,即 JdbcDaoImpl。
如果您深入研究該類,您將看到它使用的 UserDetails 實現,以及從數據庫檢索用户信息的機制。
這對於這個簡單的場景效果很好,但如果想要自定義數據庫模式,甚至如果想要使用不同的數據庫供應商,則存在一些缺點。
讓我們看看如果將配置更改為使用不同的 JDBC 服務會發生什麼。
3. 為不同的數據庫調整模式
本節將使用 MySQL 數據庫配置我們項目的身份驗證。
正如你接下來將看到的,為了實現這一點,我們需要避免使用默認模式並提供我們自己的模式。
3.1. 依賴項和數據庫配置
首先,移除 h2 依賴項,並將其替換為相應的 MySQL 庫:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>如往常一樣,我們可以從 Maven Central 獲取庫的最新版本。
現在,讓我們相應地重新設置應用程序屬性:
spring.datasource.url=
jdbc:mysql://localhost:3306/jdbc_authentication
spring.datasource.username=root
spring.datasource.password=pass3.2. 運行默認配置
當然,這些配置應該根據您的實際 MySQL 服務器進行定製。為了測試目的,我們將使用 Docker 啓動一個新的實例:
docker run -p 3306:3306
--name bael-mysql
-e MYSQL_ROOT_PASSWORD=pass
-e MYSQL_DATABASE=jdbc_authentication
mysql:latest現在讓我們運行該項目以查看默認配置是否適合 MySQL 數據庫。
實際上,應用程序無法啓動,因為存在 SQLSyntaxErrorException。這在一定程度上是合理的;正如我們所説,大多數默認的自動配置都適合 HSQLDB。
在這種情況下,帶有 withDefaultSchema 指令的 DDL 腳本使用了不適合 MySQL 的方言。
因此,我們需要避免使用此模式並提供自己的模式。
3.3. 自定義身份驗證配置
由於我們不想使用默認模式,因此需要從 AuthenticationManagerBuilder 配置中移除相應的聲明。
此外,由於我們將提供自己的 SQL 腳本,因此可以避免嘗試通過編程方式創建用户。
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth)
throws Exception {
auth.jdbcAuthentication()
.dataSource(dataSource);
}現在讓我們來查看數據庫初始化腳本。
首先,我們的 schema.sql:
CREATE TABLE users (
username VARCHAR(50) NOT NULL,
password VARCHAR(100) NOT NULL,
enabled TINYINT NOT NULL DEFAULT 1,
PRIMARY KEY (username)
);
CREATE TABLE authorities (
username VARCHAR(50) NOT NULL,
authority VARCHAR(50) NOT NULL,
FOREIGN KEY (username) REFERENCES users(username)
);
CREATE UNIQUE INDEX ix_auth_username
on authorities (username,authority);然後,我們的 <em data.sql:
-- User user/pass
INSERT INTO users (username, password, enabled)
values ('user',
'$2a$10$8.UnVuG9HHgffUDAlk8qfOuVGkqRzgVymGe07xd00DMxs.AQubh4a',
1);
INSERT INTO authorities (username, authority)
values ('user', 'ROLE_USER');最後,我們還應該修改一些其他的應用程序屬性:
- 由於我們不期望Hibernate現在創建模式,因此應該禁用 ddl-auto 屬性
- 默認情況下,Spring Boot 僅為嵌入式數據庫初始化數據源,而這裏不是這種情況
spring.sql.init.mode=always
spring.jpa.hibernate.ddl-auto=none因此,我們現在應該能夠正確啓動應用程序,並從端點驗證和檢索 Principal 數據。
請注意,spring.sql.init.mode 屬性在 Spring Boot 2.5.0 中引入;對於較早版本,我們需要使用 spring.datasource.initialization-mode。
4. 調整查詢以適應不同的模式
讓我們進一步探討一下。假設默認模式無法滿足我們的需求。
4.1. 修改默認模式
想象一下,例如,我們已經有一個數據庫,其結構略微與默認模式不同:
CREATE TABLE bael_users (
name VARCHAR(50) NOT NULL,
email VARCHAR(50) NOT NULL,
password VARCHAR(100) NOT NULL,
enabled TINYINT NOT NULL DEFAULT 1,
PRIMARY KEY (email)
);
CREATE TABLE authorities (
email VARCHAR(50) NOT NULL,
authority VARCHAR(50) NOT NULL,
FOREIGN KEY (email) REFERENCES bael_users(email)
);
CREATE UNIQUE INDEX ix_auth_email on authorities (email,authority);最後,我們的 data.sql 腳本也將適應這一更改:
-- User [email protected]/pass
INSERT INTO bael_users (name, email, password, enabled)
values ('user',
'[email protected]',
'$2a$10$8.UnVuG9HHgffUDAlk8qfOuVGkqRzgVymGe07xd00DMxs.AQubh4a',
1);
INSERT INTO authorities (email, authority)
values ('[email protected]', 'ROLE_USER');4.2. 使用新模式運行應用程序
讓我們啓動應用程序。它正確初始化,這符合邏輯,因為我們的模式是正確的。
現在,如果我們嘗試登錄,在提供憑據時會提示出現錯誤。
Spring Security 仍然在數據庫中查找 用户名 字段。 幸運的是,JDBC 身份驗證配置提供了 自定義身份驗證過程中檢索用户詳細信息所使用的查詢 的可能性。
4.3. 自定義搜索查詢
調整查詢非常簡單。我們只需在配置 AuthenticationManagerBuilder 時提供自己的 SQL 語句:
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth)
throws Exception {
auth.jdbcAuthentication()
.dataSource(dataSource)
.usersByUsernameQuery("select email,password,enabled "
+ "from bael_users "
+ "where email = ?")
.authoritiesByUsernameQuery("select email,authority "
+ "from authorities "
+ "where email = ?");
}我們現在可以再次啓動應用程序,並使用新的憑據訪問 principal</em/> 端點。
5. 結論
如我們所見,這種方法比自己創建 UserDetailService 實現要簡單得多,後者意味着一個繁瑣的過程;包括創建實體和實現 UserDetail 接口的類,以及將這些添加到我們項目中的倉庫。
缺點是,當然,它在我們的數據庫或邏輯與 Spring Security 解決方案提供的默認策略不同時,提供的靈活性有限。