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 整合Spring Data JPA

[Spring Boot] Adding JPA and Spring Data JPA

Spring Boot中使用Spring Data JPA示例

spring-boot与spring-data-JPA的简单集成使用

Spring boot集成spring-boot-starter-data-jpa环境搭建

spring boot整合spring Data JPA和freemarker