feat(api): more flexible session management

This commit is contained in:
Gauthier Roebroeck 2021-09-30 21:11:13 +08:00
parent 294b29d1d3
commit a85b5f8d28
13 changed files with 127 additions and 34 deletions

View file

@ -7,7 +7,7 @@ plugins {
kotlin("plugin.spring")
kotlin("kapt")
}
id("org.springframework.boot") version "2.5.2"
id("org.springframework.boot") version "2.5.5"
id("com.gorylenko.gradle-git-properties") version "2.3.1"
id("nu.studer.jooq") version "5.2.2"
id("org.flywaydb.flyway") version "7.10.0"
@ -22,7 +22,7 @@ dependencies {
implementation(kotlin("stdlib-jdk8"))
implementation(kotlin("reflect"))
implementation(platform("org.springframework.boot:spring-boot-dependencies:2.5.2"))
implementation(platform("org.springframework.boot:spring-boot-dependencies:2.5.5"))
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-validation")
@ -32,9 +32,10 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
implementation("org.springframework.boot:spring-boot-starter-artemis")
implementation("org.springframework.boot:spring-boot-starter-jooq")
implementation("org.springframework.session:spring-session-jdbc")
implementation("org.springframework.session:spring-session-core")
implementation("com.github.gotson:spring-session-caffeine:1.0.3")
kapt("org.springframework.boot:spring-boot-configuration-processor:2.5.2")
kapt("org.springframework.boot:spring-boot-configuration-processor:2.5.5")
implementation("org.apache.activemq:artemis-jms-server")
@ -106,7 +107,7 @@ dependencies {
testImplementation("com.tngtech.archunit:archunit-junit5:0.19.0")
developmentOnly("org.springframework.boot:spring-boot-devtools:2.5.2")
developmentOnly("org.springframework.boot:spring-boot-devtools:2.5.5")
}
val webui = "$rootDir/komga-webui"

View file

@ -0,0 +1,2 @@
drop table if exists SPRING_SESSION_ATTRIBUTES;
drop table if exists SPRING_SESSION;

View file

@ -60,10 +60,8 @@ class KomgaUserLifecycle(
private fun expireSessions(user: KomgaUser) {
logger.info { "Expiring all sessions for user: ${user.email}" }
sessionRegistry.allPrincipals
.filterIsInstance<KomgaPrincipal>()
.filter { it.user.id == user.id }
.flatMap { sessionRegistry.getAllSessions(it, false) }
sessionRegistry
.getAllSessions(KomgaPrincipal(user), false)
.forEach {
logger.info { "Expiring session: ${it.sessionId}" }
it.expireNow()

View file

@ -1,8 +1,11 @@
package org.gotson.komga.infrastructure.configuration
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.convert.DurationUnit
import org.springframework.stereotype.Component
import org.springframework.validation.annotation.Validated
import java.time.Duration
import java.time.temporal.ChronoUnit
import javax.validation.constraints.NotBlank
import javax.validation.constraints.Positive
@ -24,6 +27,9 @@ class KomgaProperties {
var rememberMe = RememberMe()
@DurationUnit(ChronoUnit.SECONDS)
var sessionTimeout: Duration = Duration.ofMinutes(30)
var nativeWebp: Boolean = true
var database = Database()

View file

@ -18,13 +18,11 @@ import org.springframework.web.cors.UrlBasedCorsConfigurationSource
import java.util.Collections
@Configuration
class CorsConfiguration(
private val komgaProperties: KomgaProperties
) {
class CorsConfiguration {
@Bean
@Conditional(CorsAllowedOriginsPresent::class)
fun corsConfigurationSource(): UrlBasedCorsConfigurationSource =
fun corsConfigurationSource(sessionHeaderName: String, komgaProperties: KomgaProperties): UrlBasedCorsConfigurationSource =
UrlBasedCorsConfigurationSource().apply {
registerCorsConfiguration(
"/**",
@ -33,6 +31,7 @@ class CorsConfiguration(
allowedMethods = HttpMethod.values().map { it.name }
allowCredentials = true
addExposedHeader(HttpHeaders.CONTENT_DISPOSITION)
addExposedHeader(sessionHeaderName)
}
)
}

View file

@ -20,6 +20,7 @@ import org.springframework.security.oauth2.core.OAuth2AuthenticationException
import org.springframework.security.oauth2.core.oidc.user.OidcUser
import org.springframework.security.oauth2.core.user.OAuth2User
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource
import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices
private val logger = KotlinLogging.logger {}
@ -32,14 +33,14 @@ class SecurityConfiguration(
private val oauth2UserService: OAuth2UserService<OAuth2UserRequest, OAuth2User>,
private val oidcUserService: OAuth2UserService<OidcUserRequest, OidcUser>,
private val sessionRegistry: SessionRegistry,
private val sessionCookieName: String,
private val userAgentWebAuthenticationDetailsSource: WebAuthenticationDetailsSource,
clientRegistrationRepository: InMemoryClientRegistrationRepository?,
) : WebSecurityConfigurerAdapter() {
private val oauth2Enabled = clientRegistrationRepository != null
override fun configure(http: HttpSecurity) {
val userAgentWebAuthenticationDetailsSource = UserAgentWebAuthenticationDetailsSource()
http
.cors {}
.csrf { it.disable() }
@ -52,6 +53,7 @@ class SecurityConfiguration(
it.antMatchers(
"/api/v1/claim",
"/api/v1/oauth2/providers",
"/set-cookie",
).permitAll()
// all other endpoints are restricted to authenticated users
@ -72,7 +74,7 @@ class SecurityConfiguration(
.logout {
it.logoutUrl("/api/v1/users/logout")
it.deleteCookies("JSESSIONID")
it.deleteCookies(sessionCookieName)
it.invalidateHttpSession(true)
}

View file

@ -1,14 +0,0 @@
package org.gotson.komga.infrastructure.security
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.core.session.SessionRegistry
import org.springframework.security.core.session.SessionRegistryImpl
@Configuration
class SessionRegistryConfiguration {
@Bean
fun sessionRegistry(): SessionRegistry {
return SessionRegistryImpl()
}
}

View file

@ -1,8 +1,10 @@
package org.gotson.komga.infrastructure.security
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource
import org.springframework.stereotype.Component
import javax.servlet.http.HttpServletRequest
@Component
class UserAgentWebAuthenticationDetailsSource : WebAuthenticationDetailsSource() {
override fun buildDetails(context: HttpServletRequest): UserAgentWebAuthenticationDetails =
UserAgentWebAuthenticationDetails(context)

View file

@ -0,0 +1,45 @@
package org.gotson.komga.infrastructure.security.session
import com.github.gotson.spring.session.caffeine.CaffeineIndexedSessionRepository
import com.github.gotson.spring.session.caffeine.config.annotation.web.http.EnableCaffeineHttpSession
import org.gotson.komga.infrastructure.configuration.KomgaProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.core.session.SessionRegistry
import org.springframework.session.FindByIndexNameSessionRepository
import org.springframework.session.config.SessionRepositoryCustomizer
import org.springframework.session.security.SpringSessionBackedSessionRegistry
import org.springframework.session.web.http.CookieSerializer
import org.springframework.session.web.http.DefaultCookieSerializer
import org.springframework.session.web.http.HttpSessionIdResolver
@EnableCaffeineHttpSession
@Configuration
class SessionConfiguration {
@Bean
fun sessionCookieName() = "SESSION"
@Bean
fun sessionHeaderName() = "X-Auth-Token"
@Bean
fun cookieSerializer(sessionCookieName: String): CookieSerializer =
DefaultCookieSerializer().apply {
setCookieName(sessionCookieName)
}
@Bean
fun httpSessionIdResolver(sessionHeaderName: String, cookieSerializer: CookieSerializer): HttpSessionIdResolver =
SmartHttpSessionIdResolver(sessionHeaderName, cookieSerializer)
@Bean
fun customizeSessionRepository(komgaProperties: KomgaProperties) =
SessionRepositoryCustomizer<CaffeineIndexedSessionRepository>() {
it.setDefaultMaxInactiveInterval(komgaProperties.sessionTimeout.seconds.toInt())
}
@Bean
fun sessionRegistry(sessionRepository: FindByIndexNameSessionRepository<*>): SessionRegistry =
SpringSessionBackedSessionRegistry(sessionRepository)
}

View file

@ -0,0 +1,29 @@
package org.gotson.komga.infrastructure.security.session
import org.springframework.session.web.http.CookieHttpSessionIdResolver
import org.springframework.session.web.http.CookieSerializer
import org.springframework.session.web.http.HeaderHttpSessionIdResolver
import org.springframework.session.web.http.HttpSessionIdResolver
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
class SmartHttpSessionIdResolver(
private val sessionHeaderName: String,
cookieSerializer: CookieSerializer,
) : HttpSessionIdResolver {
private val cookie = CookieHttpSessionIdResolver().apply { setCookieSerializer(cookieSerializer) }
private val header = HeaderHttpSessionIdResolver(sessionHeaderName)
override fun resolveSessionIds(request: HttpServletRequest): List<String> =
request.getResolver().resolveSessionIds(request)
override fun setSessionId(request: HttpServletRequest, response: HttpServletResponse, sessionId: String) {
request.getResolver().setSessionId(request, response, sessionId)
}
override fun expireSession(request: HttpServletRequest, response: HttpServletResponse) {
request.getResolver().expireSession(request, response)
}
private fun HttpServletRequest.getResolver() = if (this.getHeader(sessionHeaderName) != null) header else cookie
}

View file

@ -0,0 +1,25 @@
package org.gotson.komga.interfaces.rest
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.session.web.http.CookieSerializer
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.ResponseStatus
import org.springframework.web.bind.annotation.RestController
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
import javax.servlet.http.HttpSession
@RestController
@RequestMapping(produces = [MediaType.APPLICATION_JSON_VALUE])
class LoginController(
private val cookieSerializer: CookieSerializer,
) {
@GetMapping("api/v1/login/set-cookie")
@ResponseStatus(HttpStatus.NO_CONTENT)
fun headerToCookie(request: HttpServletRequest, response: HttpServletResponse, session: HttpSession) {
cookieSerializer.writeCookieValue(CookieSerializer.CookieValue(request, response, session.id))
}
}

View file

@ -26,6 +26,7 @@ logging:
# web: DEBUG
# com.zaxxer.hikari: DEBUG
# org.springframework.jms: DEBUG
# org.springframework.boot.autoconfigure: DEBUG
# org.springframework.security.web.FilterChainProxy: DEBUG
logback:
rollingpolicy:

View file

@ -16,6 +16,7 @@ komga:
file: \${user.home}/.komga/database.sqlite
lucene:
data-directory: \${user.home}/.komga/lucene
session-timeout: 7d
spring:
flyway:
@ -35,10 +36,6 @@ spring:
web:
resources:
add-mappings: false
session:
store-type: none
jdbc:
initialize-schema: never
server:
servlet.session.timeout: 7d