Spring boot 带JDBC身份验证的SpringSecurity5:UserDetailsService bean仍在内存中,而不是JDBC中

Spring boot 带JDBC身份验证的SpringSecurity5:UserDetailsService bean仍在内存中,而不是JDBC中,spring-boot,kotlin,spring-security,Spring Boot,Kotlin,Spring Security,我正在使用Spring引导和Kotlin构建一个具有JDBC身份验证的Spring安全示例。我已经像文档中那样配置了JDBC身份验证: 现在还不清楚为什么Spring安全性仍然保留内存中的UserDetails服务实现?如果未注释,下面的第1行将抛出UsernameNotFoundException,因为Spring上下文中的默认UserDetailsService bean位于内存实现中,而不是我刚才配置的JDBC中。如果InMemory one返回上面配置的用户就可以了,但它没有 impor

我正在使用Spring引导和Kotlin构建一个具有JDBC身份验证的Spring安全示例。我已经像文档中那样配置了JDBC身份验证:

现在还不清楚为什么Spring安全性仍然保留内存中的UserDetails服务实现?如果未注释,下面的第1行将抛出UsernameNotFoundException,因为Spring上下文中的默认UserDetailsService bean位于内存实现中,而不是我刚才配置的JDBC中。如果InMemory one返回上面配置的用户就可以了,但它没有

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()
}
这是我的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 UserDetails服务? 值得一提的是,当用户通过UI上的登录表单进行身份验证时,JDBC身份验证工作正常。

JDBC身份验证方法确保UserDetailsService可用于AuthenticationManagerBuilder.getDefaultUserDetailsService方法。 这就是为什么当用户通过UI进行身份验证时,应用程序会按预期工作

但是,它不会创建UserDetailsService bean。 使用context.getBean和@WithUserDetails都需要UserDetails服务bean

如果您想继续如上所述配置jdbcAuthentication,那么可以在测试中使用@WithMockUser之类的东西

或者,如果您想要创建UserDetailsService bean,您可以通过以下配置来实现,这与上面的配置类似。 您将需要修改数据源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
}
或者,除了我的原始SecurityConfig之外,还可以从刚刚配置的jdbcAuthentication中获取UserDetailsService bean:

它比较短,并且以适当的顺序实例化-在@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"))
    }
}

谢谢你,埃列夫特拉。AuthenticationManagerBuilder不公开UserDetailsService bean这一点解释了该行为。你的例子很好,我投了更高的票。尽管如此,我添加了另一种方法,它更简洁,并且没有显式地突出显示DDL脚本路径。
@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
}
@Bean
fun userDetailsService(auth: AuthenticationManagerBuilder): UserDetailsService = auth.defaultUserDetailsService
@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"))
    }
}