带有 JDBC 身份验证的 Spring Security 5:UserDetailsS​​ervice bean 仍然在内存中,而不是 JDBC

Posted

技术标签:

【中文标题】带有 JDBC 身份验证的 Spring Security 5:UserDetailsS​​ervice bean 仍然在内存中,而不是 JDBC【英文标题】:Spring Security 5 with JDBC Authentication: UserDetailsService bean is still in-memory rather than JDBC 【发布时间】:2020-05-09 06:38:44 【问题描述】:

我正在使用 Spring Boot 和 Kotlin 构建一个带有 JDBC 身份验证的 Spring Security 示例。我已经像文档 (https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#jc-authentication-jdbc) 中那样配置了 JDBC 身份验证:

@EnableWebSecurity
class SecurityConfig 

    @Autowired
    fun configureGlobal(
            auth: AuthenticationManagerBuilder,
            dataSource: DataSource
    ) 
        auth
                .jdbcAuthentication()
                .withDefaultSchema()
                .dataSource(dataSource)
                .withUser(User.withDefaultPasswordEncoder()
                        .username("alice")
                        .password("password")
                        .roles("USER"))
    

不清楚为什么 Spring Security 仍然保留 InMemory UserDetailsS​​ervice 实现?如果未注释,下面的第 (1) 行将引发 UsernameNotFoundException,因为 Spring Context 中的默认 UserDetailsS​​ervice bean 是 InMemory 实现而不是我刚刚配置的 JDBC。如果 InMemory 1 返回上面配置的用户就可以了,但它没有。

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.security.authentication.ProviderManager
import org.springframework.security.authentication.dao.DaoAuthenticationProvider
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration
import org.springframework.security.core.userdetails.UserDetailsService

@SpringBootApplication
class JdbcAuthenticationSampleApplication

fun main(args: Array<String>) 
    val context = runApplication<JdbcAuthenticationSampleApplication>(*args)

    // default UserDetailsService bean is still InMemory implementation
    val defaultUserDetailsService = context.getBean(UserDetailsService::class.java)
    println("Default UserDetailsService: $defaultUserDetailsService")
    // "alice" can't be found by it and it throws UsernameNotFoundException
    //defaultUserDetailsService.loadUserByUsername("alice") // (1)

    // I could get JDBC UserDetailsService only by this improper way
    val authenticationConfiguration = context.getBean(AuthenticationConfiguration::class.java)
    val authenticationManager = authenticationConfiguration.authenticationManager as ProviderManager
    val authenticationProvider = authenticationManager.providers[0] as DaoAuthenticationProvider
    val getUserDetailsService = DaoAuthenticationProvider::class.java.getDeclaredMethod("getUserDetailsService")
    getUserDetailsService.isAccessible = true
    val jdbcUserDetailsService = getUserDetailsService.invoke(authenticationProvider) as UserDetailsService
    println("JDBC UserDetailsService: $jdbcUserDetailsService")
    // should find "alice" now
    println("User: $jdbcUserDetailsService.loadUserByUsername("alice")")

    context.close()

输出是:

Default UserDetailsService: org.springframework.security.provisioning.InMemoryUserDetailsManager@6af87130
JDBC UserDetailsService: org.springframework.security.provisioning.JdbcUserDetailsManager@22a4ca4a
User: org.springframework.security.core.userdetails.User@5899680: Username: alice; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_USER

为了清楚起见,这是我的build.gradle.kts,非常标准。除此以外没有其他配置。

plugins 
    id("org.springframework.boot") version "2.2.4.RELEASE"
    id("io.spring.dependency-management") version "1.0.9.RELEASE"
    kotlin("jvm") version "1.3.61"
    kotlin("plugin.spring") version "1.3.61"


group = "sample.spring.security"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_1_8

repositories 
    mavenCentral()


dependencies 
    implementation("org.springframework.boot:spring-boot-starter-data-jdbc")
    implementation("org.springframework.boot:spring-boot-starter-security")
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    runtimeOnly("com.h2database:h2")

    testImplementation("org.springframework.boot:spring-boot-starter-test") 
        exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
    
    testImplementation("org.springframework.security:spring-security-test")

这是由于 UsernameNotFoundException 而无法启动的测试:

@SpringBootTest
@AutoConfigureTestDatabase
class JdbcAuthenticationSampleApplicationTests @Autowired constructor(
        val userDetailsService: UserDetailsService
) 

    @Test
    @WithUserDetails("alice")
    fun testUserDetailsService() 
        //SecurityContext can't be built due to UsernameNotFoundException
    

问题是为什么还有内存中的UserDetailService?以及如何正确获取 JDBC UserDetailsS​​ervice? 值得一提的是,当用户通过 UI 上的登录表单进行身份验证时,JDBC 身份验证工作正常。

【问题讨论】:

【参考方案1】:

或者,除了我原来的SecurityConfig之外,还可以从刚刚配置的jdbcAuthentication()中获取UserDetailsService bean:

@Bean
fun userDetailsService(auth: AuthenticationManagerBuilder): UserDetailsService = auth.defaultUserDetailsService

它更短并且以正确的顺序实例化 - @Autowired fun configureGlobal(...) 因为SecurityConfig 作为一个 bean,它本身在其中声明的 bean 之前被初始化。

完整配置:

@EnableWebSecurity
class SecurityConfig 

    @Bean
    fun userDetailsService(auth: AuthenticationManagerBuilder): UserDetailsService = auth.defaultUserDetailsService

    @Autowired
    fun configureGlobal(auth: AuthenticationManagerBuilder,
                        dataSource: DataSource) 
        auth.jdbcAuthentication()
                .withDefaultSchema()
                .dataSource(dataSource)
                .withUser(User.withDefaultPasswordEncoder()
                        .username("alice")
                        .password("password")
                        .roles("USER"))
    

【讨论】:

【参考方案2】:

jdbcAuthentication 方法确保UserDetailsService 可用于AuthenticationManagerBuilder.getDefaultUserDetailsService() 方法。 这就是当用户通过 UI 进行身份验证时您的应用程序按预期工作的原因。

但是,它不会创建 UserDetailsService bean。 使用 context.getBean()@WithUserDetails 都需要 UserDetailsService bean。

如果你想继续像上面那样配置jdbcAuthentication,那么你可以在你的测试中使用@WithMockUser之类的东西。

或者,如果您想创建一个UserDetailsService bean,您可以使用以下配置来执行此操作,这与您上面的配置类似。 您将需要修改 DataSource bean。这个例子简单地说明了如何使用默认模式。

@Bean
fun dataSource(): DataSource 
    return EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.H2)
            .addScript("classpath:org/springframework/security/core/userdetails/jdbc/users.ddl")
            .build()


