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
  • 阅读 ( 2134 )
  • 分类:grails

1 条评论

请先 登录 后评论
不写代码的码农
Jonny

程序猿

65 篇文章

作家榜 »

  1. 威猛的小站长 99 文章
  2. Jonny 65 文章
  3. 江南烟雨 33 文章
  4. - Nightmare 33 文章
  5. doublechina 31 文章
  6. HJ社区-肖峰 29 文章
  7. 伪摄影 20 文章
  8. Alan 14 文章