mirror of
https://github.com/gotson/komga.git
synced 2026-05-09 05:10:19 +02: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("plugin.spring")
|
||||||
kotlin("kapt")
|
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("com.gorylenko.gradle-git-properties") version "2.3.1"
|
||||||
id("nu.studer.jooq") version "5.2.2"
|
id("nu.studer.jooq") version "5.2.2"
|
||||||
id("org.flywaydb.flyway") version "7.10.0"
|
id("org.flywaydb.flyway") version "7.10.0"
|
||||||
|
|
@ -22,7 +22,7 @@ dependencies {
|
||||||
implementation(kotlin("stdlib-jdk8"))
|
implementation(kotlin("stdlib-jdk8"))
|
||||||
implementation(kotlin("reflect"))
|
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-web")
|
||||||
implementation("org.springframework.boot:spring-boot-starter-validation")
|
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-thymeleaf")
|
||||||
implementation("org.springframework.boot:spring-boot-starter-artemis")
|
implementation("org.springframework.boot:spring-boot-starter-artemis")
|
||||||
implementation("org.springframework.boot:spring-boot-starter-jooq")
|
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")
|
implementation("org.apache.activemq:artemis-jms-server")
|
||||||
|
|
||||||
|
|
@ -106,7 +107,7 @@ dependencies {
|
||||||
|
|
||||||
testImplementation("com.tngtech.archunit:archunit-junit5:0.19.0")
|
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"
|
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) {
|
private fun expireSessions(user: KomgaUser) {
|
||||||
logger.info { "Expiring all sessions for user: ${user.email}" }
|
logger.info { "Expiring all sessions for user: ${user.email}" }
|
||||||
sessionRegistry.allPrincipals
|
sessionRegistry
|
||||||
.filterIsInstance<KomgaPrincipal>()
|
.getAllSessions(KomgaPrincipal(user), false)
|
||||||
.filter { it.user.id == user.id }
|
|
||||||
.flatMap { sessionRegistry.getAllSessions(it, false) }
|
|
||||||
.forEach {
|
.forEach {
|
||||||
logger.info { "Expiring session: ${it.sessionId}" }
|
logger.info { "Expiring session: ${it.sessionId}" }
|
||||||
it.expireNow()
|
it.expireNow()
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
package org.gotson.komga.infrastructure.configuration
|
package org.gotson.komga.infrastructure.configuration
|
||||||
|
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||||
|
import org.springframework.boot.convert.DurationUnit
|
||||||
import org.springframework.stereotype.Component
|
import org.springframework.stereotype.Component
|
||||||
import org.springframework.validation.annotation.Validated
|
import org.springframework.validation.annotation.Validated
|
||||||
|
import java.time.Duration
|
||||||
|
import java.time.temporal.ChronoUnit
|
||||||
import javax.validation.constraints.NotBlank
|
import javax.validation.constraints.NotBlank
|
||||||
import javax.validation.constraints.Positive
|
import javax.validation.constraints.Positive
|
||||||
|
|
||||||
|
|
@ -24,6 +27,9 @@ class KomgaProperties {
|
||||||
|
|
||||||
var rememberMe = RememberMe()
|
var rememberMe = RememberMe()
|
||||||
|
|
||||||
|
@DurationUnit(ChronoUnit.SECONDS)
|
||||||
|
var sessionTimeout: Duration = Duration.ofMinutes(30)
|
||||||
|
|
||||||
var nativeWebp: Boolean = true
|
var nativeWebp: Boolean = true
|
||||||
|
|
||||||
var database = Database()
|
var database = Database()
|
||||||
|
|
|
||||||
|
|
@ -18,13 +18,11 @@ import org.springframework.web.cors.UrlBasedCorsConfigurationSource
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
class CorsConfiguration(
|
class CorsConfiguration {
|
||||||
private val komgaProperties: KomgaProperties
|
|
||||||
) {
|
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@Conditional(CorsAllowedOriginsPresent::class)
|
@Conditional(CorsAllowedOriginsPresent::class)
|
||||||
fun corsConfigurationSource(): UrlBasedCorsConfigurationSource =
|
fun corsConfigurationSource(sessionHeaderName: String, komgaProperties: KomgaProperties): UrlBasedCorsConfigurationSource =
|
||||||
UrlBasedCorsConfigurationSource().apply {
|
UrlBasedCorsConfigurationSource().apply {
|
||||||
registerCorsConfiguration(
|
registerCorsConfiguration(
|
||||||
"/**",
|
"/**",
|
||||||
|
|
@ -33,6 +31,7 @@ class CorsConfiguration(
|
||||||
allowedMethods = HttpMethod.values().map { it.name }
|
allowedMethods = HttpMethod.values().map { it.name }
|
||||||
allowCredentials = true
|
allowCredentials = true
|
||||||
addExposedHeader(HttpHeaders.CONTENT_DISPOSITION)
|
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.oidc.user.OidcUser
|
||||||
import org.springframework.security.oauth2.core.user.OAuth2User
|
import org.springframework.security.oauth2.core.user.OAuth2User
|
||||||
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler
|
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler
|
||||||
|
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource
|
||||||
import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices
|
import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices
|
||||||
|
|
||||||
private val logger = KotlinLogging.logger {}
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
@ -32,14 +33,14 @@ class SecurityConfiguration(
|
||||||
private val oauth2UserService: OAuth2UserService<OAuth2UserRequest, OAuth2User>,
|
private val oauth2UserService: OAuth2UserService<OAuth2UserRequest, OAuth2User>,
|
||||||
private val oidcUserService: OAuth2UserService<OidcUserRequest, OidcUser>,
|
private val oidcUserService: OAuth2UserService<OidcUserRequest, OidcUser>,
|
||||||
private val sessionRegistry: SessionRegistry,
|
private val sessionRegistry: SessionRegistry,
|
||||||
|
private val sessionCookieName: String,
|
||||||
|
private val userAgentWebAuthenticationDetailsSource: WebAuthenticationDetailsSource,
|
||||||
clientRegistrationRepository: InMemoryClientRegistrationRepository?,
|
clientRegistrationRepository: InMemoryClientRegistrationRepository?,
|
||||||
) : WebSecurityConfigurerAdapter() {
|
) : WebSecurityConfigurerAdapter() {
|
||||||
|
|
||||||
private val oauth2Enabled = clientRegistrationRepository != null
|
private val oauth2Enabled = clientRegistrationRepository != null
|
||||||
|
|
||||||
override fun configure(http: HttpSecurity) {
|
override fun configure(http: HttpSecurity) {
|
||||||
val userAgentWebAuthenticationDetailsSource = UserAgentWebAuthenticationDetailsSource()
|
|
||||||
|
|
||||||
http
|
http
|
||||||
.cors {}
|
.cors {}
|
||||||
.csrf { it.disable() }
|
.csrf { it.disable() }
|
||||||
|
|
@ -52,6 +53,7 @@ class SecurityConfiguration(
|
||||||
it.antMatchers(
|
it.antMatchers(
|
||||||
"/api/v1/claim",
|
"/api/v1/claim",
|
||||||
"/api/v1/oauth2/providers",
|
"/api/v1/oauth2/providers",
|
||||||
|
"/set-cookie",
|
||||||
).permitAll()
|
).permitAll()
|
||||||
|
|
||||||
// all other endpoints are restricted to authenticated users
|
// all other endpoints are restricted to authenticated users
|
||||||
|
|
@ -72,7 +74,7 @@ class SecurityConfiguration(
|
||||||
|
|
||||||
.logout {
|
.logout {
|
||||||
it.logoutUrl("/api/v1/users/logout")
|
it.logoutUrl("/api/v1/users/logout")
|
||||||
it.deleteCookies("JSESSIONID")
|
it.deleteCookies(sessionCookieName)
|
||||||
it.invalidateHttpSession(true)
|
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
|
package org.gotson.komga.infrastructure.security
|
||||||
|
|
||||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource
|
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
import javax.servlet.http.HttpServletRequest
|
import javax.servlet.http.HttpServletRequest
|
||||||
|
|
||||||
|
@Component
|
||||||
class UserAgentWebAuthenticationDetailsSource : WebAuthenticationDetailsSource() {
|
class UserAgentWebAuthenticationDetailsSource : WebAuthenticationDetailsSource() {
|
||||||
override fun buildDetails(context: HttpServletRequest): UserAgentWebAuthenticationDetails =
|
override fun buildDetails(context: HttpServletRequest): UserAgentWebAuthenticationDetails =
|
||||||
UserAgentWebAuthenticationDetails(context)
|
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
|
# web: DEBUG
|
||||||
# com.zaxxer.hikari: DEBUG
|
# com.zaxxer.hikari: DEBUG
|
||||||
# org.springframework.jms: DEBUG
|
# org.springframework.jms: DEBUG
|
||||||
|
# org.springframework.boot.autoconfigure: DEBUG
|
||||||
# org.springframework.security.web.FilterChainProxy: DEBUG
|
# org.springframework.security.web.FilterChainProxy: DEBUG
|
||||||
logback:
|
logback:
|
||||||
rollingpolicy:
|
rollingpolicy:
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ komga:
|
||||||
file: \${user.home}/.komga/database.sqlite
|
file: \${user.home}/.komga/database.sqlite
|
||||||
lucene:
|
lucene:
|
||||||
data-directory: \${user.home}/.komga/lucene
|
data-directory: \${user.home}/.komga/lucene
|
||||||
|
session-timeout: 7d
|
||||||
|
|
||||||
spring:
|
spring:
|
||||||
flyway:
|
flyway:
|
||||||
|
|
@ -35,10 +36,6 @@ spring:
|
||||||
web:
|
web:
|
||||||
resources:
|
resources:
|
||||||
add-mappings: false
|
add-mappings: false
|
||||||
session:
|
|
||||||
store-type: none
|
|
||||||
jdbc:
|
|
||||||
initialize-schema: never
|
|
||||||
|
|
||||||
server:
|
server:
|
||||||
servlet.session.timeout: 7d
|
servlet.session.timeout: 7d
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue