Spring Boot Data JPA @CreatedBy 和 @UpdatedBy 未填充使用 OIDC 进行身份验证
Posted
技术标签:
【中文标题】Spring Boot Data JPA @CreatedBy 和 @UpdatedBy 未填充使用 OIDC 进行身份验证【英文标题】:Spring Boot Data JPA @CreatedBy and @UpdatedBy not populating with authenticating with OIDC 【发布时间】:2019-03-25 05:09:57 【问题描述】:我想让 Spring JPA 审计与 Spring Boot 一起工作,我正在使用 Spring Security 的最新功能通过 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)
应用程序.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。
不知道我错过了什么。
【问题讨论】:
【参考方案1】:我能够复制您的问题,但在等效的 Java 设置中。问题出在您的 JpaAuditingConfiguration
课程中。如果你仔细观察你当前的JpaAuditingConfiguration
班级,这就是那里发生的事情:
-
在 Spring 初始化期间,
auditorProvider()
函数将尝试生成一个 bean。
正在那里预先检查身份验证条件(在应用程序启动期间),并且此线程(启动 Spring Boot 应用程序)根本不是经过身份验证的线程。因此它返回一个AuditorAware
实例,该实例将始终返回Unknown
。
你需要把这个类改成如下(对不起,我是用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 魔法都在起作用。您只需要在运行时返回正确的AuditorAware
实现即可。
另外请注意,我的配置不使用JwtAuthenticationToken
,我使用CustomUserDetails
实现。但这与您的问题无关,您当然可以使用当前的令牌类型 (JwtAuthenticationToken
)。就是这样,我有自己的小应用程序启动并运行,我在其中复制了您的问题。
【讨论】:
【参考方案2】: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 Boot Data JPA @CreatedBy 和 @UpdatedBy 未填充使用 OIDC 进行身份验证的主要内容,如果未能解决你的问题,请参考以下文章
[Spring Boot] Adding JPA and Spring Data JPA
Spring Boot中使用Spring Data JPA示例
spring-boot与spring-data-JPA的简单集成使用