带有 JDBC 身份验证的 Spring Security 5:UserDetailsService bean 仍然在内存中,而不是 JDBC
Posted
技术标签:
【中文标题】带有 JDBC 身份验证的 Spring Security 5:UserDetailsService 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 UserDetailsService 实现?如果未注释,下面的第 (1) 行将引发 UsernameNotFoundException,因为 Spring Context 中的默认 UserDetailsService 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 UserDetailsService? 值得一提的是,当用户通过 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:UserDetailsService bean 仍然在内存中,而不是 JDBC的主要内容,如果未能解决你的问题,请参考以下文章
无法在 Spring Boot 中使用 JDBC 身份验证创建安全连接
Spring-Security:MySQL JDBC 身份验证失败
如何在 Spring Security 中通过 jdbc 身份验证使用自定义登录页面
如何使用 Java 和 XML 配置在 Spring Security 中配置 jdbc 身份验证管理器?
使用 IAM 身份验证和 Spring JDBC(DataSource 和 JdbcTemplace)访问 AWS RDS