具有并发会话的 grails 3

Posted

技术标签:

【中文标题】具有并发会话的 grails 3【英文标题】:grails 3 with concurrent session 【发布时间】:2017-02-06 01:48:00 【问题描述】:

我正在尝试从 2.1.1 升级我的项目。到 3.1.1

并发会话有一些问题 比如……

我在浏览器“chrome”上使用用户名“AAA”登录 然后其他用户在其他浏览器上使用用户名“AAA”再次登录,然后用户名“AAA”将在浏览器“chrome”上自动注销

..这是我的代码

登录控制器

package accounter

import com.vastpalaso.security.User
import grails.converters.JSON
import grails.plugin.springsecurity.SpringSecurityUtils
import org.slf4j.Logger
import org.slf4j.LoggerFactory
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 grails.plugin.springsecurity.SpringSecurityUtils
import org.springframework.security.web.authentication.session.SessionAuthenticationException

import javax.servlet.http.HttpServletResponse

@Secured('permitAll')
class LoginController 

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

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

    private static final Logger logger = LoggerFactory.getLogger(this)

    /** 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() 
        def username = session['SPRING_SECURITY_LAST_USERNAME']
        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()
                println "test"
            
            else 
                msg = message(code: 'springSecurity.errors.login.fail')
            
        

        try 
            boolean block = false;

            block = cifService.addTryLogin(username)
            if(session)
                render([error: msg, block: block, reload: false] as JSON)
            
            else
                render([error: msg, block: block, reload: true] as JSON)
            
        
        //catch unknown RuntimeException, redirect to Error 500 server Error page
        catch (RuntimeException e) 
            logger.error(e.getMessage(), e)
            redirect(controller: "error", action: "serverError")
            return
        

        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() 
        def user = com.vastpalaso.security.User.findByUsername(springSecurityService.authentication.name)
        def userDetails = com.vastpalaso.security.UserDetails.findByUser(user)
        def cifUsergetCif

        try
            cifUsergetCif = com.vastpalaso.app.cif.CifUser.findByUserDetails(userDetails)
            session.setAttribute("company",cifUsergetCif.cif.corpName)
        
        catch (Exception e)
            println "e = "+e
            println "You are loginning as admin!"
        

        try 

            println "params = "+params
            def ipAddress = request.getHeader("Client-IP")
            if (!ipAddress) 
                ipAddress = request.getHeader("X-Forwarded-For")
            
            if (!ipAddress) 
                ipAddress = request.getRemoteAddr()
            

            try

                cifService.resetTryLoginAddInfo(userDetails, ipAddress, session.id)
            catch (Exception e)
                println "e = "+e
            

            session.setAttribute("alias", userDetails.userAlias)
            session.setAttribute("fullName", userDetails.firstName + " " + userDetails.lastName)
            session.setAttribute("change", userDetails.forceChangePassword)
            session.setAttribute("userType", userDetails.userType)


            if(userDetails.language != null)
                session[org.springframework.web.servlet.i18n.SessionLocaleResolver.LOCALE_SESSION_ATTRIBUTE_NAME] = new Locale(userDetails.language)
            
            else
                session[org.springframework.web.servlet.i18n.SessionLocaleResolver.LOCALE_SESSION_ATTRIBUTE_NAME] = new Locale("id")
            
            buildMenuList()
            println "test = "
            if (params.callback) 
                render"$params.callback ($[success: true,id: userDetails.id ,change: userDetails.forceChangePassword, username: springSecurityService.authentication.name, fullName: (userDetails.firstName + " " + userDetails.lastName)] as JSON)"
            
            else 
                render([success: true,id: userDetails.id, change: userDetails.forceChangePassword, username: springSecurityService.authentication.name, fullName: (userDetails.firstName + " " + userDetails.lastName)] as JSON)
            
        
        //catch unknown RuntimeException, redirect to Error 500 server Error page
        catch (RuntimeException e) 
            logger.error(e.getMessage(), e)
            redirect(controller: "error", action: "serverError")
            return
        

        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
    

    def concurrentSession = 

        def msg = "Your account is logged in from another browser or location."

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

    

这是 resouces.groovy

// Place your Spring DSL code here
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.vastpalaso.helper.CustomSessionLogoutHandler
import org.springframework.security.web.session.ConcurrentSessionFilter
beans = 

    sessionRegistry(SessionRegistryImpl)

    customSessionLogoutHandler(CustomSessionLogoutHandler,ref('sessionRegistry'))

    concurrencyFilter(ConcurrentSessionFilter) 
        sessionRegistry = sessionRegistry
        logoutHandlers = [ref("rememberMeServices"), ref("securityContextLogoutHandler")]
        expiredUrl='/login/concurrentSession'
    

    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')])


    jmsConnectionFactory(org.apache.activemq.ActiveMQConnectionFactory)  brokerURL = "tcp://localhost:61616" 

这是我从page复制的代码

package com.vastpalaso.helper;

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());
    

【问题讨论】:

@Quchie 这是我的代码 【参考方案1】:

这将过期当前登录的用户(相同的用户名和密码)。新用户可以继续登录,没有任何问题。

通过将其添加到 src 文件夹来创建 SessionAuthenticationStrategy 的新实现。在 grails 3 中,(src/main/groovy) 在 grails (2.x src/groovy) 中。 我根据您想要实现的目标将其称为自定义名称,并将其保存为 ConcurrentSingleSessionAuthenticationStrategy.groovy

    package com.myapp.test;

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

    import org.springframework.security.core.Authentication;
    import org.springframework.security.web.session.HttpSessionEventPublisher;
    import org.springframework.util.Assert;
    import org.springframework.security.core.session.SessionRegistry;
    import grails.plugin.springsecurity.SpringSecurityUtils;
    import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;

    /**
     * Strategy used to register a user with the @link SessionRegistry after successful
     * @link Authentication.
     *
     * <p>
     * @link RegisterSessionAuthenticationStrategy is typically used in combination with
     * @link CompositeSessionAuthenticationStrategy and
     * @link ConcurrentSessionControlAuthenticationStrategy, but can be used on its own if
     * tracking of sessions is desired but no need to control concurrency.
     *
     * <p>
     * NOTE: When using a @link SessionRegistry it is important that all sessions (including
     * timed out sessions) are removed. This is typically done by adding
     * @link HttpSessionEventPublisher.
     *
     * @see CompositeSessionAuthenticationStrategy
     *
     * @author Luke Taylor
     * @author Rob Winch
     * @since 3.2
     */
    public class ConcurrentSingleSessionAuthenticationStrategy implements
            SessionAuthenticationStrategy 
        private SessionRegistry sessionRegistry;

        /**
         * @param sessionRegistry the session registry which should be updated when the
         * authenticated session is changed.
         */
        public ConcurrentSingleSessionAuthenticationStrategy(SessionRegistry sessionRegistry) 
            Assert.notNull(sessionRegistry, "SessionRegistry cannot be null");
            this.sessionRegistry = sessionRegistry;
        
        /**
         * In addition to the steps from the superclass, the sessionRegistry will be removing
         * with the new session information.
         */
        public void onAuthentication(Authentication authentication,
                HttpServletRequest request, HttpServletResponse response) 

            def sessions = sessionRegistry.getAllSessions(
                    authentication.getPrincipal(), false);

            def principals = sessionRegistry.getAllPrincipals()
            sessions.each
                if(it.principal == authentication.getPrincipal())
                    it.expireNow()
                
            


        
    

