1. Introduction to the Spring Security Plugin
The Spring Security plugin simplifies the integration of Spring Security into Grails applications. The plugin provides sensible defaults with many configuration options for customization. Nearly everything is configurable or replaceable in the plugin and in Spring Security itself, which makes extensive use of interfaces.
This guide documents configuration defaults and describes how to configure and extend the Spring Security plugin for Grails applications.
1.1. Installation
1.1.1. Prerequisites
Ensure you have the following set up:
-
A Grails project
-
A working internet connection
1.1.2. Installation Steps
-
Open your Grails project.
-
Locate your
build.gradle
file in the root directory of your project. -
Add the Spring Security Core Plugin dependency to the
dependencies
section in yourbuild.gradle
:dependencies { // ... other dependencies implementation 'org.grails.plugins:spring-security-core:6.1.2' }
-
Save the
build.gradle
file. -
Open a terminal or command prompt.
-
Navigate to your project’s root directory using the
cd
command. -
Run the following Gradle command to update your project’s dependencies:
./gradlew clean build
-
The Spring Security Core Plugin 6.0.0 is now installed and integrated into your Grails project.
-
You can start using the plugin’s features and commands in your application.
-
Run the s2-quickstart script to generate domain classes and add the initial configuration settings in
application.groovy
:./gradlew runCommand "-Pargs=s2-quickstart com.yourapp User Role"
1.1.3. Verifying Installation
To verify that the plugin has been successfully installed, you can run a simple test:
-
In your Grails project, create a new controller or use an existing one.
-
Add a secure annotation, such as
@Secured(['ROLE_USER'])
, to a method in your controller. -
Run your Grails application using the command:
./gradlew bootRun
-
Access the URL associated with the method you secured. If the plugin is correctly installed, it should enforce the security constraint you defined.
1.2. Configuration and Customization
The Spring Security plugin’s configuration is primarily managed within the grails-app/conf/application.groovy
file, although an alternative is to house plugin configuration within application.yml
. Default values are stored in the grails-app/conf/DefaultSecurityConfig.groovy
file, with additional application-specific settings appended to application.groovy
(or application.yml
). This configuration structure involves merging default and custom values, giving precedence to application-specific settings.
1.2.1. Environment-Specific Configuration
This configuration approach accommodates environment-specific needs. For instance, during development, you might require less restrictive security rules compared to a production environment. To handle environment-specific parameters, utilize the environments
block.
1.2.2. Property Prefix and Overrides
To distinguish these configuration properties from others in Grails or from different plugins, all the plugin-specific configuration properties begin with grails.plugin.springsecurity
. When overriding these properties, ensure to use the grails.plugin.springsecurity
prefix. For example:
grails.plugin.springsecurity.password.algorithm = 'bcrypt'
1.2.3. Integration with CXF Grails Plugin
If your application incorporates the CXF Grails plugin, it’s crucial to arrange the dependencies correctly. Place the CXF dependency above the Spring Security plugin within the dependencies
block, as shown below:
dependencies {
implementation 'org.grails.plugins:cxf:3.1.1'
// CXF above security.
implementation 'org.grails.plugins:spring-security-core:6.1.2'
}
1.3. Quick Start Guide
Getting started with the plugin is simple and efficient. Follow these steps to enhance the security of your Grails application:
Begin by installing the Spring Security plugin into your Grails project. Add the following dependency to your build.gradle
:
implementation 'org.grails.plugins:spring-security-core:6.1.2'
After installation, execute the s2-quickstart
initialization script. This sets up essential classes and configurations required for the plugin’s functionality. In your terminal, run:
./gradlew runCommand -Pargs="s2-quickstart com.yourapp User Role"
1.3.1. Plugin Configuration and Setup
The Spring Security plugin streamlines configuration and setup through a combination of steps:
-
Programmatic Servlet API Configuration:
Unlike earlier versions that utilized `web.xml`, the plugin now registers its servlet API configuration, including the Spring Security filter chain, programmatically.
-
Configure Spring Beans:
The plugin configures Spring beans within the application context to implement various functionality components. Dependency management automatically handles the selection of appropriate jar files.
By following these steps, your Grails application will be ready to leverage the Spring Security plugin for enhanced security. While in-depth knowledge of Spring Security isn’t mandatory, having a basic understanding of its underlying implementation can be helpful. For more details, refer to the [Spring Security documentation](https://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/).
2. What’s New in Grails Spring Security Core Plugin 6.0.0
This release is more than just an upgrade; it’s a transformative step towards elevating your application’s security, compatibility, and development experience. Get ready to embark on a journey of enhancements that will empower you to build robust, modern web applications. Let’s dive into the exciting changes that version 6.0.0 brings.
2.1. Elevated Security and Compatibility
Version 6.0.0 of the Grails Spring Security Core Plugin brings enhanced security features and improved compatibility with Grails 6. With a revamped underlying Spring Security framework powered by version 5.8.6, your application gains access to the latest security enhancements and bug fixes, safeguarding it against evolving threats and vulnerabilities.
2.2. Streamlined Commands for Grails 6
One of the notable improvements is the seamless transition to Grails 6. The plugin now offers upgraded commands that are fully compatible with Grails 6. This is a vital enhancement as it ensures your existing scripts remain functional without compatibility issues. The familiar commands like s2-quickstart
have been refined, enabling you to trigger them using Gradle tasks. For example, you can now execute:
./gradlew runCommand "-Pargs=s2-quickstart com.yourapp User Role"
This aligns perfectly with Grails 6’s development landscape, making your application’s migration smoother than ever before.
2.3. Effortless Documentation Navigation
We understand the importance of hassle-free documentation navigation. With the Internal Docs task now enhanced, generating documentation becomes a breeze. But that’s not all – we’ve introduced a slick select drop-down feature. This feature allows you to effortlessly switch between documentation for different releases. Whether you’re exploring the latest version or revisiting older ones, finding the information you need has never been easier.
2.4. Embracing the Grails 6 Framework
In the ever-evolving world of web development, the Grails Spring Security Core Plugin is keeping pace. Version 6.0.0 ensures seamless integration with Grails 6, enabling you to harness the cutting-edge features and improvements of the Grails framework. This integration ensures that your application can leverage both the power of Grails and the robust security features of the plugin.
3. Domain Classes
By default the plugin uses regular Grails domain classes to access its required data. It’s easy to create your own user lookup code though, which can access a database or any other source to retrieve user and authority data. See Custom UserDetailsService for how to implement this.
To use the standard user lookup you’ll need at a minimum a “person” and an “authority” domain class. In addition, if you want to store URL <==> Role mappings in the database (this is one of multiple approaches for defining the mappings) you need a “requestmap” domain class. If you use the recommended approach for mapping the many-to-many relationship between “person” and “authority”, you also need a domain class to map the join table.
To use the user/group lookup you’ll also need a “group” domain class. If you are using the recommended approach for mapping many-to-many relationship between “person” and “group” and between “group” and “authority” you’ll need a domain class for each to map the join tables. You can still additionally use “requestmap” with this approach.
The s2-quickstart script creates initial domain classes for you. You specify the package and class names, and it creates the corresponding domain classes. After that you can customize them as you like. You can add additional properties, methods, and so on, as long as the core security-related functionality remains.
3.1. Person Class
Spring Security uses an Authentication object to determine whether the current user is allowed to perform a secured action, such as accessing a URL, manipulating a secured domain object, invoking a secured method, and so on. This object is created during login. Typically overlap occurs between the need for authentication data and the need to represent a user in the application in ways that are unrelated to security. The mechanism for populating the authentication is completely pluggable in Spring Security; you only need to provide an implementation of UserDetailsService and implement its one method, loadUserByUsername(String username)
.
By default the plugin uses a Grails “person” domain class to manage this data. username
, enabled
, and password
are the default names of the core required properties. You can easily plug in your own implementation (Custom UserDetailsService), and rename the class, package, and properties. In addition, you should define an authorities
property to retrieve roles; this can be a property or a getAuthorities()
method, and it can be defined through a traditional GORM many-to-many or a custom mapping.
Assuming you choose com.mycompany.myapp
as your package, and User
as your class name, you’ll generate this class:
User.groovy
package com.mycompany.myapp
import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString
import grails.compiler.GrailsCompileStatic
@GrailsCompileStatic
@EqualsAndHashCode(includes='username')
@ToString(includes='username', includeNames=true, includePackage=false)
class User implements Serializable {
private static final long serialVersionUID = 1
String username
String password
boolean enabled = true
boolean accountExpired
boolean accountLocked
boolean passwordExpired
Set<Role> getAuthorities() {
(UserRole.findAllByUser(this) as List<UserRole>)*.role as Set<Role>
}
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
package com.mycompany.myapp
import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString
import grails.compiler.GrailsCompileStatic
@GrailsCompileStatic
@EqualsAndHashCode(includes='username')
@ToString(includes='username', includeNames=true, includePackage=false)
class User implements Serializable {
private static final long serialVersionUID = 1
String username
String password
boolean enabled = true
String email (1)
String firstName (1)
String lastName (1)
boolean accountExpired
boolean accountLocked
boolean passwordExpired
def someMethod() { (2)
...
}
Set<Role> getAuthorities() {
(UserRole.findAllByUser(this) as List<UserRole>)*.role as Set<Role>
}
static constraints = {
password blank: false, password: true
username blank: false, unique: true
}
static mapping = {
password column: '`password`'
}
}
1 | Other properties |
2 | Convenience methods |
The getAuthorities()
method is analagous to defining static hasMany = [authorities: Authority]
in a traditional many-to-many mapping. This way GormUserDetailsService
can call user.authorities
during login to retrieve the roles without the overhead of a bidirectional many-to-many mapping.
The class and property names are configurable using these configuration attributes:
Property | Default Value | Meaning |
---|---|---|
userLookup.userDomainClassName |
none |
User class name |
userLookup.usernamePropertyName |
“username” |
User class username property |
userLookup.passwordPropertyName |
“password” |
User class password property |
userLookup.authoritiesPropertyName |
“authorities” |
User class role collection property |
userLookup.enabledPropertyName |
“enabled” |
User class enabled property |
userLookup.accountExpiredPropertyName |
“accountExpired” |
User class account expired property |
userLookup.accountLockedPropertyName |
“accountLocked” |
User class account locked property |
userLookup.passwordExpiredPropertyName |
“passwordExpired” |
User class password expired property |
userLookup.authorityJoinClassName |
none |
User/Role many-many join class name |
3.2. Authority Class
The Spring Security plugin uses an “authority” class to represent a user’s roles in the application. In general this class restricts URLs to users who have been assigned the required access rights. A user can be granted multiple roles to indicate various access rights in the application, and should have at least one. A basic user who can access only non-restricted resources but can still authenticate is a bit unusual. Spring Security usually functions fine if a user has no granted authorities, but fails in a few places that assume one or more. So if a user authenticates successfully but has no granted roles, the plugin grants the user a “virtual” role, ROLE_NO_ROLES
. Thus the user satisfies Spring Security’s requirements but cannot access secure resources, as you would not associate any secure resources with this role.
Note that you aren’t required to use roles at all; an application with simple security requirements could use the |
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 - they’re specified in grails-app/conf/application.groovy
.
Assuming you choose com.mycompany.myapp
as your package, and Role
as your class name, you’ll generate this class:
Role.groovy
package com.mycompany.myapp
import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString
import grails.compiler.GrailsCompileStatic
@GrailsCompileStatic
@EqualsAndHashCode(includes='authority')
@ToString(includes='authority', includeNames=true, includePackage=false)
class Role implements Serializable {
private static final long serialVersionUID = 1
String authority
static constraints = {
authority blank: false, unique: true
}
static mapping = {
cache true
}
}
The class and property names are configurable using these configuration attributes:
Property | Default Value | Meaning |
---|---|---|
authority.className |
none |
Role class name |
authority.nameField |
“authority” |
Role class role name property |
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 it’s important that the prefix not be blank as the prefix is used to differentiate between role names and tokens such as IS_AUTHENTICATED_FULLY/IS_AUTHENTICATED_ANONYMOUSLY/etc., and SpEL expressions. The role names should be primarily an internal implementation detail; if you want to display friendlier names in a UI, it’s simple to remove the prefix first. |
3.3. PersonAuthority Class
The typical approach to mapping the relationship between “person” and “authority” is a many-to-many. Users have multiple roles, and roles are shared by multiple users. This approach can be problematic in Grails, because a popular role, for example, ROLE_USER
, will be granted to many users in your application. GORM uses collections to manage adding and removing related instances and maps many-to-many relationships bidirectionally. Granting a role to a user requires loading all existing users who have that role because the collection is a Set
. So even though no uniqueness concerns may exist, Hibernate loads them all to enforce uniqueness. The recommended approach in the plugin is to map a domain class to the join table that manages the many-to-many, and using that to grant and revoke roles to users.
Like the other domain classes, this class is generated for you, so you don’t need to deal with the details of mapping it. Assuming you choose com.mycompany.myapp
as your package, and User
and Role
as your class names, you’ll generate this class:
UserRole.groovy
package com.mycompany.myapp
import grails.gorm.DetachedCriteria
import groovy.transform.ToString
import org.codehaus.groovy.util.HashCodeHelper
import grails.compiler.GrailsCompileStatic
@GrailsCompileStatic
@ToString(cache=true, includeNames=true, includePackage=false)
class UserRole implements Serializable {
private static final long serialVersionUID = 1
User user
Role role
@Override
boolean equals(other) {
if (other instanceof UserRole) {
other.userId == user?.id && other.roleId == role?.id
}
}
@Override
int hashCode() {
int hashCode = HashCodeHelper.initHash()
if (user) {
hashCode = HashCodeHelper.updateHash(hashCode, user.id)
}
if (role) {
hashCode = HashCodeHelper.updateHash(hashCode, role.id)
}
hashCode
}
static UserRole get(long userId, long roleId) {
criteriaFor(userId, roleId).get()
}
static boolean exists(long userId, long roleId) {
criteriaFor(userId, roleId).count()
}
private static DetachedCriteria criteriaFor(long userId, long roleId) {
UserRole.where {
user == User.load(userId) &&
role == Role.load(roleId)
}
}
static UserRole create(User user, Role role, boolean flush = false) {
def instance = new UserRole(user: user, role: role)
instance.save(flush: flush)
instance
}
static boolean remove(User u, Role r) {
if (u != null && r != null) {
UserRole.where { user == u && role == r }.deleteAll()
}
}
static int removeAll(User u) {
u == null ? 0 : UserRole.where { user == u }.deleteAll() as int
}
static int removeAll(Role r) {
r == null ? 0 : UserRole.where { role == r }.deleteAll() as int
}
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']
}
}
}
}
}
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:
User user = ...
Role role = ...
UserRole.create user, role
Revoking a role is similar:
User user = ...
Role role = ...
UserRole.remove user, role
The class name is the only configurable attribute:
Property | Default Value | Meaning |
---|---|---|
userLookup.authorityJoinClassName |
none |
User/Role many-many join class name |
3.4. Group Class
The plugin provides you the option of creating an access inheritance level between “person” and “authority”: the “group”. The next three classes you will read about (including this one) are only used in a “person”/“group”/“authority” implementation. Rather than granting authorities directly to a “person”, you can create a “group”, map authorities to it, and then map a “person” to that “group”. For applications that have a one or more groups of users who need the same level of access, having one or more “group” instances makes managing changes to access levels easier because the authorities that make up that access level are encapsulated in the “group”, and a single change will affect all of the users.
If you run the s2-quickstart script with the group name specified and use com.mycompany.myapp
as your package and RoleGroup
and Role
as your class names, you’ll generate this class:
RoleGroup.groovy
package com.mycompany.myapp
import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString
import grails.compiler.GrailsCompileStatic
@GrailsCompileStatic
@EqualsAndHashCode(includes='name')
@ToString(includes='name', includeNames=true, includePackage=false)
class RoleGroup implements Serializable {
private static final long serialVersionUID = 1
String name
Set<Role> getAuthorities() {
(RoleGroupRole.findAllByRoleGroup(this) as List<RoleGroupRole>)*.role as Set<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:
getAuthorities()
method when using role groupsSet<RoleGroup> getAuthorities() {
(UserRoleGroup.findAllByUser(this) as List<UserRoleGroup>)*.roleGroup as Set<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
.
Property | Default Value | Meaning |
---|---|---|
useRoleGroups |
|
Whether to use “authority group” implementation when loading user authorities |
authority.groupAuthorityNameField |
none (the s2-quickstart script uses the name “authorities”) |
RoleGroup class role collection property |
3.5. PersonGroup Class
The typical approach to mapping the relationship between “person” and “group” is a many-to-many. In a standard implementation, users have multiple roles, and roles are shared by multiple users. In a group implementation, users have multiple groups, and groups are shared by multiple users. For the same reason we would use a join class between “person” and “authority”, we should use one between “person” and “group”. Please note that when using groups, there should not be a join class between “person” and “authority”, since “group” resides between the two.
If you run the s2-quickstart script with the group name specified, this class will be generated for you, so you don’t need to deal with the details of mapping it. Assuming you choose com.mycompany.myapp
as your package, and User
and RoleGroup
as your class names, you’ll generate this class:
UserRoleGroup.groovy
package com.mycompany.myapp
import grails.gorm.DetachedCriteria
import groovy.transform.ToString
import org.codehaus.groovy.util.HashCodeHelper
import grails.compiler.GrailsCompileStatic
@GrailsCompileStatic
@ToString(cache=true, includeNames=true, includePackage=false)
class UserRoleGroup implements Serializable {
private static final long serialVersionUID = 1
User user
RoleGroup roleGroup
@Override
boolean equals(other) {
if (other instanceof UserRoleGroup) {
other.userId == user?.id && other.roleGroupId == roleGroup?.id
}
}
@Override
int hashCode() {
int hashCode = HashCodeHelper.initHash()
if (user) {
hashCode = HashCodeHelper.updateHash(hashCode, user.id)
}
if (roleGroup) {
hashCode = HashCodeHelper.updateHash(hashCode, roleGroup.id)
}
hashCode
}
static UserRoleGroup get(long userId, long roleGroupId) {
criteriaFor(userId, roleGroupId).get()
}
static boolean exists(long userId, long roleGroupId) {
criteriaFor(userId, roleGroupId).count()
}
private static DetachedCriteria criteriaFor(long userId, long roleGroupId) {
UserRoleGroup.where {
user == User.load(userId) &&
roleGroup == RoleGroup.load(roleGroupId)
}
}
static UserRoleGroup create(User user, RoleGroup roleGroup, boolean flush = false) {
def instance = new UserRoleGroup(user: user, roleGroup: roleGroup)
instance.save(flush: flush)
instance
}
static boolean remove(User u, RoleGroup rg) {
if (u != null && rg != null) {
UserRoleGroup.where { user == u && roleGroup == rg }.deleteAll()
}
}
static int removeAll(User u) {
u == null ? 0 : UserRoleGroup.where { user == u }.deleteAll() as int
}
static int removeAll(RoleGroup rg) {
rg == null ? 0 : UserRoleGroup.where { roleGroup == rg }.deleteAll() as int
}
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']
}
}
}
}
}
static mapping = {
id composite: ['roleGroup', 'user']
version false
}
}
3.6. GroupAuthority Class
The typical approach to mapping the relationship between “group” and “authority” is a many-to-many. In a standard implementation, users have multiple roles, and roles are shared by multiple users. In a group implementation, groups have multiple roles and roles are shared by multiple groups. For the same reason we would use a join class between “person” and “authority”, we should use one between “group” and “authority”.
If you run the s2-quickstart script with the group name specified, this class will be generated for you, so you don’t need to deal with the details of mapping it. Assuming you choose com.mycompany.myapp
as your package, and RoleGroup
and Role
as your class names, you’ll generate this class:
RoleGroupRole.groovy
package com.mycompany.myapp
import grails.gorm.DetachedCriteria
import groovy.transform.ToString
import org.codehaus.groovy.util.HashCodeHelper
import grails.compiler.GrailsCompileStatic
@GrailsCompileStatic
@ToString(cache=true, includeNames=true, includePackage=false)
class RoleGroupRole implements Serializable {
private static final long serialVersionUID = 1
RoleGroup roleGroup
Role role
@Override
boolean equals(other) {
if (other instanceof RoleGroupRole) {
other.roleId == role?.id && other.roleGroupId == roleGroup?.id
}
}
@Override
int hashCode() {
int hashCode = HashCodeHelper.initHash()
if (roleGroup) {
hashCode = HashCodeHelper.updateHash(hashCode, roleGroup.id)
}
if (role) {
hashCode = HashCodeHelper.updateHash(hashCode, role.id)
}
hashCode
}
static RoleGroupRole get(long roleGroupId, long roleId) {
criteriaFor(roleGroupId, roleId).get()
}
static boolean exists(long roleGroupId, long roleId) {
criteriaFor(roleGroupId, roleId).count()
}
private static DetachedCriteria criteriaFor(long roleGroupId, long roleId) {
RoleGroupRole.where {
roleGroup == RoleGroup.load(roleGroupId) &&
role == Role.load(roleId)
}
}
static RoleGroupRole create(RoleGroup roleGroup, Role role, boolean flush = false) {
def instance = new RoleGroupRole(roleGroup: roleGroup, role: role)
instance.save(flush: flush)
instance
}
static boolean remove(RoleGroup rg, Role r) {
if (rg != null && r != null) {
RoleGroupRole.where { roleGroup == rg && role == r }.deleteAll()
}
}
static int removeAll(Role r) {
r == null ? 0 : RoleGroupRole.where { role == r }.deleteAll() as int
}
static int removeAll(RoleGroup rg) {
rg == null ? 0 : RoleGroupRole.where { roleGroup == rg }.deleteAll() as int
}
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
}
}
3.7. Requestmap Class
Optionally, use this class to store request mapping entries in the database instead of defining them with annotations or in application.groovy
. This option makes the class configurable at runtime; you can add, remove and edit rules without restarting your application.
Property | Default Value | Meaning |
---|---|---|
requestMap.className |
none |
requestmap class name |
requestMap.urlField |
“url” |
URL pattern property name |
requestMap.configAttributeField |
“configAttribute” |
authority pattern property name |
requestMap.httpMethodField |
“httpMethod” |
HTTP method property name (optional, does not have to exist in the class if you don’t require URL/method security) |
Assuming you choose com.mycompany.myapp
as your package, and Requestmap
as your class name, you’ll generate this class:
Requestmap.groovy
package com.mycompany.myapp
import org.springframework.http.HttpMethod
import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString
import grails.compiler.GrailsCompileStatic
@GrailsCompileStatic
@EqualsAndHashCode(includes=['configAttribute', 'httpMethod', 'url'])
@ToString(includes=['configAttribute', 'httpMethod', 'url'], cache=true, includeNames=true, includePackage=false)
class RequestMap implements Serializable {
private static final long serialVersionUID = 1
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.
4. Configuring Request Mappings to Secure URLs
You can choose among the following approaches to configuring request mappings for secure application URLs. The goal is to map URL patterns to the roles required to access those URLs.
-
@Secured
annotations (default approach) - Defining Secured Annotations -
A simple Map in
application.groovy
- Static Map -
Requestmap
domain class instances stored in the database - Requestmap Instances Stored in the Database
You can only use one method at a time. You configure it with the securityConfigType
attribute; the value has to be an SecurityConfigType
enum value or the name of the enum as a String.
4.1. Pessimistic Lockdown
Many applications are mostly public, with some pages only accessible to authenticated users with various roles. In this case, it might make sense to leave URLs open by default and restrict access on a case-by-case basis. However, if your application is primarily secure, you can use a pessimistic lockdown approach to deny access to all URLs that do not have an applicable URL <==> Role request mapping. But the pessimistic approach is safer; if you forget to restrict access to a URL using the optimistic approach, it might take a while to discover that unauthorized users can access the URL, but if you forget to allow access when using the pessimistic approach, no user can access it and the error should be quickly discovered.
The pessimistic approach is the default, and there are two configuration options that apply. If rejectIfNoRule
is true
(the default) then any URL that has no request mappings (an annotation, entry in controllerAnnotations.staticRules
or interceptUrlMap
, or a Requestmap
instance) will be denied to all users. The other option is fii.rejectPublicInvocations
and if it is true
(the default) then un-mapped URLs will trigger an IllegalArgumentException
and will show the error page. This is uglier, but more useful because it’s very clear that there is a misconfiguration. When fii.rejectPublicInvocations
is false
but rejectIfNoRule
is true
you just see the “Sorry, you’re not authorized to view this page.” error 403 message.
Note 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
)
rejectIfNoRule
grails.plugin.springsecurity.rejectIfNoRule = true
grails.plugin.springsecurity.fii.rejectPublicInvocations = false
and to reject with the error 500 page, use these (optionally omit rejectPublicInvocations
since it defaults to true
):
fii.rejectPublicInvocations
grails.plugin.springsecurity.rejectIfNoRule = false
grails.plugin.springsecurity.fii.rejectPublicInvocations = true
Note that if you set rejectIfNoRule
or rejectPublicInvocations
to true
you’ll need to configure the staticRules
map to include URLs that can’t otherwise be guarded:
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']]
]
Note that the syntax of the The preceding
Now in addition to the default mappings, we require an authentication with |
This is needed when using annotations; if you use the grails.plugin.springsecurity.interceptUrlMap
map in application.groovy
you’ll need to add these URLs too, and likewise when using Requestmap
instances. If you don’t use annotations, you must add rules for the login and logout controllers also. You can add Requestmaps manually, or in BootStrap.groovy, for example:
rejectIfNoRule
or fii.rejectPublicInvocations
for (String url in [
'/', '/error', '/index', '/index.gsp', '/**/favicon.ico', '/shutdown',
'/**/js/**', '/**/css/**', '/**/images/**',
'/login', '/login.*', '/login/*',
'/logout', '/logout.*', '/logout/*']) {
new Requestmap(url: url, configAttribute: 'permitAll').save()
}
springSecurityService.clearCachedRequestmaps()
The analogous interceptUrlMap settings would be:
interceptUrlMap
configuration when using rejectIfNoRule
or fii.rejectPublicInvocations
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, you’ll have to specify access rules for the associated URLs, e.g.
[pattern: '/login/impersonate', access: ['ROLE_ADMIN']],
[pattern: '/logout/impersonate', access: ['permitAll']]
4.2. URLs and Authorities
In each approach you configure a mapping for a URL pattern to the role(s) that are required to access those URLs, for example, /admin/user/**
requires ROLE_ADMIN
. In addition, you can combine the role(s) with SpEL expressions and/or tokens such as IS_AUTHENTICATED_ANONYMOUSLY, IS_AUTHENTICATED_REMEMBERED, and IS_AUTHENTICATED_FULLY. One or more voters (Voters) will process any tokens and enforce a rule based on them:
-
IS_AUTHENTICATED_ANONYMOUSLY
-
signifies that anyone can access this URL. By default the
AnonymousAuthenticationFilter
ensures an “anonymous”Authentication
with no roles so that every user has an authentication. The token accepts any authentication, even anonymous. -
The SpEL expression
permitAll
is equivalent toIS_AUTHENTICATED_ANONYMOUSLY
and is typically more intuitive to use
-
-
IS_AUTHENTICATED_REMEMBERED
-
requires the user to be authenticated through a remember-me cookie or an explicit login.
-
The SpEL expression
isAuthenticated() or isRememberMe()
is equivalent toIS_AUTHENTICATED_REMEMBERED
and is typically more intuitive to use
-
-
IS_AUTHENTICATED_FULLY
-
requires the user to be fully authenticated with an explicit login.
-
The SpEL expression
isFullyAuthenticated()
is equivalent toIS_AUTHENTICATED_FULLY
and is typically more intuitive to use
-
With IS_AUTHENTICATED_FULLY
you can implement a security scheme whereby users can check a remember-me checkbox during login and be auto-authenticated each time they return to your site, but must still log in with a password for some parts of the site. For example, allow regular browsing and adding items to a shopping cart with only a cookie, but require an explicit login to check out or view purchase history.
For more information on IS_AUTHENTICATED_FULLY
, IS_AUTHENTICATED_REMEMBERED
, and IS_AUTHENTICATED_ANONYMOUSLY
, see the Javadoc for AuthenticatedVoter
The plugin isn’t compatible with Grails
both actions will be allowed if the user has permission to access the The workaround is to create separate forms without using |
4.3. Comparing the Approaches
Each approach has its advantages and disadvantages. Annotations and the application.groovy
Map are less flexible because they are configured once in the code and you can update them only by restarting the application (in prod mode anyway). In practice this limitation is minor, because security mappings for most applications are unlikely to change at runtime.
On the other hand, storing Requestmap
entries enables runtime-configurability. This approach gives you a core set of rules populated at application startup that you can edit, add to, and delete as needed. However, it separates the security rules from the application code, which is less convenient than having the rules defined in grails-app/conf/application.groovy
or in the applicable controllers using annotations.
URLs must be mapped in lowercase if you use the Requestmap
or grails-app/conf/application.groovy
map approaches. For example, if you have a FooBarController, its urls will be of the form /fooBar/list, /fooBar/create, and so on, but these must be mapped as /foobar/, /foobar/list, /foobar/create. This mapping is handled automatically for you if you use annotations.
4.4. Defining Secured Annotations
You can use an @Secured
annotation (either the standard org.springframework.security.access.annotation.Secured
or the plugin’s grails.plugin.springsecurity.annotation.Secured
which has the same attributes and features but also supports defining a closure as the config attribute to make authorization decisions) in your controllers to configure which roles are required for which actions. To use annotations, specify securityConfigType="Annotation"
, or leave it unspecified because it’s the default:
securityConfigType
as “Annotation”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 they’re more specific.
For example, given this controller:
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 /myapp/secureAnnotated/index
) and be authenticated and have ROLE_ADMIN
or ROLE_SUPERUSER
to see /myapp/secureAnnotated/adminEither
. Any user can access /myapp/secureAnnotated/anybody
if you have disabled “strict” mode (using rejectIfNoRule
), and nobody can access the action by default since it has no access rule configured.
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 closure’s delegate
is set to a subclass of WebSecurityExpressionRoot
, and also the Spring ApplicationContext
as the ctx
property:
@Secured
@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:
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 isn’t 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 {
@Secured(value = ['ROLE_ADMIN'], httpMethod = 'GET')
def create() {
...
}
@Secured(value = ['ROLE_ADMIN'], httpMethod = 'POST')
def save() {
...
}
}
Here you must have ROLE_ADMIN for both the create
and save
actions but create
requires a GET request (since it renders the form to create a new instance) and save
requires POST (since it’s the action that the form posts to).
4.4.1. Securing RESTful domain classes
Since Grails 2.3, domain classes can be annotated with the grails.rest.Resource
AST transformation, which will generate internally a controller with the default CRUD operations.
You can also use the @Secured
annotation on such domain classes:
@Resource
@Secured('ROLE_ADMIN')
class Thing {
String name
}
4.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 |
4.5. Static Map
To use a static map in application.groovy
to secure URLs, first specify securityConfigType="InterceptUrlMap"
:
securityConfigType
as “InterceptUrlMap”grails.plugin.springsecurity.securityConfigType = "InterceptUrlMap"
Define a Map in application.groovy
:
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.
interceptUrlMap
mappingsgrails.plugin.springsecurity.interceptUrlMap = [
...
[pattern: '/secure/**', access: ['ROLE_ADMIN']],
[pattern: '/finance/**', access: ['ROLE_FINANCE', 'IS_AUTHENTICATED_FULLY']]
]
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.
interceptUrlMap
order[pattern: '/secure/**', access: ['ROLE_ADMIN', 'ROLE_SUPERUSER']],
[pattern: '/secure/reallysecure/**', access: ['ROLE_SUPERUSER']]
then this would fail - it wouldn’t 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
interceptUrlMap
order[pattern: '/secure/reallysecure/**', access: ['ROLE_SUPERUSER']],
[pattern: '/secure/**', access: ['ROLE_ADMIN', 'ROLE_SUPERUSER']]
4.6. Requestmap Instances Stored in the Database
With this approach you use the Requestmap
domain class to store mapping entries in the database. Requestmap
has a url
property that contains the secured URL pattern and a configAttribute
property containing a comma-delimited list of required roles, SpEL expressions, and/or tokens such as IS_AUTHENTICATED_FULLY
, IS_AUTHENTICATED_REMEMBERED
, and IS_AUTHENTICATED_ANONYMOUSLY
.
To use Requestmap
entries, specify securityConfigType="Requestmap"
:
securityConfigType
as “Requestmap”grails.plugin.springsecurity.securityConfigType = "Requestmap"
You create Requestmap
entries as you create entries in any Grails domain class:
Requestmap
entriesfor (String url in [
'/', '/error', '/index', '/index.gsp', '/**/favicon.ico', '/shutdown',
'/assets/**', '/**/js/**', '/**/css/**', '/**/images/**',
'/login', '/login.*', '/login/*',
'/logout', '/logout.*', '/logout/*']) {
new Requestmap(url: url, configAttribute: 'permitAll').save()
}
new Requestmap(url: '/profile/**', configAttribute: 'ROLE_USER').save()
new Requestmap(url: '/admin/**', configAttribute: 'ROLE_ADMIN').save()
new Requestmap(url: '/admin/role/**', configAttribute: 'ROLE_SUPERVISOR').save()
new Requestmap(url: '/admin/user/**',
configAttribute: 'ROLE_ADMIN,ROLE_SUPERVISOR').save()
new Requestmap(url: '/login/impersonate',
configAttribute: 'ROLE_SWITCH_USER,IS_AUTHENTICATED_FULLY').save()
springSecurityService.clearCachedRequestmaps()
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 IS_AUTHENTICATED_FULLY
, IS_AUTHENTICATED_REMEMBERED
, or IS_AUTHENTICATED_ANONYMOUSLY
with one or more roles means the user must have one of the roles and satisty the IS_AUTHENTICATED
rule.
Unlike the application.groovy
Map approach (Static Map), you do not need to revise the Requestmap
entry order because the plugin calculates the most specific rule that applies to the current request.
4.6.1. Requestmap Cache
Requestmap
entries are cached for performance, but caching affects runtime configurability. If you create, edit, or delete an instance, the cache must be flushed and repopulated to be consistent with the database. You can call springSecurityService.clearCachedRequestmaps()
to do this. For example, if you create a RequestmapController
the save
action should look like this (and the update and delete actions should similarly call clearCachedRequestmaps()
):
clearCachedRequestmaps()
class RequestmapController {
def springSecurityService
...
def save(Requestmap requestmap) {
if (!requestmap.save(flush: true)) {
render view: 'create', model: [requestmapInstance: requestmap]
return
}
springSecurityService.clearCachedRequestmaps()
flash.message = ...
redirect action: 'show', id: requestmap.id
}
}
4.7. Using Expressions to Create Descriptive, Fine-Grained Rules
Spring Security uses the Spring Expression Language (SpEL), which allows you to declare the rules for guarding URLs more descriptively than does the traditional approach, and also allows much more fine-grained rules. Where you traditionally would specify a list of role names and/or special tokens (for example, IS_AUTHENTICATED_FULLY
), with Spring Security’s expression support, you can instead use the embedded scripting language to define simple or complex access rules.
You can use expressions with any of the previously described approaches to securing application URLs. For example, consider this annotated controller:
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
new Requestmap(url: "/secure/someAction",
configAttribute: "hasRole('ROLE_ADMIN')").save()
new Requestmap(url: "/secure/someOtherAction",
configAttribute: "authentication.name == 'ralph'").save()
and the corresponding static mappings would be
grails.plugin.springsecurity.interceptUrlMap
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:
Expression | Description |
---|---|
|
Returns |
|
Returns |
|
Allows direct access to the principal object representing the current user |
|
Allows direct access to the current |
|
Always evaluates to |
|
Always evaluates to |
|
Returns |
|
Returns |
|
Returns |
|
Returns |
|
the HTTP request, allowing expressions such as “isFullyAuthenticated() or request.getMethod().equals('OPTIONS')” |
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:
Traditional Config | Expression |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
5. Helper Classes
Use the plugin helper classes in your application to avoid dealing with some lower-level details of Spring Security.
5.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
.
5.1.1. ifLoggedIn
Displays the inner body content if the user is authenticated.
Example:
<sec:ifLoggedIn>
<sec:ifLoggedIn>
Welcome Back!
</sec:ifLoggedIn>
5.1.2. ifNotLoggedIn
Displays the inner body content if the user is not authenticated.
Example:
<sec:ifNotLoggedIn>
<sec:ifNotLoggedIn>
<g:link controller='login' action='auth'>Login</g:link>
</sec:ifNotLoggedIn>
5.1.3. ifAllGranted
Displays the inner body content only if all of the listed roles are granted.
Example:
<sec:ifAllGranted>
<sec:ifAllGranted roles='ROLE_ADMIN,ROLE_SUPERVISOR'>
...
secure stuff here
...
</sec:ifAllGranted>
5.1.4. ifAnyGranted
Displays the inner body content if at least one of the listed roles are granted.
Example:
<sec:ifAnyGranted>
<sec:ifAnyGranted roles='ROLE_ADMIN,ROLE_SUPERVISOR'>
...
secure stuff here
...
</sec:ifAnyGranted>
5.1.5. ifNotGranted
Displays the inner body content if none of the listed roles are granted.
Example:
<sec:ifNotGranted>
<sec:ifNotGranted roles='ROLE_USER'>
...
non-user stuff here
...
</sec:ifNotGranted>
5.1.6. loggedInUserInfo
Displays the value of the specified UserDetails property if logged in. For example, to show the username property:
<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:
<sec:loggedInUserInfo>
for a nonstandard propertyWelcome Back <sec:loggedInUserInfo field='fullName'/>
5.1.7. username
Displays the value of the UserDetails username
property if logged in.
<sec:username>
<sec:ifLoggedIn>
Welcome Back <sec:username/>!
</sec:ifLoggedIn>
<sec:ifNotLoggedIn>
<g:link controller='login' action='auth'>Login</g:link>
</sec:ifNotLoggedIn>
5.1.8. ifSwitched
Displays the inner body content only if the current user switched from another user. (See also Switch User.)
<sec:ifSwitched>
and <sec:ifNotSwitched>
<sec:ifLoggedIn>
Logged in as <sec:username/>
</sec:ifLoggedIn>
<sec:ifSwitched>
<form action='${request.contextPath}/logout/impersonate' method='POST'>
<input type='submit' value="Resume as ${grails.plugin.springsecurity.SpringSecurityUtils.switchedUserOriginalUsername}"/>
</form>
</sec:ifSwitched>
<sec:ifNotSwitched>
<sec:ifAllGranted roles='ROLE_SWITCH_USER'>
<form action='${request.contextPath}/login/impersonate'
method='POST'>
Switch to user: <input type='text' name='username'/><br/>
<input type='submit' value='Switch'/> </form>
</sec:ifAllGranted>
</sec:ifNotSwitched>
5.1.9. ifNotSwitched
Displays the inner body content only if the current user has not switched from another user.
5.1.10. switchedUserOriginalUsername
Renders the original user’s username if the current user switched from another user.
<sec:switchedUserOriginalUsername>
<sec:ifSwitched>
<form action='${request.contextPath}/logout/impersonate' method='POST'>
<input type='submit' value="Resume as ${grails.plugin.springsecurity.SpringSecurityUtils.switchedUserOriginalUsername}"/>
</form>
</sec:ifSwitched>
5.1.11. access
Renders the body if the specified expression evaluates to true
or specified URL is allowed.
<sec:access>
with an expression<sec:access expression="hasRole('ROLE_USER')">
You're a user
</sec:access>
<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
<sec:access>
with a controller and action<sec:access controller='admin' action='user'>
<g:link controller='admin' action='user'>Manage Users</g:link>
</sec:access>
or if you have a named URL mapping you can refer to that:
<sec:access>
with a URL mapping<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
<sec:access>
with <g:createLink>
<sec:access url='${createLink(controller: 'admin', action: 'user', base: '/')}'>
<g:link controller='admin' action='user'>Manage Users</g:link>
</sec:access>
Be sure to include the base: '/'
attribute in this case to avoid appending the context name to the URL.
5.1.12. noAccess
Renders the body if the specified expression evaluates to false
or URL isn’t allowed.
<sec:noAccess>
<sec:noAccess expression="hasRole('ROLE_USER')">
You're not a user
</sec:noAccess>
5.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:
<sec:link>
with an expression<sec:link controller='myController' action='myAction' expression="hasRole('ROLE_USER')">My link text</sec:link>
To use access controls defined, for example, in the interceptUrlMap:
<sec:link>
without an expression<sec:link controller='myController' action='myAction'>My link text</sec:link>
By default, nothing will be rendered if the specified expression evaluates to false
or URL is not allowed. To render only the text that would have been linked, set the fallback
attribute:
<sec:link fallback='true'>
without an expression<sec:link controller='myController' action='myAction' fallback='true'>This text will display but won't be linked if the user doesn't have access</sec:link>
5.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
5.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 user’s 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:
getCurrentUser()
class SomeController {
def springSecurityService
def someAction() {
def user = springSecurityService.currentUser
...
}
}
5.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 doesn’t 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:
loadCurrentUser()
class SomeController {
def springSecurityService
def someAction(Long id) {
def user = springSecurityService.isLoggedIn() ?
springSecurityService.loadCurrentUser() :
null
if (user) {
CreditCard card = CreditCard.findByIdAndUser(id, user)
...
}
...
}
}
5.2.3. isLoggedIn()
Checks whether there is a currently logged-in user.
Example:
isLoggedIn()
class SomeController {
def springSecurityService
def someAction() {
if (springSecurityService.isLoggedIn()) {
...
}
else {
...
}
}
}
5.2.4. getAuthentication()
Retrieves the current user’s Authentication. If authenticated, this will typically be a UsernamePasswordAuthenticationToken.
If not authenticated and the AnonymousAuthenticationFilter is active (true by default) then the anonymous user’s authentication will be returned. This 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
.
Example:
getAuthentication()
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
...
}
}
5.2.5. getPrincipal()
Retrieves the currently logged in user’s Principal
. If authenticated, the principal will be a grails.plugin.springsecurity.userdetails.GrailsUser
, unless you have created a 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:
getPrincipal()
class SomeController {
def springSecurityService
def someAction() {
def principal = springSecurityService.principal
String username = principal.username
def authorities = principal.authorities // a Collection of GrantedAuthority
boolean enabled = principal.enabled
...
}
}
5.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 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:
encodePassword()
class PersonController {
def springSecurityService
def updateAction(Person person) {
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 an PersistenceEventListener or in the User domain class (using |
5.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:
updateRole()
class RoleController {
def springSecurityService
def update(Role role) {
if (!springSecurityService.updateRole(role, params)) {
render view: 'edit', model: [roleInstance: role]
return
}
flash.message = "The role was updated"
redirect action: show, id: role.id
}
}
5.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:
deleteRole()
class RoleController {
def springSecurityService
def delete(Role role) {
try {
springSecurityService.deleteRole role
flash.message = "The role was deleted"
redirect action: list
}
catch (DataIntegrityViolationException e) {
flash.message = "Unable to delete the role"
redirect action: show, id: params.id
}
}
}
5.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 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:
clearCachedRequestmaps()
class RequestmapController {
def springSecurityService
def save(Requestmap requestmap) {
if (!requestmap.save(flush: true)) {
render view: 'create', model: [requestmapInstance: requestmap]
return
}
springSecurityService.clearCachedRequestmaps()
flash.message = "Requestmap created"
redirect action: show, id: requestmap.id
}
}
5.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 user’s 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:
reauthenticate()
class UserController {
def springSecurityService
def update(User user) {
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
}
flash.message = "The user was updated"
redirect action: show, id: user.id
}
}
5.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.
5.3.1. authoritiesToRoles()
Extracts role names from an array or Collection
of GrantedAuthority.
5.3.2. getPrincipalAuthorities()
Retrieves the currently logged-in user’s authorities. It is empty (but never null
) if the user is not logged in.
5.3.3. parseAuthoritiesString()
Splits a comma-delimited String containing role names into a List
of GrantedAuthority.
5.3.4. ifAllGranted()
Checks whether the current user has all specified roles (a comma-delimited String of role names). Primarily used by SecurityTagLib.ifAllGranted
.
5.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
.
5.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
.
5.3.7. getSecurityConfig()
Retrieves the security part of the Configuration
(from grails-app/conf/application.groovy
merged with the plugin’s default configuration).
5.3.8. loadSecondaryConfig()
Used by dependent plugins to add configuration attributes.
5.3.9. reloadSecurityConfig()
Forces a reload of the security configuration.
5.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.
grails.plugin.springsecurity.ajaxCheckClosure
grails.plugin.springsecurity.ajaxCheckClosure = { request ->
// return true or false
}
You can also force the request to be treated as Ajax by appending &ajax=true
to your request query string.
5.3.11. registerProvider()
Used by dependent plugins to register an AuthenticationProvider bean name.
5.3.12. registerFilter()
Used by dependent plugins to register a filter bean name in a specified position in the filter chain.
5.3.13. isSwitched()
Checks whether the current user switched from another user.
5.3.14. getSwitchedUserOriginalUsername()
Gets the original user’s username if the current user switched from another user.
5.3.15. doWithAuth()
Executes a Closure with the current authentication. The one-parameter version which takes just a Closure assumes that there’s 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
aren’t 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 isn’t 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.
6. 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, AbstractAuthenticationEvent and AbstractAuthorizationEvent.
6.1. Event Notification
You can set up event notifications in two ways. The sections that follow describe each approach in more detail.
-
Register an event listener, ignoring events that do not interest you. Spring allows only partial event subscription; you use generics to register the class of events that interest you, and you are notified of that class and all subclasses.
-
Register one or more callback closures in
grails-app/conf/application.groovy
that take advantage of the plugin’sgrails.plugin.springsecurity.SecurityEventListener
. The listener does the filtering for you.
6.1.1. AuthenticationEventPublisher
Spring Security publishes events using an AuthenticationEventPublisher which in turn fire events using the ApplicationEventPublisher. By default no events are fired since the AuthenticationEventPublisher
instance registered is a grails.plugin.springsecurity.authentication.NullAuthenticationEventPublisher
. But you can enable event publishing by setting grails.plugin.springsecurity.useSecurityEventListener = true
in grails-app/conf/application.groovy
.
You can use the useSecurityEventListener
setting to temporarily disable and enable the callbacks, or enable them per-environment.
6.1.2. UsernameNotFoundException
Most authentication exceptions trigger an event with a similar name as described in this table:
Exception | Event |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
This holds for all exceptions except UsernameNotFoundException
which triggers an AuthenticationFailureBadCredentialsEvent
just like a BadCredentialsException
. This is a good idea since it doesn’t expose extra information - there’s no differentiation between a bad password and a missing user. In addition, by default a missing user will trigger a BadCredentialsException
for the same reasons. You can configure Spring Security to re-throw the original UsernameNotFoundException
instead of converting it to a BadCredentialsException
by setting grails.plugin.springsecurity.dao.hideUserNotFoundExceptions = false
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
).
6.2. Registering an Event Listener
Enable events with grails.plugin.springsecurity.useSecurityEventListener = true
and create one or more Groovy or Java classes, for example:
MySecurityEventListener.groovy
package com.foo.bar
import org.springframework.context.ApplicationListener
import org.springframework.security.authentication.event.AuthenticationSuccessEvent
class MySecurityEventListener
implements ApplicationListener<AuthenticationSuccessEvent> {
void onApplicationEvent(AuthenticationSuccessEvent event) {
// handle the event
}
}
Register the class in grails-app/conf/spring/resources.groovy
:
resources.groovy
import com.foo.bar.MySecurityEventListener
beans = {
mySecurityEventListener(MySecurityEventListener)
}
6.3. Registering Callback Closures
Alternatively, enable events with grails.plugin.springsecurity.useSecurityEventListener = true
and register one or more callback closure(s) in grails-app/conf/application.groovy
and let SecurityEventListener
do the filtering.
Implement the event handlers that you need, for example:
application.groovy
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 a user authenticates, Spring Security initially fires an |
7. User, Authority (Role), and Requestmap Properties
Properties you are most likely to override are the User
and Authority
(and Requestmap
if you use the database to store mappings) class and property names.
Property | Default Value | Meaning |
---|---|---|
userLookup.userDomainClassName |
none |
User class name |
userLookup.usernamePropertyName |
“username” |
User class username property |
userLookup.usernameIgnoreCase |
“false” |
Ignore case when searching for usernamePropertyName |
userLookup.passwordPropertyName |
“password” |
User class password property |
userLookup.authoritiesPropertyName |
“authorities” |
User class role collection property |
userLookup.enabledPropertyName |
“enabled” |
User class enabled property |
userLookup.accountExpiredPropertyName |
“accountExpired” |
User class account expired property |
userLookup.accountLockedPropertyName |
“accountLocked” |
User class account locked property |
userLookup.passwordExpiredPropertyName |
“passwordExpired” |
User class password expired property |
userLookup.authorityJoinClassName |
none |
User/Role many-many join class name |
authority.className |
none |
Role class name |
authority.nameField |
“authority” |
Role class role name property |
requestMap.className |
none |
Requestmap class name |
requestMap.urlField |
“url” |
Requestmap class URL pattern property |
requestMap.configAttributeField |
“configAttribute” |
Requestmap class role/token property |
8. 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
8.1. Basic and Digest Authentication
To use HTTP Basic Authentication in your application, set the useBasicAuth
attribute to true
. Also change the basic.realmName
default value to one that suits your application, for example:
grails.plugin.springsecurity.useBasicAuth = true
grails.plugin.springsecurity.basic.realmName = "Ralph's Bait and Tackle"
Property | Default | Description |
---|---|---|
useBasicAuth |
|
Whether to use Basic authentication |
basic.realmName |
“Grails Realm” |
Realm name displayed in the browser authentication popup |
basic.credentialsCharset |
“UTF-8” |
The character set used to decode Base64-encoded data |
With this authentication in place, users are prompted with the standard browser login dialog instead of being redirected to a login page.
If you don’t 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:
grails.plugin.springsecurity.filterChain.chainMap = [
[pattern: '/webservice/**', filters: 'JOINED_FILTERS,-exceptionTranslationFilter'],
[pattern: '/**', filters: 'JOINED_FILTERS,-basicAuthenticationFilter,-basicExceptionTranslationFilter']
]
In this example we’re 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 their names with -
.
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 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.
Property | Default Value | Meaning |
---|---|---|
useDigestAuth |
|
Whether to use Digest authentication |
digest.realmName |
“Grails Realm” |
Realm name displayed in the browser popup |
digest.key |
“changeme” |
Key used to build the nonce for authentication; it should be changed but that’s not required |
digest.nonceValiditySeconds |
|
How long a nonce stays valid |
digest.passwordAlreadyEncoded |
|
Whether you are managing the password hashing yourself |
digest.createAuthenticatedToken |
|
If |
digest.useCleartextPasswords |
|
If |
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 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 you’re not using a salt value, so you’ll need to change the code in
to
|
8.2. Certificate (X.509) Login Authentication
Another authentication mechanism supported by Spring Security is certificate-based, or “mutual authentication”. It requires HTTPS, and you must configure the server to require a client certificate (ordinarily only the server provides a certificate). Your username is extracted from the client certificate if it is valid, and you are “pre-authenticated”. As long as a corresponding username exists in the database, your authentication succeeds and you are not asked for a password. Your Authentication
contains the authorities associated with your username.
The table describes available configuration options.
Property | Default Value | Meaning |
---|---|---|
useX509 |
|
Whether to support certificate-based logins |
x509.continueFilterChainOnUnsuccessfulAuthentication |
|
Whether to proceed when an authentication attempt fails to allow other authentication mechanisms to process the request |
x509.subjectDnRegex |
“CN=(.*?)(?:,|$)” |
Regular expression for extracting the username from the certificate’s subject name |
x509.checkForPrincipalChanges |
|
Whether to re-extract the username from the certificate and check that it’s still the current user when a valid |
x509.invalidateSessionOnPrincipalChange |
|
Whether to invalidate the session if the principal changed (based on a |
x509.subjectDnClosure |
none |
If set, the plugin’s |
x509.throwExceptionWhenTokenRejected |
|
If |
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.
8.3. Remember-Me Cookie
Spring Security supports creating a remember-me cookie so that users are not required to log in with a username and password for each session. This is optional and is usually implemented as a checkbox on the login form; the default auth.gsp
supplied by the plugin has this feature.
Property | Default Value | Meaning |
---|---|---|
rememberMe.cookieName |
|
remember-me cookie name; should be unique per application |
rememberMe.cookieDomain |
none |
remember-me cookie domain |
rememberMe.alwaysRemember |
|
If |
rememberMe.tokenValiditySeconds |
|
Max age of the cookie in seconds |
rememberMe.parameter |
|
Login form remember-me checkbox name |
rememberMe.key |
|
Value used to encode cookies; should be unique per application |
rememberMe.useSecureCookie |
none |
Whether to use a secure cookie or not; if |
rememberMe.createSessionOnSuccess |
|
Whether to create a session of one doesn’t exist to ensure that the |
rememberMe.persistent |
|
If |
rememberMe.persistentToken.domainClassName |
none |
Domain class used to manage persistent logins |
rememberMe.persistentToken.seriesLength |
16 |
Number of characters in the cookie’s |
rememberMe.persistentToken.tokenLength |
16 |
Number of characters in the cookie’s |
atr.rememberMeClass |
remember-me authentication class |
You are most likely to change these attributes:
-
rememberMe.cookieName
. Purely aesthetic as most users will not look at their cookies, but you probably want the display name to be application-specific rather than “grails_remember_me”. -
rememberMe.key
. Part of a salt when the cookie is hashed. Changing the default makes it harder to execute brute-force attacks. -
rememberMe.tokenValiditySeconds
. Default is two weeks; set it to what makes sense for your application.
8.3.1. Persistent Logins
The remember-me cookie is very secure, but for an even stronger solution you can use persistent logins that store the username in the database. See the Spring Security docs for a description of the implementation.
Persistent login is also useful for authentication schemes like Facebook, where you do not manage passwords in your database, but most of the other user information is stored locally. Without a password you cannot use the standard cookie format, so persistent logins enable remember-me cookies in these scenarios.
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
.
8.4. Ajax Authentication
The typical pattern of using web site authentication to access restricted pages involves intercepting access requests for secure pages, redirecting to a login page (possibly off-site, for example when using a Single Sign-on implementation such as CAS), and redirecting back to the originally-requested page after a successful login. Each page can also have a login link to allow explicit logins at any time.
Another option is to also have a login link on each page and to use JavaScript to present a login form within the current page in a popup. The JavaScript code submits the authentication request and displays success or error messages as appropriate.
The plugin supports Ajax logins, but you need to create your own client-side code. There are only a few necessary changes, and of course the sample code here is pretty basic so you should enhance it for your needs.
The approach here involves editing your template page(s) to show “You’re logged in as …” text if logged in and a login link if not, along with a hidden login form that is shown using JavaScript.
This example uses jQuery and jqModal, a jQuery plugin that creates and manages dialogs and popups. Download jqModal.js
and copy it to grails-app/assets/javascripts
, and download jqModal.css
and copy it to grails-app/assets/stylesheets
.
Create grails-app/assets/javascripts/ajaxLogin.js
and add this JavaScript code:
ajaxLogin.js
var onLogin;
$.ajaxSetup({
beforeSend: function(jqXHR, event) {
if (event.url != $("#ajaxLoginForm").attr("action")) {
// save the 'success' function for later use if
// it wasn't triggered by an explicit login click
onLogin = event.success;
}
},
statusCode: {
// Set up a global Ajax error handler to handle 401
// unauthorized responses. If a 401 status code is
// returned the user is no longer logged in (e.g. when
// the session times out), so re-display the login form.
401: function() {
showLogin();
}
}
});
function showLogin() {
var ajaxLogin = $("#ajaxLogin");
ajaxLogin.css("text-align", "center");
ajaxLogin.jqmShow();
}
function logout(event) {
event.preventDefault();
$.ajax({
url: $("#_logout").attr("href"),
method: "POST",
success: function(data, textStatus, jqXHR) {
window.location = "/";
},
error: function(jqXHR, textStatus, errorThrown) {
console.log("Logout error, textStatus: " + textStatus +
", errorThrown: " + errorThrown);
}
});
}
function authAjax() {
$("#loginMessage").html("Sending request ...").show();
var form = $("#ajaxLoginForm");
$.ajax({
url: form.attr("action"),
method: "POST",
data: form.serialize(),
dataType: "JSON",
success: function(json, textStatus, jqXHR) {
if (json.success) {
form[0].reset();
$("#loginMessage").empty();
$("#ajaxLogin").jqmHide();
$("#loginLink").html(
'Logged in as ' + json.username +
' (<a href="' + $("#_logout").attr("href") +
'" id="logout">Logout</a>)');
$("#logout").click(logout);
if (onLogin) {
// execute the saved event.success function
onLogin(json, textStatus, jqXHR);
}
}
else if (json.error) {
$("#loginMessage").html('<span class="errorMessage">' +
json.error + "</error>");
}
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;
}
#ajaxLogin .inner {
width: 400px;
padding-bottom: 6px;
margin: 60px auto;
text-align: left;
border: 1px solid #aab;
background-color: #f0f0fa;
-moz-box-shadow: 2px 2px 2px #eee;
-webkit-box-shadow: 2px 2px 2px #eee;
-khtml-box-shadow: 2px 2px 2px #eee;
box-shadow: 2px 2px 2px #eee;
}
#ajaxLogin .inner .fheader {
padding: 18px 26px 14px 26px;
background-color: #f7f7ff;
margin: 0px 0 14px 0;
color: #2e3741;
font-size: 18px;
font-weight: bold;
}
#ajaxLogin .inner .cssform p {
clear: left;
margin: 0;
padding: 4px 0 3px 0;
padding-left: 105px;
margin-bottom: 20px;
height: 1%;
}
#ajaxLogin .inner .cssform input[type="text"],
#ajaxLogin .inner .cssform input[type="password"] {
width: 150px;
}
#ajaxLogin .inner .cssform label {
font-weight: bold;
float: left;
text-align: right;
margin-left: -105px;
width: 150px;
padding-top: 3px;
padding-right: 10px;
}
.ajaxLoginButton {
background-color: #efefef;
font-weight: bold;
padding: 0.5em 1em;
display: -moz-inline-stack;
display: inline-block;
vertical-align: middle;
white-space: nowrap;
overflow: visible;
text-decoration: none;
-moz-border-radius: 0.3em;
-webkit-border-radius: 0.3em;
border-radius: 0.3em;
}
.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;
}
There’s 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 /grails-app/assets/stylesheets/application.css
:
application.css
/*
...
*= require ajaxLogin
*= require jqModal
...
*/
We’ll need some GSP code to define the HTML, so create grails-app/views/includes/_ajaxLogin.gsp
and add this:
_ajaxLogin.gsp
<span id="logoutLink" style="display: none;">
<g:link elementId='_logout' controller='logout'>Logout</g:link>
</span>
<span id="loginLink" style="position: relative; margin-right: 30px; float: right">
<sec:ifLoggedIn>
Logged in as <sec:username/> (<g:link elementId='logout' controller='logout'>Logout</g:link>)
</sec:ifLoggedIn>
<sec:ifNotLoggedIn>
<a href="#" onclick="showLogin(); return false;">Login</a>
</sec:ifNotLoggedIn>
</span>
<div id="ajaxLogin" class="jqmWindow" style="z-index: 3000;">
<div class="inner">
<div class="fheader">Please Login..</div>
<form action="${request.contextPath}/login/authenticate" method="POST"
id="ajaxLoginForm" name="ajaxLoginForm" class="cssform" autocomplete="off">
<p>
<label for="username">Username:</label>
<input type="text" class="text_" name="username" id="username" />
</p>
<p>
<label for="password">Password</label>
<input type="password" class="text_" name="password" id="password" />
</p>
<p>
<label for="remember_me">Remember me</label>
<input type="checkbox" class="chk" id="remember_me" name="remember-me"/>
</p>
<p>
<input type="submit" id="authAjax" name="authAjax"
value="Login" class="ajaxLoginButton" />
<input type="button" id="cancelLogin" value="Cancel"
class="ajaxLoginButton" />
</p>
</form>
<div style="display: none; text-align: left;" id="loginMessage"></div>
</div>
</div>
And finally, update the grails-app/views/layouts/main.gsp
layout to include _ajaxLogin.gsp
, adding it after the <body>
tag:
main.gsp
<html lang="en" class="no-js">
<head>
...
<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, you’ll need to change the url in thesuccess
callback to the correct post-logout value, e.g.window.location = "/appname";
if you have configured the contextPath to be "/appname"
-
8.4.1. How Does Ajax login Work?
Most Ajax libraries include an X-Requested-With
header that indicates that the request was made by XMLHttpRequest
instead of being triggered by clicking a regular hyperlink or form submit button. The plugin uses this header to detect Ajax login requests, and uses subclasses of some of Spring Security’s classes to use different redirect urls for Ajax requests than regular requests. Instead of showing full pages, LoginController
has JSON-generating methods ajaxSuccess()
, ajaxDenied()
, and authfail()
that generate JSON that the login Javascript code can use to appropriately display success or error messages.
To summarize, the typical flow would be
-
click the link to display the login form
-
enter authentication details and click Login
-
the form is submitted using an Ajax request
-
if the authentication succeeds:
-
a redirect to
/login/ajaxSuccess
occurs (this URL is configurable) -
the rendered response is JSON and it contains two values, a boolean value
success
with the valuetrue
and a string valueusername
with the authenticated user’s login name -
the client determines that the login was successful and updates the page to indicate the the user is logged in; this is necessary since there’s no page redirect like there would be for a non-Ajax login
-
-
if the authentication fails:
-
a redirect to
/login/authfail?ajax=true
occurs (this URL is configurable) -
the rendered response is JSON and it contains one value, a string value
error
with the displayable error message; this will be different depending on why the login was unsuccessful (bad username or password, account locked, etc.) -
the client determines that the login was not successful and displays the error message
-
-
note that both a successful and an unsuccessful login will trigger the
onSuccess
Ajax callback; theonError
callback will only be triggered if there’s an exception or network issue
9. Authentication Providers
The plugin registers authentication providers that perform authentication by implementing the AuthenticationProvider interface.
Property | Default Value | Meaning |
---|---|---|
providerNames |
|
Bean names of authentication providers |
Use daoAuthenticationProvider
to authenticate using the User and Role database tables, rememberMeAuthenticationProvider
to log in with a rememberMe cookie, and anonymousAuthenticationProvider
to create an “anonymous” authentication if no other provider authenticates.
To customize this list, you define a providerNames
attribute with a list of bean names. The beans must be declared either by the plugin, or yourself in resources.groovy
. Suppose you have a custom MyAuthenticationProvider
in resources.groovy
:
resources.groovy
import com.foo.MyAuthenticationProvider
beans = {
myAuthenticationProvider(MyAuthenticationProvider) {
// attributes
}
}
You register the provider in grails-app/conf/application.groovy
as:
grails.plugin.springsecurity.providerNames
grails.plugin.springsecurity.providerNames = [
'myAuthenticationProvider',
'anonymousAuthenticationProvider',
'rememberMeAuthenticationProvider']
10. Custom UserDetailsService
When you authenticate users from a database using DaoAuthenticationProvider (the default mode in the plugin if you have not enabled OpenID, LDAP, and so on), an implementation of UserDetailsService is required. This class is responsible for returning a concrete implementation of UserDetails. The plugin provides grails.plugin.springsecurity.userdetails.GormUserDetailsService
as its UserDetailsService
implementation and grails.plugin.springsecurity.userdetails.GrailsUser
(which extends Spring Security’s User) as its UserDetails
implementation.
You can extend or replace GormUserDetailsService
with your own implementation by defining a bean in grails-app/conf/spring/resources.groovy
with the same bean name, userDetailsService
. This works because application beans are configured after plugin beans and there can only be one bean for each name. The plugin uses an extension of UserDetailsService
, grails.plugin.springsecurity.userdetails.GrailsUserDetailsService
, which adds the method UserDetails loadUserByUsername(String username, boolean loadRoles)
to support use cases like in LDAP where you often infer all roles from LDAP but might keep application-specific user details in the database. Create the class in src/groovy
and not in grails-app/services
- although the interface name includes “Service”, this is just a coincidence and the bean wouldn’t benefit from being a Grails service.
In the following example, the UserDetails
and GrailsUserDetailsService
implementation adds the full name of the user domain class in addition to the standard information. If you extract extra data from your domain class, you are less likely to need to reload the user from the database. Most of your common data can be kept along with your security credentials.
This example adds in a fullName
property. Keeping the full name cached avoids hitting the database just for that lookup. GrailsUser
already adds the id
value from the domain class to so we can do a more efficient database load of the user. If all you have is the username, then you need to call User.findByUsername(principal.username)
, 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 Hibernate’s first-level and second-level caches.
There is not much to implement other than your application-specific lookup code:
MyUserDetails.groovy
package com.mycompany.myapp
import grails.plugin.springsecurity.userdetails.GrailsUser
import org.springframework.security.core.GrantedAuthority
class MyUserDetails extends GrailsUser {
final String fullName
MyUserDetails(String username, String password, boolean enabled,
boolean accountNonExpired, boolean credentialsNonExpired,
boolean accountNonLocked,
Collection<GrantedAuthority> authorities,
long id, String fullName) {
super(username, password, enabled, accountNonExpired,
credentialsNonExpired, accountNonLocked, authorities, id)
this.fullName = fullName
}
}
MyUserDetailsService.groovy
package com.mycompany.myapp
import grails.plugin.springsecurity.SpringSecurityUtils
import grails.plugin.springsecurity.userdetails.GrailsUserDetailsService
import grails.plugin.springsecurity.userdetails.NoStackUsernameNotFoundException
import grails.gorm.transactions.Transactional
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.core.userdetails.UsernameNotFoundException
class MyUserDetailsService implements GrailsUserDetailsService {
/**
* Some Spring Security classes (e.g. RoleHierarchyVoter) expect at least
* one role, so we give a user with no granted roles this one which gets
* past that restriction but doesn't grant anything.
*/
static final List NO_ROLES = [new SimpleGrantedAuthority(SpringSecurityUtils.NO_ROLE)]
UserDetails loadUserByUsername(String username, boolean loadRoles)
throws UsernameNotFoundException {
return loadUserByUsername(username)
}
@Transactional(readOnly=true, noRollbackFor=[IllegalArgumentException, UsernameNotFoundException])
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = User.findByUsername(username)
if (!user) throw new NoStackUsernameNotFoundException()
def roles = user.authorities
// or if you are using role groups:
// def roles = user.authorities.collect { it.authorities }.flatten().unique()
def authorities = roles.collect {
new SimpleGrantedAuthority(it.authority)
}
return new MyUserDetails(user.username, user.password, user.enabled,
!user.accountExpired, !user.passwordExpired,
!user.accountLocked, authorities ?: NO_ROLES, user.id,
user.firstName + " " + user.lastName)
}
}
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:
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 grails.plugin.springsecurity.userdetails.GormUserDetailsService
- the methods are all protected so you can override as needed.
This approach works with all beans defined in SpringSecurityCoreGrailsPlugin.doWithSpring()
- you can replace or subclass any of the Spring beans to provide your own functionality when the standard extension mechanisms are insufficient.
10.1. Flushing the Cached Authentication
If you store mutable data in your custom UserDetails
implementation (such as full name in the preceding example), be sure to rebuild the Authentication
if it changes. springSecurityService
has a reauthenticate
method that does this for you:
reauthenticate()
after making a change that affects the cached authenticationclass MyController {
def springSecurityService
def someAction() {
def user = ...
// update user data
user.save()
springSecurityService.reauthenticate user.username
...
}
}
11. Password and Account Protection
The sections that follow discuss approaches to protecting passwords and user accounts.
11.1. Password Hashing
By default the plugin uses the bcrypt algorithm to hash passwords. You can customize this with the grails.plugin.springsecurity.password.algorithm
attribute as described below. In addition you can increase the security of your passwords by adding a salt, which can be a property of the UserDetails
instance, a global static value, or any custom value you want.
bcrypt is a much more secure alternative to the message digest approaches since it supports a customizable work level which when increased takes more computation time to hash the users' passwords, but also dramatically increases the cost of brute force attacks. Given how easy it is to use GPUs to crack passwords, you should definitely consider using bcrypt for new projects and switching to it for existing projects. Note that due to the approach used by bcrypt, you cannot add an additional salt like you can with the message digest algorithms.
Enable bcrypt by using the 'bcrypt'
value for the algorithm
config attribute:
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.
Property | Default | Description |
---|---|---|
password.algorithm |
“bcrypt” |
passwordEncoder algorithm; “bcrypt” to use bcrypt, “pbkdf2” to use PBKDF2, or any message digest algorithm that is supported in your JDK |
password.encodeHashAsBase64 |
|
If |
password.bcrypt.logrounds |
|
the number of rekeying rounds to use when using bcrypt |
password.hash.iterations |
|
the number of iterations which will be executed on the hashed password/salt when using a message digest algorithm |
The bcrypt logrounds and iterations are set to a lower number to improve speed while testing.
If you rely on them to be higher, set them manually when testing.
|
11.2. Salted Passwords
The Spring Security plugin uses hashed passwords and a digest algorithm that you specify. For enhanced protection against dictionary attacks, you should use a salt in addition to digest hashing.
Note that if you use bcrypt (the default setting) or pbkdf2, do not configure a salt (e.g. the |
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.
11.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 grails.plugin.springsecurity.userdetails.GrailsUser
, which extends the standard Spring Security User class and not your “person” domain class. This limits the available properties unless you use a custom UserDetailsService (Custom UserDetailsService).
As long as the username does not change, this approach works well for the salt. If you choose a property that the user can change, the user cannot log in again after changing it unless you re-hash the password with the new value. So it’s best to use a property that doesn’t change.
Another option is to generate a random salt when creating users and store this in the database by adding a new property to the “person” class. This approach requires a custom UserDetailsService
because you need a custom UserDetails
implementation that also has a “salt” property, but this is more flexible and works in cases where users can change their username.
11.2.2. SystemWideSaltSource and Custom SaltSource
Spring Security supplies a simple SaltSource
implementation, SystemWideSaltSource, which uses the same salt for each user. It’s less robust than using a different value for each user but still better than no salt at all.
An example override of the salt source bean using SystemWideSaltSource would look like this:
SystemWideSaltSource
as the saltSource
bean in application.groovy
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 plugin’s implementation with your own by defining a bean in grails-app/conf/spring/resources.groovy
with the name saltSource
:
saltSource
bean in application.groovy
import com.foo.bar.MySaltSource
beans = {
saltSource(MySaltSource) {
// set properties
}
}
11.2.3. Hashing Passwords
Regardless of the implementation, you need to be aware of what value to use for a salt when creating or updating users, for example, in a save
or update
action in a UserController
. When hashing the password, use the two-parameter version of springSecurityService.encodePassword()
:
class UserController {
def springSecurityService
def save(User user) {
user.password = springSecurityService.encodePassword(
params.password, user.username)
if (!user.save(flush: true)) {
render view: 'create', model: [userInstance: user]
return
}
flash.message = "The user was created"
redirect action: show, id: user.id
}
def update(User user) {
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
}
flash.message = "The user was updated"
redirect action: show, id: user.id
}
}
If you are encoding the password in the User domain class (using |
11.3. Account Locking and Forcing Password Change
Spring Security supports four ways of disabling a user account. When you attempt to log in, the UserDetailsService
implementation creates an instance of UserDetails
that uses these accessor methods:
-
isAccountNonExpired()
-
isAccountNonLocked()
-
isCredentialsNonExpired()
-
isEnabled()
If you use the s2-quickstart script to create a user domain class, it creates a class with corresponding properties to manage this state.
When an accessor returns true
for accountExpired
, accountLocked
, or passwordExpired
or returns false
for enabled
, a corresponding exception is thrown:
Accessor | Property | Exception |
---|---|---|
|
|
|
|
|
|
|
|
|
|
|
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:
grails.plugin.springsecurity.failureHandler.exceptionMappings
configurationimport org.springframework.security.authentication.LockedException
import org.springframework.security.authentication.DisabledException
import org.springframework.security.authentication.AccountExpiredException
import 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:
Property | Default |
---|---|
errors.login.disabled |
“Sorry, your account is disabled.” |
errors.login.expired |
“Sorry, your account has expired.” |
errors.login.passwordExpired |
“Sorry, your password has expired.” |
errors.login.locked |
“Sorry, your account is locked.” |
errors.login.fail |
“Sorry, we were not able to find a user with that username and password.” |
You can customize these messages by setting the corresponding property in application.groovy
, for example:
grails.plugin.springsecurity.errors.login.locked = "None shall pass."
You can use this functionality to manually lock a user’s account or expire the password, but you can automate the process. For example, use the Quartz plugin to periodically expire everyone’s password and force them to go to a page where they update it. Keep track of the date when users change their passwords and use a Quartz job to expire their passwords once the password is older than a fixed max age.
Here’s an example for a password expired workflow. You’d need a simple action to display a password reset form (similar to the login form):
passwordExpired()
controller actiondef passwordExpired() {
[username: session['SPRING_SECURITY_LAST_USERNAME']]
}
and the form would look something like this:
<div id='login'>
<div class='inner'>
<g:if test='${flash.message}'>
<div class='login_message'>${flash.message}</div>
</g:if>
<div class='fheader'>Please update your password..</div>
<g:form action='updatePassword' id='passwordResetForm' class='cssform' autocomplete='off'>
<p>
<label for='username'>Username</label>
<span class='text_'>${username}</span>
</p>
<p>
<label for='password'>Current Password</label>
<g:passwordField name='password' class='text_' />
</p>
<p>
<label for='password'>New Password</label>
<g:passwordField name='password_new' class='text_' />
</p>
<p>
<label for='password'>New Password (again)</label>
<g:passwordField name='password_new_2' class='text_' />
</p>
<p>
<input type='submit' value='Reset' />
</p>
</g:form>
</div>
</div>
It’s important that you not allow the user to specify the username (it’s 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:
updatePassword()
controller actiondef updatePassword(String password, String password_new, String password_new_2) {
String username = session['SPRING_SECURITY_LAST_USERNAME']
if (!username) {
flash.message = 'Sorry, an error has occurred'
redirect controller: 'login', action: 'auth'
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'
render view: 'passwordExpired', model: [username: session['SPRING_SECURITY_LAST_USERNAME']]
return
}
User user = User.findByUsername(username)
if (!passwordEncoder.matches(password, user.password)) {
flash.message = 'Current password is incorrect'
render view: 'passwordExpired', model: [username: session['SPRING_SECURITY_LAST_USERNAME']]
return
}
if (passwordEncoder.matches(password_new, user.password)) {
flash.message = 'Please choose a different password from your current one'
render view: 'passwordExpired', model: [username: session['SPRING_SECURITY_LAST_USERNAME']]
return
}
user.password = password_new
user.passwordExpired = false
user.save() // if you have password constraints check them here
redirect controller: 'login', action: 'auth'
}
11.3.1. User Cache
If the cacheUsers
configuration property is set to true
, Spring Security caches UserDetails
instances to save trips to the database (the default is false
). This optimization is minor, because typically only two small queries occur during login — one to load the user, and one to load the authorities.
If you enable this feature, you must remove any cached instances after making a change that affects login. If you do not remove cached instances, even though a user’s account is locked or disabled, logins succeed because the database is bypassed. By removing the cached data, you force at trip to the database to retrieve the latest updates.
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() {
def users = User.executeQuery(
'from User u where u.passwordChangeDate <= :cutoffDate',
[cutoffDate: new Date() - 180])
for (user in users) {
// 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) {
log.error "problem expiring password for user $user.username : $e.message", e
}
}
}
}
If your application includes a dependency for
|
12. URL Properties
The table shows configurable URL-related properties.
Property | Default Value | Meaning |
---|---|---|
apf.filterProcessesUrl |
“/login/authenticate” |
Login form post URL, intercepted by Spring Security filter |
apf.usernameParameter |
“username” |
Login form username parameter |
apf.passwordParameter |
“password” |
Login form password parameter |
apf.allowSessionCreation |
|
Whether to allow authentication to create an HTTP session |
apf.postOnly |
|
Whether to allow only POST login requests |
apf.continueChainBefore SuccessfulAuthentication |
|
whether to continue calling subsequent filters in the filter chain |
apf.storeLastUsername |
|
Whether to store the login username in the HTTP session |
failureHandler.defaultFailureUrl |
“/login/authfail?login_error=1” |
Redirect URL for failed logins |
failureHandler.ajaxAuthFailUrl |
“/login/authfail?ajax=true” |
Redirect URL for failed Ajax logins |
failureHandler.exceptionMappings |
none |
Map of exception class name (subclass of AuthenticationException) to which the URL will redirect for that exception type after authentication failure |
failureHandler.useForward |
|
Whether to render the error page ( |
failureHandler.allowSessionCreation |
|
Whether to enable session creation to store the authentication failure exception |
successHandler.defaultTargetUrl |
“/” |
Default post-login URL if there is no saved request that triggered the login |
successHandler.alwaysUseDefault |
|
If |
successHandler.targetUrlParameter |
“spring-security-redirect” |
Name of optional login form parameter that specifies destination after successful login |
successHandler.useReferer |
|
Whether to use the HTTP |
successHandler.ajaxSuccessUrl |
“/login/ajaxSuccess” |
URL for redirect after successful Ajax login |
auth.loginFormUrl |
“/login/auth” |
URL of login page |
auth.forceHttps |
|
If |
auth.ajaxLoginFormUrl |
“/login/authAjax” |
URL of Ajax login page |
auth.useForward |
|
Whether to render the login page ( |
logout.afterLogoutUrl |
“/” |
URL for redirect after logout |
logout.filterProcessesUrl |
“/logoff” |
Logout URL, intercepted by Spring Security filter |
logout.handlerNames |
|
Logout handler bean names. See Logout Handlers |
logout.clearAuthentication |
|
If |
logout.invalidateHttpSession |
|
Whether to invalidate the HTTP session when logging out |
logout.targetUrlParameter |
none |
the querystring parameter name for the post-logout URL |
logout.alwaysUseDefaultTargetUrl |
|
whether to always use the |
logout.redirectToReferer |
|
whether to use the |
logout.postOnly |
|
If |
adh.errorPage |
“/login/denied” |
Location of the 403 error page (or set to |
adh.ajaxErrorPage |
“/login/ajaxDenied” |
Location of the 403 error page for Ajax requests |
adh.useForward |
|
If |
ajaxHeader |
“X-Requested-With” |
Header name sent by Ajax library, used to detect Ajax |
ajaxCheckClosure |
none |
An optional closure that can determine if a request is Ajax |
redirectStrategy.contextRelative |
|
If |
switchUser URLs |
See Switch User, under Customizing URLs |
|
fii.alwaysReauthenticate |
|
If |
fii.rejectPublicInvocations |
|
Disallow URL access when there is no request mapping |
fii.validateConfigAttributes |
|
Whether to check that all |
fii.publishAuthorizationSuccess |
|
Whether to publish an |
fii.observeOncePerRequest |
|
If |
13. Hierarchical Roles
Hierarchical roles are a convenient way to reduce clutter in your request mappings.
Property | Default Value | Meaning |
---|---|---|
roleHierarchy |
none |
Hierarchical role definition |
roleHierarchyEntryClassName |
none |
Domain class used to manage persistent role hierarchy entries |
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 {
@Secured(['ROLE_ADMIN', 'ROLE_FINANCE_ADMIN', 'ROLE_SUPERADMIN'])
def someAction() {
...
}
}
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:
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
.
13.1. Persistent role hierarchy
Specifying a static string in the roleHierarchy
property will be sufficient for most applications, but you can also store the information in your database. This is particularly useful if you’re also storing requestmaps in the database. To use persistant storage, run the s2-create-role-hierarchy-entry script. This will create the domain class and enable persistent storage by registering its name as the roleHierarchyEntryClassName
setting in grails-app/conf/application.groovy
.
For example, running
./gradlew runCommand "-Pargs=s2-create-role-hierarchy-entry com.yourapp.RoleHierarchyEntry"
will generate this class in grails-app/domain/com/yourapp/RoleHierarchyEntry.groovy
:
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 {
private static final long serialVersionUID = 1
String entry
static constraints = {
entry blank: false, unique: true
}
static mapping = {
cache true
}
}
To store the equivalent entries for the ROLE_SUPERADMIN / ROLE_FINANCE_ADMIN / ROLE_ADMIN hierarchy, add code like this to a method in a transactional service:
RoleHierarchyEntry
instancesif (!RoleHierarchyEntry.count()) {
new RoleHierarchyEntry(entry: 'ROLE_SUPERADMIN > ROLE_FINANCE_ADMIN').save()
new RoleHierarchyEntry(entry: 'ROLE_FINANCE_ADMIN > ROLE_ADMIN').save()
}
Remember to update the roleHierarchy
beans hierarchy
definition by calling SpringSecurityService#reloadDBRoleHierarchy
, or your model changes are not reflected in the running application.
14. Switch User
To enable a user to switch from the current Authentication
to another user’s, set the useSwitchUserFilter
attribute to true
. This feature is similar to the “su” command in Unix. It enables, for example, an admin to act as a regular user to perform some actions, and then switch back.
This feature is very powerful; it allows full access to everything the switched-to user can access without requiring the user’s password. Limit who can use this feature by guarding the user switch URL with a role, for example, |
14.1. Switching to Another User
To switch to another user, typically you create a form that submits to /login/impersonate
:
<sec:ifAllGranted roles='ROLE_SWITCH_USER'>
<form action='/login/impersonate' method='POST'>
Switch to user: <input type='text' name='username'/> <br/>
<input type='submit' value='Switch'/>
</form>
</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:
controllerAnnotations.staticRules
grails.plugin.springsecurity.controllerAnnotations.staticRules = [
...
[pattern: '/login/impersonate', access: ['ROLE_SWITCH_USER', 'IS_AUTHENTICATED_FULLY']]
]
If you use Requestmap
s, create a rule like this (for example, in BootStrap
):
new Requestmap(url: '/login/impersonate',
configAttribute: 'ROLE_SWITCH_USER,IS_AUTHENTICATED_FULLY').save(flush: true)
If you use the static application.groovy
map, add the rule there:
interceptUrlMap
grails.plugin.springsecurity.interceptUrlMap = [
...
[pattern: '/login/impersonate', access: ['ROLE_SWITCH_USER', 'IS_AUTHENTICATED_FULLY']]
]
14.2. Switching Back to Original User
To resume as the original user, POST to /logout/impersonate
.
<sec:ifSwitched>
<form action='${request.contextPath}/logout/impersonate' method='POST'>
<input type='submit' value="Resume as ${grails.plugin.springsecurity.SpringSecurityUtils.switchedUserOriginalUsername}"/>
</form>
</sec:ifSwitched>
14.3. Customizing URLs
You can customize the URLs that are used for this feature, although it is rarely necessary:
grails.plugin.springsecurity.switchUser.switchUserUrl = ...
grails.plugin.springsecurity.switchUser.exitUserUrl = ...
grails.plugin.springsecurity.switchUser.targetUrl = ...
grails.plugin.springsecurity.switchUser.switchFailureUrl = ...
Property | Default | Meaning |
---|---|---|
useSwitchUserFilter |
|
Whether to use the switch user filter |
switchUser.switchUserUrl |
“/login/impersonate” |
URL to access (via POST) to switch to another user |
switchUser.exitUserUrl |
“/logout/impersonate” |
URL to access (via POST) to switch to another user |
switchUser.switchUserMatcher |
|
An alternative to |
switchUser.exitUserMatcher |
|
An alternative to |
switchUser.targetUrl |
Same as |
URL for redirect after switching |
switchUser.switchFailureUrl |
Same as |
URL for redirect after an error during an attempt to switch |
switchUser.usernameParameter |
|
The username request parameter name |
14.4. GSP Code
One approach to supporting the switch user feature is to add code to one or more of your GSP templates. In this example the current username is displayed, and if the user has switched from another (using the sec:ifSwitched
tag) then a “resume” button is displayed. If not, and the user has the required role, a form is displayed to allow input of the username to switch to:
<sec:ifLoggedIn>
Logged in as <sec:username/>
</sec:ifLoggedIn>
<sec:ifSwitched>
<form action='${request.contextPath}/logout/impersonate' method='POST'>
<input type='submit' value="Resume as ${grails.plugin.springsecurity.SpringSecurityUtils.switchedUserOriginalUsername}"/>
</form>
</sec:ifSwitched>
<sec:ifNotSwitched>
<sec:ifAllGranted roles='ROLE_SWITCH_USER'>
<form action='${request.contextPath}/login/impersonate' method='POST'>
Switch to user: <input type='text' name='username'/><br/>
<input type='submit' value='Switch'/>
</form>
</sec:ifAllGranted>
</sec:ifNotSwitched>
15. Filters
There are a few different approaches to configuring filter chains.
15.1. Default Approach to Configuring Filter Chains
The default is to use configuration attributes to determine which extra filters to use (for example, Basic Auth, Switch User, etc.) and add these to the “core” filters. For example, setting grails.plugin.springsecurity.useSwitchUserFilter = true
adds switchUserProcessingFilter
to the filter chain (and in the correct order). The filter chain built here is applied to all URLs. If you need more flexibility, you can use filterChain.chainMap
as discussed in chainMap below.
15.2. filterNames
To define custom filters, to remove a core filter from the Spring Security filter chain (not recommended), or to otherwise have control over the Spring Security filter chain, you can specify the filterNames
property as a list of strings. As with the default approach, the Spring Security filter chain built here is applied to all URLs.
For example:
grails.plugin.springsecurity.filterChain.filterNames
configurationgrails.plugin.springsecurity.filterChain.filterNames = [
'securityContextPersistenceFilter', 'logoutFilter',
'authenticationProcessingFilter', 'myCustomProcessingFilter',
'rememberMeAuthenticationFilter', 'anonymousAuthenticationFilter',
'exceptionTranslationFilter', 'filterInvocationInterceptor'
]
This example creates a Spring Security filter chain corresponding to the Spring beans with the specified names.
15.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.
grails.plugin.springsecurity.filterChain.chainMap
configurationgrails.plugin.springsecurity.filterChain.chainMap = [
[pattern: '/urlpattern1/**', filters: 'filter1,filter2,filter3,filter4'],
[pattern: '/urlpattern2/**', filters: 'filter1,filter3,filter5'],
[pattern: '/**', filters: 'JOINED_FILTERS']
]
The format of |
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.
There’s 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:
JOINED_FILTERS
in a filterChain.chainMap
configurationgrails.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.
none
in a filterChain.chainMap
configurationgrails.plugin.springsecurity.filterChain.chainMap = [
[pattern: '/someurlpattern/**', filters: 'none'],
[pattern: '/**', filters: 'JOINED_FILTERS']
]
15.4. clientRegisterFilter
An alternative to setting the filterNames
property is grails.plugin.springsecurity.SpringSecurityUtils.clientRegisterFilter()
. This property allows 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 it’s configured). You cannot register a filter in the same position as another, so it’s 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.
16. Channel Security
Use channel security to configure which URLs require HTTP and which require HTTPS.
Property | Default Value | Meaning |
---|---|---|
portMapper.httpPort |
|
HTTP port your application uses |
portMapper.httpsPort |
|
HTTPS port your application uses |
secureChannel.definition |
none |
Map of URL pattern to channel rule |
secureChannel.secureHeaderName |
|
The name of the header to check for HTTPS |
secureChannel.secureHeaderValue |
|
The header value for |
secureChannel.secureConfigAttributeKeyword |
|
The config attribute token to use for marking a pattern as requiring HTTPS. |
secureChannel.insecureHeaderName |
|
The name of the header to check for HTTP |
secureChannel.insecureHeaderValue |
|
The header value for |
secureChannel.insecureConfigAttributeKeyword |
|
The config attribute token to use for marking a pattern as requiring HTTP. |
Build a List
of single-entry Map
s under the secureChannel.definition
key, where URL patterns are stored under the key “pattern”, and the values are stored under the key “access” and are one of the access keywords REQUIRES_SECURE_CHANNEL
, REQUIRES_INSECURE_CHANNEL
, or ANY_CHANNEL
:
grails.plugin.springsecurity.secureChannel.definition
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 |
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.
16.1. Header checking
The default implementation of channel security is fairly simple; if you’re using HTTP but HTTPS is required, you get redirected to the corresponding SSL URL and vice versa. But when using a load balancer such as an F5 BIG-IP it’s not possible to just check secure/insecure. In that case you can configure the load balancer to set a request header indicating the current state. To use this approach, set the useHeaderCheckChannelSecurity
configuration property to true
and optionally change the header names or values:
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 you’re not secure, redirect to secure) and the insecure header value is “https” (i.e. if you’re 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 = '...'
17. IP Address Restrictions
Ordinarily you can guard URLs sufficiently with roles, but the plugin provides an extra layer of security with its ability to restrict by IP address.
Property | Default Value | Meaning |
---|---|---|
ipRestrictions |
none |
Map of URL patterns to IP address patterns. |
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 Map
s, 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:
grails.plugin.springsecurity.ipRestrictions
configurationgrails.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 |
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 |
18. Session Fixation Prevention
To guard against session-fixation attacks set the useSessionFixationPrevention
attribute to true
:
grails.plugin.springsecurity.useSessionFixationPrevention = true
Upon successful authentication a new HTTP session is created and the previous session’s 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 it’s 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.
Property | Default Value | Meaning |
---|---|---|
useSessionFixationPrevention |
|
Whether to use session fixation prevention |
sessionFixationPrevention.migrate |
|
Whether to copy the session attributes of the existing session to the new session after login |
sessionFixationPrevention.alwaysCreateSession |
|
Whether to always create a session even if one did not exist at the start of the request |
19. Logout Handlers
You register a list of logout handlers by implementing the LogoutHandler interface. The list is called when a user explicitly logs out.
By default, a securityContextLogoutHandler
bean is registered to clear the SecurityContextHolder. Also, unless you are using Facebook or OpenID, rememberMeServices
bean is registered to reset your cookie. (Facebook and OpenID authenticate externally so we don’t have access to the password to create a remember-me cookie.) If you are using Facebook, a facebookLogoutHandler
is registered to reset its session cookies.
To customize this list, you define a logout.handlerNames
attribute with a list of bean names.
Property | Default Value | Meaning |
---|---|---|
logout.handlerNames |
|
Logout handler bean names |
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
:
resources.groovy
import com.foo.MyLogoutHandler
beans = {
myLogoutHandler(MyLogoutHandler) {
// attributes
}
}
You register it in grails-app/conf/application.groovy
as:
grails.plugin.springsecurity.logout.handlerNames
grails.plugin.springsecurity.logout.handlerNames = [
'rememberMeServices', 'securityContextLogoutHandler', 'myLogoutHandler'
]
20. Voters
Voters are classes that implement the Spring Security AccessDecisionVoter interface and are used to confirm whether a successful authentication is authorized for the current request.
You can register the voters to use with the voterNames
setting; each element in the collection is the name of an existing Spring bean.
Property | Default Value | Meaning |
---|---|---|
voterNames |
|
Bean names of voters |
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 application’s 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'
]
21. Miscellaneous Properties
Property | Default Value | Meaning |
---|---|---|
active |
|
Whether the plugin is enabled |
printStatusMessages |
|
Whether to print status messages such as “Configuring Spring Security Core …” |
rejectIfNoRule |
|
“strict” mode where a request mapping is required for all resources; if |
anon.key |
“foo” |
anonymousProcessingFilter key |
atr.anonymousClass |
|
Anonymous token class |
useHttpSession EventPublisher |
|
If |
cacheUsers |
|
If |
useSecurity EventListener |
|
If |
dao.reflectionSaltSourceProperty |
none |
Which property to use for the reflection-based salt source. See Salted Passwords |
dao.hideUserNotFoundExceptions |
|
if |
requestCache.createSession |
|
Whether caching |
roleHierarchy |
none |
Hierarchical role definition. See Hierarchical Roles |
voterNames |
|
Bean names of voters. See Voters |
providerNames |
|
Bean names of authentication providers. See Authentication Providers |
securityConfigType |
“Annotation” |
Type of request mapping to use, one of “Annotation”, “Requestmap”, or “InterceptUrlMap” (or the corresponding enum value from |
controllerAnnotations.lowercase |
|
Whether to do URL comparisons using lowercase |
controllerAnnotations.staticRules |
none |
Extra rules that cannot be mapped using annotations |
interceptUrlMap |
none |
Request mapping definition when using “InterceptUrlMap”. See Static Map |
registerLoggerListener |
|
If |
scr.allowSessionCreation |
|
Whether to allow creating a session in the |
scr.disableUrlRewriting |
|
Whether to disable URL rewriting (and the jsessionid attribute) |
scr.springSecurityContextKey |
|
The HTTP session key to store the |
scpf.forceEagerSessionCreation |
|
Whether to eagerly create a session in the |
sch.strategyName |
|
The strategy to use for storing the |
debug.useFilter |
|
Whether to use the |
providerManager.eraseCredentialsAfterAuthentication |
|
Whether to remove the password from the |
22. Tutorials
22.1. Using Controller Annotations to Secure URLs
22.1.1. 1. Create your Grails application.
$ grails create-app bookstore $ cd bookstore
22.1.2. 2. “Install” the plugin by adding it to build.gradle
dependencies {
...
compile 'org.grails.plugins:spring-security-core:{project-version}'
...
}
Run the compile command to resolve dependencies and ensure everything is correct:
$ grails compile
22.1.3. 3. Create the User and Role domain classes.
./gradlew runCommand "-Pargs=s2-quickstart com.mycompany.myapp User Role"
You can choose your names for your domain classes and package; these are just examples.
Depending on your database, some domain class names might not be valid, especially those relating to security. Before you create names like “User” or “Group”, make sure they are not reserved keywords in your database, or escape the name with backticks in the
|
If you are using Spring Core version 3.1.2 or later and GORM 6.0.10 or later, the script creates this User class:
grails-app/domain/com/mycompany/myapp/User.groovy
package com.mycompany.myapp
import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString
import grails.compiler.GrailsCompileStatic
@GrailsCompileStatic
@EqualsAndHashCode(includes='username')
@ToString(includes='username', includeNames=true, includePackage=false)
class User implements Serializable {
private static final long serialVersionUID = 1
String username
String password
boolean enabled = true
boolean accountExpired
boolean accountLocked
boolean passwordExpired
Set<Role> getAuthorities() {
(UserRole.findAllByUser(this) as List<UserRole>)*.role as Set<Role>
}
static constraints = {
password blank: false, password: true
username blank: false, unique: true
}
static mapping = {
password column: '`password`'
}
}
and a password encoder listener to manage password encoding:
grails-app/conf/spring/resources.groovy
import com.mycompany.myapp.UserPasswordEncoderListener
// Place your Spring DSL code here
beans = {
userPasswordEncoderListener(UserPasswordEncoderListener)
}
src/main/groovy/com/mycompany/myapp/UserPasswordEncoderListener.groovy
package com.mycompany.myapp
import grails.plugin.springsecurity.SpringSecurityService
import org.grails.datastore.mapping.engine.event.AbstractPersistenceEvent
import org.grails.datastore.mapping.engine.event.PreInsertEvent
import org.grails.datastore.mapping.engine.event.PreUpdateEvent
import org.springframework.beans.factory.annotation.Autowired
import grails.events.annotation.gorm.Listener
import groovy.transform.CompileStatic
@CompileStatic
class UserPasswordEncoderListener {
@Autowired
SpringSecurityService springSecurityService
@Listener(User)
void onPreInsertEvent(PreInsertEvent event) {
encodePasswordForEvent(event)
}
@Listener(User)
void onPreUpdateEvent(PreUpdateEvent event) {
encodePasswordForEvent(event)
}
private void encodePasswordForEvent(AbstractPersistenceEvent event) {
if (event.entityObject instanceof User) {
User u = event.entityObject as User
if (u.password && ((event instanceof PreInsertEvent) || (event instanceof PreUpdateEvent && u.isDirty('password')))) {
event.getEntityAccess().setProperty('password', encodePassword(u.password))
}
}
}
private String encodePassword(String password) {
springSecurityService?.passwordEncoder ? springSecurityService.encodePassword(password) : password
}
}
Previous versions of the plugin’s script manage the password encoding directly in domain class:
grails-app/domain/com/mycompany/myapp/User.groovy
package com.mycompany.myapp
import grails.plugin.springsecurity.SpringSecurityService
import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString
import grails.compiler.GrailsCompileStatic
@GrailsCompileStatic
@EqualsAndHashCode(includes='username')
@ToString(includes='username', includeNames=true, includePackage=false)
class User implements Serializable {
private static final long serialVersionUID = 1
SpringSecurityService springSecurityService
String username
String password
boolean enabled = true
boolean accountExpired
boolean accountLocked
boolean passwordExpired
Set<Role> getAuthorities() {
(UserRole.findAllByUser(this) as List<UserRole>)*.role as Set<Role>
}
def beforeInsert() {
encodePassword()
}
def beforeUpdate() {
if (isDirty('password')) {
encodePassword()
}
}
protected void encodePassword() {
password = springSecurityService?.passwordEncoder ? springSecurityService.encodePassword(password) : password
}
static transients = ['springSecurityService']
static constraints = {
password blank: false, password: true
username blank: false, unique: true
}
static mapping = {
password column: '`password`'
}
}
Service injection in GORM entities is disabled by default since Grails 3.2.8. Read documentation about Spring Autowiring of Domain Instances to learn how to turn autowire on. |
s2-quickstart
script generates this Role too:
Role.groovy
package com.mycompany.myapp
import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString
import grails.compiler.GrailsCompileStatic
@GrailsCompileStatic
@EqualsAndHashCode(includes='authority')
@ToString(includes='authority', includeNames=true, includePackage=false)
class Role implements Serializable {
private static final long serialVersionUID = 1
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.mycompany.myapp
import grails.gorm.DetachedCriteria
import groovy.transform.ToString
import org.codehaus.groovy.util.HashCodeHelper
import grails.compiler.GrailsCompileStatic
@GrailsCompileStatic
@ToString(cache=true, includeNames=true, includePackage=false)
class UserRole implements Serializable {
private static final long serialVersionUID = 1
User user
Role role
@Override
boolean equals(other) {
if (other instanceof UserRole) {
other.userId == user?.id && other.roleId == role?.id
}
}
@Override
int hashCode() {
int hashCode = HashCodeHelper.initHash()
if (user) {
hashCode = HashCodeHelper.updateHash(hashCode, user.id)
}
if (role) {
hashCode = HashCodeHelper.updateHash(hashCode, role.id)
}
hashCode
}
static UserRole get(long userId, long roleId) {
criteriaFor(userId, roleId).get()
}
static boolean exists(long userId, long roleId) {
criteriaFor(userId, roleId).count()
}
private static DetachedCriteria criteriaFor(long userId, long roleId) {
UserRole.where {
user == User.load(userId) &&
role == Role.load(roleId)
}
}
static UserRole create(User user, Role role, boolean flush = false) {
def instance = new UserRole(user: user, role: role)
instance.save(flush: flush)
instance
}
static boolean remove(User u, Role r) {
if (u != null && r != null) {
UserRole.where { user == u && role == r }.deleteAll()
}
}
static int removeAll(User u) {
u == null ? 0 : UserRole.where { user == u }.deleteAll() as int
}
static int removeAll(Role r) {
r == null ? 0 : UserRole.where { role == r }.deleteAll() as int
}
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']
}
}
}
}
}
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 plugin’s default implementation of the Spring Security |
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 you’re 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 shouldn’t be changed in most cases. However to keep things simple for this tutorial we’ll change it (using the |
The plugin has no support for CRUD actions or GSPs for your domain classes; the spring-security-ui
plugin supplies a UI for those. So for now you will create roles and users in grails-app/init/BootStrap.groovy
. (See step 7.)
22.1.4. 4. Create a controller that will be restricted by role.
$ grails create-controller com.mycompany.myapp.Secure
This command creates grails-app/controllers/com/mycompany/myapp/SecureController.groovy
. Add some output so you can verify that things are working:
SecureController.groovy
package com.mycompany.myapp
class SecureController {
def index() {
render 'Secure access only'
}
}
22.1.5. 5. Edit grails-app/init/BootStrap.groovy to add a test user.
BootStrap.groovy
package com.mycompany.myapp
import grails.gorm.transactions.Transactional
class BootStrap {
def init = {
addTestUser()
}
@Transactional
void addTestUser() {
def adminRole = new Role(authority: 'ROLE_ADMIN').save()
def testUser = new User(username: 'me', password: 'password').save()
UserRole.create testUser, adminRole
UserRole.withSession {
it.flush()
it.clear()
}
assert User.count() == 1
assert Role.count() == 1
assert UserRole.count() == 1
}
}
Some things to note about the preceding BootStrap.groovy
:
-
The example does not use a traditional GORM many-to-many mapping for the User <==> Role relationship; instead you are mapping the join table with the
UserRole
class. This performance optimization helps significantly when many users have one or more common roles. -
We explicitly flush (using
withSession
) becauseBootStrap
does not run in a transaction or OpenSessionInView.
22.1.6. 6. Start the server.
$ grails run-app
22.1.7. 7. Verify that you cannot access the page yet.
Before you secure the page, navigate to http://localhost:8080/secure to verify that you cannot access the page yet. You will be redirected to the login page, but after a successful authentication (log in with the username and password you used for the test user in BootStrap.groovy) you will see an error page:
Sorry, you're not authorized to view this page.
This is because with the default configuration, all URLs are denied unless there is an access rule specified.
22.1.8. 8. Apply the annotation.
Edit grails-app/controllers/com/mycompany/myapp/SecureController.groovy
to import the annotation class and apply the annotation to restrict (and grant) access.
SecureController.groovy
package com.mycompany.myapp
import grails.plugin.springsecurity.annotation.Secured
class SecureController {
@Secured('ROLE_ADMIN')
def index() {
render 'Secure access only'
}
}
or
SecureController.groovy
package com.mycompany.myapp
import grails.plugin.springsecurity.annotation.Secured
@Secured('ROLE_ADMIN')
class SecureController {
def index() {
render 'Secure access only'
}
}
You can annotate the entire controller or individual actions. In this case you have only one action, so you can do either.
22.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.
22.1.10. 10. Test the Remember Me functionality.
Check the checkbox, and once you’ve tested the secure page, close your browser and reopen it. Navigate again the the secure page. Because a cookie is stored, you should not need to log in again. Logout at any time by navigating to http://localhost:8080/logout.
22.1.11. 11. Create a CRUD UI.
Optionally, create a CRUD UI to work with users and roles.
Run grails generate-all for the domain classes:
$ grails generate-all com.mycompany.myapp.User
$ grails generate-all com.mycompany.myapp.Role
Since the User domain class handles password hashing, there are no changes required in the generated controllers.
Be sure to add an @Secured
annotation to both of the generated controllers to make them accessible.
23. Example Applications
Sometimes the best way to learn is by example. We have an ever-expanding list of example apps created to do just that… help you learn how to utilize the grails-spring-security-core plugin in your current application.
23.1. The Repos
A comprehensive list of example spring security apps may be found at:
23.2. The Example Apps
23.2.1. spring-security-ui
A sample Grails App which uses the Grails Spring Security UI and Spring Security Core Plugins.
The Spring Security UI plugin provides CRUD screens and other user management workflows.
23.2.2. grails-spring-security-spring-boot-actuators
A sample Grails App which secures a Spring Boot Actuator endpoint using the Spring Security Core Plugin.
Spring Boot Actuators provide ways to monitor the health and performance of your application along with other metadata information.
23.2.3. grails-ssc-mongodb
A sample Grails App which uses the Spring Security Core Plugin and MongoDB.
MongoDB is an open source, document-oriented database.
23.2.4. grails-spring-security-params
A sample Grails App which uses the Spring Security Core Plugin to demonstrate how to use a closure with the @Secured
annotation.
23.2.5. grails-spring-security-group
A sample Grails App which uses the Spring Security Core Plugin and Group Authentication as described in the documentation.
Rather than granting authorities directly to a “person”, you can create a “group”, map authorities to it, and then map a “person” to that “group”. For applications that have a one or more groups of users who need the same level of access, having one or more “group” instances makes managing changes to access levels easier.
23.2.6. grails-spring-security-hierarchical-roles
A sample Grails App which uses the Spring Security Core Plugin and Hierarchical Roles as described in the documentation.
Hierarchical roles are a convenient way to reduce clutter in your request mappings.
23.2.7. grails-spring-security-ajax
A sample Grails App which uses the Spring Security Core Plugin and Ajax Authentication as described in the documentation.
The Spring Security Core Plugin supports Ajax logins, but you need to create your own client-side code.
24. Controller Methods
The plugin registers some convenience methods into all controllers in your application. As of version 3.1.0 this is implemented by a trait that is applied to all controllers but was implemented in earlier versions by adding methods to each controller’s MetaClass
. All are accessor methods, so they can be called as methods or properties. They include:
24.1. isLoggedIn
Returns true
if there is an authenticated user.
isLoggedIn()
class MyController {
def someAction() {
if (isLoggedIn()) {
...
}
...
if (!isLoggedIn()) {
...
}
// or
if (loggedIn) {
...
}
if (!loggedIn) {
...
}
}
}
24.2. getPrincipal
Retrieves the current authenticated user’s Principal (a GrailsUser
instance unless you’ve customized this) or null
if not authenticated.
getPrincipal()
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 PersonDomainClassName.get(springSecurityService.principal.id)
(the typical way that this is often done).
getAuthenticatedUser()
class MyController {
def someAction() {
if (isLoggedIn()) {
String email = getAuthenticatedUser().email
...
}
// or
if (isLoggedIn()) {
String email = authenticatedUser.email
...
}
}
}
25. Internationalization
The plugin includes i18n messages in several languages. To customize or translate these, add messages for the following keys to your i18n resource bundle(s) for each exception:
Message | Default Value | Exception |
---|---|---|
springSecurity.errors.login.expired |
“Sorry, your account has expired.” |
|
springSecurity.errors.login.passwordExpired |
“Sorry, your password has expired.” |
|
springSecurity.errors.login.disabled |
“Sorry, your account is disabled.” |
|
springSecurity.errors.login.locked |
“Sorry, your account is locked.” |
|
springSecurity.errors.login.fail |
“Sorry, we were not able to find a user with that username and password.” |
Other exceptions |
You can customize all messages in auth.gsp and denied.gsp:
Message | 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 |
“Sorry, you’re not authorized to view this page.” |
26. Scripts
26.1. s2-quickstart
Creates a user and role class (and optionally a requestmap class) in the specified package. If you specify a role-group name with the groupClassName argument, role/group classes will also be generated. If you specify the uiOnly flag, no domain classes are created but the plugin settings are initialized (useful with LDAP, Mock, Shibboleth, etc.)
The general format is:
./gradlew runCommand "-Pargs=s2-quickstart DOMAIN_CLASS_PACKAGE USER_CLASS_NAME ROLE_CLASS_NAME [REQUESTMAP_CLASS_NAME] [--groupClassName=GROUP_CLASS_NAME]"
./gradlew runCommand "-Pargs=s2-quickstart com.yourapp User Role"
./gradlew runCommand "-Pargs=s2-quickstart com.yourapp User Role --groupClassName=RoleGroup"
./gradlew runCommand "-Pargs=s2-quickstart com.yourapp Person Authority Requestmap"
./gradlew runCommand "-Pargs=s2-quickstart --uiOnly"
-
Updates
grails-app/conf/application.groovy
with security configuration settings and creates domain classes ingrails-app/domain
unless the uiOnly flag is specified
26.2. s2-create-persistent-token
Creates a persistent token domain class for storing remember-me cookie information in the database. The general format is:
./gradlew runCommand "-Pargs=s2-create-persistent-token <classname>"
./gradlew runCommand "-Pargs=s2-create-persistent-token com.yourapp.PersistentLogin"
This creates the domain class in the specified package, and also registers the name in grails-app/conf/application.groovy
, along with enabling persistent remember-me.
26.3. s2-create-role-hierarchy-entry
Creates a persistent role hierarchy entry domain class for storing role hierarchy information in the database. The general format is:
./gradlew runCommand "-Pargs=s2-create-role-hierarchy-entry <classname>"
./gradlew runCommand "-Pargs=s2-create-role-hierarchy-entry com.yourapp.RoleHierarchyEntry"
This creates the domain class in the specified package, and also registers the name in grails-app/conf/application.groovy
, along with enabling persistent role hierarchy storage and lookup.
27. Debugging
If you need debug information, you can specify the following entries in logback.groovy:
logger 'org.springframework.security', DEBUG, ['STDOUT'], false
logger 'grails.plugin.springsecurity', DEBUG, ['STDOUT'], false