1. 概述

在本教程中,我们将探讨 Spring 提供的通过 JDBC 进行认证的能力。我们会基于一个已有的 DataSource 配置来实现数据库认证。

在我们之前的 使用数据库实现 UserDetailsService 认证 文章中,我们通过手动实现 UserDetailsService 接口来实现数据库认证。

而这一次,我们将使用 AuthenticationManagerBuilder#jdbcAuthentication 方法,看看这种更简单的实现方式的优缺点。

2. 使用嵌入式 H2 数据库进行认证

我们先从使用嵌入式 H2 数据库进行认证开始。这个过程相对简单,因为 Spring Boot 已经为这类场景提供了大量自动配置。

2.1. 依赖和数据库配置

按照我们之前 Spring Boot 配置 H2 数据库 的文章步骤:

  1. 添加 spring-boot-starter-data-jpah2 依赖
  2. application.properties 中配置数据库连接
  3. 启用 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. 验证配置

我们创建一个简单接口来获取当前认证用户信息:

@RestController
@RequestMapping("/principal")
public class UserController {

    @GetMapping
    public Principal retrievePrincipal(Principal principal) {
        return principal;
    }
}

同时配置安全策略,允许访问 H2 控制台并保护 /principal 接口:

@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();
    }

}

⚠️ 注意:这里只是为了演示。实际项目中通常不会启用 H2 控制台。

启动应用后,访问 H2 控制台可以看到 Spring 自动创建了 usersauthorities 表。

尝试登录并访问 /principal 接口,验证用户信息是否正确返回。

2.4. 底层机制

前面提到我们也可以通过实现 UserDetailsService 接口来定制数据库认证逻辑。

而这次我们使用的是 Spring Security 提供的默认实现类 JdbcDaoImpl

这个类负责从数据库中加载用户信息,并构建 UserDetails 对象。对于简单场景非常实用,但在需要自定义表结构或使用不同数据库时存在局限性。

3. 适配不同数据库的 Schema

接下来我们尝试使用 MySQL 数据库,并自定义 Schema。

3.1. 依赖和数据库配置

移除 h2 依赖,添加 MySQL 驱动:

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.33</version>
</dependency>

更新 application.properties

spring.datasource.url=jdbc:mysql://localhost:3306/jdbc_authentication
spring.datasource.username=root
spring.datasource.password=pass

3.2. 运行默认配置

我们尝试运行项目,发现启动失败,提示 SQLSyntaxErrorException

这是因为 .withDefaultSchema() 使用的 DDL 脚本是为 H2 设计的,不适用于 MySQL。

3.3. 自定义 Schema

我们需要移除 .withDefaultSchema() 并手动创建 Schema:

@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);

创建 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');

修改配置文件:

spring.sql.init.mode=always
spring.jpa.hibernate.ddl-auto=none

现在项目应能正常启动,并完成认证。

⚠️ 注意:spring.sql.init.mode 是 Spring Boot 2.5+ 引入的属性,旧版本请使用 spring.datasource.initialization-mode

4. 自定义查询语句

现在我们更进一步,如果默认的 Schema 和查询语句都不符合我们的需求怎么办?

4.1. 修改 Schema

假设我们已有如下结构的表:

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 user@example.com/pass
INSERT INTO bael_users (name, email, password, enabled)
  values ('user',
    'user@example.com',
    '$2a$10$8.UnVuG9HHgffUDAlk8qfOuVGkqRzgVymGe07xd00DMxs.AQubh4a',
    1);

INSERT INTO authorities (email, authority)
  values ('user@example.com', 'ROLE_USER');

4.2. 启动应用

应用能正常启动,但登录时会提示错误,因为 Spring Security 默认使用 username 字段进行查询。

4.3. 自定义查询语句

我们可以自定义查询语句来适配新的 Schema:

@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 = ?");
}

这样 Spring Security 就会根据我们提供的 SQL 查询用户信息。

再次启动应用并访问 /principal 接口,应能成功认证并获取用户信息。

5. 总结

✅ 使用 jdbcAuthentication() 是一种简单快捷的数据库认证方式,无需手动实现 UserDetailsService 和相关实体类、Repository。

❌ 但缺点也很明显:当数据库结构或业务逻辑与默认策略不一致时,灵活性较差。

如果你的项目结构简单,或可以适配 Spring Security 的默认 Schema,这种方式非常实用。否则建议自定义实现 UserDetailsService

完整示例可在 GitHub 查看:Spring Security JDBC 认证实例。我们还提供了 PostgreSQL 的示例,本文中未展示以保持简洁。


原始标题:Spring Security: Exploring JDBC Authentication