spring-boot - Spring Boot Data JPA @CreatedBy 和 @UpdatedBy 未填充使用 OIDC 进行身份验证
问题描述
我想让 Spring JPA 审计与 Spring Boot 一起使用,我正在使用 Spring Security 的最新功能通过 Keycloak 进行身份验证。
springBootVersion = '2.1.0.RC1'
我正在关注 Spring Security 团队的示例https://github.com/jzheaux/messaging-app/tree/springone2018-demo/resource-server
资源服务器配置.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)
}
}
应用程序.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 被调用并将用户设置为未知,但在传递 access_token 时,如果条件仍然为假。
不知道我错过了什么。
解决方案
I was able to replicate your issue, but in an equivalent Java setup. The issue is in your JpaAuditingConfiguration
class. If you observe your current JpaAuditingConfiguration
class closely this is what happens there:
- During Spring initialization the
auditorProvider()
function will try to generate a bean. - The authentication condition is being checked there upfront(during application startup) and this thread(which starts the Spring Boot App) is NOT an authenticated thread at all. Hence it returns an
AuditorAware
instance that will always returnUnknown
.
You need to change this class as follows(Sorry, I wrote it in Java, please convert it to 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";
}
}
};
}
}
You can try this. Also, I suspect that with your current setup you would get updatedOn and createdOn correctly populated. If yes, that means all the JPA and EntityListener magic is actually working. You just need to return the correct implementation of AuditorAware
at runtime.
Also note that, my config does not use JwtAuthenticationToken
and I use a CustomUserDetails
implementation. But that's not related to your problem, and you can of course use your current token type (JwtAuthenticationToken
). Its just that, I had my own little app up and running inside which I replicated your issue.
推荐阅读
- python - 使用数据库和多处理时的最佳实践?
- windows - 由于空间问题,WSL 无法找到文件或目录
- python - 循环遍历系列时搜索多个子字符串。(蟒蛇/熊猫)
- java - 用spring boot开发的一个问答网站添加5分钟倒计时
- elasticsearch - 为什么弹性搜索 Junit 测试在 /tmp 文件夹中创建 jna 文件夹?
- audio - 为 Google 任务的任务完成添加声音 (TasksBoard)
- android - Android ContentValues put 方法
- python - 从自己的训练检查点加载用于推理的 MMDetection 会产生垃圾检测
- html - 您可以使用 RTE 创建自定义 MailChimp 模板块吗?
- python - 尝试标准化图像数组时出现内存错误