@Bean
fun users(dataSource: DataSource): UserDetailsManager 
    val userDetailsManager = JdbcUserDetailsManager(dataSource)
    userDetailsManager.createUser(User.withDefaultPasswordEncoder()
            .username("alice")
            .password("password")
            .roles("USER")
            .build())
    return userDetailsManager

【讨论】:

谢谢你,Eleftheria。 AuthenticationManagerBuilder 没有暴露 UserDetailsService bean 的观点解释了这种行为。你的例子效果很好,我赞成它。虽然,我添加了另一种方式,它更简洁一些,并且没有明确突出显示 DDL 脚本路径。

以上是关于带有 JDBC 身份验证的 Spring Security 5:UserDetailsS​​ervice bean 仍然在内存中,而不是 JDBC的主要内容,如果未能解决你的问题,请参考以下文章

无法在 Spring Boot 中使用 JDBC 身份验证创建安全连接

Spring-Security:MySQL JDBC 身份验证失败

如何在 Spring Security 中通过 jdbc 身份验证使用自定义登录页面

如何使用 Java 和 XML 配置在 Spring Security 中配置 jdbc 身份验证管理器?

使用 IAM 身份验证和 Spring JDBC(DataSource 和 JdbcTemplace)访问 AWS RDS

带有 Spring Security 登录和身份验证的 Angular