Grails Spring Security 最大并发会话

Posted

技术标签:

【中文标题】Grails Spring Security 最大并发会话【英文标题】:Grails Spring Security max concurrent session 【发布时间】:2016-04-22 00:12:14 【问题描述】:

我有带有 spring 安全插件 (2.0-RC5) 的 grails 2.5.1 应用程序。我想阻止每个用户的当前会话数。我读了一些博客,但它不起作用。(http://www.block-consult.com/blog/2012/01/20/restricting-concurrent-user-sessions-in-grails-2-using-spring-security-core-plugin/) 我的资源.groovy

beans = 
  sessionRegistry(SessionRegistryImpl)

    concurrencyFilter(ConcurrentSessionFilter,sessionRegistry,'/main/index')
        logoutHandlers = [ref("rememberMeServices"), ref("securityContextLogoutHandler")]
    
    concurrentSessionControlStrategy(ConcurrentSessionControlAuthenticationStrategy, sessionRegistry) 
        exceptionIfMaximumExceeded = true
        maximumSessions = 1

    

在我的 boostrap.groovy 中

 def init =  servletContext ->
    SpringSecurityUtils.clientRegisterFilter('concurrencyFilter', SecurityFilterPosition.CONCURRENT_SESSION_FILTER)
  

我的 config.groovy 我已经添加了这个:

grails.plugin.springsecurity.useHttpSessionEventPublisher = true

谢谢..

【问题讨论】:

【参考方案1】:

首先,如果您决定继续使用我的解决方案,请让我警告您。

SessionRegistryImpl 不可扩展。您需要根据您的扩​​展计划(例如数据网格)创建自己的可扩展实现。会话复制还不够。 目前,默认注销处理程序没有正确删除 SessionRegistry。所以我创建了一个名为 CustomSessionLogoutHandler 的示例注销处理程序。 您必须覆盖 grails spring-security-core 登录控制器来处理 SessionAuthenticationException。 您可以将可以登录 maximumSessions = 1 的用户数更改为 -1 以获得无限会话。

首先,在 resources.groovy 中

import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy;
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy;
import com.basic.CustomSessionLogoutHandler


// Place your Spring DSL code here
beans = 

sessionRegistry(SessionRegistryImpl)

customSessionLogoutHandler(CustomSessionLogoutHandler,ref('sessionRegistry')    )

concurrentSessionControlAuthenticationStrategy(ConcurrentSessionControlAuthenticationStrategy,ref('sessionRegistry'))
    exceptionIfMaximumExceeded = true
    maximumSessions = 1


sessionFixationProtectionStrategy(SessionFixationProtectionStrategy)
    migrateSessionAttributes = true
    alwaysCreateSession = true

registerSessionAuthenticationStrategy(RegisterSessionAuthenticationStrategy,ref('sessionRegistry'))

sessionAuthenticationStrategy(CompositeSessionAuthenticationStrategy,[ref('concurrentSessionControlAuthenticationStrategy'),ref('sessionFixationProtectionStrategy'),ref('registerSessionAuthenticationStrategy')])


在 config.groovy 中确保 customSessionLogoutHandler 位于 securityContextLogoutHandler 之前:

grails.plugin.springsecurity.logout.handlerNames = ['customSessionLogoutHandler','securityContextLogoutHandler'] 

ConcurrentSessionControlAuthenticationStrategy 使用这个 i18n。因此,您可以使用您的语言:

ConcurrentSessionControlAuthenticationStrategy.exceededAllowed = Maximum sessions for this principal exceeded. 0

这是我的示例CustomSessionLogoutHandler,您可以将其保存在 src/groovy/com/basic/CustomSessionLogoutHandler.groovy 中:

/*
* Copyright 2002-2013 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.basic;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.util.Assert;
import org.springframework.security.core.session.SessionRegistry;

/**
 * @link CustomSessionLogoutHandler is in charge of removing the @link SessionRegistry upon logout. A
 * new @link SessionRegistry will then be generated by the framework upon the next request.
 *
 * @author Mohd Qusyairi
 * @since 0.1
 */
public final class CustomSessionLogoutHandler implements LogoutHandler 
    private final SessionRegistry sessionRegistry;

    /**
     * Creates a new instance
     * @param sessionRegistry the @link SessionRegistry to use
     */
    public CustomSessionLogoutHandler(SessionRegistry sessionRegistry) 
        Assert.notNull(sessionRegistry, "sessionRegistry cannot be null");
        this.sessionRegistry = sessionRegistry;
    

    /**
     * Clears the @link SessionRegistry
     *
     * @see org.springframework.security.web.authentication.logout.LogoutHandler#logout(javax.servlet.http.HttpServletRequest,
     * javax.servlet.http.HttpServletResponse,
     * org.springframework.security.core.Authentication)
     */
    public void logout(HttpServletRequest request, HttpServletResponse response,
            Authentication authentication) 
        this.sessionRegistry.removeSessionInformation(request.getSession().getId());
    

如果您也需要,我的示例登录控制器(我从源代码复制)。只需在项目中另存为普通控制器,因为它将覆盖默认值。当我处理 SessionAuthenticationException 时,请参见下面的第 115 行:

/* Copyright 2013-2016 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.basic

import grails.converters.JSON
import org.springframework.security.access.annotation.Secured
import org.springframework.security.authentication.AccountExpiredException
import org.springframework.security.authentication.AuthenticationTrustResolver
import org.springframework.security.authentication.CredentialsExpiredException
import org.springframework.security.authentication.DisabledException
import org.springframework.security.authentication.LockedException
import org.springframework.security.core.Authentication
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.web.WebAttributes
import org.springframework.security.web.authentication.session.SessionAuthenticationException
import javax.servlet.http.HttpServletResponse
import grails.plugin.springsecurity.SpringSecurityUtils

@Secured('permitAll')
class LoginController 

    /** Dependency injection for the authenticationTrustResolver. */
    AuthenticationTrustResolver authenticationTrustResolver

    /** Dependency injection for the springSecurityService. */
    def springSecurityService

    /** Default action; redirects to 'defaultTargetUrl' if logged in, /login/auth otherwise. */
    def index() 
        if (springSecurityService.isLoggedIn()) 
            redirect uri: conf.successHandler.defaultTargetUrl
        
        else 
            redirect action: 'auth', params: params
        
    

    /** Show the login page. */
    def auth() 

        def conf = getConf()

        if (springSecurityService.isLoggedIn()) 
            redirect uri: conf.successHandler.defaultTargetUrl
            return
        

        String postUrl = request.contextPath + conf.apf.filterProcessesUrl
        render view: 'auth', model: [postUrl: postUrl,
                                     rememberMeParameter: conf.rememberMe.parameter,
                                     usernameParameter: conf.apf.usernameParameter,
                                     passwordParameter: conf.apf.passwordParameter,
                                     gspLayout: conf.gsp.layoutAuth]
    

    /** The redirect action for Ajax requests. */
    def authAjax() 
        response.setHeader 'Location', conf.auth.ajaxLoginFormUrl
        render(status: HttpServletResponse.SC_UNAUTHORIZED, text: 'Unauthorized')
    

    /** Show denied page. */
    def denied() 
        if (springSecurityService.isLoggedIn() && authenticationTrustResolver.isRememberMe(authentication)) 
            // have cookie but the page is guarded with IS_AUTHENTICATED_FULLY (or the equivalent expression)
            redirect action: 'full', params: params
            return
        

        [gspLayout: conf.gsp.layoutDenied]
    

    /** Login page for users with a remember-me cookie but accessing a IS_AUTHENTICATED_FULLY page. */
    def full() 
        def conf = getConf()
        render view: 'auth', params: params,
               model: [hasCookie: authenticationTrustResolver.isRememberMe(authentication),
                       postUrl: request.contextPath + conf.apf.filterProcessesUrl,
                       rememberMeParameter: conf.rememberMe.parameter,
                       usernameParameter: conf.apf.usernameParameter,
                       passwordParameter: conf.apf.passwordParameter,
                       gspLayout: conf.gsp.layoutAuth]
    

    /** Callback after a failed login. Redirects to the auth page with a warning message. */
    def authfail() 

        String msg = ''
        def exception = session[WebAttributes.AUTHENTICATION_EXCEPTION]
        if (exception) 
            if (exception instanceof AccountExpiredException) 
                msg = message(code: 'springSecurity.errors.login.expired')
            
            else if (exception instanceof CredentialsExpiredException) 
                msg = message(code: 'springSecurity.errors.login.passwordExpired')
            
            else if (exception instanceof DisabledException) 
                msg = message(code: 'springSecurity.errors.login.disabled')
            
            else if (exception instanceof LockedException) 
                msg = message(code: 'springSecurity.errors.login.locked')
            
            else if (exception instanceof SessionAuthenticationException)
                msg = exception.getMessage()
            
            else 
                msg = message(code: 'springSecurity.errors.login.fail')
            
        

        if (springSecurityService.isAjax(request)) 
            render([error: msg] as JSON)
        
        else 
            flash.message = msg
            redirect action: 'auth', params: params
        
    

    /** The Ajax success redirect url. */
    def ajaxSuccess() 
        render([success: true, username: authentication.name] as JSON)
    

    /** The Ajax denied redirect url. */
    def ajaxDenied() 
        render([error: 'access denied'] as JSON)
    

    protected Authentication getAuthentication() 
        SecurityContextHolder.context?.authentication
    

    protected ConfigObject getConf() 
        SpringSecurityUtils.securityConfig
    

【讨论】:

这个springSecurityService是哪里来的?我有一个不包含它的 Spring Boot/Security 应用程序。 @Quchie 在此用于禁用使用相同用户名的另一个用户登录?禁用多重登录怎么样? @akiong 是的,这将使用用户名阻止同一用户登录。 @sonoerin 抱歉,这是针对 grails 2.x 而不是 spring boot。 @akiong 嗨,我认为你不再需要 concurrencyFilter 了。如果删除任何 concurrentSessioncontrolAuthenticationStrategy,则可以忽略 expiredUrl。您的目标应该是删除 sessionRegistry 中的相同用户,以确保只有 1 个具有相同用户名的用户可以登录。

以上是关于Grails Spring Security 最大并发会话的主要内容,如果未能解决你的问题,请参考以下文章

Grails - grails-spring-security-rest - 无法从 application.yml 加载 jwt 机密

grails-spring-security-rest 插件和悲观锁定

Grails + spring-security-core:用户登录后如何分配角色?

Grails Spring Security注释问题

Grails spring-security-oauth-google:如何设置

Grails Spring Security 插件网址