Kotlin Spring会话已定义安全配置上的并发会话控制

Kotlin Spring会话已定义安全配置上的并发会话控制,spring,spring-security,spring-jdbc,spring-session,Spring,Spring Security,Spring Jdbc,Spring Session,这是Github问题页面上的重新发布 困惑于为什么在使用此命令添加带有Spring会话的并发会话控件时没有行为更改 版本: Spring Security5.2.1.发布 春季会议JDBC2.2.0.发布版 弹簧靴2.2.4.释放 我们代码中的代码片段: SecurityConfig.kt @EnableWebSecurity @Profile("auth") @Configuration class SecurityConfig<S: Session> : WebSecurit

这是Github问题页面上的重新发布

困惑于为什么在使用此命令添加带有Spring会话的并发会话控件时没有行为更改

版本:
  • Spring Security5.2.1.发布
  • 春季会议JDBC2.2.0.发布版
  • 弹簧靴2.2.4.释放
我们代码中的代码片段:

SecurityConfig.kt

@EnableWebSecurity
@Profile("auth")
@Configuration
class SecurityConfig<S: Session> : WebSecurityConfigurerAdapter() {
     //<some-code-here>
    @Autowired
    private lateinit var sessionRepository: FindByIndexNameSessionRepository<S>

override fun configure(http: HttpSecurity) {
        /*
            TODO  enforce requiring application/json content type everywhere (apparently except file upload)
        */

        val publicPaths = arrayOf(
            "/api/v1/centralPortal/current/forgot-password",
            "/api/v1/centralPortal/current/reset-password/*",
            "/api/v1/centralPortal/current/onboard/*",
            "/login",
            "/api/v1/public/**" // CSP reporting is there, needs CSRF disabled
        )

        val filter = JsonLoginFilter(objectMapper, audit, rateLimiter, userManagementService, getAuthMode(env))
        filter.rateLimitPPS = rateLimitPermits / rateLimitSeconds
        filter.setSessionAuthenticationStrategy(sas)
        filter.setAuthenticationManager(authenticationManager())
        filter.setAuthenticationSuccessHandler { request, response, _ ->
            val csrfToken = request.getAttribute(CsrfToken::class.java.name) as CsrfToken
            response.addHeader(csrfToken.headerName, csrfToken.token)
        }
        filter.setAuthenticationFailureHandler { request, response, exception ->
            excHandlerAdvice.handleUnauthenticated(request, response, exception, filter.obtainRetries()) }

        http
            .cors().and()
            .httpBasic().disable()
            .formLogin().disable()
            .rememberMe().disable()
            .headers()
                // do custom handling of the X-Frame-Options header because some pages need to be iframed
                .frameOptions().disable()
                .addHeaderWriter(ConfigurableFrameOptionsHeaderWriter("/assets/pdfjs/web/viewer.html", "/ws/frame.html"))
                .and()
            .csrf()
                .ignoringAntMatchers(*publicPaths)
                .and()
            .authorizeRequests()
                .antMatchers("/manifest.json", "/manifest.webmanifest").permitAll()
                .antMatchers("/", "/index.js", "/main.js", "/vendor.js", "/service-worker.js", "/workbox-v*/workbox-sw.js", "/precache.*.js", "/*.ico", "/ui/**", "/index.html").permitAll()
                .regexMatchers("^/\\w\\w/ui/.*$").permitAll()
                .antMatchers("/assets/**/*.webp", "/assets/**/*.png", "/assets/**/*.jpg", "/assets/**/*.svg", "/assets/**/*.gif").permitAll()
                .antMatchers("/assets/**/*.woff", "/assets/**/*.woff2", "/assets/**/*.ttf", "/assets/**/*.eot").permitAll()
                .antMatchers("/assets/**/*.css").permitAll()
                .antMatchers(*publicPaths).permitAll()
                .antMatchers("/img/logo.png").permitAll()
                .anyRequest().authenticated()
                .and()
            .exceptionHandling()
                .accessDeniedHandler { request, response, accessDeniedException -> excHandlerAdvice.handleAccessDenied(request, response, accessDeniedException) }
                .authenticationEntryPoint { request, response, authException -> excHandlerAdvice.handleUnauthenticated(request, response, authException) }
                .and()
            .addFilter(filter)
            .logout()
                .logoutSuccessHandler { request, response, auth -> Unit } // don't redirect
                .and()

        http.headers()
            .cacheControl().disable()
            .addHeaderWriter(CacheControlWriterWithWorkaround())

        // Concurrency control code
        http.sessionManagement()
            .maximumSessions(1)
            .maxSessionsPreventsLogin(true)
            .sessionRegistry(sessionRegistry())
    }

