带有 JDBC 身份验证的 Spring Security 5:UserDetailsService Bean 仍在内存



我正在使用Spring Boot和Kotlin构建一个带有JDBC身份验证的Spring Security示例。我已经配置了 JDBC 身份验证,如文档 (https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#jc-authentication-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 UserDetailsService 实现?下面的第 (1( 行如果取消注释,则会抛出 UsernameNotFoundException,因为 Spring 上下文中的默认 UserDetailsService Bean 是 InMemory 实现,而不是我刚刚配置的 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()
}

输出为:

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")
}

以下是由于用户名未发现异常而无法启动的测试:

@SpringBootTest
@AutoConfigureTestDatabase
class JdbcAuthenticationSampleApplicationTests @Autowired constructor(
val userDetailsService: UserDetailsService
) {
@Test
@WithUserDetails("alice")
fun testUserDetailsService() {
//SecurityContext can't be built due to UsernameNotFoundException
}
}

问题是为什么内存中仍然有UserDetailService?我怎样才能正确地获得JDBC用户详细信息服务? 值得一提的是,当用户通过 UI 上的登录表单进行身份验证时,JDBC 身份验证工作正常。

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

但是,它不会创建UserDetailsService豆。
使用context.getBean()@WithUserDetails都期望UserDetailsServicebean。

如果要继续按上述方式配置jdbcAuthentication,则可以在测试中使用类似@WithMockUser的内容。

或者,如果要创建UserDetailsServiceBean,可以使用以下配置来实现,该配置类似于上面的配置。
您将需要修改DataSourceBean。此示例仅说明如何使用默认架构。

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

最新更新