具有弹簧安全性和反应性弹簧的自定义身份验证

时间:2018-07-30 16:18:10

标签: spring-boot spring-security kotlin spring-webflux

我有一个自定义身份验证方案。我有一个REST端点,它在http uri路径中有userId,在HTTP标头中有token。我想检查这样的请求是否由具有有效令牌的有效用户执行。用户和令牌存储在mongo集合中。

我不知道我应该在哪个班级授权用户。

我的SecurityConfig

@EnableWebFluxSecurity
class SecurityConfig {

  @Bean
  fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {

    val build = http
        .httpBasic().disable()
        .formLogin().disable()
        .csrf().disable()
        .logout().disable()
        .authenticationManager(CustomReactiveAuthenticationManager())
        .securityContextRepository(CustomServerSecurityContextRepository())
        .authorizeExchange().pathMatchers("/api/measurement/**").hasAuthority("ROLE_USER")
        .anyExchange().permitAll().and()

    return build.build()
  }

  @Bean
  fun userDetailsService(): MapReactiveUserDetailsService {
    val user = User.withDefaultPasswordEncoder()
        .username("sampleDeviceIdV1")
        .password("foo")
        .roles("USER")
        .build()

    return MapReactiveUserDetailsService(user)
  }
}

我的ServerSecurityContextRepository

class CustomServerSecurityContextRepository : ServerSecurityContextRepository {

  override fun load(exchange: ServerWebExchange): Mono<SecurityContext> {
    val authHeader = exchange.request.headers.getFirst(HttpHeaders.AUTHORIZATION)
    val path = exchange.request.uri.path


    return if (path.startsWith("/api/measurement/") && authHeader != null && authHeader.startsWith(prefix = "Bearer ")) {
      val deviceId = path.drop(17)

      val authToken = authHeader.drop(7)
      val auth = UsernamePasswordAuthenticationToken(deviceId, authToken)
      Mono.just(SecurityContextImpl(auth))
    } else {
      Mono.empty()
    }
  }

  override fun save(exchange: ServerWebExchange?, context: SecurityContext?): Mono<Void> {
    return Mono.empty()
  }
}

出现两个问题:

  1. ServerSecurityContextRepository是从交易所获取用户名和令牌的好地方-还是有更好的地方呢?

  2. 我应该在哪里执行身份验证(对照mongo集合检查令牌和用户名)? 我的自定义AuthenticationManager不会在任何地方被调用。我应该在ServerSecurityContextRepository内执行所有操作还是在ReactiveAuthenticationManager内执行用户和令牌验证?也许其他课程更合适?

1 个答案:

答案 0 :(得分:2)

事实证明,网络上的某些教程是完全错误的。

我设法使用以下代码配置了所有内容:

class DeviceAuthenticationConverter : Function<ServerWebExchange, Mono<Authentication>> {
  override fun apply(exchange: ServerWebExchange): Mono<Authentication> {
    val authHeader: String? = exchange.request.headers.getFirst(HttpHeaders.AUTHORIZATION)
    val path: String? = exchange.request.uri.path

    return when {
      isValidPath(path) && isValidHeader(authHeader) -> Mono.just(UsernamePasswordAuthenticationToken(path?.drop(17), authHeader?.drop(7)))
      else -> Mono.empty()
    }
  }

  private fun isValidPath(path: String?) = path != null && path.startsWith(API_MEASUREMENT)

  private fun isValidHeader(authHeader: String?) = authHeader != null && authHeader.startsWith(prefix = "Bearer ")

}

和配置:

@EnableWebFluxSecurity
class SecurityConfig {

  companion object {
    const val API_MEASUREMENT = "/api/measurement/"
    const val API_MEASUREMENT_PATH = "$API_MEASUREMENT**"
    const val DEVICE = "DEVICE"
    const val DEVICE_ID = "deviceId"
  }

  @Bean
  fun securityWebFilterChain(http: ServerHttpSecurity, authenticationManager: ReactiveAuthenticationManager) =
      http
          .httpBasic().disable()
          .formLogin().disable()
          .csrf().disable()
          .logout().disable()
          .authorizeExchange().pathMatchers(API_MEASUREMENT_PATH).hasRole(DEVICE)
          .anyExchange().permitAll().and().addFilterAt(authenticationWebFilter(authenticationManager), AUTHENTICATION).build()

  @Bean
  fun userDetailsService(tokenRepository: TokenRepository) = MongoDeviceTokenReactiveUserDetailsService(tokenRepository)

  @Bean
  fun tokenRepository(template: ReactiveMongoTemplate, passwordEncoder: PasswordEncoder) = MongoTokenRepository(template, passwordEncoder)

  @Bean
  fun tokenFacade(tokenRepository: TokenRepository) = TokenFacade(tokenRepository)

  @Bean
  fun authManager(userDetailsService: ReactiveUserDetailsService) = UserDetailsRepositoryReactiveAuthenticationManager(userDetailsService)

  private fun authenticationWebFilter(reactiveAuthenticationManager: ReactiveAuthenticationManager) =
      AuthenticationWebFilter(reactiveAuthenticationManager).apply {
        setAuthenticationConverter(DeviceAuthenticationConverter())
        setRequiresAuthenticationMatcher(
            ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, API_MEASUREMENT_PATH)
        )
      }

  @Bean
  fun passwordEncoder() = PasswordEncoderFactories.createDelegatingPasswordEncoder()
}