    private fun sessionRegistry(): SpringSessionBackedSessionRegistry<S> {
        return SpringSessionBackedSessionRegistry(this.sessionRepository)
    }
}
因此,我基于代码和文档的预期行为是,当有当前登录用户,然后另一个用户登录到另一个浏览器时,它应该阻止使用
maxSessionPreventsLogin(true)
登录

因此,上面代码的实际行为是,2个具有相同用户的浏览器可以登录,这是我没有预料到的,因为我遵循了文档。我不确定我是否遗漏了什么,因为这是我第一次遇到Spring会话和Spring Security。还请注意,会话存储在数据库中,而不是内存中

如果有人能告诉我正确的Spring会话配置的步骤,或者文档中应该有一个关于在执行此操作之前配置什么,或者什么时候不适用的方法的先兆,我会非常高兴


谢谢

JsonLoginFilter来自哪里?没有额外的过滤器,我能够让代码按预期工作。您可能希望尝试删除该筛选器,并查看问题是否仍然存在。这将有助于缩小问题的来源。@Eleftheria Stein Kousathan好主意,我将尝试从那里进行调试,我还编辑了上面的问题并添加了JsonLoginFilter代码。JsonLoginFilter是我们自定义身份验证所必需的,这扩展了
UsernamePasswordAuthenticationFilter
@EleftheriaStein KousathanaI请看您正在设置
filter.setSessionAuthenticationStrategy(sas)
SessionAuthenticationStrategy
保存您在
.sessionManagement()
中设置的信息。如果您覆盖它,那么它将不再有权访问
maximumSessions
maxSessionsPreventsLogin
@EleftheriaStein Kousathana谢谢,
SessionAuthenticationStrategy
当前设置为
SessionFixationProtectionStrategy
这超出了我对spring的了解(3周前开始),由于这已经在我们的安全配置中,我想这可能是我的即插即用。无论如何,我将尝试删除
。setSessionAuthenticationStrategy
,看看是否有什么作用。非常感谢。
data class UserLogin(val username: String, val password: String)

class JsonLoginFilter(private val objectMapper: ObjectMapper, val audit: AuditService?, val rateLimiter: RateLimiterAspect, val userManagementService: UserManagementService?, val authMode: AuthMode) : UsernamePasswordAuthenticationFilter() {

    var rateLimitPPS: Double = 1 / 2.0 // rate limiter permits per second default value

    private var _parsed: UserLogin? = null
    private val log = loggerFor<JsonLoginFilter>()

    fun parsedLogin(request: HttpServletRequest): UserLogin {
        val body = request.inputStream
        return objectMapper.readValue(body, UserLogin::class.java)
    }

    private fun loginAttempt(success: Boolean) {
        log.info("Login attempt username=${_parsed?.username} success=$success")
        if (success) {
            resetRetries()
        }
        audit?.eventWithPrincipal(
                AuditObjectCategory.USERS,
                "User",
                _parsed?.username,
                if (success) OtherActions.USER_LOGIN else OtherActions.USER_LOGIN_FAIL,
                _parsed?.username
        )
    }

    override fun attemptAuthentication(request: HttpServletRequest, response: HttpServletResponse?): Authentication {
        try {
            rateLimiter.rateLimit(request, "login", rateLimitPPS)
            _parsed = parsedLogin(request)
            val authResult = super.attemptAuthentication(request, response)
            loginAttempt(authResult != null)
            return authResult
        } catch (e: RateLimiterException) {
            throw e
        } catch (ex: AuthenticationException) {
            loginAttempt(false)
            if (authMode == AuthMode.BUILTIN) userManagementService?.checkUserRetries(_parsed!!.username)
            throw ex
        }
    }

    fun obtainRetries(): Int? {
        return if (authMode == AuthMode.BUILTIN) userManagementService?.getUserRetries(_parsed!!.username) else 0
    }

    fun resetRetries() {
        if (authMode == AuthMode.BUILTIN) {
            userManagementService?.resetUserRetries(_parsed!!.username)
        }
    }

    override fun obtainPassword(request: HttpServletRequest): String {
        return _parsed!!.password
    }

    override fun obtainUsername(request: HttpServletRequest): String {
        return _parsed!!.username
    }
}

spring.session.store-type=jdbc