Spring Security Core 3.1.1
Spring Security Core 3.1.1
Burt Beckwith
Version 3.1.1
Table of Contents
1. Introduction to the Spring Security Plugin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.1. Release History and Acknowledgment . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.2. Configuration Settings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.3. Getting Started . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
2. Whats New in Version 3.0 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
2.1. Installation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
2.2. Configuration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
3. Whats New in Version 2.0 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
3.1. Package changes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
3.2. Configuration prefix changes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
3.3. More aggressively secure by default . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
3.4. @Secured annotation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
3.5. Anonymous authentication . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
3.6. No HQL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
3.7. Changes in generated classes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
3.8. SecurityContextHolder strategy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
3.9. Debug filter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
3.10. Storing usernames in the session. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
3.11. @Authorities annotation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
3.12. Miscellaneous changes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
4. Domain Classes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
4.1. Person Class . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
4.2. Authority Class . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
4.3. PersonAuthority Class . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
4.4. Group Class . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
4.5. PersonGroup Class . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
4.6. GroupAuthority Class . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
4.7. Requestmap Class. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
5. Configuring Request Mappings to Secure URLs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
5.1. Pessimistic Lockdown . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
5.2. URLs and Authorities . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
5.3. Comparing the Approaches . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
5.4. Defining Secured Annotations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
5.5. Static Map. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
5.6. Requestmap Instances Stored in the Database . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
5.7. Using Expressions to Create Descriptive, Fine-Grained Rules . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
6. Helper Classes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
6.1. SecurityTagLib . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
6.2. SpringSecurityService . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
6.3. SpringSecurityUtils . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
7. Events . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
7.1. Event Notification . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
7.2. Registering an Event Listener . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
7.3. Registering Callback Closures . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
8. User, Authority (Role), and Requestmap Properties . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
9. Authentication . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
9.1. Basic and Digest Authentication . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
9.2. Certificate (X.509) Login Authentication . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
9.3. Remember-Me Cookie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
9.4. Ajax Authentication . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
10. Authentication Providers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
11. Custom UserDetailsService . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
11.1. Flushing the Cached Authentication . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
12. Password and Account Protection . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
12.1. Password Hashing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
12.2. Salted Passwords . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
12.3. Account Locking and Forcing Password Change . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
13. URL Properties . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
14. Hierarchical Roles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
14.1. Persistent role hierarchy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
15. Switch User . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
15.1. Switching to Another User . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
15.2. Switching Back to Original User . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
15.3. Customizing URLs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
15.4. GSP Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
16. Filters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
16.1. Default Approach to Configuring Filter Chains . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
16.2. filterNames . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
16.3. chainMap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
16.4. clientRegisterFilter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
17. Channel Security . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
17.1. Header checking . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101
18. IP Address Restrictions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
19. Session Fixation Prevention . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
20. Logout Handlers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104
21. Voters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
22. Miscellaneous Properties . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
23. Tutorials. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109
23.1. Using Controller Annotations to Secure URLs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109
2.0-RC4 release
May 19, 2014
2.0-RC3 release
October 4, 2013
2.0-RC2 release
JIRA Issues
October 3, 2013
2.0-RC1 release
April 6, 2012
1.2.7.3 release
JIRA Issues
February 2, 2012
1.2.7.2 release
JIRA Issues
January 18, 2012
1.2.7.1 release
JIRA Issues
December 30, 2011
1.2.7 release
JIRA Issues
December 2, 2011
1.2.6 release
JIRA Issues
December 1, 2011
1.2.5 release
October 18, 2011
1.2.4 release
October 15, 2011
1.2.3 release
October 15, 2011
1.2.2 release
JIRA Issues
August 17, 2011
1.2.1 release
JIRA Issues
July 31, 2011
1.2 release
JIRA Issues
May 23, 2011
1.1.3 release
JIRA Issues
February 26, 2011
1.1.2 release
February 26, 2011
1.1.1 release
JIRA Issues
August 8, 2010
1.1 release
JIRA Issues
August 1, 2010
1.0.1 release
July 27, 2010
1.0 release
JIRA Issues
July 16, 2010
0.4.2 release
JIRA Issues
June 29, 2010
0.4.1 release
JIRA Issues
June 21, 2010
0.4 release
JIRA Issues
3
grails.plugin.springsecurity.password.algorithm = 'bcrypt'
To get started using the Spring Security plugin with your Grails application, see Tutorials.
You do not need deep knowledge of Spring Security to use the plugin, but it is helpful to understand
the underlying implementation. See the Spring Security documentation.
(the
apf.filterProcessesUrl
config
setting)
changed
to
(the
switchUser.exitUserUrl
config
setting)
changed
to
Note that the 2.x.x plugin was written primarily in Java, with Groovy used only for dynamic calls,
but in version 3 all Java classes were converted to Groovy with the @CompileStatic annotation. Java
was used because Spring Security is configured as a chain of servlet filters that fire for every
request (including static resources) and the cumulative cost of many small Groovy performance
hits can be non-trivial. But with @CompileStatic we get the best of both worlds - Java performance,
and Groovy compactness. If youre curious you can see these changes in this GitHub commit.
Also, since Grails 3 no longer supports Gant scripts, the plugins scripts were converted to the newer
approach. This should have no effect on usage as the calling syntax and results are the same as
before, although the console output looks somewhat different. You can see these changes in this
GitHub commit.
period characters in map keys. There are now no configuration properties that
are single maps; all have been converted to lists of single-entry maps. This
includes controllerAnnotations.staticRules and interceptUrlMap (see Configuring
Request Mappings to Secure URLs), ipRestrictions (see IP Address Restrictions),
filterChain.chainMap
(see
Filters),
secureChannel.definition
(see
Channel
2.1. Installation
The installation process has changed in version 3+ of the plugin, but theyre the same as for any
Grails 3 plugin. Simply add an entry in the dependencies block of your build.gradle file, changing
the version as needed:
build.gradle
dependencies {
...
compile 'org.grails.plugins:spring-security-core:3.1.1'
...
Run the s2-quickstart script to generate domain classes and add the initial configuration settings in
application.groovy.
2.2. Configuration
In Grails 2, configuration settings were stored in grails-app/conf/Config.groovy, but theyre in
YAML format in grails-app/conf/application.yml now. You can use the Groovy ConfigObject style if
you want, in grails-app/conf/application.groovy. The file isnt created by the create-app script but
if you create it manually it will be recognized. When you run any of the plugin scripts, settings are
added in application.groovy (it will be created if necessary) but if you prefer to keep your settings
in YAML format, feel free to move them to application.yml. Note that this wont work for values
that arent supported in YAML format, for example Closures or other Java or Groovy objects.
grails.plugin.springsecurity.logout.postOnly = false
New applications should use bcrypt or PBKDF2, but if you didnt change the default settings in
previous versions of the plugin and want to continue using the same algorithm, use these settings:
grails.plugin.springsecurity.password.algorithm = 'SHA-256'
grails.plugin.springsecurity.password.hash.iterations = 1
grails.plugin.springsecurity.useSessionFixationPrevention = false
@Secured(value=["hasRole('ROLE_ADMIN')"], httpMethod='POST')
def someMethod() { ... }
In addition, you can define a closure in the annotation which will be called during access checking.
The closure must return true or false and has all of the methods and properties that are available
when
using
SpEL
expressions,
since
the
closures
delegate
is
set
to
subclass
of
@Secured(closure = {
assert request
assert ctx
authentication.name == 'admin1'
})
def someMethod() { ... }
Since you still have to be careful to differentiate between anonymous and non-anonymous
authentications, the plugin now creates an anonymous Authentication which will be an instance of
grails.plugin.springsecurity.authentication.GrailsAnonymousAuthenticationToken with a standard
org.springframework.security.core.userdetails.User instance as its Principal. The authentication
will have a single granted role, ROLE_ANONYMOUS.
3.6. No HQL
Some parts of the code used HQL queries, for example in the generated UserRole class and in
SpringSecurityService.findRequestmapsByRole. These have been replaced
by where queries to
the
plugin
includes
the
grails.plugin.springsecurity.LoginController.groovy
and
(userLookup.userDomainClassName,
requestMap.className,
set
the
grails.plugin.springsecurity.sch.strategyName
config
property
to
can
enable
debug
filter
based
on
the
10
development mode so consider adding the property that enables it inside an environments block in
Config.groovy
environments {
development {
grails.logging.jul.usebridge = true
grails.plugin.springsecurity.debug.useFilter = true
}
production {
grails.logging.jul.usebridge = false
}
}
Also add the implementation class name in your Log4j configuration:
info 'grails.plugin.springsecurity.web.filter.DebugFilter'
annotation
in
the
plugin
is
grails.plugin.springsecurity.annotation,
not
you
could
configure
the
details
class
that
was
constructed
by
the
11
MyAuthenticationDetailsSource.groovy
package com.mycompany
import javax.servlet.http.HttpServletRequest
import org.springframework.security.authentication.AuthenticationDetailsSource
class MyAuthenticationDetailsSource implements
AuthenticationDetailsSource<HttpServletRequest, MyWebAuthenticationDetails> {
import com.mycompany.MyAuthenticationDetailsSource
beans = {
authenticationDetailsSource(MyAuthenticationDetailsSource) {
12
package com.mycompany.myapp
import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString
13
@EqualsAndHashCode(includes='username')
@ToString(includes='username', includeNames=true, includePackage=false)
class User implements Serializable {
transient springSecurityService
String username
String password
boolean enabled = true
boolean accountExpired
boolean accountLocked
boolean passwordExpired
Set<Role> getAuthorities() {
UserRole.findAllByUser(this)*.role
}
def beforeInsert() {
encodePassword()
}
def beforeUpdate() {
if (isDirty('password')) {
encodePassword()
}
}
static constraints = {
password blank: false, password: true
username blank: false, unique: true
}
static mapping = {
password column: '`password`'
}
Optionally, you can add other properties such as email, firstName, and lastName, convenience
methods, and so on:
User.groovy
14
package com.mycompany.myapp
import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString
@EqualsAndHashCode(includes='username')
@ToString(includes='username', includeNames=true, includePackage=false)
class User implements Serializable {
transient springSecurityService
String username
String password
boolean enabled = true
String email
String firstName
String lastName
boolean accountExpired
boolean accountLocked
boolean passwordExpired
Set<Role> getAuthorities() {
UserRole.findAllByUser(this)*.role
}
def someMethod() {
...
}
def beforeInsert() {
encodePassword()
}
def beforeUpdate() {
if (isDirty('password')) {
encodePassword()
}
}
static constraints = {
15
static mapping = {
password column: '`password`'
}
Default Value
Meaning
userLookup.userDomainClas none
sName
userLookup.usernamePrope username
rtyName
userLookup.passwordPrope password
rtyName
userLookup.authoritiesProp authorities
ertyName
userLookup.enabledPropert enabled
yName
userLookup.accountExpired accountExpired
PropertyName
userLookup.accountLockedP accountLocked
ropertyName
userLookup.passwordExpire passwordExpired
dPropertyName
userLookup.authorityJoinCl
assName
none
16
roles, the plugin grants the user a virtual role, ROLE_NO_ROLES. Thus the user satisfies Spring
Securitys requirements but cannot access secure resources, as you would not associate any secure
resources with this role.
Note that you arent required to use roles at all; an application with simple
Like the person class, the authority class has a default name, Authority, and a default name for
its one required property, authority. If you want to use another existing domain class, it simply has
to have a property for name. As with the name of the class, the names of the properties can be
whatever you want - theyre specified in grails-app/conf/application.groovy.
Assuming you choose com.mycompany.myapp as your package, and Role as your class name, youll
generate this class:
Role.groovy
package com.mycompany.myapp
import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString
@EqualsAndHashCode(includes='authority')
@ToString(includes='authority', includeNames=true, includePackage=false)
class Role implements Serializable {
String authority
static constraints = {
authority blank: false, unique: true
}
static mapping = {
cache true
}
The class and property names are configurable using these configuration attributes:
Table 2. Role class configuration options
Property
Default Value
Meaning
authority.className
none
authority.nameField
authority
17
Role names must start with ROLE_. This is configurable in Spring Security, but
not in the plugin. It would be possible to allow different prefixes, but its
important that the prefix not be blank as the prefix is used to differentiate
between
role
names
and
tokens
such
as
package com.mycompany.myapp
import grails.gorm.DetachedCriteria
import groovy.transform.ToString
import org.apache.commons.lang.builder.HashCodeBuilder
@ToString(cache=true, includeNames=true, includePackage=false)
class UserRole implements Serializable {
User user
Role role
@Override
boolean equals(other) {
if (other instanceof UserRole) {
other.userId == user?.id && other.roleId == role?.id
}
}
18
@Override
int hashCode() {
def builder = new HashCodeBuilder()
if (user) builder.append(user.id)
if (role) builder.append(role.id)
builder.toHashCode()
}
static constraints = {
role validator: { Role r, UserRole ur ->
if (ur.user?.id) {
UserRole.withNewSession {
if (UserRole.exists(ur.user.id, r.id)) {
return ['userRole.exists']
}
19
static mapping = {
id composite: ['user', 'role']
version false
}
The helper methods make it easy to grant or revoke roles. Assuming you have already loaded a user
and a role, you grant the role to the user as follows:
Listing 1. Granting a role
Default Value
Meaning
userLookup.authorityJoinCl
assName
none
20
your package and RoleGroup and Role as your class names, youll generate this class:
RoleGroup.groovy
package com.mycompany.myapp
import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString
@EqualsAndHashCode(includes='name')
@ToString(includes='name', includeNames=true, includePackage=false)
class RoleGroup implements Serializable {
String name
Set<Role> getAuthorities() {
RoleGroupRole.findAllByRoleGroup(this)*.role
}
static constraints = {
name blank: false, unique: true
}
static mapping = {
cache true
}
When running the s2-quickstart script with the group name specified, the person class will be
generated differently to accommodate the use of groups. Assuming you use com.mycompany.myapp as
your package and User and RoleGroup as your class names, the getAuthorities() method will be
generated like so:
Listing 3. The generated getAuthorities() method when using role groups
Set<RoleGroup> getAuthorities() {
UserRoleGroup.findAllByUser(this)*.roleGroup
}
The plugin assumes the attribute authorities will provide the authority collection for each class,
but you can change the property names in grails-app/conf/application.groovy. You also must
ensure that the property useRoleGroups is set to true in order for GormUserDetailsService to properly
retrieve the authorities.
Table 4. RoleGroup configuration options
21
Property
Default Value
Meaning
useRoleGroups
false
package com.mycompany.myapp
import grails.gorm.DetachedCriteria
import groovy.transform.ToString
import org.apache.commons.lang.builder.HashCodeBuilder
@ToString(cache=true, includeNames=true, includePackage=false)
class UserRoleGroup implements Serializable {
User user
RoleGroup roleGroup
@Override
boolean equals(other) {
if (other instanceof UserRoleGroup) {
other.userId == user?.id && other.roleGroupId == roleGroup?.id
}
}
@Override
int hashCode() {
def builder = new HashCodeBuilder()
if (user) builder.append(user.id)
22
if (roleGroup) builder.append(roleGroup.id)
builder.toHashCode()
static constraints = {
user validator: { User u, UserRoleGroup ug ->
if (ug.roleGroup?.id) {
UserRoleGroup.withNewSession {
if (UserRoleGroup.exists(u.id, ug.roleGroup.id)) {
return ['userGroup.exists']
}
}
}
}
}
23
static mapping = {
id composite: ['roleGroup', 'user']
version false
}
package com.mycompany.myapp
import grails.gorm.DetachedCriteria
import groovy.transform.ToString
import org.apache.commons.lang.builder.HashCodeBuilder
@ToString(cache=true, includeNames=true, includePackage=false)
class RoleGroupRole implements Serializable {
RoleGroup roleGroup
Role role
@Override
boolean equals(other) {
if (other instanceof RoleGroupRole) {
other.roleIid == role?.id && other.roleGroupId == roleGroup?.id
}
}
@Override
int hashCode() {
def builder = new HashCodeBuilder()
if (roleGroup) builder.append(roleGroup.id)
if (role) builder.append(role.id)
builder.toHashCode()
}
24
static constraints = {
role validator: { Role r, RoleGroupRole rg ->
if (rg.roleGroup?.id) {
RoleGroupRole.withNewSession {
if (RoleGroupRole.exists(rg.roleGroup.id, r.id)) {
return ['roleGroup.exists']
}
}
}
}
}
static mapping = {
id composite: ['roleGroup', 'role']
version false
25
Default Value
Meaning
requestMap.className
none
requestMap.urlField
url
requestMap.configAttribute
Field
configAttribute
requestMap.httpMethodFiel httpMethod
d
Assuming you choose com.mycompany.myapp as your package, and Requestmap as your class name,
youll generate this class:
26
Requestmap.groovy
package com.mycompany.myapp
import org.springframework.http.HttpMethod
import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString
@EqualsAndHashCode(includes=['configAttribute', 'httpMethod', 'url'])
@ToString(includes=['configAttribute', 'httpMethod', 'url'], cache=true,
includeNames=true, includePackage=false)
class Requestmap implements Serializable {
String configAttribute
HttpMethod httpMethod
String url
static constraints = {
configAttribute blank: false
httpMethod nullable: true
url blank: false, unique: 'httpMethod'
}
static mapping = {
cache true
}
To use Requestmap entries to guard URLs, see Requestmap Instances Stored in the Database.
27
that
the
two
settings
are
mutually
exclusive.
If
rejectIfNoRule
is
true
then
fii.rejectPublicInvocations is ignored because the request will transition to the login page or the
error 403 page. If you want the more obvious error page, set fii.rejectPublicInvocations to true
and rejectIfNoRule to false to allow that check to occur.
To reject un-mapped URLs with a 403 error code, use these settings (or none since rejectIfNoRule
defaults to true)
Listing 4. Enabling rejectIfNoRule
grails.plugin.springsecurity.rejectIfNoRule = true
grails.plugin.springsecurity.fii.rejectPublicInvocations = false
28
and to reject with the error 500 page, use these (optionally omit rejectPublicInvocations since it
defaults to true):
Listing 5. Enabling fii.rejectPublicInvocations
grails.plugin.springsecurity.rejectIfNoRule = false
grails.plugin.springsecurity.fii.rejectPublicInvocations = true
Note that if you set rejectIfNoRule or rejectPublicInvocations to true youll need to configure the
staticRules map to include URLs that cant otherwise be guarded:
Listing 6. Example controllerAnnotations.staticRules configuration when using rejectIfNoRule or
fii.rejectPublicInvocations
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']]
]
29
Note that the syntax of the staticRules block has changed from previous versions
of the plugin where the keys were URL patterns and the values were access rules
(roles, expressions, etc.) To avoid issues in configuration parsing and to allow
optionally specifying the HTTP method associated with one or more of the rules,
the staticRules block is now specified as a List of Maps. Each Map defines one
combination of url pattern and access rules (and optionally HTTP method). If
there are multiple access rules, specify them as a List of Strings; if there is only
one access rule, its value can be a String or a single-element List.
The preceding staticRules example includes the default mappings defined when
running the s2-quickstart script. Heres a more complete example using all
configuration options:
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: '/user/**',
access: 'ROLE_USER'],
[pattern: '/admin/**',
access: ['ROLE_ADMIN',
'isFullyAuthenticated()']],
[pattern: '/thing/register', access: 'isAuthenticated()',
httpMethod: 'PUT']
]
Now in addition to the default mappings, we require an authentication with
ROLE_USER for any URL starting with /user, a fully authenticated authentication
(i.e. an explicit login was performed without using remember-me) with ROLE_USER
for any URL starting with /admin, and finally to access the URL /thing/register
the user must be authenticated with any role(s) but must use a PUT request.
This
is
needed
when
using
annotations;
if
you
use
the
30
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: '/logout/**',
access: ['permitAll']]
]
In addition, when you enable the switch-user feature, youll have to specify access rules for the
associated URLs, e.g.
31
SpEL
expression
isAuthenticated()
or
isRememberMe()
is
equivalent
to
more
information
on
IS_AUTHENTICATED_FULLY,
IS_AUTHENTICATED_REMEMBERED,
and
<g:form>
...
<g:actionSubmit class="save" action="update" value='Update' />
<g:actionSubmit class="delete" action="delete" value="'Delete' />
</g:form>
both actions will be allowed if the user has permission to access the /person/index
url, which would often be the case.
The workaround is to create separate forms without using actionSubmit and
explicitly set the action on the <g:form> tags, which will result in form
submissions to the expected urls and properly guarded urls.
32
can
use
an
@Secured
annotation
org.springframework.security.access.annotation.Secured
(either
or
the
the
standard
plugins
grails.plugin.springsecurity.securityConfigType = "Annotation"
You can define the annotation at the class level, meaning that the specified roles are required for all
actions, or at the action level, or both. If the class and an action are annotated then the action
annotation values will be used since theyre more specific.
For example, given this controller:
33
package com.mycompany.myapp
import grails.plugin.springsecurity.annotation.Secured
class SecureAnnotatedController {
@Secured('ROLE_ADMIN')
def index() {
render 'you have ROLE_ADMIN'
}
@Secured(['ROLE_ADMIN', 'ROLE_SUPERUSER'])
def adminEither() {
render 'you have ROLE_ADMIN or SUPERUSER'
}
def anybody() {
render 'anyone can see this' // assuming you're not using "strict" mode,
// otherwise the action is not viewable by anyone
}
you
must
be
authenticated
and
have
ROLE_ADMIN
to
see
/myapp/secureAnnotated
(or
using
SpEL
expressions,
since
the
closures
delegate
is
set
to
subclass
of
@Secured(closure = {
assert request
assert ctx
authentication.name == 'admin1'
})
def someMethod() {
...
}
Often most actions in a controller require similar access rules, so you can also define annotations at
the class level:
34
package com.mycompany.myapp
import grails.plugin.springsecurity.annotation.Secured
@Secured('ROLE_ADMIN')
class SecureClassAnnotatedController {
def index() {
render 'index: you have ROLE_ADMIN'
}
def otherAction() {
render 'otherAction: you have ROLE_ADMIN'
}
@Secured('ROLE_SUPERUSER')
def super() {
render 'super: you have ROLE_SUPERUSER'
}
Here you need to be authenticated and have ROLE_ADMIN to see /myapp/secureClassAnnotated (or
/myapp/secureClassAnnotated/index) or /myapp/secureClassAnnotated/otherAction. However, you
must have ROLE_SUPERUSER to access /myapp/secureClassAnnotated/super. The action-scope annotation
overrides the class-scope annotation. Note that strict mode isnt applicable here since all actions
have an access rule defined (either explicitly or inherited from the class-level annotation).
Additionally, you can specify the HTTP method that is required in each annotation for the access
rule, e.g.
package com.mycompany.myapp
import grails.plugin.springsecurity.annotation.Secured
class SecureAnnotatedController {
Here you must have ROLE_ADMIN for both the create and save actions but create requires a GET
35
request (since it renders the form to create a new instance) and save requires POST (since its the
action that the form posts to).
@Resource
@Secured('ROLE_ADMIN')
class Thing {
String name
5.4.2. controllerAnnotations.staticRules
You can also define static mappings that cannot be expressed in the controllers, such as '/**' or for
JavaScript, CSS, or image URLs. Use the controllerAnnotations.staticRules property, for example:
grails.plugin.springsecurity.controllerAnnotations.staticRules = [
...
[pattern: '/js/admin/**',
access: ['ROLE_ADMIN']],
[pattern: '/someplugin/**', access: ['ROLE_ADMIN']]
]
This example maps all URLs associated with SomePluginController, which has URLs of the form
/somePlugin/, to ROLE_ADMIN; annotations are not an option here because you would not edit plugin
code for a change like this.
When mapping URLs for controllers that are mapped in UrlMappings.groovy, you
need to secure the un-url-mapped URLs. For example if you have a
FooBarController that you map to /foo/bar/$action, you must register that in
use
static
map
in
securityConfigType="InterceptUrlMap":
36
application.groovy
to
secure
URLs,
first
specify
grails.plugin.springsecurity.securityConfigType = "InterceptUrlMap"
Define a Map in application.groovy:
Listing 13. Example grails.plugin.springsecurity.interceptUrlMap
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/**',
access: ['permitAll']],
[pattern: '/logout',
access: ['permitAll']],
[pattern: '/logout/**',
access: ['permitAll']]
]
and add any custom mappings as needed, e.g.
Listing 14. Custom interceptUrlMap mappings
grails.plugin.springsecurity.interceptUrlMap = [
...
[pattern: '/secure/**', access: ['ROLE_ADMIN']],
[pattern: '/finance/**', access: ['ROLE_FINANCE', 'isFullyAuthenticated()']]
]
When using this approach, make sure that you order the rules correctly. The first applicable rule is
used, so for example if you have a controller that has one set of rules but an action that has stricter
access rules, e.g.
Listing 15. Incorrect interceptUrlMap order
[pattern: '/secure/**',
[pattern: '/secure/reallysecure/**',
then this would fail - it wouldnt restrict access to /secure/reallysecure/list to a user with
ROLE_SUPERUSER since the first URL pattern matches, so the second would be ignored. The correct
mapping would be
37
grails.plugin.springsecurity.securityConfigType = "Requestmap"
You create Requestmap entries as you create entries in any Grails domain class:
Listing 18. Creating Requestmap entries
new
Requestmap(url: '/profile/**',
configAttribute: 'ROLE_USER').save()
Requestmap(url: '/admin/**',
configAttribute: 'ROLE_ADMIN').save()
Requestmap(url: '/admin/role/**', configAttribute: 'ROLE_SUPERVISOR').save()
Requestmap(url: '/admin/user/**',
configAttribute: 'ROLE_ADMIN,ROLE_SUPERVISOR').save()
Requestmap(url: '/login/impersonate',
configAttribute: 'ROLE_SWITCH_USER,isFullyAuthenticated()').save()
The configAttribute value can have a single value or have multiple comma-delimited values. In this
example only users with ROLE_ADMIN or ROLE_SUPERVISOR can access /admin/user/** urls, and only
users with ROLE_SWITCH_USER can access the switch-user url (/login/impersonate) and in addition
must be authenticated fully, i.e. not using a remember-me cookie. Note that when specifying
multiple
roles,
the
user
must
have
at
least
one
of
them,
but
when
combining
38
entry order because the plugin calculates the most specific rule that applies to the current request.
class RequestmapController {
def springSecurityService
...
springSecurityService.clearCachedRequestmaps()
flash.message = ...
redirect action: 'show', id: requestmap.id
39
package com.yourcompany.yourapp
import grails.plugin.springsecurity.annotation.Secured
class SecureController {
@Secured("hasRole('ROLE_ADMIN')")
def someAction() {
...
}
@Secured("authentication.name == 'ralph'")
def someOtherAction() {
...
}
In this example, someAction requires ROLE_ADMIN, and someOtherAction requires that the user be
logged in with username ralph.
The corresponding Requestmap URLs would be
Listing 21. Creating Requestmap instances
configAttribute: "hasRole('ROLE_ADMIN')").save()
new Requestmap(url: "/secure/someOtherAction",
grails.plugin.springsecurity.interceptUrlMap = [
[pattern: '/secure/someAction',
access: ["hasRole('ROLE_ADMIN')"]],
[pattern: '/secure/someOtherAction', access: ["authentication.name == 'ralph'"]]
]
The Spring Security docs have a table listing the standard expressions, which is copied here for
reference:
Table 6. Spring Security expressions
Expression
Description
hasRole(role)
40
Expression
Description
hasAnyRole([role1,role2])
principal
authentication
permitAll
denyAll
isAnonymous()
isRememberMe()
isAuthenticated()
isFullyAuthenticated()
request
In addition, you can use a web-specific expression hasIpAddress. However, you may find it more
convenient to separate IP restrictions from role restrictions by using the IP address filter (IP
Address Restrictions).
To help you migrate traditional configurations to expressions, this table compares various
configurations and their corresponding expressions:
Table 7. Traditional configurations and associated expressions
Traditional Config
Expression
ROLE_ADMIN
hasRole('ROLE_ADMIN')
ROLE_USER,ROLE_ADMIN
hasAnyRole('ROLE_USER','ROLE_ADMIN')
ROLE_ADMIN,IS_AUTHENTICATED_FULLY
hasRole('ROLE_ADMIN') and
isFullyAuthenticated()
IS_AUTHENTICATED_ANONYMOUSLY
permitAll
IS_AUTHENTICATED_REMEMBERED
isAuthenticated() or isRememberMe()
IS_AUTHENTICATED_FULLY
isFullyAuthenticated()
41
6.1. SecurityTagLib
The plugin includes GSP tags to support conditional display based on whether the user is
authenticated, and/or has the required role to perform a particular action. These tags are in the sec
namespace and are implemented in grails.plugin.springsecurity.SecurityTagLib.
6.1.1. ifLoggedIn
Displays the inner body content if the user is authenticated.
Example:
Listing 23. Example using <sec:ifLoggedIn>
<sec:ifLoggedIn>
Welcome Back!
</sec:ifLoggedIn>
6.1.2. ifNotLoggedIn
Displays the inner body content if the user is not authenticated.
Example:
Listing 24. Example using <sec:ifNotLoggedIn>
<sec:ifNotLoggedIn>
<g:link controller='login' action='auth'>Login</g:link>
</sec:ifNotLoggedIn>
6.1.3. ifAllGranted
Displays the inner body content only if all of the listed roles are granted.
Example:
42
<sec:ifAllGranted roles='ROLE_ADMIN,ROLE_SUPERVISOR'>
...
secure stuff here
...
</sec:ifAllGranted>
6.1.4. ifAnyGranted
Displays the inner body content if at least one of the listed roles are granted.
Example:
Listing 26. Example using <sec:ifAnyGranted>
<sec:ifAnyGranted roles='ROLE_ADMIN,ROLE_SUPERVISOR'>
...
secure stuff here
...
</sec:ifAnyGranted>
6.1.5. ifNotGranted
Displays the inner body content if none of the listed roles are granted.
Example:
Listing 27. Example using <sec:ifNotGranted>
<sec:ifNotGranted roles='ROLE_USER'>
...
non-user stuff here
...
</sec:ifNotGranted>
6.1.6. loggedInUserInfo
Displays the value of the specified UserDetails property if logged in. For example, to show the
username property:
Listing 28. Example using <sec:loggedInUserInfo>
<sec:loggedInUserInfo field='username'/>
If you have customized the UserDetails (e.g. with a custom UserDetailsService) to add a fullName
property, you access it as follows:
43
6.1.7. username
Displays the value of the UserDetails username property if logged in.
Listing 30. Example using <sec:username>
<sec:ifLoggedIn>
Welcome Back <sec:username/>!
</sec:ifLoggedIn>
<sec:ifNotLoggedIn>
<g:link controller='login' action='auth'>Login</g:link>
</sec:ifNotLoggedIn>
6.1.8. ifSwitched
Displays the inner body content only if the current user switched from another user. (See also
Switch User.)
Listing 31. Example using <sec:ifSwitched> and <sec:ifNotSwitched>
<sec:ifLoggedIn>
Logged in as <sec:username/>
</sec:ifLoggedIn>
<sec:ifSwitched>
<a href='${request.contextPath}/logout/impersonate'>
Resume as <sec:switchedUserOriginalUsername/>
</a>
</sec:ifSwitched>
<sec:ifNotSwitched>
<sec:ifAllGranted roles='ROLE_SWITCH_USER'>
<form action='${request.contextPath}/login/impersonate'
method='POST'>
</sec:ifAllGranted>
</sec:ifNotSwitched>
44
6.1.9. ifNotSwitched
Displays the inner body content only if the current user has not switched from another user.
6.1.10. switchedUserOriginalUsername
Renders the original users username if the current user switched from another user.
Listing 32. Example using <sec:switchedUserOriginalUsername>
<sec:ifSwitched>
<a href='${request.contextPath}/logout/impersonate'>
Resume as <sec:switchedUserOriginalUsername/>
</a>
</sec:ifSwitched>
6.1.11. access
Renders the body if the specified expression evaluates to true or specified URL is allowed.
Listing 33. Example using <sec:access> with an expression
<sec:access expression="hasRole('ROLE_USER')">
You're a user
</sec:access>
Listing 34. Example using <sec:access> with a URL
<sec:access url='/admin/user'>
<g:link controller='admin' action='user'>Manage Users</g:link>
</sec:access>
You can also guard access to links generated from controller and action names or named URL
mappings instead of hard-coding the values, for example
Listing 35. Example using <sec:access> with a controller and action
45
<sec:access mapping='manageUsers'>
<g:link mapping='manageUsers'>Manage Users</g:link>
</sec:access>
For even more control of the generated URL (still avoiding hard-coding) you can use createLink to
build the URL, for example
Listing 37. Example using <sec:access> with <g:createLink>
6.1.12. noAccess
Renders the body if the specified expression evaluates to false or URL isnt allowed.
Listing 38. Example using <sec:noAccess>
<sec:noAccess expression="hasRole('ROLE_USER')">
You're not a user
</sec:noAccess>
6.1.13. link
A wrapper around the standard Grails link tag that renders if the specified expression evaluates to
true or URL is allowed.
To define the expression to evaluate within the tag itself:
Listing 39. Example using <sec:link> with an expression
46
6.2. SpringSecurityService
grails.plugin.springsecurity.SpringSecurityService provides security utility functions. It is a
regular Grails service, so you use dependency injection to inject it into a controller, service, taglib,
and so on:
def springSecurityService
6.2.1. getCurrentUser()
Retrieves a domain class instance for the currently authenticated user. During authentication a
user/person domain class instance is retrieved to get the users password, roles, etc. and the id of
the instance is saved. This method uses the id and the domain class to re-load the instance, or the
username if the UserDetails instance is not a GrailsUser.
If you do not need domain class data other than the id, you should use the loadCurrentUser method
instead.
Example:
Listing 41. Example using getCurrentUser()
class SomeController {
def springSecurityService
def someAction() {
def user = springSecurityService.currentUser
...
}
6.2.2. loadCurrentUser()
Often it is not necessary to retrieve the entire domain class instance, for example when using it in a
query where only the id is needed as a foreign key. This method uses the GORM load method to
create a proxy instance. This will never be null, but can be invalid if the id doesnt correspond to a
row in the database, although this is very unlikely in this scenario because the instance would have
been there during authentication.
If you need other data than just the id, use the getCurrentUser method instead.
Example:
47
class SomeController {
def springSecurityService
6.2.3. isLoggedIn()
Checks whether there is a currently logged-in user.
Example:
Listing 43. Example using isLoggedIn()
class SomeController {
def springSecurityService
def someAction() {
if (springSecurityService.isLoggedIn()) {
...
}
else {
...
}
}
6.2.4. getAuthentication()
Retrieves the current users Authentication. If authenticated, this will typically be a
UsernamePasswordAuthenticationToken.
If not authenticated and the AnonymousAuthenticationFilter is active (true by default) then the
anonymous
users
authentication
will
be
returned.
This
will
be
an
instance
of
48
class SomeController {
def springSecurityService
def someAction() {
def auth = springSecurityService.authentication
String username = auth.username
def authorities = auth.authorities // a Collection of GrantedAuthority
boolean authenticated = auth.authenticated
...
}
6.2.5. getPrincipal()
Retrieves the currently logged in users Principal. If authenticated, the principal will be a
grails.plugin.springsecurity.userdetails.GrailsUser,
unless
you
have
created
custom
UserDetailsService, in which case it will be whatever implementation of UserDetails you use there.
If not authenticated and the AnonymousAuthenticationFilter is active (true by default) then a
standard org.springframework.security.core.userdetails.User is used.
Example:
Listing 45. Example using getPrincipal()
class SomeController {
def springSecurityService
def someAction() {
def principal =
String username
def authorities
boolean enabled
...
}
springSecurityService.principal
= principal.username
= principal.authorities // a Collection of GrantedAuthority
= principal.enabled
6.2.6. encodePassword()
Hashes a password with the configured hashing scheme. By default the plugin uses bcrypt, but you
can configure the scheme with the grails.plugin.springsecurity.password.algorithm attribute in
49
application.groovy. The supported values are bcrypt to use bcrypt, pbkdf2 to use PBKDF2, or any
message digest algorithm that is supported in your JDK; see this Java page for the available
algorithms.
You are strongly discouraged from using MD5 or SHA-1 algorithms because of
their well-known vulnerabilities. You should also use a salt for your passwords,
which greatly increases the computational complexity of computing passwords if
your database gets compromised. See Salted Passwords.
Example:
Listing 46. Example using encodePassword()
class PersonController {
def springSecurityService
params.salt = person.salt
if (person.password != params.password) {
params.password = springSecurityService.encodePassword(password, salt)
def salt = ... // e.g. randomly generated using some utility method
params.salt = salt
}
person.properties = params
if (!person.save(flush: true)) {
render view: 'edit', model: [person: person]
return
}
redirect action: 'show', id: person.id
If you are hashing the password in the User domain class (using beforeInsert and
6.2.7. updateRole()
Updates a role and, if you use Requestmap instances to secure URLs, updates the role name in all
affected Requestmap definitions if the name was changed.
Example:
50
class RoleController {
def springSecurityService
6.2.8. deleteRole()
Deletes a role and, if you use Requestmap instances to secure URLs, removes the role from all
affected Requestmap definitions. If a Requestmap's config attribute is only the role name (for example,
[pattern: '/foo/bar', access: 'ROLE_FOO']), it is deleted.
Example:
Listing 48. Example using deleteRole()
class RoleController {
def springSecurityService
6.2.9. clearCachedRequestmaps()
Flushes the Requestmaps cache and triggers a complete reload. If you use Requestmap instances to
secure URLs, the plugin loads and caches all Requestmap instances as a performance optimization.
This action saves database activity because the requestmaps are checked for each request. Do not
51
allow the cache to become stale. When you create, edit or delete a Requestmap, flush the cache. Both
updateRole() and deleteRole() call clearCachedRequestmaps()for you. Call this method when you
create a new Requestmap or do other Requestmap work that affects the cache.
Example:
Listing 49. Example using clearCachedRequestmaps()
class RequestmapController {
def springSecurityService
springSecurityService.clearCachedRequestmaps()
flash.message = "Requestmap created"
redirect action: show, id: requestmap.id
6.2.10. reauthenticate()
Rebuilds an Authentication for the given username and registers it in the security context. You
typically use this method after updating a users authorities or other data that is cached in the
Authentication or Principal. It also removes the user from the user cache to force a refresh at next
login.
Example:
52
class UserController {
def springSecurityService
params.salt = user.salt
if (params.password) {
params.password = springSecurityService.encodePassword(params.password, salt)
def salt = ... // e.g. randomly generated using some utility method
params.salt = salt
}
user.properties = params
if (!user.save(flush: true)) {
render view: 'edit', model: [userInstance: user]
return
}
if (springSecurityService.loggedIn &&
springSecurityService.principal.username == user.username) {
springSecurityService.reauthenticate user.username
}
6.3. SpringSecurityUtils
grails.plugin.springsecurity.SpringSecurityUtils is a utility class with static methods that you can
call directly without using dependency injection. It is primarily an internal class but can be called
from application code.
6.3.1. authoritiesToRoles()
Extracts role names from an array or Collection of GrantedAuthority.
6.3.2. getPrincipalAuthorities()
Retrieves the currently logged-in users authorities. It is empty (but never null) if the user is not
logged in.
6.3.3. parseAuthoritiesString()
Splits a comma-delimited String containing role names into a List of GrantedAuthority.
53
6.3.4. ifAllGranted()
Checks whether the current user has all specified roles (a comma-delimited String of role names).
Primarily used by SecurityTagLib.ifAllGranted.
6.3.5. ifNotGranted()
Checks whether the current user has none of the specified roles (a comma-delimited String of role
names). Primarily used by SecurityTagLib.ifNotGranted.
6.3.6. ifAnyGranted()
Checks whether the current user has any of the specified roles (a comma-delimited String of role
names). Primarily used by SecurityTagLib.ifAnyGranted.
6.3.7. getSecurityConfig()
Retrieves the security part of the Configuration (from grails-app/conf/application.groovy merged
with the plugins default configuration).
6.3.8. loadSecondaryConfig()
Used by dependent plugins to add configuration attributes.
6.3.9. reloadSecurityConfig()
Forces a reload of the security configuration.
6.3.10. isAjax()
Checks whether the request was triggered by an Ajax call. The standard way is to determine
whether X-Requested-With request header is set and has the value XMLHttpRequest. In addition, you
can configure the name of the header with the grails.plugin.springsecurity.ajaxHeader
configuration attribute, but this is not recommended because all major JavaScript toolkits use the
standard name. Further, you can register a closure in application.groovy with the name
ajaxCheckClosure that will be used to check if a request is an Ajax request. It is passed the request as
its single argument, e.g.
Listing 51. Customizing Ajax detection with grails.plugin.springsecurity.ajaxCheckClosure
54
6.3.11. registerProvider()
Used by dependent plugins to register an AuthenticationProvider bean name.
6.3.12. registerFilter()
Used by dependent plugins to register a filter bean name in a specified position in the filter chain.
6.3.13. isSwitched()
Checks whether the current user switched from another user.
6.3.14. getSwitchedUserOriginalUsername()
Gets the original users username if the current user switched from another user.
6.3.15. doWithAuth()
Executes a Closure with the current authentication. The one-parameter version which takes just a
Closure assumes that theres an authentication in the HTTP Session and that the Closure is running
in a separate thread from the web request, so the SecurityContext and Authentication arent
available to the standard ThreadLocal. This is primarily of use when you explicitly launch a new
thread from a controller action or service called in request scope, not from a Quartz job which isnt
associated with an authentication in any thread.
The two-parameter version takes a username and a Closure to authenticate as. This is will
authenticate as the specified user and execute the closure with that authentication. It restores the
authentication to the one that was active if it exists, or clears the context otherwise. This is similar
to run-as and switch-user but is only local to the Closure.
55
Chapter 7. Events
Spring Security fires application events after various security-related actions such as successful
login,
unsuccessful
login,
and
so
on.
Spring
Security
uses
two
main
event
classes,
7.1.1. AuthenticationEventPublisher
Spring Security publishes events using an AuthenticationEventPublisher which in turn fire events
using
the
ApplicationEventPublisher.
AuthenticationEventPublisher
By
default
instance
no
events
are
fired
registered
grails.plugin.springsecurity.authentication.NullAuthenticationEventPublisher.
since
the
is
But
a
you
can
7.1.2. UsernameNotFoundException
Most authentication exceptions trigger an event with a similar name as described in this table:
Table 8. Exceptions and associated events
Exception
Event
AccountExpiredException
AuthenticationFailureExpiredEvent
AuthenticationServiceException
AuthenticationFailureServiceExceptionEvent
LockedException
AuthenticationFailureLockedEvent
CredentialsExpiredException
AuthenticationFailureCredentialsExpiredEvent
DisabledException
AuthenticationFailureDisabledEvent
BadCredentialsException
AuthenticationFailureBadCredentialsEvent
UsernameNotFoundException
AuthenticationFailureBadCredentialsEvent
ProviderNotFoundException
AuthenticationFailureProviderNotFoundEvent
56
This
holds
for
all
exceptions
except
UsernameNotFoundException
which
triggers
an
of
converting
it
to
BadCredentialsException
grails.plugin.springsecurity.dao.hideUserNotFoundExceptions
false
by
setting
in
grails-
app/conf/application.groovy.
Fortunately all subclasses of AbstractAuthenticationFailureEvent have a getException() method
that gives you access to the exception that triggered the event, so you can use that to differentiate
between a bad password and a missing user (if hideUserNotFoundExceptions=false).
package com.foo.bar
import org.springframework.context.ApplicationListener
import org.springframework.security.authentication.event.AuthenticationSuccessEvent
class MySecurityEventListener
implements ApplicationListener<AuthenticationSuccessEvent> {
import com.foo.bar.MySecurityEventListener
beans = {
mySecurityEventListener(MySecurityEventListener)
}
57
grails.plugin.springsecurity.useSecurityEventListener = true
grails.plugin.springsecurity.onInteractiveAuthenticationSuccessEvent = { e, appCtx ->
// handle InteractiveAuthenticationSuccessEvent
}
grails.plugin.springsecurity.onAbstractAuthenticationFailureEvent = { e, appCtx ->
// handle AbstractAuthenticationFailureEvent
}
grails.plugin.springsecurity.onAuthenticationSuccessEvent = { e, appCtx ->
// handle AuthenticationSuccessEvent
}
grails.plugin.springsecurity.onAuthenticationSwitchUserEvent = { e, appCtx ->
// handle AuthenticationSwitchUserEvent
}
grails.plugin.springsecurity.onAuthorizationEvent = { e, appCtx ->
// handle AuthorizationEvent
}
None of these closures are required; if none are configured, nothing will be called. Just implement
the event handlers that you need.
When
user
authenticates,
Spring
Security
initially
fires
an
in
the
SecurityContextHolder,
which
means
that
the
springSecurityService methods that access the logged-in user will not work. Later
in
the
processing
second
InteractiveAuthenticationSuccessEvent,
and
event
is
when
this
fired,
an
happens
the
58
Default Value
Meaning
userLookup.userDomainClas none
sName
userLookup.usernamePrope username
rtyName
userLookup.passwordPrope password
rtyName
userLookup.authoritiesProp authorities
ertyName
userLookup.enabledPropert enabled
yName
userLookup.accountExpired accountExpired
PropertyName
userLookup.accountLockedP accountLocked
ropertyName
userLookup.passwordExpire passwordExpired
dPropertyName
userLookup.authorityJoinCl
assName
none
authority.className
none
authority.nameField
authority
requestMap.className
none
requestMap.urlField
url
requestMap.configAttribute
Field
configAttribute
59
Chapter 9. Authentication
The Spring Security plugin supports several approaches to authentication.
The default approach stores users and roles in your database, and uses an HTML login form which
prompts the user for a username and password. The plugin also supports other approaches as
described in the sections below, as well as add-on plugins that provide external authentication
providers such as LDAP, and single sign-on using CAS
grails.plugin.springsecurity.useBasicAuth = true
grails.plugin.springsecurity.basic.realmName = "Ralph's Bait and Tackle"
Table 10. Basic Authentication configuration options
Property
Default
Description
useBasicAuth
false
basic.realmName
Grails Realm
basic.credentialsCharset
UTF-8
With this authentication in place, users are prompted with the standard browser login dialog
instead of being redirected to a login page.
If you dont want all of your URLs guarded by Basic authentication, you can partition the URL
patterns and apply Basic authentication to some, but regular form login to others. For example, if
you have a web service that uses Basic authentication for /webservice/** URLs, you would
configure that using the chainMap config attribute:
Listing 55. Example filter chain mappings for Basic authentication
grails.plugin.springsecurity.filterChain.chainMap = [
[pattern: '/webservice/**', filters: 'JOINED_FILTERS,-exceptionTranslationFilter'],
[pattern: '/**',
filters: 'JOINED_FILTERS,-basicAuthenticationFilter,basicExceptionTranslationFilter']
]
In this example were using the JOINED_FILTERS keyword instead of explicitly listing the filter names.
Specifying JOINED_FILTERS means to use all of the filters that were configured using the various
config options. In each case we also specify that we want to exclude one or more filters by prefixing
60
want
everything
except
for
the
Basic
authentication
filter
and
its
configured
ExceptionTranslationFilter.
Digest Authentication is similar to Basic but is more secure because it does not send your password
in obfuscated cleartext. Digest resembles Basic in practice - you get the same browser popup dialog
when you authenticate. But because the credential transfer is genuinely hashed (instead of just
Base64-encoded as with Basic authentication) you do not need SSL to guard your logins.
Table 11. Digest Authentication configuration options
Property
Default Value
Meaning
useDigestAuth
false
digest.realmName
Grails Realm
digest.key
changeme
digest.nonceValiditySeconds 300
digest.passwordAlreadyEnco false
ded
digest.createAuthenticatedT false
oken
digest.useCleartextPassword false
s
Digest authentication has a problem in that by default you store cleartext passwords in your
database. This is because the browser hashes your password along with the username and Realm
name, and this is compared to the password hashed using the same algorithm during
authentication. The browser does not know about your MessageDigest algorithm or salt source, so to
hash them the same way you need to load a cleartext password from the database.
The plugin does provide an alternative, although it has no configuration options (in particular the
digest algorithm cannot be changed). If digest.useCleartextPasswords is false (the default), then the
passwordEncoder
bean
is
replaced
with
an
instance
of
61
grails.plugin.springsecurity.authentication.encoding.DigestAuthPasswordEncoder.
This
encoder
uses the same approach as the browser, that is, it combines your password along with your
username and Realm name essentially as a salt, and hashes with MD5. MD5 is not recommended in
general, but given the typical size of the salt it is reasonably safe to use.
The only required attribute is useDigestAuth, which you must set to true, but you probably also
want to change the realm name:
grails.plugin.springsecurity.useDigestAuth = true
grails.plugin.springsecurity.digest.realmName = "Ralph's Bait and Tackle"
Digest authentication cannot be applied to a subset of URLs like Basic authentication can. This is
due to the password encoding issues. So you cannot use the chainMap attribute here - all URLs will
be guarded.
Note that since the Digest authentication password encoder is different from the
typical encoders you must pass the username as the salt value. The code in the
generated User class assumes youre not using a salt value, so youll need to
change the code in encodePassword() from
password = springSecurityService.encodePassword(password)
to
Default Value
Meaning
useX509
false
62
Property
Default Value
Meaning
x509.continueFilterChainOn true
UnsuccessfulAuthentication
x509.subjectDnRegex
CN=(.*?)(?:,|$)
x509.checkForPrincipalChan false
ges
x509.invalidateSessionOnPri true
ncipalChange
x509.subjectDnClosure
none
x509.throwExceptionWhenT false
okenRejected
If true thrown a
BadCredentialsException
The details of configuring your server for SSL and configuring browser certificates are beyond the
scope of this document. If you use Tomcat, see its SSL documentation. To get a test environment
working, see the instructions in this discussion at Stack Overflow.
Default Value
Meaning
rememberMe.cookieName
grails_remember_me
rememberMe.cookieDomain none
rememberMe.alwaysRemem false
ber
63
Property
Default Value
Meaning
rememberMe.parameter
remember-me
rememberMe.key
grailsRocks
rememberMe.useSecureCoo none
kie
rememberMe.createSession
OnSuccess
true
rememberMe.persistent
false
rememberMe.persistentTok
en.domainClassName
none
rememberMe.persistentTok
en.seriesLength
16
rememberMe.persistentTok
en.tokenLength
16
atr.rememberMeClass
RememberMeAuthenticatio
nToken
64
To use this feature, run the s2-create-persistent-token script. This will create the domain class, and
register its name in grails-app/conf/application.groovy. It will also enable persistent logins by
setting rememberMe.persistent to true.
var onLogin;
$.ajaxSetup({
beforeSend: function(jqXHR, event) {
if (event.url != $("#ajaxLoginForm").attr("action")) {
onLogin = event.success;
}
},
statusCode: {
401: function() {
showLogin();
}
}
65
});
function showLogin() {
var ajaxLogin = $("#ajaxLogin");
ajaxLogin.css("text-align", "center");
ajaxLogin.jqmShow();
}
function logout(event) {
event.preventDefault();
$.ajax({
url: $("#_logout").attr("href"),
method: "POST",
window.location = "/";
},
}
});
}
function authAjax() {
$("#loginMessage").html("Sending request ...").show();
66
else {
$("#loginMessage").html(jqXHR.responseText);
}
},
error: function(jqXHR, textStatus, errorThrown) {
if (jqXHR.status == 401 && jqXHR.getResponseHeader("Location")) {
// the login request itself wasn't allowed, possibly because the
// post url is incorrect and access was denied to it
$("#loginMessage").html('<span class="errorMessage">' +
'Sorry, there was a problem with the login request</error>');
}
else {
var responseText = jqXHR.responseText;
if (responseText) {
var json = $.parseJSON(responseText);
if (json.error) {
$("#loginMessage").html('<span class="errorMessage">' +
json.error + "</error>");
return;
}
}
else {
responseText = "Sorry, an error occurred (status: " +
textStatus + ", error: " + errorThrown + ")";
}
$("#loginMessage").html('<span class="errorMessage">' +
responseText + "</error>");
}
}
});
$(function() {
$("#ajaxLogin").jqm({ closeOnEsc: true });
$("#ajaxLogin").jqmAddClose("#cancelLogin");
$("#ajaxLoginForm").submit(function(event) {
event.preventDefault();
authAjax();
});
$("#authAjax").click(authAjax);
$("#logout").click(logout);
});
and create grails-app/assets/stylesheets/ajaxLogin.css and add this CSS:
ajaxLogin.css
#ajaxLogin {
padding:
0px;
text-align: center;
display:
none;
67
}
#ajaxLogin .inner {
width:
padding-bottom:
margin:
text-align:
border:
background-color:
-moz-box-shadow:
-webkit-box-shadow:
-khtml-box-shadow:
box-shadow:
}
400px;
6px;
60px auto;
left;
1px solid #aab;
#f0f0fa;
2px 2px 2px #eee;
2px 2px 2px #eee;
2px 2px 2px #eee;
2px 2px 2px #eee;
.cssform p {
left;
0;
4px 0 3px 0;
105px;
20px;
1%;
68
.ajaxLoginButton:hover, .ajaxLoginButton:focus {
background-color: #999999;
color: #ffffff;
}
#ajaxLogin .inner .login_message {
padding: 6px 25px 20px 25px;
color:
#c33;
}
#ajaxLogin .inner .text_ {
width: 120px;
}
#ajaxLogin .inner .chk {
height: 12px;
}
.errorMessage {
color: red;
}
Theres no need to register the JavaScript files in grails-app/assets/javascripts/application.js if
you have this require_tree directive:
application.js
//= require_tree .
but you can explicitly include them if you want. Register the two CSS files in /grailsapp/assets/stylesheets/application.css:
69
application.css
/*
...
*= require ajaxLogin
*= require jqModal
...
*/
Well need some GSP code to define the HTML, so create grails-app/views/includes/_ajaxLogin.gsp
and add this:
70
_ajaxLogin.gsp
<p>
<label for="username">Username:</label>
</p>
<p>
<label for="password">Password</label>
</p>
<p>
</p>
<p>
class="ajaxLoginButton" />
</p>
</form>
71
main.gsp
...
<g:layoutHead/>
</head>
<body>
<g:render template='/includes/ajaxLogin'/>
...
<g:layoutBody/>
</body>
</html>
The important aspects of this code are:
There is a <span> positioned in the top-right that shows the username and a logout link when
logged in, and a login link otherwise.
The form posts to the same URL as the regular form, /login/authenticate, and is mostly the
same except for the addition of a Cancel button (you can also dismiss the dialog by clicking
outside of it or with the escape key).
Error messages are displayed within the popup <div>.
Because there is no page redirect after successful login, the Javascript replaces the login link to
give a visual indication that the user is logged in.
The Logout link also uses Ajax to submit a POST request to the standard logout url and redirect
you to the index page after the request finishes.
Note that in the JavaScript logout function, youll need to change the url in the success
callback to the correct post-logout value, e.g. window.location = "/appname"; if you have
configured the contextPath to be "/appname"
73
Default Value
Meaning
providerNames
Use daoAuthenticationProvider to authenticate using the User and Role database tables,
rememberMeAuthenticationProvider
to
log
in
with
rememberMe
cookie,
and
import com.foo.MyAuthenticationProvider
beans = {
myAuthenticationProvider(MyAuthenticationProvider) {
// attributes
}
}
You register the provider in grails-app/conf/application.groovy as:
Listing 57. Registering a custom authentication provider name in
grails.plugin.springsecurity.providerNames
grails.plugin.springsecurity.providerNames = [
'myAuthenticationProvider',
'anonymousAuthenticationProvider',
'rememberMeAuthenticationProvider']
74
for
each
name.
The
plugin
uses
an
extension
grails.plugin.springsecurity.userdetails.GrailsUserDetailsService,
UserDetailsService,
of
which
adds
the
method
but
if
you
have
the
id
you
can
call
User.get(principal.id). Even if you have a unique index on the username database column, loading
by primary key is usually more efficient because it takes advantage of Hibernates first-level and
second-level caches.
There is not much to implement other than your application-specific lookup code:
75
MyUserDetails.groovy
package com.mycompany.myapp
import grails.plugin.springsecurity.userdetails.GrailsUser
import org.springframework.security.core.GrantedAuthority
class MyUserDetails extends GrailsUser {
76
this.fullName = fullName
MyUserDetailsService.groovy
package com.mycompany.myapp
import
import
import
import
import
import
import
grails.plugin.springsecurity.SpringSecurityUtils
grails.plugin.springsecurity.userdetails.GrailsUserDetailsService
grails.plugin.springsecurity.userdetails.NoStackUsernameNotFoundException
grails.transaction.Transactional
org.springframework.security.core.authority.SimpleGrantedAuthority
org.springframework.security.core.userdetails.UserDetails
org.springframework.security.core.userdetails.UsernameNotFoundException
* one role, so we give a user with no granted roles this one which gets
*/
static final List NO_ROLES = [new
SimpleGrantedAuthority(SpringSecurityUtils.NO_ROLE)]
@Transactional(readOnly=true, noRollbackFor=[IllegalArgumentException,
UsernameNotFoundException])
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
77
The loadUserByUsername method is transactional, but read-only, to avoid lazy loading exceptions
when accessing the authorities collection. There are obviously no database updates here but this is
a convenient way to keep the Hibernate Session open to enable accessing the roles.
To use your implementation, register it in grails-app/conf/spring/resources.groovy like this:
Listing 58. Registering a custom UserDetailsService in resources.groovy
import com.mycompany.myapp.MyUserDetailsService
beans = {
userDetailsService(MyUserDetailsService)
}
Another
option
for
loading
users
and
roles
from
the
database
is
to
subclass
class MyController {
def springSecurityService
def someAction() {
def user = ...
// update user data
user.save()
springSecurityService.reauthenticate user.username
...
}
78
grails.plugin.springsecurity.password.algorithm = 'bcrypt'
and optionally changing the number of rekeying rounds (which will affect the time it takes to hash
passwords), e.g.
grails.plugin.springsecurity.password.bcrypt.logrounds = 15
Note that the number of rounds must be between 4 and 31.
PBKDF2 is also supported.
The table shows configurable password hashing attributes.
If you want to use a message digest hashing algorithm, see this Java page for the available
algorithms.
Table 15. Password Hashing configuration options
Property
Default
Description
password.algorithm
bcrypt
79
Property
Default
Description
password.encodeHashAsBas false
e64
password.bcrypt.logrounds
10
password.hash.iterations
10000
Note that if you use bcrypt (the default setting) or pbkdf2, do not configure a salt
(e.g. the dao.reflectionSaltSourceProperty property or a custom saltSource bean)
because these algorithms use their own internally.
There are two approaches to using salted passwords in the plugin - defining a property in the
UserDetails class to access by reflection, or by directly implementing SaltSource yourself.
12.2.1. dao.reflectionSaltSourceProperty
Set the dao.reflectionSaltSourceProperty configuration property:
grails.plugin.springsecurity.dao.reflectionSaltSourceProperty = 'username'
This
property
belongs
to
the
UserDetails
class.
By
default
it
is
an
instance
of
80
import org.springframework.security.authentication.dao.SystemWideSaltSource
beans = {
saltSource(SystemWideSaltSource) {
systemWideSalt = 'the_salt_value'
}
}
To have full control over the process, you can implement the SaltSource interface and replace the
plugins
implementation
with
your
own
by
defining
bean
in
grails-
import com.foo.bar.MySaltSource
beans = {
saltSource(MySaltSource) {
// set properties
}
}
81
class UserController {
def springSecurityService
if (params.password) {
params.password = springSecurityService.encodePassword(
params.password, user.username)
}
if (!user.save(flush: true)) {
render view: 'edit', model: [userInstance: user]
return
}
if (springSecurityService.loggedIn &&
springSecurityService.principal.username == user.username) {
springSecurityService.reauthenticate user.username
}
If you are encoding the password in the User domain class (using beforeInsert
and encodePassword) then dont call springSecurityService.encodePassword() in
your controller since youll double-hash the password and users wont be able to
log in. Its best to encapsulate the password handling logic in the domain class. In
newer versions of the plugin (version 1.2 and higher) code is auto-generated in
the user class so youll need to adjust that password hashing for your salt
approach.
82
Property
Exception
isAccountNonExpired()
accountExpired
AccountExpiredException
isAccountNonLocked()
accountLocked
LockedException
isCredentialsNonExpired()
passwordExpired
CredentialsExpiredException
isEnabled()
enabled
DisabledException
You can configure exception mappings in application.groovy to associate a URL to any or all of
these exceptions to determine where to redirect after a failure, for example:
Listing 63. Example grails.plugin.springsecurity.failureHandler.exceptionMappings configuration
import
import
import
import
org.springframework.security.authentication.LockedException
org.springframework.security.authentication.DisabledException
org.springframework.security.authentication.AccountExpiredException
org.springframework.security.authentication.CredentialsExpiredException
grails.plugin.springsecurity.failureHandler.exceptionMappings = [
[exception: LockedException.name,
url: '/user/accountLocked'],
[exception: DisabledException.name,
url: '/user/accountDisabled'],
[exception: AccountExpiredException.name,
url: '/user/accountExpired'],
[exception: CredentialsExpiredException.name, url: '/user/passwordExpired']
]
Without a mapping for a particular exception, the user is redirected to the standard login fail page
(by default /login/authfail), which displays an error message from this table:
Table 17. Login failure messages
83
Property
Default
errors.login.disabled
errors.login.expired
errors.login.passwordExpired
errors.login.locked
errors.login.fail
You can customize these messages by setting the corresponding property in application.groovy, for
example:
def passwordExpired() {
[username: session['SPRING_SECURITY_LAST_USERNAME']]
}
and the form would look something like this:
84
<div id='login'>
<div class='inner'>
<g:if test='${flash.message}'>
<div class='login_message'>${flash.message}</div>
</g:if>
<p>
<label for='username'>Username</label>
<span class='text_'>${username}</span>
</p>
<p>
</p>
<p>
</p>
<p>
</p>
<p>
</p>
</g:form>
</div>
</div>
Its important that you not allow the user to specify the username (its available in the HTTP
session) but that you require the current password, otherwise it would be simple to forge a
password reset.
The GSP form would submit to an action like this one:
85
return
}
if (!password || !password_new || !password_new_2 || password_new !=
password_new_2) {
flash.message = 'Please enter your current password and a valid new password'
return
}
User user = User.findByUsername(username)
if (!passwordEncoder.isPasswordValid(user.password, password, null /*salt*/)) {
return
}
if (passwordEncoder.isPasswordValid(user.password, password_new, null /*salt*/)) {
return
}
user.password = password_new
user.passwordExpired = false
user.save() // if you have password constraints check them here
86
Here is a sample Quartz job that demonstrates how to find and disable users with passwords that
are too old:
ExpirePasswordsJob.groovy
package com.mycompany.myapp
class ExpirePasswordsJob {
static triggers = {
cron name: 'myTrigger', cronExpression: '0 0 0 * * ?' // midnight daily
}
def userCache
void execute() {
// flush each separately so one failure doesn't rollback all of the others
try {
user.passwordExpired = true
user.save(flush: true)
userCache.removeUserFromCache user.username
catch (e) {
}
}
}
If your application includes a dependency for org.hibernate:hibernate-ehcache (to
provide an Ehcache-based 2nd-level cache implementation) you might have a
conflict with the Ehcache dependency. hibernate-ehcache has a dependency for
ehcache-core, but this plugin has a dependency for ehcache, so you will end up
with both jars in your classpath. hibernate-ehcache works fine with the full
compile 'org.hibernate:hibernate-ehcache', {
exclude module: 'ehcache-core'
}
87
Default Value
Meaning
apf.filterProcessesUrl
/login/authenticate
apf.usernameParameter
username
apf.passwordParameter
password
apf.allowSessionCreation
true
apf.postOnly
true
apf.continueChainBefore
SuccessfulAuthentication
false
apf.storeLastUsername
false
failureHandler.exceptionMa none
ppings
failureHandler.useForward
false
failureHandler.allowSession true
Creation
successHandler.defaultTarg
etUrl
successHandler.alwaysUseD false
efault
88
Property
Default Value
successHandler.targetUrlPar spring-security-redirect
ameter
successHandler.useReferer
false
Meaning
Name of optional login form
parameter that specifies destination
after successful login
Whether to use the HTTP Referer
header to determine post-login
destination
successHandler.ajaxSuccess /login/ajaxSuccess
Url
auth.loginFormUrl
/login/auth
auth.forceHttps
false
auth.ajaxLoginFormUrl
/login/authAjax
auth.useForward
false
logout.afterLogoutUrl
logout.filterProcessesUrl
/logoff
logout.handlerNames
['rememberMeServices',
Logout handler bean names. See
'securityContextLogoutHandl Logout Handlers
er']
logout.clearAuthentication
true
logout.invalidateHttpSession true
logout.targetUrlParameter
none
logout.alwaysUseDefaultTar false
getUrl
logout.redirectToReferer
false
logout.postOnly
true
adh.errorPage
/login/denied
adh.ajaxErrorPage
/login/ajaxDenied
89
Property
Default Value
Meaning
adh.useForward
true
ajaxHeader
X-Requested-With
ajaxCheckClosure
none
redirectStrategy.contextRela false
tive
switchUser URLs
fii.alwaysReauthenticate
false
fii.rejectPublicInvocations
true
fii.validateConfigAttributes
true
fii.publishAuthorizationSucc false
ess
fii.observeOncePerRequest
90
true
Default Value
Meaning
roleHierarchy
none
roleHierarchyEntryClassNa
me
none
For example, if you have several types of admin roles that can be used to access a URL pattern
and you do not use hierarchical roles, you need to specify all the admin roles:
package com.mycompany.myapp
import grails.plugin.springsecurity.annotation.Secured
class SomeController {
However, if you have a business rule that says ROLE_FINANCE_ADMIN implies being granted ROLE_ADMIN,
and that ROLE_SUPERADMIN implies being granted ROLE_FINANCE_ADMIN, you can express that hierarchy
as:
grails.plugin.springsecurity.roleHierarchy = '''
ROLE_SUPERADMIN > ROLE_FINANCE_ADMIN
ROLE_FINANCE_ADMIN > ROLE_ADMIN
'''
Then you can simplify your mappings by specifying only the roles that are required:
91
package com.mycompany.myapp
import grails.plugin.springsecurity.annotation.Secured
class SomeController {
@Secured('ROLE_ADMIN')
def someAction() {
...
}
You can also reduce the number of granted roles in the database. Where previously you had to
grant ROLE_SUPERADMIN, ROLE_FINANCE_ADMIN, and ROLE_ADMIN, now you only need to grant
ROLE_SUPERADMIN.
92
RoleHierarchyEntry.groovy
package com.yourapp
import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString
@EqualsAndHashCode(includes='entry')
@ToString(includes='entry', includeNames=true, includePackage=false)
class RoleHierarchyEntry implements Serializable {
String entry
static constraints = {
entry blank: false, unique: true
}
static mapping = {
cache true
}
if (!RoleHierarchyEntry.count()) {
new RoleHierarchyEntry(entry: 'ROLE_SUPERADMIN > ROLE_FINANCE_ADMIN').save()
new RoleHierarchyEntry(entry: 'ROLE_FINANCE_ADMIN > ROLE_ADMIN').save()
}
93
user can access without requiring the users password. Limit who can use this
feature by guarding the user switch URL with a role, for example,
ROLE_SWITCH_USER, ROLE_ADMIN, and so on.
<sec:ifAllGranted roles='ROLE_SWITCH_USER'>
</sec:ifAllGranted>
Here the form is guarded by a check that the logged-in user has ROLE_SWITCH_USER and is not shown
otherwise. You also need to guard the user switch URL, and the approach depends on your mapping
scheme. If you use annotations, add a rule to the controllerAnnotations.staticRules attribute:
Listing 69. Guarding the switch user url with controllerAnnotations.staticRules
grails.plugin.springsecurity.controllerAnnotations.staticRules = [
...
[pattern: '/login/impersonate', access: ['ROLE_SWITCH_USER',
'isFullyAuthenticated()']]
]
If you use Requestmaps, create a rule like this (for example, in BootStrap):
Listing 70. Guarding the switch user url with a database requestmap
configAttribute: 'ROLE_SWITCH_USER,isFullyAuthenticated()').save(flush:
true)
94
If you use the static application.groovy map, add the rule there:
Listing 71. Guarding the switch user url with interceptUrlMap
grails.plugin.springsecurity.interceptUrlMap = [
...
[pattern: '/login/impersonate', access: ['ROLE_SWITCH_USER',
'isFullyAuthenticated()']]
]
<sec:ifSwitched>
<a href='${request.contextPath}/logout/impersonate'>
Resume as <sec:switchedUserOriginalUsername/>
</a>
</sec:ifSwitched>
grails.plugin.springsecurity.switchUser.switchUserUrl = ...
grails.plugin.springsecurity.switchUser.exitUserUrl = ...
grails.plugin.springsecurity.switchUser.targetUrl = ...
grails.plugin.springsecurity.switchUser.switchFailureUrl = ...
Table 20. Switch user configuration options
Property
Default
Meaning
useSwitchUserFilter
false
switchUser.switchUserUrl
/login/impersonate
switchUser.exitUserUrl
/logout/impersonate
switchUser.targetUrl
Same as
URL for redirect after switching
successHandler.defaultTarge
tUrl
switchUser.switchFailureUrl Same as
URL for redirect after an error during
failureHandler.defaultFailu an attempt to switch
reUrl
95
Property
Default
Meaning
<sec:ifLoggedIn>
Logged in as <sec:username/>
</sec:ifLoggedIn>
<sec:ifSwitched>
<a href='${request.contextPath}/logout/impersonate'>
Resume as <sec:switchedUserOriginalUsername/>
</a>
</sec:ifSwitched>
<sec:ifNotSwitched>
<sec:ifAllGranted roles='ROLE_SWITCH_USER'>
</sec:ifAllGranted>
</sec:ifNotSwitched>
96
16.2. filterNames
To define custom filters, to remove a core filter from the chain (not recommended), or to otherwise
have control over the filter chain, you can specify the filterNames property as a list of strings. As
with the default approach, the filter chain built here is applied to all URLs.
For example:
Listing 74. Sample grails.plugin.springsecurity.filterChain.filterNames configuration
grails.plugin.springsecurity.filterChain.filterNames = [
'securityContextPersistenceFilter', 'logoutFilter',
'authenticationProcessingFilter', 'myCustomProcessingFilter',
'rememberMeAuthenticationFilter', 'anonymousAuthenticationFilter',
'exceptionTranslationFilter', 'filterInvocationInterceptor'
]
This example creates a filter chain corresponding to the Spring beans with the specified names.
16.3. chainMap
Use the filterChain.chainMap attribute to define which filters are applied to different URL patterns.
You define a Map that specifies one or more lists of filter bean names, each with a corresponding
URL pattern.
Listing 75. Sample grails.plugin.springsecurity.filterChain.chainMap configuration
grails.plugin.springsecurity.filterChain.chainMap = [
[pattern: '/urlpattern1/**', filters: 'filter1,filter2,filter3,filter4'],
[pattern: '/urlpattern2/**', filters: 'filter1,filter3,filter5'],
[pattern: '/**',
filters: 'JOINED_FILTERS']
]
97
where the keys were the access patterns and the values were filter names. The
old format is no longer supported and your configurations must be updated to the
newer format.
In this example, four filters are applied to URLs matching /urlpattern1/** and three different filters
are applied to URLs matching /urlpattern2/**. In addition the special token JOINED_FILTERS is
applied to all URLs. This is a conventient way to specify that all defined filters (configured either
with configuration rules like useSwitchUserFilter or explicitly using filterNames) should apply to
this pattern.
The order of the mappings is important. Each URL will be tested in order from top to bottom to find
the first matching one. So you need a /** catch-all rule at the end for URLs that do not match one of
the earlier rules.
Theres also a filter negation syntax that can be very convenient. Rather than specifying all of the
filter names (and risking forgetting one or putting them in the wrong order), you can use the
JOINED_FILTERS keyword and one or more filter names prefixed with a - . This means to use all
configured filters except for the excluded ones. For example, if you had a web service that uses
Basic Auth for /webservice/** URLs, you would configure that using:
Listing 76. Using JOINED_FILTERS in a filterChain.chainMap configuration
grails.plugin.springsecurity.filterChain.chainMap = [
[pattern: '/webservice/**', filters: 'JOINED_FILTERS,-exceptionTranslationFilter'],
[pattern: '/**',
filters: 'JOINED_FILTERS,-basicAuthenticationFilter,basicExceptionTranslationFilter']
]
For the /webservice/** URLs, we want all filters except for the standard ExceptionTranslationFilter
since we want to use just the one configured for Basic Auth. And for the /** URLs (everything else)
we want everything except for the Basic Auth filter and its configured ExceptionTranslationFilter.
Additionally, you can use a chainMap configuration to declare one or more URL patterns which
should have no filters applied. Use the name 'none' for these patterns, e.g.
Listing 77. Using none in a filterChain.chainMap configuration
grails.plugin.springsecurity.filterChain.chainMap = [
[pattern: '/someurlpattern/**', filters: 'none'],
[pattern: '/**',
filters: 'JOINED_FILTERS']
]
16.4. clientRegisterFilter
An
alternative
to
setting
the
filterNames
property
is
98
you to add a custom filter to the chain at a specified position. Each standard filter has a
corresponding position in the chain (see grails.plugin.springsecurity.SecurityFilterPosition for
details).
So
if
you
have
created
an
application-specific
filter,
register
it
in
grails-
app/conf/spring/resources.groovy:
import com.mycompany.myapp.MyFilter
import org.springframework.boot.context.embedded.FilterRegistrationBean
beans = {
myFilter(MyFilter) {
// properties
}
myFilterDeregistrationBean(FilterRegistrationBean) {
filter = ref('myFilter')
enabled = false
}
Note that in addition to the filter bean, there is also a disabled FilterRegistrationBean registered.
This is needed because Spring Boot automatically registers filter beans in the ApplicationContext, so
you must register your own FilterRegistrationBean and set its enabled property to false to prevent
this.
Then register the filter in grails-app/init/BootStrap.groovy:
import grails.plugin.springsecurity.SecurityFilterPosition
import grails.plugin.springsecurity.SpringSecurityUtils
class BootStrap {
def init = {
SpringSecurityUtils.clientRegisterFilter(
'myFilter', SecurityFilterPosition.OPENID_FILTER.order + 10)
}
This bootstrap code registers your filter just after the Open ID filter (if its configured). You cannot
register a filter in the same position as another, so its a good idea to add a small delta to its position
to put it after or before a filter that it should be next to in the chain. The Open ID filter position is
just an example - add your filter in the position that makes sense.
99
Default Value
Meaning
portMapper.httpPort
8080
portMapper.httpsPort
8443
secureChannel.definition
none
secureChannel.secureHeade 'X-Forwarded-Proto'
rName
secureChannel.secureHeade 'http'
rValue
secureChannel.secureConfig 'REQUIRES_SECURE_CHANNEL'
AttributeKeyword
secureChannel.insecureHea 'X-Forwarded-Proto'
derName
secureChannel.insecureHea 'https'
derValue
grails.plugin.springsecurity.secureChannel.definition = [
[pattern: '/login/**',
access: 'REQUIRES_SECURE_CHANNEL'],
[pattern: '/maps/**',
access: 'REQUIRES_INSECURE_CHANNEL'],
[pattern: '/images/login/**', access: 'REQUIRES_SECURE_CHANNEL'],
[pattern: '/images/**',
access: 'ANY_CHANNEL']
]
The format of secureChannel.definition has changed from previous versions to
avoid configuration parsing issues. In previous versions the property was a single
Map, where the keys were the access patterns and the values were one of the
access keywords above. The old format is no longer supported and your
configurations must be updated to the newer format.
100
URLs are checked in order, so be sure to put more specific rules before less specific. In the
preceding example, /images/login/** is more specific than /images/**, so it appears first in the
configuration.
grails.plugin.springsecurity.secureChannel.useHeaderCheckChannelSecurity = true
By default the header name is X-Forwarded-Proto and the secure header value is http (i.e. if
youre not secure, redirect to secure) and the insecure header value is https (i.e. if youre secure,
redirect to insecure). You can change any or all of these default values though:
grails.plugin.springsecurity.secureChannel.secureHeaderName = '...'
grails.plugin.springsecurity.secureChannel.secureHeaderValue = '...'
grails.plugin.springsecurity.secureChannel.insecureHeaderName = '...'
grails.plugin.springsecurity.secureChannel.insecureHeaderValue = '...'
101
Default Value
Meaning
ipRestrictions
none
For example, make an admin-only part of your site accessible only from IP addresses of the local
LAN or VPN, such as 192.168.1.xxx or 10.xxx.xxx.xxx. You can also set this up at your firewall
and/or routers, but it is convenient to encapsulate it within your application.
To use this feature, specify an ipRestrictions configuration as a List of Maps, one for each
combination of URL pattern to IP address patterns that can access those URLs. The IP patterns can
be single-value strings, or multi-value lists of strings. They can use CIDR masks, and can specify
either IPv4 or IPv6 patterns. For example, given this configuration:
Listing 79. Sample grails.plugin.springsecurity.ipRestrictions configuration
grails.plugin.springsecurity.ipRestrictions = [
[pattern: '/pattern1/**', access: '123.234.345.456'],
[pattern: '/pattern2/**', access: '10.0.0.0/8'],
[pattern: '/pattern3/**', access: ['10.10.200.42', '10.10.200.63']]
]
pattern1 URLs can be accessed only from the external address 123.234.345.456, pattern2 URLs can
be accessed only from a 10.xxx.xxx.xxx intranet address, and pattern3 URLs can be accessed only
from 10.10.200.42 or 10.10.200.63. All other URL patterns are accessible from any IP address.
The format of ipRestrictions has changed from previous versions to avoid
configuration parsing issues. In previous versions the property was a single Map,
where the keys were the access patterns and the values were the IP addresses
that are allowed. The old format is no longer supported and your configurations
must be updated to the newer format.
All addresses can always be accessed from localhost regardless of IP pattern, primarily to support
local development mode.
You cannot compare IPv4 and IPv6 addresses, so if your server supports both, you
need to specify the IP patterns using the address format that is actually being
used. Otherwise the filter throws exceptions. One option is to set the
java.net.preferIPv4Stack system property, for example, by adding it to JAVA_OPTS
or GRAILS_OPTS as -Djava.net.preferIPv4Stack=true.
102
grails.plugin.springsecurity.useSessionFixationPrevention = true
Upon successful authentication a new HTTP session is created and the previous sessions attributes
are copied into it. If you start your session by clicking a link that was generated by someone trying
to hack your account, which contained an active session id, you are no longer sharing the previous
session after login. You have your own session.
Session fixation is less of a problem now that Grails by default does not include jsessionid in URLs
(see this JIRA issue), but its still a good idea to use this feature.
Note that there is an issue when using the cookie-session plugin; see this issue for more details.
The table shows configuration options for session fixation.
Table 23. Session Fixation Prevention configuration options
Property
Default Value
useSessionFixationPreventio true
n
sessionFixationPrevention.
migrate
true
sessionFixationPrevention.al false
waysCreateSession
Meaning
Whether to use session fixation
prevention
Whether to copy the session attributes
of the existing session to the new
session after login
Whether to always create a session
even if one did not exist at the start of
the request
103
Default Value
Meaning
logout.handlerNames
['rememberMeServices',
Logout handler bean names
'securityContextLogoutHandl
er']
The beans must be declared either by the plugin or by you in resources.groovy. For example,
suppose you have a custom MyLogoutHandler in resources.groovy:
Listing 80. Registering a custom logout handler in resources.groovy
import com.foo.MyLogoutHandler
beans = {
myLogoutHandler(MyLogoutHandler) {
// attributes
}
}
You register it in grails-app/conf/application.groovy as:
Listing 81. Adding a custom logout handler in grails.plugin.springsecurity.logout.handlerNames
grails.plugin.springsecurity.logout.handlerNames = [
'rememberMeServices', 'securityContextLogoutHandler', 'myLogoutHandler'
]
104
Default Value
Meaning
voterNames
['authenticatedVoter',
'roleVoter',
'webExpressionVoter',
'closureVoter']
The default voters include a RoleHierarchyVoter to ensure users have the required roles for the
request, an AuthenticatedVoter to support IS_AUTHENTICATED_FULLY, IS_AUTHENTICATED_REMEMBERED,
and IS_AUTHENTICATED_ANONYMOUSLY tokens, a WebExpressionVoter to evaluate SpEL expressions, and
a grails.plugin.springsecurity.access.vote.ClosureVoter to invoke annotation closures.
To customize this list, you define a voterNames attribute with a list of bean names. Any existing bean
that implements the interface can be used, whether it is declared by this plugin, in your
applications resources.groovy, another plugin, or any other source.
Suppose you have registered a bean for a custom MyAccessDecisionVoter in resources.groovy:
import com.foo.MyAccessDecisionVoter
beans = {
myAccessDecisionVoter(MyAccessDecisionVoter) {
// attributes
}
}
You register it in grails-app/conf/application.groovy as:
grails.plugin.springsecurity.voterNames = [
'authenticatedVoter', 'roleVoter', 'webExpressionVoter',
'closureVoter', 'myAccessDecisionVoter'
]
105
Default Value
Meaning
active
true
printStatusMessages
true
rejectIfNoRule
true
anon.key
foo
anonymousProcessingFilter key
atr.anonymousClass
useHttpSession
EventPublisher
false
cacheUsers
false
useSecurity EventListener
false
If true, configure
SecurityEventListener. See Events
dao.reflectionSaltSourcePro none
perty
dao.hideUserNotFoundExce
ptions
true
requestCache.createSession
true
roleHierarchy
none
106
Property
Default Value
Meaning
voterNames
['authenticatedVoter',
'roleVoter',
'closureVoter']
providerNames
securityConfigType
Annotation
controllerAnnotations.lower true
case
controllerAnnotations.static none
Rules
interceptUrlMap
none
registerLoggerListener
false
scr.allowSessionCreation
true
scr.disableUrlRewriting
true
sch.strategyName
debug.useFilter
false
107
Property
Default Value
providerManager.eraseCred true
entialsAfterAuthentication
108
Meaning
Whether to remove the password
from the Authentication and its child
objects after successful authentication
$ grails compile
static mapping = {
table '`user`'
}
The script creates this User class:
User.groovy
package com.testapp
109
import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString
@EqualsAndHashCode(includes='username')
@ToString(includes='username', includeNames=true, includePackage=false)
class User implements Serializable {
transient springSecurityService
String username
String password
boolean enabled = true
boolean accountExpired
boolean accountLocked
boolean passwordExpired
Set<Role> getAuthorities() {
UserRole.findAllByUser(this)*.role
}
def beforeInsert() {
encodePassword()
}
def beforeUpdate() {
if (isDirty('password')) {
encodePassword()
}
}
static constraints = {
password blank: false, password: true
username blank: false, unique: true
}
static mapping = {
password column: '`password`'
}
110
Earlier versions of the plugin didnt include password hashing logic in the
domain class, but it makes the code a lot cleaner.
package com.testapp
import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString
@EqualsAndHashCode(includes='authority')
@ToString(includes='authority', includeNames=true, includePackage=false)
class Role implements Serializable {
String authority
static constraints = {
authority blank: false, unique: true
}
static mapping = {
cache true
}
and a domain class that maps the many-to-many join class, UserRole:
UserRole.groovy
package com.testapp
import grails.gorm.DetachedCriteria
import groovy.transform.ToString
import org.apache.commons.lang.builder.HashCodeBuilder
@ToString(cache=true, includeNames=true, includePackage=false)
class UserRole implements Serializable {
User user
Role role
@Override
boolean equals(other) {
if (other instanceof UserRole) {
111
@Override
int hashCode() {
def builder = new HashCodeBuilder()
if (user) builder.append(user.id)
if (role) builder.append(role.id)
builder.toHashCode()
}
static constraints = {
role validator: { Role r, UserRole ur ->
if (ur.user?.id) {
UserRole.withNewSession {
112
if (UserRole.exists(ur.user.id, r.id)) {
return ['userRole.exists']
}
static mapping = {
id composite: ['user', 'role']
version false
}
These generated files are not part of the plugin - these are your application files.
They are examples to get you started, so you can edit them as you please. They
contain the minimum needed for the plugins default implementation of the
Spring Security UserDetailsService (which like everything in the plugin is
customizable - see Custom UserDetailsService).
The script has edited (or created) grails-app/conf/application.groovy and added the configuration
for your domain classes. Make sure that the changes are correct.
While youre looking at application.groovy, add this config override to make the sample app easier
to work with:
grails.plugin.springsecurity.logout.postOnly = false
By default only POST requests can be used to logout; this is a very sensible default
and shouldnt be changed in most cases. However to keep things simple for this
tutorial well change it (using the logout.postOnly config override above) to avoid
having to create a GSP form that POSTs to /logout.
The plugin has no support for CRUD actions or GSPs for your domain classes; the spring-securityui plugin supplies a UI for those. So for now you will create roles and users in grailsapp/init/BootStrap.groovy. (See step 7.)
113
SecureController.groovy
package com.testapp
class SecureController {
def index() {
import com.testapp.Role
import com.testapp.User
import com.testapp.UserRole
class BootStrap {
def init = {
UserRole.withSession {
it.flush()
it.clear()
}
assert User.count() == 1
assert Role.count() == 2
assert UserRole.count() == 1
114
package com.testapp
import grails.plugin.springsecurity.annotation.Secured
class SecureController {
@Secured('ROLE_ADMIN')
def index() {
render 'Secure access only'
}
or
115
SecureController.groovy
package com.testapp
import grails.plugin.springsecurity.annotation.Secured
@Secured('ROLE_ADMIN')
class SecureController {
def index() {
23.1.9. 9. Restart.
Shut down the app and run grails run-app again, and navigate again to http://localhost:8080/secure.
This time you should again be able to see the secure page after successfully authenticating.
116
24.1. isLoggedIn
Returns true if there is an authenticated user.
Listing 82. Example use of isLoggedIn()
class MyController {
def someAction() {
if (isLoggedIn()) {
...
}
...
if (!isLoggedIn()) {
...
}
// or
if (loggedIn) {
...
}
if (!loggedIn) {
...
}
24.2. getPrincipal
Retrieves the current authenticated users Principal (a GrailsUser instance unless youve
customized this) or null if not authenticated.
117
class MyController {
def someAction() {
if (isLoggedIn()) {
String username = getPrincipal().username
...
}
// or
if (isLoggedIn()) {
String username = principal.username
...
}
24.3. getAuthenticatedUser
Loads the user domain class instance from the database that corresponds to the currently
authenticated user, or null if not authenticated. This is the equivalent of adding a dependency
injection
for
springSecurityService
and
calling
class MyController {
def someAction() {
if (isLoggedIn()) {
String email = getAuthenticatedUser().email
...
}
// or
if (isLoggedIn()) {
String email = authenticatedUser.email
...
}
118
Default Value
Exception
AccountExpiredException
CredentialsExpiredException
DisabledException
LockedException
Other exceptions
Default Value
springSecurity.login.title
Login
springSecurity.login.header
Please Login
springSecurity.login.button
Login
springSecurity.login.username.label
Username
springSecurity.login.password.label
Password
springSecurity.login.remember.me.label
Remember me
springSecurity.denied.title
Denied
springSecurity.denied.message
119
26.2. s2-create-persistent-token
Purpose
Creates a persistent token domain class for storing remember-me cookie information in the
database. The general format is:
120
Description
This creates the domain class in the specified package, and also registers the name in grailsapp/conf/application.groovy, along with enabling persistent remember-me.
26.3. s2-create-role-hierarchy-entry
Purpose
Creates a persistent role hierarchy entry domain class for storing role hierarchy information in the
database. The general format is:
121