Grails4+spring security实现单用户登录
- 描述
- 1、新建项目目录结构如图所示
- 2、打开根目录下的build.gradle文件,dependencies中添加spring-security依赖
- 3、创建用户、角色的domain
- 4、创建登录控制器LoginController
- 5、创建注销控制器 LogoutController
- 6、自定义一个ConcurrentSingleSessionAuthenticationStrategy类实现SessionAuthenticationStrategy接口覆盖默认方法
- 7、打开grails-app/conf/spring/resource.groovy,配置DSL
- 8、在grails-app/conf目录下创建application.groovy类
- 9、在grails-app/conf目录下创建application.yml类
- 10、打开grails-app/init/BootStrap.groovy
描述
本文档将实现单用户登录,实际效果是:当一个用户在一个地方登录了之后,另一个地方也用该用户登录,前一个登录被迫下线,每次登录都会用新的session替换旧的session。
1、新建项目目录结构如图所示
2、打开根目录下的build.gradle文件,dependencies中添加spring-security依赖
compile "org.grails.plugins:spring-security-core:4.0.0"
3、创建用户、角色的domain
也可用命令快速生成域类:
grails s2-quickstart com.system UserInfo RoleInfo
- 3.1 用户(UserInfo)
package com.systemimport groovy.transform.EqualsAndHashCode
import groovy.transform.ToString@EqualsAndHashCode(includes='username')
@ToString(includes='username', includeNames=true, includePackage=false)
class UserInfo implements Serializable {transient springSecurityServiceprivate static final long serialVersionUID = 1String usernameString passwordboolean enabled = trueboolean accountExpiredboolean accountLockedboolean passwordExpiredString nicknameSet<RoleInfo> getAuthorities() {(UserRole.findAllByUser(this) as List<UserRole>)*.role as Set<RoleInfo>}static constraints = {password blank: false, password: trueusername blank: false, unique: truenickname 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.systemimport 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 = 1String authorityString remarkstatic constraints = {authority blank: false, unique: trueremark blank: false}static mapping = {cache true}}
- 3.3 用户-角色关联(UserRole)
package com.systemimport 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 = 1UserInfo userRoleInfo role@Overrideboolean equals(other) {if (other instanceof UserRole) {other.userId == user?.id && other.roleId == role?.id}}@Overrideint 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.systemimport 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.WebAttributesimport 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.defaultTargetUrlreturn}String postUrl = request.contextPath + conf.apf.filterProcessesUrlrender 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.ajaxLoginFormUrlrender(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: paramsreturn}[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 = msgredirect 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.systemimport 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 hereredirectStrategy.sendRedirect request, response, SpringSecurityUtils.securityConfig.logout.filterProcessesUrl // '/logoff'response.flushBuffer()}
}
6、自定义一个ConcurrentSingleSessionAuthenticationStrategy类实现SessionAuthenticationStrategy接口覆盖默认方法
package com.sessionimport org.springframework.security.core.Authentication
import org.springframework.security.core.session.SessionRegistry
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy
import org.springframework.util.Assertimport 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
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 = truealwaysCreateSession = 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类
//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/conf目录下创建application.yml类
---
grails:profile: webcodegen:defaultPackage: longiwebgorm:reactor:# Whether to translate GORM events into Reactor events# Disabled by default for performance reasonsevents: falsecontrollers:upload:maxFileSize: 2000000000maxRequestSize: 2000000000
myParams:name: 'MrWt'age: '23'post: 'PM'NxLgg:appid: 'wx171071cfc94cb83a'appsecret: 'e86e2e3d51facec23vr27269b6d79bc6'merchantid: '1528776352'merchantkey: '0D0tsOh0wFyjvAppL06idNE29hedecac'RemoveIMEIPlat: '142,143,165,166,241,242,243,342,343'logFilePath: 'd:\\log'meterType: '1'unreported: '0'# temporaryTask / unreported: 1为开启,0为关闭temporaryTask: '0'maxTime: 20info:app:name: '@info.app.name@'version: '@info.app.version@'grailsVersion: '@info.app.grailsVersion@'
spring:jmx:unique-names: truemain:banner-mode: "off"groovy:template:check-template-location: falsedevtools:restart:additional-exclude:- '*.gsp'- '**/*.gsp'- '*.gson'- '**/*.gson'- 'logback.groovy'- '*.properties'
management:endpoints:enabled-by-default: false
---
grails:mime:disable:accept:header:userAgents:- Gecko- WebKit- Presto- Tridenttypes:all: '*/*'atom: application/atom+xmlcss: text/csscsv: text/csvform: application/x-www-form-urlencodedhtml:- text/html- application/xhtml+xmljs: text/javascriptjson:- application/json- text/jsonmultipartForm: multipart/form-datapdf: application/pdfrss: application/rss+xmltext: text/plainhal:- application/hal+json- application/hal+xmlxml:- text/xml- application/xmlurlmapping:cache:maxsize: 1000controllers:defaultScope: singletonconverters:encoding: UTF-8views:default:codec: htmlgsp:encoding: UTF-8htmlcodec: xmlcodecs:expression: nonescriptlet: htmltaglib: nonestaticparts: noneresources:pattern:'/**'
management:endpoints:jmx:unique-names: true
---
hibernate:cache:queries: falseuse_second_level_cache: falseuse_query_cache: false
dataSource:pooled: truejmxExport: truedriverClassName: org.h2.Driverusername: sapassword: ''
#redis:
# poolConfig:
# maxIdle: 10
# doesnotexist: true
# host: 192.168.0.86
# port: 6379
# timeout: 5000
# password: ''
# jedis:
# pool:
# # 最大空闲连接数
# max-idle: 500
# # 最小空闲连接数
# min-idle: 50
# # 等待可用连接的最大时间,负数为不限制
# max-wait: -1
# # 最大活跃连接数,负数为不限制
# max-active: -1environments:
# 开发库连接development:dataSource:bySearch:testWhileIdle: truevalidationQuery: SELECT 1timeBestweenEvictionRunsMillis: 3600000 #每个小时确认连接是否可用dbCreate: nonedriverClassName: com.mysql.cj.jdbc.Driverusername: rootpassword: MrWt5678url: jdbc:mysql://192.168.0.81:3306/longistation?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=false&tinyInt1isBit=falseproperties:jmxEnabled: trueinitialSize: 10maxActive: 50maxIdle: 20minIdle: 10maxWait: 30000removeAbandoned: trueremoveAbandonedTimeout: 60000logAbandoned: falsetestOnBorrow: falsetestOnReturn: falsetestWhileIdle: truevalidationQuery: select 1#validationQueryTimeout: 5timeBetweenEvictionRunsMillis: 3600000minEvictableIdleTimeMillis: 3600000numTestsPerEvictionRun: 100jdbcInterceptors: ConnectionStatedefaultTransactionIsolation: 2 # TRANSACTION_READ_COMMITTED
# 测试库连接test:dataSource:dbCreate: nonedriverClassName: com.mysql.cj.jdbc.Driverusername: rootpassword: 123456url: jdbc:mysql://192.168.0.173:3306/test?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC&useSSL=false&tinyInt1isBit=false
# 生产库链接,tomcat发布版production:dataSource:dbCreate: nonedriverClassName: com.mysql.cj.jdbc.Driverusername: longipassword:MrWt5678url: jdbc:mysql://192.168.0.8:3306/longistation?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=false&tinyInt1isBit=falseproperties:jmxEnabled: trueinitialSize: 10maxActive: 50 #最大连接数maxIdle: 20 #最大空闲连接minIdle: 10 #最小空闲连接maxWait: 30000 #超时等待时间以毫秒为单位removeAbandoned: true # 当active连接快到maxActive连接的时候,是否回收无效的连接回收removeAbandonedTimeout: 60000 # 超时时间(以秒数为单位)logAbandoned: false # 是否在log中打印出回收Connection的错误信息,包括在哪个地方用了Connection却忘记关闭了testOnBorrow: falsetestOnReturn: falsetestWhileIdle: truevalidationQuery: select 1#validationQueryTimeout: 5timeBetweenEvictionRunsMillis: 3600000 # 连接空闲了多久会被空闲连接回收器回收minEvictableIdleTimeMillis: 3600000 # 每timeBetweenEvictionRunsMills运行一次空闲连接回收器(独立线程)numTestsPerEvictionRun: 50 # 每次检查numTestsPerEvictionRun个连接jdbcInterceptors: ConnectionStatedefaultTransactionIsolation: 2 # TRANSACTION_READ_COMMITTEDdataSources:mysqlTest:dbCreate: nonedialect: org.hibernate.dialect.MySQLInnoDBDialectdriverClassName: com.mysql.cj.jdbc.Driverusername: rootpassword: MrWt123456url: jdbc:mysql://192.168.0.8:3306/mysql?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC&useSSL=falseproperties:jmxEnabled: trueinitialSize: 10maxActive: 50maxIdle: 20minIdle: 10maxWait: 30000removeAbandoned: trueremoveAbandonedTimeout: 60000logAbandoned: falsetestOnBorrow: falsetestOnReturn: falsetestWhileIdle: truevalidationQuery: select 1timeBetweenEvictionRunsMillis: 3600000minEvictableIdleTimeMillis: 3600000numTestsPerEvictionRun: 100jdbcInterceptors: ConnectionStatedefaultTransactionIsolation: 2oracleTest:dbCreate: nonedialect: org.hibernate.dialect.OracleDialectdriverClassName: oracle.jdbc.driver.OracleDriverusername: lggmrhotpassword: MrWt6666url: jdbc:oracle:thin:@192.168.0.89:1521:orclsqlServerTest:dbCreate: nonepooled: truejmxExport: truedriverClassName: com.microsoft.sqlserver.jdbc.SQLServerDriverusername: oapassword: MrWt5678url: jdbc:sqlserver://192.168.0.3:1433;database=zkteco_database;sqlServerTest43:dbCreate: nonepooled: truejmxExport: truedriverClassName: com.microsoft.sqlserver.jdbc.SQLServerDriverusername: oapassword: MrWt5678url: jdbc:sqlserver://192.168.0.3:1433;database=STCard;sqlServerTest14:dbCreate: nonepooled: truejmxExport: truedriverClassName: com.microsoft.sqlserver.jdbc.SQLServerDriverusername: u8password: MrWt5678url: jdbc:sqlserver://192.168.0.1:1433;database=UFDATA_333_2017;
10、打开grails-app/init/BootStrap.groovy
- 10.1 保存用户、角色、用户-角色信息
import com.system.RoleInfo
import com.system.UserInfo
import com.system.UserRoleclass 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, trueUserRole.create user2, role2, trueUserRole.create user3, role3, true}def destroy = {}
}
最后到这里就完成了,可以启动项目进行测试了。快去创建自己的controller和gsp吧。