mirror of
https://github.com/gotson/komga.git
synced 2025-12-20 07:23:34 +01:00
feat(api): more flexible session management
This commit is contained in:
parent
294b29d1d3
commit
a85b5f8d28
13 changed files with 127 additions and 34 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
drop table if exists SPRING_SESSION_ATTRIBUTES;
|
||||
drop table if exists SPRING_SESSION;
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue