<optgroup id="gajtf"></optgroup>
  • <meter id="gajtf"></meter>
    <mark id="gajtf"><ruby id="gajtf"></ruby></mark>

  • <small id="gajtf"><strong id="gajtf"></strong></small>
  • <mark id="gajtf"><button id="gajtf"></button></mark>
    <listing id="gajtf"><dfn id="gajtf"><s id="gajtf"></s></dfn></listing><label id="gajtf"><ruby id="gajtf"></ruby></label>
    <mark id="gajtf"></mark>

    Grails3+spring security實現單端登錄(單用戶登錄)

    grails3+spring security core 3.1.2實現權限管理,單端登錄,如果在多個地方登錄,另一端強制下線

    碼云地址:https://gitee.com/GntLee/single

    描述:

    本文檔將實現單用戶登錄,實際效果是:當一個用戶在一個地方登錄了之后,另一個地方也用該用戶登錄,前一個登錄被迫下線,每次登錄都會用新的session替換舊的session。

    1、新建項目

    2、打開根目錄下的build.gradle文件,dependencies中添加spring-security依賴

    compile 'org.grails.plugins:spring-security-core:3.1.2'
    

    3、創建用戶、角色的domain

    也可用命令快速生成域類:

    grails s2-quickstart com.system UserInfo RoleInfo

    3.1 用戶(UserInfo)

    package com.system
    
    import groovy.transform.EqualsAndHashCode
    import groovy.transform.ToString
    
    @EqualsAndHashCode(includes='username')
    @ToString(includes='username', includeNames=true, includePackage=false)
    class UserInfo implements Serializable {
    
        transient springSecurityService
    
        private static final long serialVersionUID = 1
    
        String username
        String password
        boolean enabled = true
        boolean accountExpired
        boolean accountLocked
        boolean passwordExpired
        String nickname
    
        Set getAuthorities() {
            (UserRole.findAllByUser(this) as List)*.role as Set
        }
    
        static constraints = {
            password blank: false, password: true
            username blank: false, unique: true
            nickname nullable: true, maxSize: 15
        }
    
        static mapping = {
            password column: '`password`'
        }
    
        def beforeInsert() {
            encodePassword()
        }
    
        def beforeUpdate() {
            if (isDirty('password')) {
                encodePassword()
            }
        }
    
        protected void encodePassword() {
            password = springSecurityService?.passwordEncoder ? springSecurityService.encodePassword(password) : password
        }
    
    }
    

    3.2 RoleInfo(角色)

    package com.system
    
    import groovy.transform.EqualsAndHashCode
    import groovy.transform.ToString
    
    @EqualsAndHashCode(includes='authority')
    @ToString(includes='authority', includeNames=true, includePackage=false)
    class RoleInfo implements Serializable {
    
        private static final long serialVersionUID = 1
    
        String authority
        String remark
    
        static constraints = {
            authority blank: false, unique: true
            remark blank: false
        }
    
        static mapping = {
            cache true
        }
    
    }
    

    3.3 用戶-角色關聯(UserRole)

    package com.system
    
    import grails.gorm.DetachedCriteria
    import groovy.transform.ToString
    import org.codehaus.groovy.util.HashCodeHelper
    
    @ToString(cache=true, includeNames=true, includePackage=false)
    class UserRole implements Serializable {
    
        private static final long serialVersionUID = 1
    
        UserInfo user
        RoleInfo role
    
        @Override
        boolean equals(other) {
            if (other instanceof UserRole) {
                other.userId == user?.id && other.roleId == role?.id
            }
        }
    
        @Override
        int hashCode() {
            int hashCode = HashCodeHelper.initHash()
            if (user) {
                hashCode = HashCodeHelper.updateHash(hashCode, user.id)
            }
            if (role) {
                hashCode = HashCodeHelper.updateHash(hashCode, role.id)
            }
            hashCode
        }
    
        static UserRole get(long userId, long roleId) {
            criteriaFor(userId, roleId).get()
        }
    
        static boolean exists(long userId, long roleId) {
            criteriaFor(userId, roleId).count()
        }
    
        private static DetachedCriteria criteriaFor(long userId, long roleId) {
            UserRole.where {
                user == UserInfo.load(userId) &&
                        role == RoleInfo.load(roleId)
            }
        }
    
        static UserRole create(UserInfo user, RoleInfo role, boolean flush = false) {
            def instance = new UserRole(user: user, role: role)
            instance.save(flush: flush)
            instance
        }
    
        static boolean remove(UserInfo u, RoleInfo r) {
            if (u != null && r != null) {
                UserRole.where { user == u && role == r }.deleteAll()
            }
        }
    
        static int removeAll(UserInfo u) {
            u == null ? 0 : UserRole.where { user == u }.deleteAll() as int
        }
    
        static int removeAll(RoleInfo r) {
            r == null ? 0 : UserRole.where { role == r }.deleteAll() as int
        }
    
        static constraints = {
            role validator: { RoleInfo r, UserRole ur ->
                if (ur.user?.id) {
                    UserRole.withNewSession {
                        if (UserRole.exists(ur.user.id, r.id)) {
                            return ['userRole.exists']
                        }
                    }
                }
            }
        }
    
        static mapping = {
            id composite: ['user', 'role']
            version false
        }
    }
    

    4、創建登錄控制器LoginController

    package com.system
    
    import grails.converters.JSON
    import grails.plugin.springsecurity.SpringSecurityUtils
    import org.springframework.context.MessageSource
    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 javax.servlet.http.HttpServletResponse
    
    @Secured('permitAll')
    class LoginController {
    
        /** 依賴注入認證接口authenticationTrustResolver. */
        AuthenticationTrustResolver authenticationTrustResolver
    
        /** 依賴注入springSecurityService. */
        def springSecurityService
    
        /** 依賴注入messageSource. */
        MessageSource messageSource
    
        /** 若登錄成功,直接跳轉到首頁,否則跳轉到auth頁面登錄 */
        def index() {
    
            if (springSecurityService.isLoggedIn()) {
                redirect uri: conf.successHandler.defaultTargetUrl
            }
            else {
                redirect action: 'auth', params: params
            }
        }
    
        /**登錄頁面*/
        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')
        }
    
        /** 普通請求拒絕訪問 */
        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]
        }
    
        /** ajax登錄認證失敗信息提示 */
        def authfail() {
    
            String msg = ''
            def exception = session[WebAttributes.AUTHENTICATION_EXCEPTION]
            if (exception) {
                if (exception instanceof AccountExpiredException) {
                    msg = messageSource.getMessage('springSecurity.errors.login.expired', null, "Account Expired", request.locale)
                }
                else if (exception instanceof CredentialsExpiredException) {
                    msg = messageSource.getMessage('springSecurity.errors.login.passwordExpired', null, "Password Expired", request.locale)
                }
                else if (exception instanceof DisabledException) {
                    msg = messageSource.getMessage('springSecurity.errors.login.disabled', null, "Account Disabled", request.locale)
                }
                else if (exception instanceof LockedException) {
                    msg = messageSource.getMessage('springSecurity.errors.login.locked', null, "Account Locked", request.locale)
                }
                else {
                    msg = messageSource.getMessage('springSecurity.errors.login.fail', null, "Authentication Failure", request.locale)
                }
            }
    
            if (springSecurityService.isAjax(request)) {
                render([error: msg] as JSON)
            }
            else {
                flash.message = msg
                redirect action: 'auth', params: params
            }
        }
    
        /** ajax登錄成功 */
        def ajaxSuccess() {
            render([success: true, username: authentication.name] as JSON)
        }
    
        /** ajaax拒絕訪問 */
        def ajaxDenied() {
            render([error: 'access denied'] as JSON)
        }
    
        protected Authentication getAuthentication() {
    
            SecurityContextHolder.context?.authentication
        }
    
        protected ConfigObject getConf() {
            SpringSecurityUtils.securityConfig
        }
    
        /** 單用戶登錄(已登錄返回給用戶提示) */
        def already() {
            render view: "already"
        }
    }
    

    5、創建注銷控制器 LogoutController

    package com.system
    
    import grails.plugin.springsecurity.SpringSecurityUtils
    import org.springframework.security.access.annotation.Secured
    import org.springframework.security.web.RedirectStrategy
    
    @Secured('permitAll')
    class LogoutController {
    
        /** 依賴注入RedirectStrategy. */
        RedirectStrategy redirectStrategy
    
        /**
         * 注銷方法
         */
        def index() {
    
    //        if (!request.post && SpringSecurityUtils.getSecurityConfig().logout.postOnly) {
    //            response.sendError HttpServletResponse.SC_METHOD_NOT_ALLOWED // 405
    //            return
    //        }
    
            // TODO put any pre-logout code here
            redirectStrategy.sendRedirect request, response, SpringSecurityUtils.securityConfig.logout.filterProcessesUrl // '/logoff'
            response.flushBuffer()
        }
    }
    

    6、1、自定義一個ConcurrentSingleSessionAuthenticationStrategy類實現SessionAuthenticationStrategy接口覆蓋默認方法

    package com.session
    
    import org.springframework.security.core.Authentication
    import org.springframework.security.core.session.SessionRegistry
    import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy
    import org.springframework.util.Assert
    
    import javax.servlet.http.HttpServletRequest
    import javax.servlet.http.HttpServletResponse
    
    /**
     * 會話管理類
     */
    class ConcurrentSingleSessionAuthenticationStrategy implements SessionAuthenticationStrategy {
    
        private SessionRegistry sessionRegistry
    
        /**
         * @param 將新的會話賦值給sessionRegistry
         */
        public ConcurrentSingleSessionAuthenticationStrategy(SessionRegistry sessionRegistry) {
            Assert.notNull(sessionRegistry, "SessionRegistry cannot be null")
            this.sessionRegistry = sessionRegistry
        }
        /**
         * 覆蓋父類的onAuthentication方法
         * 用新的session替換就的session
         */
        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()
                }
            }
    
    
        }
    }
    

    (注:此類我是在src/main/groovy里面創建的,你也可以在其他地方創建)

    7、打開grails-app/conf/spring/resource.groovy,配置DSL

    v2-9481a6bbb256b8f5e46dab595764d72e_hd.jpg

    7.1 配置

    import com.session.ConcurrentSingleSessionAuthenticationStrategy
    import org.springframework.security.core.session.SessionRegistryImpl
    import org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy
    import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy
    import org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy
    import org.springframework.security.web.session.ConcurrentSessionFilter
    
    // Place your Spring DSL code here
    beans = {
    
        sessionRegistry(SessionRegistryImpl)
        //很重要
        sessionFixationProtectionStrategy(SessionFixationProtectionStrategy){
            migrateSessionAttributes = true
            alwaysCreateSession = true
        }
        // "/login/already"為重定向請求
        concurrentSingleSessionAuthenticationStrategy(ConcurrentSingleSessionAuthenticationStrategy,ref('sessionRegistry'))
        registerSessionAuthenticationStrategy(RegisterSessionAuthenticationStrategy,ref('sessionRegistry'))
        sessionAuthenticationStrategy(CompositeSessionAuthenticationStrategy,[ref('concurrentSingleSessionAuthenticationStrategy'), ref('sessionFixationProtectionStrategy'), ref('registerSessionAuthenticationStrategy')])
        concurrentSessionFilter(ConcurrentSessionFilter, ref('sessionRegistry'), "/login/already")
    }
    

    8、在grails-app/conf目錄下創建application.groovy類

    8.1 配置

    //grails.plugin.springsecurity.successHandler.alwaysUseDefault = true
    //grails.plugin.springsecurity.successHandler.defaultTargetUrl = '/your-url' //登錄成功后跳轉地址
    grails.plugin.springsecurity.userLookup.usernamePropertyName ="username"
    grails.plugin.springsecurity.userLookup.passwordPropertyName ="password"
    grails.plugin.springsecurity.authority.className="com.system.RoleInfo"
    grails.plugin.springsecurity.userLookup.userDomainClassName="com.system.UserInfo"
    grails.plugin.springsecurity.userLookup.authorityJoinClassName="com.system.UserRole"
    grails.plugin.springsecurity.controllerAnnotations.staticRules = [
            [pattern: '/',                 access: ['permitAll']],
            [pattern: '/error',            access: ['permitAll']],
            [pattern: '/index',            access: ['permitAll']],
            [pattern: '/index.gsp',        access: ['permitAll']],
            [pattern: '/shutdown',         access: ['permitAll']],
            [pattern: '/assets/**',        access: ['permitAll']],
            [pattern: '/**/js/**',         access: ['permitAll']],
            [pattern: '/**/css/**',        access: ['permitAll']],
            [pattern: '/**/images/**',     access: ['permitAll']],
            [pattern: '/**/favicon.ico',   access: ['permitAll']],
            [pattern: '/login/already.gsp',access: ['permitAll']],
    
            [pattern: '/user/**',        access: 'ROLE_USER'],
            [pattern: '/admin/**',       access: ['ROLE_ADMIN', 'isFullyAuthenticated()']]
    ]
    grails.plugin.springsecurity.interceptUrlMap = [
            [pattern: '/',               access: ['permitAll']],
            [pattern: '/error',          access: ['permitAll']],
            [pattern: '/index',          access: ['permitAll']],
            [pattern: '/index.gsp',      access: ['permitAll']],
            [pattern: '/shutdown',       access: ['permitAll']],
            [pattern: '/assets/**',      access: ['permitAll']],
            [pattern: '/**/js/**',       access: ['permitAll']],
            [pattern: '/**/css/**',      access: ['permitAll']],
            [pattern: '/**/images/**',   access: ['permitAll']],
            [pattern: '/**/favicon.ico', access: ['permitAll']],
            [pattern: '/login/**',       access: ['permitAll']],
            [pattern: '/login/already',  access: ['permitAll']],
            [pattern: '/logout/**',      access: ['permitAll']]
    ]
    
    grails.plugin.springsecurity.filterChain.filterNames = [ 'securityContextPersistenceFilter', 'logoutFilter', 'concurrentSessionFilter', 'rememberMeAuthenticationFilter', 'anonymousAuthenticationFilter', 'exceptionTranslationFilter', 'filterInvocationInterceptor' ]
    

    9、打開grails-app/init/BootStrap.groovy

    9.1 保存用戶、角色、用戶-角色信息

    import com.system.RoleInfo
    import com.system.UserInfo
    import com.system.UserRole
    
    class BootStrap {
    
        def init = { servletContext ->
    
            //創建角色
            def role1 = new RoleInfo(authority: "ROLE_ADMIN", remark: "管理員").save()
            def role2 = new RoleInfo(authority: "ROLE_SUPSYS", remark: "超級管理員").save()
            def role3 = new RoleInfo(authority: "ROLE_USER", remark: "普通用戶").save()
    
            //創建用戶
            def user1 = new UserInfo(username: "admin", password: "admin").save()
            def user2 = new UserInfo(username: "super", password: "super").save()
            def user3 = new UserInfo(username: "user", password: "user").save()
    
            //用戶角色關聯
            UserRole.create user1, role1, true
            UserRole.create user2, role2, true
            UserRole.create user3, role3, true
    
        }
    
        def destroy = {
        }
    }
    

    最后到這里就完成了,可以啟動項目進行測試了,需要說明的是,在此過程中沒有設計到gsp頁面的代碼,同學們自己寫吧。文檔可能有語意不明的地方,還望各位同學多多包涵。有不清楚的Q我:342418262,相互交流學習!


    可以參考:

    https://classpattern.com/spring-security-sessionregistry-on-grails

    https://www.oschina.net/question/1446415_167338

    http://www.tothenew.com/blog/restricting-concurrent-sessions-for-a-single-user-using-grails-and-spring-security/

    http://lucaslward.github.io/blog/2012/08/08/using-spring-security-concurrent/


    更簡單!

    • 發表于 2017-11-15 10:23
    • 閱讀 ( 4170 )
    • 分類:grails

    1 條評論

    請先 登錄 后評論
    不寫代碼的碼農
    Jonny

    程序猿

    65 篇文章

    作家榜 ?

    1. 威猛的小站長 119 文章
    2. Jonny 65 文章
    3. 江南煙雨 33 文章
    4. - Nightmare 33 文章
    5. doublechina 31 文章
    6. HJ社區-肖峰 29 文章
    7. 偽攝影 20 文章
    8. Alan 14 文章
    小喜通天报