resources.groovy 中:

    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 org.springframework.security.core.session.SessionRegistryImpl;
    import com.myapp.test.ConcurrentSingleSessionAuthenticationStrategy;
    import org.springframework.security.web.session.ConcurrentSessionFilter
    // Place your Spring DSL code here
    beans = 
            sessionRegistry(SessionRegistryImpl)
            //I see you did not have this. Very dangerous!
            sessionFixationProtectionStrategy(SessionFixationProtectionStrategy)
                migrateSessionAttributes = true
                alwaysCreateSession = true
            
            //Initiate the bean
            concurrentSingleSessionAuthenticationStrategy(ConcurrentSingleSessionAuthenticationStrategy,ref('sessionRegistry'))
            registerSessionAuthenticationStrategy(RegisterSessionAuthenticationStrategy,ref('sessionRegistry'))
            sessionAuthenticationStrategy(CompositeSessionAuthenticationStrategy,[ref('concurrentSingleSessionAuthenticationStrategy'),ref('sessionFixationProtectionStrategy'),ref('registerSessionAuthenticationStrategy')])
            concurrentSessionFilter(ConcurrentSessionFilter,ref('sessionRegistry'))
    

在config中,最后添加这一行:

grails.plugin.springsecurity.filterChain.filterNames = [ 'securityContextPersistenceFilter', 'logoutFilter', 'concurrentSessionFilter', 'rememberMeAuthenticationFilter', 'anonymousAuthenticationFilter', 'exceptionTranslationFilter', 'filterInvocationInterceptor' ]

希望这会有所帮助。

【讨论】:

在 grails 3 中,我不需要向 config.groovy 添加一些东西,因为 grails 3 没有 config.groovy @Quchie 嘿..您的代码运行良好..顺便说一句,我该如何更改“此会话已过期(可能是由于同一用户尝试多次并发登录)。” 您可以使用 application.groovy 或 .yml .application.groovy 与 grails 2.x 中的 config.groovy 相同 我尝试将此代码 grails.plugin.springsecurity.filterChain.filterNames = ['securityContextPersistenceFilter','concurrentSessionFilter','anonymousAuthenticationFilter', 'exceptionTranslationFilter', 'filterInvocationInterceptor'] 放入 application.groovy,但我的登录不起作用 啊,是的!经过几次尝试,我想出了grails.plugin.springsecurity.filterChain.filterNames = [ 'securityContextPersistenceFilter', 'logoutFilter', 'concurrentSessionFilter', 'rememberMeAuthenticationFilter', 'anonymousAuthenticationFilter', 'exceptionTranslationFilter', 'filterInvocationInterceptor' ],它适用于我的情况。我还添加了logoutFilter,它没有包含在以前的解决方案中,并且需要按预期进行注销。

以上是关于具有并发会话的 grails 3的主要内容,如果未能解决你的问题,请参考以下文章

在 grails 中维护会话

如何防止 Grails 3 创建会话?

Grails:在用户会话期间存储数据的最佳方法[关闭]

Grails 3.0.1 和休眠会话错误

如何在 grails 中使用会话

Grails 在服务类中获取会话和管理