Spring 引导数据 JPA @CreatedBy 和 @UpdatedBy 未填充 OIDC 身份验证
Spring Boot Data JPA @CreatedBy and @UpdatedBy not populating with authenticating with OIDC
我想让 Spring JPA 审计与 Spring Boot 一起工作,我正在使用 Spring 安全的最新功能对 Keycloak 进行身份验证。
springBootVersion = '2.1.0.RC1'
我正在关注 spring 安全团队 https://github.com/jzheaux/messaging-app/tree/springone2018-demo/resource-server
的示例
ResourceServerConfig.kt
@EnableWebSecurity
class OAuth2ResourceServerSecurityConfiguration(val resourceServerProperties: OAuth2ResourceServerProperties) : WebSecurityConfigurerAdapter() {
@Throws(Exception::class)
override fun configure(http: HttpSecurity) {
http
.authorizeRequests()
.antMatchers("/api/**").authenticated()
.anyRequest().anonymous()
.and()
.oauth2ResourceServer()
.authenticationEntryPoint(MoreInformativeAuthenticationEntryPoint())
.jwt()
.jwtAuthenticationConverter(GrantedAuthoritiesExtractor())
.decoder(jwtDecoder())
}
private fun jwtDecoder(): JwtDecoder {
val issuerUri = this.resourceServerProperties.jwt.issuerUri
val jwtDecoder = JwtDecoders.fromOidcIssuerLocation(issuerUri) as NimbusJwtDecoderJwkSupport
val withIssuer = JwtValidators.createDefaultWithIssuer(issuerUri)
val withAudience = DelegatingOAuth2TokenValidator(withIssuer, AudienceValidator())
jwtDecoder.setJwtValidator(withAudience)
return jwtDecoder
}
}
class MoreInformativeAuthenticationEntryPoint : AuthenticationEntryPoint {
private val delegate = BearerTokenAuthenticationEntryPoint()
private val mapper = ObjectMapper().setSerializationInclusion(JsonInclude.Include.NON_NULL)
@Throws(IOException::class, ServletException::class)
override fun commence(request: HttpServletRequest, response: HttpServletResponse,
reason: AuthenticationException) {
this.delegate.commence(request, response, reason)
if (reason.cause is JwtValidationException) {
val validationException = reason.cause as JwtValidationException
val errors = validationException.errors
this.mapper.writeValue(response.writer, errors)
}
}
}
class GrantedAuthoritiesExtractor : JwtAuthenticationConverter() {
override fun extractAuthorities(jwt: Jwt): Collection<GrantedAuthority> {
val scopes = jwt.claims["scope"].toString().split(" ")
return scopes.map { SimpleGrantedAuthority(it) }
}
}
class AudienceValidator : OAuth2TokenValidator<Jwt> {
override fun validate(token: Jwt): OAuth2TokenValidatorResult {
val audience = token.audience
return if (!CollectionUtils.isEmpty(audience) && audience.contains("mobile-client")) {
OAuth2TokenValidatorResult.success()
} else {
OAuth2TokenValidatorResult.failure(MISSING_AUDIENCE)
}
}
companion object {
private val MISSING_AUDIENCE = BearerTokenError("invalid_token", HttpStatus.UNAUTHORIZED,
"The token is missing a required audience.", null)
}
}
application.yaml
spring:
application:
name: sociter
datasource:
url: jdbc:postgresql://localhost:5432/sociter
username: postgres
password: 123123
driver-class-name: org.postgresql.Driver
jpa:
hibernate:
ddl-auto: update
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: http://localhost:8080/auth/realms/sociter/protocol/openid-connect/certs
issuer-uri: http://localhost:8080/auth/realms/sociter
JpaAuditingConfiguration.kt
@Configuration
@EnableJpaAuditing
(auditorAwareRef = "auditorProvider")
class JpaAuditingConfiguration {
@Bean
fun auditorProvider(): AuditorAware<String> {
return if (SecurityContextHolder.getContext().authentication != null) {
val oauth2 = SecurityContextHolder.getContext().authentication as JwtAuthenticationToken
val claims = oauth2.token.claims
val userId = claims["sub"]
AuditorAware { Optional.of(userId.toString()) }
} else
AuditorAware { Optional.of("Unknown") }
}
}
BaseEntity.kt
@MappedSuperclass
@JsonIgnoreProperties(value = ["createdOn, updatedOn"], allowGetters = true)
@EntityListeners(AuditingEntityListener::class)
abstract class BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
val id: UUID = UUID.randomUUID()
@Column(nullable = false, updatable = false)
@CreatedDate
var createdOn: LocalDateTime = LocalDateTime.now()
@Column(nullable = true)
@LastModifiedDate
var updatedOn: LocalDateTime? = null
@Column(nullable = true, updatable = false)
@CreatedBy
var createdBy: String? = null
@Column(nullable = true)
@LastModifiedBy
var updatedBy: String? = null
}
我正在将 createdBy 和 UpdatedBy 设置为未知。在调试期间,auditorProvider bean 被调用并将用户设置为 Unknown 但是在传递 access_token 时,如果条件仍然为 false。
不确定我错过了什么。
我能够重现您的问题,但在等效的 Java 设置中。问题出在您的 JpaAuditingConfiguration
class 中。如果您仔细观察当前的 JpaAuditingConfiguration
class,就会发生以下情况:
- 在 Spring 初始化期间,
auditorProvider()
函数将尝试生成一个 bean。
- 在那里预先检查身份验证条件(在应用程序启动期间)并且此线程(启动 Spring Boot App)根本不是经过身份验证的线程。因此,它 return 是一个
AuditorAware
实例,它将始终 return Unknown
。
你需要把这个class改成如下(对不起,我写在Java,请把它转换成Kotlin):
@Configuration
@EnableJpaAuditing(auditorAwareRef = "auditorProvider")
public class JPAAuditConfig {
@Bean
public AuditorAware<String> auditorProvider() {
return new AuditorAware<String>() {
@Override
public String getCurrentAuditor() {
if (SecurityContextHolder.getContext().getAuthentication() != null) {
OAuth2Authentication auth = (OAuth2Authentication) SecurityContextHolder.getContext().getAuthentication();
Object principal = auth.getUserAuthentication().getPrincipal();
CustomUserDetails userDetails = (CustomUserDetails) principal;
return userDetails.getUsername();
} else {
return "Unknown";
}
}
};
}
}
你可以试试这个。另外,我怀疑使用您当前的设置,您会正确填充 updatedOn 和 createdOn。如果是,则意味着所有 JPA 和 EntityListener 魔法实际上都在起作用。您只需要 return 在运行时 AuditorAware
的正确实现。
另请注意,我的配置不使用 JwtAuthenticationToken
,而是使用 CustomUserDetails
实现。但这与您的问题无关,您当然可以使用当前的令牌类型 (JwtAuthenticationToken
)。只是,我有自己的小应用程序,运行 在里面我复制了你的问题。
Arun Patra 的上述回答适用于 Java。我必须对 Kotlin 执行以下操作。
@Configuration
@EnableJpaAuditing(auditorAwareRef = "auditorProvider")
class JpaAuditingConfiguration {
@Bean
fun auditorProvider(): AuditorAware<String> {
return CustomAuditorAware()
}
}
private class CustomAuditorAware : AuditorAware<String> {
override fun getCurrentAuditor(): Optional<String> {
return if (SecurityContextHolder.getContext().authentication != null) {
val oauth2 = SecurityContextHolder.getContext().authentication as JwtAuthenticationToken
val loggedInUserId = oauth2.token.claims["sub"].toString()
Optional.of(loggedInUserId)
} else {
Optional.of("Unknown")
}
}
}
我想让 Spring JPA 审计与 Spring Boot 一起工作,我正在使用 Spring 安全的最新功能对 Keycloak 进行身份验证。
springBootVersion = '2.1.0.RC1'
我正在关注 spring 安全团队 https://github.com/jzheaux/messaging-app/tree/springone2018-demo/resource-server
的示例ResourceServerConfig.kt
@EnableWebSecurity
class OAuth2ResourceServerSecurityConfiguration(val resourceServerProperties: OAuth2ResourceServerProperties) : WebSecurityConfigurerAdapter() {
@Throws(Exception::class)
override fun configure(http: HttpSecurity) {
http
.authorizeRequests()
.antMatchers("/api/**").authenticated()
.anyRequest().anonymous()
.and()
.oauth2ResourceServer()
.authenticationEntryPoint(MoreInformativeAuthenticationEntryPoint())
.jwt()
.jwtAuthenticationConverter(GrantedAuthoritiesExtractor())
.decoder(jwtDecoder())
}
private fun jwtDecoder(): JwtDecoder {
val issuerUri = this.resourceServerProperties.jwt.issuerUri
val jwtDecoder = JwtDecoders.fromOidcIssuerLocation(issuerUri) as NimbusJwtDecoderJwkSupport
val withIssuer = JwtValidators.createDefaultWithIssuer(issuerUri)
val withAudience = DelegatingOAuth2TokenValidator(withIssuer, AudienceValidator())
jwtDecoder.setJwtValidator(withAudience)
return jwtDecoder
}
}
class MoreInformativeAuthenticationEntryPoint : AuthenticationEntryPoint {
private val delegate = BearerTokenAuthenticationEntryPoint()
private val mapper = ObjectMapper().setSerializationInclusion(JsonInclude.Include.NON_NULL)
@Throws(IOException::class, ServletException::class)
override fun commence(request: HttpServletRequest, response: HttpServletResponse,
reason: AuthenticationException) {
this.delegate.commence(request, response, reason)
if (reason.cause is JwtValidationException) {
val validationException = reason.cause as JwtValidationException
val errors = validationException.errors
this.mapper.writeValue(response.writer, errors)
}
}
}
class GrantedAuthoritiesExtractor : JwtAuthenticationConverter() {
override fun extractAuthorities(jwt: Jwt): Collection<GrantedAuthority> {
val scopes = jwt.claims["scope"].toString().split(" ")
return scopes.map { SimpleGrantedAuthority(it) }
}
}
class AudienceValidator : OAuth2TokenValidator<Jwt> {
override fun validate(token: Jwt): OAuth2TokenValidatorResult {
val audience = token.audience
return if (!CollectionUtils.isEmpty(audience) && audience.contains("mobile-client")) {
OAuth2TokenValidatorResult.success()
} else {
OAuth2TokenValidatorResult.failure(MISSING_AUDIENCE)
}
}
companion object {
private val MISSING_AUDIENCE = BearerTokenError("invalid_token", HttpStatus.UNAUTHORIZED,
"The token is missing a required audience.", null)
}
}
application.yaml
spring:
application:
name: sociter
datasource:
url: jdbc:postgresql://localhost:5432/sociter
username: postgres
password: 123123
driver-class-name: org.postgresql.Driver
jpa:
hibernate:
ddl-auto: update
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: http://localhost:8080/auth/realms/sociter/protocol/openid-connect/certs
issuer-uri: http://localhost:8080/auth/realms/sociter
JpaAuditingConfiguration.kt
@Configuration
@EnableJpaAuditing
(auditorAwareRef = "auditorProvider")
class JpaAuditingConfiguration {
@Bean
fun auditorProvider(): AuditorAware<String> {
return if (SecurityContextHolder.getContext().authentication != null) {
val oauth2 = SecurityContextHolder.getContext().authentication as JwtAuthenticationToken
val claims = oauth2.token.claims
val userId = claims["sub"]
AuditorAware { Optional.of(userId.toString()) }
} else
AuditorAware { Optional.of("Unknown") }
}
}
BaseEntity.kt
@MappedSuperclass
@JsonIgnoreProperties(value = ["createdOn, updatedOn"], allowGetters = true)
@EntityListeners(AuditingEntityListener::class)
abstract class BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
val id: UUID = UUID.randomUUID()
@Column(nullable = false, updatable = false)
@CreatedDate
var createdOn: LocalDateTime = LocalDateTime.now()
@Column(nullable = true)
@LastModifiedDate
var updatedOn: LocalDateTime? = null
@Column(nullable = true, updatable = false)
@CreatedBy
var createdBy: String? = null
@Column(nullable = true)
@LastModifiedBy
var updatedBy: String? = null
}
我正在将 createdBy 和 UpdatedBy 设置为未知。在调试期间,auditorProvider bean 被调用并将用户设置为 Unknown 但是在传递 access_token 时,如果条件仍然为 false。
不确定我错过了什么。
我能够重现您的问题,但在等效的 Java 设置中。问题出在您的 JpaAuditingConfiguration
class 中。如果您仔细观察当前的 JpaAuditingConfiguration
class,就会发生以下情况:
- 在 Spring 初始化期间,
auditorProvider()
函数将尝试生成一个 bean。 - 在那里预先检查身份验证条件(在应用程序启动期间)并且此线程(启动 Spring Boot App)根本不是经过身份验证的线程。因此,它 return 是一个
AuditorAware
实例,它将始终 returnUnknown
。
你需要把这个class改成如下(对不起,我写在Java,请把它转换成Kotlin):
@Configuration
@EnableJpaAuditing(auditorAwareRef = "auditorProvider")
public class JPAAuditConfig {
@Bean
public AuditorAware<String> auditorProvider() {
return new AuditorAware<String>() {
@Override
public String getCurrentAuditor() {
if (SecurityContextHolder.getContext().getAuthentication() != null) {
OAuth2Authentication auth = (OAuth2Authentication) SecurityContextHolder.getContext().getAuthentication();
Object principal = auth.getUserAuthentication().getPrincipal();
CustomUserDetails userDetails = (CustomUserDetails) principal;
return userDetails.getUsername();
} else {
return "Unknown";
}
}
};
}
}
你可以试试这个。另外,我怀疑使用您当前的设置,您会正确填充 updatedOn 和 createdOn。如果是,则意味着所有 JPA 和 EntityListener 魔法实际上都在起作用。您只需要 return 在运行时 AuditorAware
的正确实现。
另请注意,我的配置不使用 JwtAuthenticationToken
,而是使用 CustomUserDetails
实现。但这与您的问题无关,您当然可以使用当前的令牌类型 (JwtAuthenticationToken
)。只是,我有自己的小应用程序,运行 在里面我复制了你的问题。
Arun Patra 的上述回答适用于 Java。我必须对 Kotlin 执行以下操作。
@Configuration
@EnableJpaAuditing(auditorAwareRef = "auditorProvider")
class JpaAuditingConfiguration {
@Bean
fun auditorProvider(): AuditorAware<String> {
return CustomAuditorAware()
}
}
private class CustomAuditorAware : AuditorAware<String> {
override fun getCurrentAuditor(): Optional<String> {
return if (SecurityContextHolder.getContext().authentication != null) {
val oauth2 = SecurityContextHolder.getContext().authentication as JwtAuthenticationToken
val loggedInUserId = oauth2.token.claims["sub"].toString()
Optional.of(loggedInUserId)
} else {
Optional.of("Unknown")
}
}
}