update user session on event
- update handleRemoteAccountEvent to update user session on change event in AccountListener - add RedisSessionService - add fromAccountResponse to RemoteAuthenticationUtil - extract make remote authentication from RemoteAuthenticationManager to RemoteAuthenticationUtil
This commit is contained in:
@@ -2,6 +2,7 @@ package ltd.hlaeja.listener
|
|||||||
|
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import ltd.hlaeja.library.accountRegistry.event.AccountMessage
|
import ltd.hlaeja.library.accountRegistry.event.AccountMessage
|
||||||
|
import ltd.hlaeja.service.RedisSessionService
|
||||||
import org.apache.kafka.clients.consumer.ConsumerRecord
|
import org.apache.kafka.clients.consumer.ConsumerRecord
|
||||||
import org.springframework.kafka.annotation.KafkaListener
|
import org.springframework.kafka.annotation.KafkaListener
|
||||||
import org.springframework.stereotype.Component
|
import org.springframework.stereotype.Component
|
||||||
@@ -9,10 +10,15 @@ import org.springframework.stereotype.Component
|
|||||||
val log = KotlinLogging.logger {}
|
val log = KotlinLogging.logger {}
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
class AccountListener {
|
class AccountListener(
|
||||||
|
private val sessionService: RedisSessionService,
|
||||||
|
) {
|
||||||
|
|
||||||
@KafkaListener(topics = ["account"])
|
@KafkaListener(topics = ["account"])
|
||||||
fun handleRemoteAccountEvent(record: ConsumerRecord<String, AccountMessage>) {
|
fun handleRemoteAccountEvent(record: ConsumerRecord<String, AccountMessage>) {
|
||||||
log.trace { "Received event: ${record.key()} for user: ${record.value().userId}" }
|
log.trace { "Received event: ${record.key()} for user: ${record.value().userId}" }
|
||||||
|
if (record.key() == "change" && record.value().change.any { it in setOf("enabled", "username", "roles") }) {
|
||||||
|
sessionService.updateUser(record.value().userId).subscribe()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
package ltd.hlaeja.security.manager
|
package ltd.hlaeja.security.manager
|
||||||
|
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import io.jsonwebtoken.Claims
|
|
||||||
import io.jsonwebtoken.Jws
|
|
||||||
import io.jsonwebtoken.JwtException
|
import io.jsonwebtoken.JwtException
|
||||||
import java.util.UUID
|
|
||||||
import ltd.hlaeja.jwt.service.PublicJwtService
|
import ltd.hlaeja.jwt.service.PublicJwtService
|
||||||
import ltd.hlaeja.security.user.RemoteAuthentication
|
|
||||||
import ltd.hlaeja.security.user.RemoteUserDetail
|
|
||||||
import ltd.hlaeja.service.AccountRegistryService
|
import ltd.hlaeja.service.AccountRegistryService
|
||||||
|
import ltd.hlaeja.util.RemoteAuthenticationUtil
|
||||||
import ltd.hlaeja.util.toAuthenticationRequest
|
import ltd.hlaeja.util.toAuthenticationRequest
|
||||||
import org.springframework.context.ApplicationEventPublisher
|
import org.springframework.context.ApplicationEventPublisher
|
||||||
import org.springframework.security.authentication.AuthenticationServiceException
|
import org.springframework.security.authentication.AuthenticationServiceException
|
||||||
@@ -17,7 +13,6 @@ import org.springframework.security.authentication.event.AuthenticationFailureBa
|
|||||||
import org.springframework.security.authentication.event.AuthenticationSuccessEvent
|
import org.springframework.security.authentication.event.AuthenticationSuccessEvent
|
||||||
import org.springframework.security.core.Authentication
|
import org.springframework.security.core.Authentication
|
||||||
import org.springframework.security.core.AuthenticationException
|
import org.springframework.security.core.AuthenticationException
|
||||||
import org.springframework.security.core.authority.SimpleGrantedAuthority
|
|
||||||
import org.springframework.stereotype.Component
|
import org.springframework.stereotype.Component
|
||||||
import reactor.core.publisher.Mono
|
import reactor.core.publisher.Mono
|
||||||
|
|
||||||
@@ -34,7 +29,7 @@ class RemoteAuthenticationManager(
|
|||||||
authentication: Authentication,
|
authentication: Authentication,
|
||||||
): Mono<Authentication> = accountRegistryService.authenticate(authentication.toAuthenticationRequest())
|
): Mono<Authentication> = accountRegistryService.authenticate(authentication.toAuthenticationRequest())
|
||||||
.map(::processToken)
|
.map(::processToken)
|
||||||
.doOnNext { publisher.publishEvent(AuthenticationSuccessEvent(it)) }
|
.doOnNext { publisher.publishEvent(AuthenticationSuccessEvent(it)) }
|
||||||
.doOnError { ex ->
|
.doOnError { ex ->
|
||||||
if (ex is AuthenticationException) {
|
if (ex is AuthenticationException) {
|
||||||
publisher.publishEvent(AuthenticationFailureBadCredentialsEvent(authentication, ex))
|
publisher.publishEvent(AuthenticationFailureBadCredentialsEvent(authentication, ex))
|
||||||
@@ -44,7 +39,7 @@ class RemoteAuthenticationManager(
|
|||||||
private fun processToken(
|
private fun processToken(
|
||||||
response: ltd.hlaeja.library.accountRegistry.Authentication.Response,
|
response: ltd.hlaeja.library.accountRegistry.Authentication.Response,
|
||||||
): Authentication = try {
|
): Authentication = try {
|
||||||
publicJwtService.verify(response.token) { claims -> makeRemoteAuthentication(claims) }
|
publicJwtService.verify(response.token) { claims -> RemoteAuthenticationUtil.fromJwtClaims(claims) }
|
||||||
} catch (e: JwtException) {
|
} catch (e: JwtException) {
|
||||||
throw "An error occurred while processing token: ${e.message}"
|
throw "An error occurred while processing token: ${e.message}"
|
||||||
.let {
|
.let {
|
||||||
@@ -52,26 +47,4 @@ class RemoteAuthenticationManager(
|
|||||||
AuthenticationServiceException(it, e)
|
AuthenticationServiceException(it, e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun makeRemoteAuthentication(
|
|
||||||
claims: Jws<Claims>,
|
|
||||||
) = RemoteAuthentication(
|
|
||||||
makeRemoteUserDetail(claims),
|
|
||||||
makeSimpleGrantedAuthorities(claims),
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun makeSimpleGrantedAuthorities(
|
|
||||||
claims: Jws<Claims>,
|
|
||||||
): MutableList<SimpleGrantedAuthority> = (claims.payload["role"] as String)
|
|
||||||
.split(",")
|
|
||||||
.map { SimpleGrantedAuthority("ROLE_$it") }
|
|
||||||
.toMutableList()
|
|
||||||
|
|
||||||
private fun makeRemoteUserDetail(
|
|
||||||
claims: Jws<Claims>,
|
|
||||||
): RemoteUserDetail = RemoteUserDetail(
|
|
||||||
UUID.fromString(claims.payload["id"] as String),
|
|
||||||
claims.payload["username"] as String,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
48
src/main/kotlin/ltd/hlaeja/service/RedisSessionService.kt
Normal file
48
src/main/kotlin/ltd/hlaeja/service/RedisSessionService.kt
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package ltd.hlaeja.service
|
||||||
|
|
||||||
|
import java.util.UUID
|
||||||
|
import ltd.hlaeja.security.user.RemoteAuthentication
|
||||||
|
import ltd.hlaeja.util.RemoteAuthenticationUtil
|
||||||
|
import org.springframework.security.core.context.SecurityContextImpl
|
||||||
|
import org.springframework.session.data.redis.ReactiveRedisIndexedSessionRepository
|
||||||
|
import org.springframework.session.data.redis.ReactiveRedisIndexedSessionRepository.RedisSession
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import reactor.core.publisher.Flux
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class RedisSessionService(
|
||||||
|
private val redisSessionRepository: ReactiveRedisIndexedSessionRepository,
|
||||||
|
private val accountRegistryService: AccountRegistryService,
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun updateUser(
|
||||||
|
user: UUID,
|
||||||
|
): Flux<RedisSession> = findByUser(user)
|
||||||
|
.flatMapMany { map ->
|
||||||
|
getUserAccount(user).flatMapMany { response ->
|
||||||
|
Flux.fromIterable(map.values).map { session -> session to response }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.flatMap { (session: RedisSession, response: RemoteAuthentication) ->
|
||||||
|
session.setAttribute("SPRING_SECURITY_CONTEXT", SecurityContextImpl(response))
|
||||||
|
save(session)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getUserAccount(
|
||||||
|
user: UUID,
|
||||||
|
) = accountRegistryService.getAccount(user)
|
||||||
|
.map(RemoteAuthenticationUtil::fromAccountResponse)
|
||||||
|
|
||||||
|
private fun findByUser(
|
||||||
|
user: UUID,
|
||||||
|
): Mono<MutableMap<String, RedisSession>> = redisSessionRepository.findByPrincipalName(user.toString())
|
||||||
|
.doOnNext { ltd.hlaeja.listener.log.trace { "Found ${it.size} session(s) for user $user" } }
|
||||||
|
.filter { map -> map.isNotEmpty() }
|
||||||
|
|
||||||
|
private fun save(
|
||||||
|
session: RedisSession,
|
||||||
|
): Mono<RedisSession> = redisSessionRepository.save(session)
|
||||||
|
.thenReturn(session)
|
||||||
|
.doOnNext { ltd.hlaeja.listener.log.trace { "Save session: ${it.id}" } }
|
||||||
|
}
|
||||||
44
src/main/kotlin/ltd/hlaeja/util/RemoteAuthenticationUtil.kt
Normal file
44
src/main/kotlin/ltd/hlaeja/util/RemoteAuthenticationUtil.kt
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package ltd.hlaeja.util
|
||||||
|
|
||||||
|
import io.jsonwebtoken.Claims
|
||||||
|
import io.jsonwebtoken.Jws
|
||||||
|
import java.util.UUID
|
||||||
|
import ltd.hlaeja.library.accountRegistry.Account
|
||||||
|
import ltd.hlaeja.security.user.RemoteAuthentication
|
||||||
|
import ltd.hlaeja.security.user.RemoteUserDetail
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority
|
||||||
|
|
||||||
|
object RemoteAuthenticationUtil {
|
||||||
|
|
||||||
|
fun fromJwtClaims(
|
||||||
|
claims: Jws<Claims>,
|
||||||
|
): RemoteAuthentication = makeRemoteAuthentication(
|
||||||
|
id = UUID.fromString(claims.payload["id"] as String),
|
||||||
|
username = claims.payload["username"] as String,
|
||||||
|
roles = (claims.payload["role"] as String).split(","),
|
||||||
|
authenticated = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun fromAccountResponse(
|
||||||
|
response: Account.Response,
|
||||||
|
): RemoteAuthentication = makeRemoteAuthentication(
|
||||||
|
id = response.id,
|
||||||
|
username = response.username,
|
||||||
|
roles = response.roles,
|
||||||
|
authenticated = response.enabled,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun makeRemoteAuthentication(
|
||||||
|
id: UUID,
|
||||||
|
username: String,
|
||||||
|
roles: List<String>,
|
||||||
|
authenticated: Boolean,
|
||||||
|
) = RemoteAuthentication(
|
||||||
|
remoteUserDetail = RemoteUserDetail(
|
||||||
|
id = id,
|
||||||
|
username = username,
|
||||||
|
),
|
||||||
|
authorities = roles.map { SimpleGrantedAuthority("ROLE_$it") }.toMutableList(),
|
||||||
|
authenticated = authenticated,
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user