Это репост со страницы проблем Github.
Запутался, почему не происходит изменения поведения при добавлении элемента управления сеансом параллелизма с сеансом Spring с этой документацией
Версии:
- Spring Security 5.2.1.RELEASE
- Spring Session-JDB C 2.2.0.RELEASE
- Пружинный ботинок 2.2.4.RELEASE
Фрагменты кода из нашего кода:
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)
}
}
JsonLoginFilter.kt
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
Таким образом, мое ожидаемое поведение, основанное на коде и документации, было бы, когда есть текущий пользователь входа в систему, тогда другой пользователь входит в другой браузер, это должно предотвратить вход с помощью maxSessionPreventsLogin(true)
.
Таким образом, поведение фактического в приведенном выше коде состоит в том, что 2 браузера с одним и тем же пользователем могут войти в систему, чего я не ожидал, так как я следовал за документами. Я не совсем уверен, что что-то упустил, так как я впервые сталкиваюсь с Spring Session и Spring Security. Также обратите внимание, что сеансы хранятся в БД, а не в памяти.
Мне бы очень хотелось, если бы кто-то указал мне на шаги по правильной настройке сеанса Spring, или, возможно, документация должна иметь Предшественник о том, что настраивать, прежде чем делать это, или что, когда не применимо.
Спасибо