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.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<RoleInfo> getAuthorities() {
(UserRole.findAllByUser(this) as List<UserRole>)*.role as Set<RoleInfo>
}
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、自定义一个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
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类
//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: web
codegen:
defaultPackage: longiweb
gorm:
reactor:
# Whether to translate GORM events into Reactor events
# Disabled by default for performance reasons
events: false
controllers:
upload:
maxFileSize: 2000000000
maxRequestSize: 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: 20
info:
app:
name: '@info.app.name@'
version: '@info.app.version@'
grailsVersion: '@info.app.grailsVersion@'
spring:
jmx:
unique-names: true
main:
banner-mode: "off"
groovy:
template:
check-template-location: false
devtools:
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
- Trident
types:
all: '*/*'
atom: application/atom+xml
css: text/css
csv: text/csv
form: application/x-www-form-urlencoded
html:
- text/html
- application/xhtml+xml
js: text/javascript
json:
- application/json
- text/json
multipartForm: multipart/form-data
pdf: application/pdf
rss: application/rss+xml
text: text/plain
hal:
- application/hal+json
- application/hal+xml
xml:
- text/xml
- application/xml
urlmapping:
cache:
maxsize: 1000
controllers:
defaultScope: singleton
converters:
encoding: UTF-8
views:
default:
codec: html
gsp:
encoding: UTF-8
htmlcodec: xml
codecs:
expression: none
scriptlet: html
taglib: none
staticparts: none
resources:
pattern:'/**'
management:
endpoints:
jmx:
unique-names: true
---
hibernate:
cache:
queries: false
use_second_level_cache: false
use_query_cache: false
dataSource:
pooled: true
jmxExport: true
driverClassName: org.h2.Driver
username: sa
password: ''
#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: -1
environments:
# 开发库连接
development:
dataSource:
bySearch:
testWhileIdle: true
validationQuery: SELECT 1
timeBestweenEvictionRunsMillis: 3600000 #每个小时确认连接是否可用
dbCreate: none
driverClassName: com.mysql.cj.jdbc.Driver
username: root
password: MrWt5678
url: jdbc:mysql://192.168.0.81:3306/longistation?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=false&tinyInt1isBit=false
properties:
jmxEnabled: true
initialSize: 10
maxActive: 50
maxIdle: 20
minIdle: 10
maxWait: 30000
removeAbandoned: true
removeAbandonedTimeout: 60000
logAbandoned: false
testOnBorrow: false
testOnReturn: false
testWhileIdle: true
validationQuery: select 1
#validationQueryTimeout: 5
timeBetweenEvictionRunsMillis: 3600000
minEvictableIdleTimeMillis: 3600000
numTestsPerEvictionRun: 100
jdbcInterceptors: ConnectionState
defaultTransactionIsolation: 2 # TRANSACTION_READ_COMMITTED
# 测试库连接
test:
dataSource:
dbCreate: none
driverClassName: com.mysql.cj.jdbc.Driver
username: root
password: 123456
url: jdbc:mysql://192.168.0.173:3306/test?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC&useSSL=false&tinyInt1isBit=false
# 生产库链接,tomcat发布版
production:
dataSource:
dbCreate: none
driverClassName: com.mysql.cj.jdbc.Driver
username: longi
password:MrWt5678
url: jdbc:mysql://192.168.0.8:3306/longistation?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=false&tinyInt1isBit=false
properties:
jmxEnabled: true
initialSize: 10
maxActive: 50 #最大连接数
maxIdle: 20 #最大空闲连接
minIdle: 10 #最小空闲连接
maxWait: 30000 #超时等待时间以毫秒为单位
removeAbandoned: true # 当active连接快到maxActive连接的时候,是否回收无效的连接回收
removeAbandonedTimeout: 60000 # 超时时间(以秒数为单位)
logAbandoned: false # 是否在log中打印出回收Connection的错误信息,包括在哪个地方用了Connection却忘记关闭了
testOnBorrow: false
testOnReturn: false
testWhileIdle: true
validationQuery: select 1
#validationQueryTimeout: 5
timeBetweenEvictionRunsMillis: 3600000 # 连接空闲了多久会被空闲连接回收器回收
minEvictableIdleTimeMillis: 3600000 # 每timeBetweenEvictionRunsMills运行一次空闲连接回收器(独立线程)
numTestsPerEvictionRun: 50 # 每次检查numTestsPerEvictionRun个连接
jdbcInterceptors: ConnectionState
defaultTransactionIsolation: 2 # TRANSACTION_READ_COMMITTED
dataSources:
mysqlTest:
dbCreate: none
dialect: org.hibernate.dialect.MySQLInnoDBDialect
driverClassName: com.mysql.cj.jdbc.Driver
username: root
password: MrWt123456
url: jdbc:mysql://192.168.0.8:3306/mysql?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC&useSSL=false
properties:
jmxEnabled: true
initialSize: 10
maxActive: 50
maxIdle: 20
minIdle: 10
maxWait: 30000
removeAbandoned: true
removeAbandonedTimeout: 60000
logAbandoned: false
testOnBorrow: false
testOnReturn: false
testWhileIdle: true
validationQuery: select 1
timeBetweenEvictionRunsMillis: 3600000
minEvictableIdleTimeMillis: 3600000
numTestsPerEvictionRun: 100
jdbcInterceptors: ConnectionState
defaultTransactionIsolation: 2
oracleTest:
dbCreate: none
dialect: org.hibernate.dialect.OracleDialect
driverClassName: oracle.jdbc.driver.OracleDriver
username: lggmrhot
password: MrWt6666
url: jdbc:oracle:thin:@192.168.0.89:1521:orcl
sqlServerTest:
dbCreate: none
pooled: true
jmxExport: true
driverClassName: com.microsoft.sqlserver.jdbc.SQLServerDriver
username: oa
password: MrWt5678
url: jdbc:sqlserver://192.168.0.3:1433;database=zkteco_database;
sqlServerTest43:
dbCreate: none
pooled: true
jmxExport: true
driverClassName: com.microsoft.sqlserver.jdbc.SQLServerDriver
username: oa
password: MrWt5678
url: jdbc:sqlserver://192.168.0.3:1433;database=STCard;
sqlServerTest14:
dbCreate: none
pooled: true
jmxExport: true
driverClassName: com.microsoft.sqlserver.jdbc.SQLServerDriver
username: u8
password: MrWt5678
url: 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.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 = {
}
}
最后到这里就完成了,可以启动项目进行测试了。快去创建自己的controller和gsp吧。