(Quick Reference)

22 Tutorials - Reference Documentation

Authors: Burt Beckwith, Beverley Talbott

Version: 2.0.0

22 Tutorials

22.1 Using Controller Annotations to Secure URLs

1. Create your Grails application.

$ grails create-app bookstore
$ cd bookstore

2. Install the plugin by adding it to BuildConfig.groovy

plugins {
   …
   compile ':spring-security-core:2.0.0'
}

Run the compile script to resolve the dependencies and ensure everything is correct:

$ grails compile

3. Create the User and Role domain classes.

$ grails s2-quickstart com.testapp 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 mapping block, e.g.

static mapping = {
   table '`user`'
}

The script creates this User class:

package com.testapp

import groovy.transform.EqualsAndHashCode import groovy.transform.ToString

@EqualsAndHashCode(includes='username') @ToString(includes='username', includeNames=true, includePackage=false) class User implements Serializable {

private static final long serialVersionUID = 1

transient springSecurityService

String username String password boolean enabled = true boolean accountExpired boolean accountLocked boolean passwordExpired

User(String username, String password) { this() this.username = username this.password = password }

Set<Role> getAuthorities() { UserRole.findAllByUser(this)*.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 = { username blank: false, unique: true password blank: false }

static mapping = { password column: '`password`' } }

Earlier versions of the plugin didn't include password hashing logic in the domain class, but it makes the code a lot cleaner.

and this Role class:

package com.testapp

import groovy.transform.EqualsAndHashCode import groovy.transform.ToString

@EqualsAndHashCode(includes='authority') @ToString(includes='authority', includeNames=true, includePackage=false) class Role implements Serializable {

private static final long serialVersionUID = 1

String authority

Role(String authority) { this() this.authority = 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:

package com.testapp

import grails.gorm.DetachedCriteria import groovy.transform.ToString

import org.apache.commons.lang.builder.HashCodeBuilder

@ToString(cache=true, includeNames=true, includePackage=false) class UserRole implements Serializable {

private static final long serialVersionUID = 1

User user Role role

UserRole(User u, Role r) { this() user = u role = r }

@Override boolean equals(other) { if (!(other instanceof UserRole)) { return false }

other.user?.id == user?.id && other.role?.id == role?.id }

@Override int hashCode() { def builder = new HashCodeBuilder() if (user) builder.append(user.id) if (role) builder.append(role.id) builder.toHashCode() }

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, insert: true) instance }

static boolean remove(User u, Role r, boolean flush = false) { if (u == null || r == null) return false

int rowCount = UserRole.where { user == u && role == r }.deleteAll()

if (flush) { UserRole.withSession { it.flush() } }

rowCount }

static void removeAll(User u, boolean flush = false) { if (u == null) return

UserRole.where { user == u }.deleteAll()

if (flush) { UserRole.withSession { it.flush() } } }

static void removeAll(Role r, boolean flush = false) { if (r == null) return

UserRole.where { role == r }.deleteAll()

if (flush) { UserRole.withSession { it.flush() } } }

static constraints = { role validator: { Role r, UserRole ur -> if (ur.user == null || ur.user.id == null) return boolean existing = false UserRole.withNewSession { existing = UserRole.exists(ur.user.id, r.id) } if (existing) { return 'userRole.exists' } } }

static mapping = { id composite: ['user', 'role'] version false } }

The script has edited grails-app/conf/Config.groovy and added the configuration for your domain classes. Make sure that the changes are correct.

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 UserDetailsService (which like everything in the plugin is customizable).

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/conf/BootStrap.groovy. (See step 7.)

4. Create a controller that will be restricted by role.

$ grails create-controller com.testapp.Secure

This command creates grails-app/controllers/com/testapp/ SecureController.groovy. Add some output so you can verify that things are working:

package com.testapp

class SecureController { def index() { render 'Secure access only' } }

5. Edit grails-app/conf/BootStrap.groovy to add a test user.

import com.testapp.Role
import com.testapp.User
import com.testapp.UserRole

class BootStrap {

def init = { servletContext ->

def adminRole = new Role('ROLE_ADMIN').save() def userRole = new Role('ROLE_USER').save()

def testUser = new User('me', 'password').save()

UserRole.create testUser, adminRole, true

assert User.count() == 1 assert Role.count() == 2 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 the 3-arg UserRole.create() call) because BootStrap does not run in a transaction or OpenSessionInView.

6. Start the server.

$ grails run-app

7. Before you secure the page, navigate to http://localhost:8080/bookstore/secure to verify that you cannot access see 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.

8. Edit grails-app/controllers/SecureController.groovy to import the annotation class and apply the annotation to restrict (and grant) access.

package com.testapp

import grails.plugin.springsecurity.annotation.Secured

class SecureController {

@Secured('ROLE_ADMIN') def index() { render 'Secure access only' } }

or

@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.

9. Shut down the app and run grails run-app again, and navigate again to http://localhost:8080/bookstore/secure.

This time you should again be able to see the secure page after successfully authenticating.

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 is cookie stored, you should not need to log in again. Logout at any time by navigating to http://localhost:8080/bookstore/logout.

11. Optionally, create a CRUD UI to work with users and roles.

Run grails generate-all for the domain classes:

$ grails generate-all com.testapp.User

$ grails generate-all com.testapp.Role

Since the User domain class handles password hashing, there are no changes required in the generated controllers.