码云地址: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
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://lucaslward.github.io/blog/2012/08/08/using-spring-security-concurrent/
更简单!
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!