7. Seguridad con Spring Security plugin.
En esta séptima sesión del módulo de Groovy&Grails veremos como podemos utilizar un plugin de Grails que nos permitirá descargar toda la implementación de la seguridad de nuestra aplicación en Spring Security.
Mediante este plugin podremos asignar determinados roles a usuarios y posteriormente crear algunos permisos que podrán ser asignados tanto a usuarios como a roles.
7.1. Instalación del plugin
En la introducción a Grails decíamos que, aquello que no está implementado en el núcleo de Grails, es muy probable que lo podamos encontrar entre los plugins, realizados tanto por la comunidad de usuarios como por la gente de Spring Source.
En este caso, el plugin es mantenido directamente por la gente de Spring Source, algo que nos garantiza su durabilidad y mantenimiento. Este plugin se llama spring-security-core y podemos encontrar toda la información relativa al mismo en la dirección http://grails.org/plugin/spring-security-core.
Tal y como se explica en la página del plugin, éste tiene una serie de plugins adicionales que permiten la autenticación mediante OpenID, LDAP o Kerberos (entre otras). Además, a esta lista se han añadido recientemente otros plugins que permiten la autenticación utilizando Facebook o Twitter.
El plugin de spring-security-core es uno de los mejor documentados de todos los desarrollados y podrás encontrar esta documentación en la dirección http://grails-plugins.github.io/grails-spring-security-core/guide/index.html y su autor, Burt Beckwith (@burtbeckwith) uno de los commiters más activos de la comunidad.
En Grails, para instalar cualquier plugin tenemos el comando grails install-plugin con lo que para instalar el plugin de spring-security-core únicamente deberíamos ejecutar el comando grails install-plugin spring-security-core. Pero además, podemos editar el archivo BuildConfig.groovy y añadir una nueva línea en la parte dedicada a los plugins:
plugins {
...
compile ":spring-security-core:2.0-RC6"
}
Nosotros vamos a optar por esta segunda opción puesto que quedarán contemplados todos los plugins que instalemos en nuestra aplicación en un único archivo que luego vamos a poder compartir con el resto de desarrolladores de la aplicación.
Durante la instalación del plugin se realizará automáticamente la descarga de todas las dependencias del plugin. En algunos casos, la instalación de plugins supone la creación de nuevos comandos en Grails y éste es el caso de spring-security-core, en el que, tal y como nos indican una vez finalizada la instalación nos indica que podemos ejecutar el comando grails s2-quickstart que nos servirá para inicializar el plugin y crear las clases de dominio necesarias.
El nuevo comando instalado s2-quickstart necesita de una serie de parámetros que serán los siguientes
-
paquete donde queremos crear las clases de dominio a generar
-
clase de dominio para los usuarios que habitualmente se llamará User o Person
-
clase de dominio para los roles que habitualmente se llamará Role o Authority
-
clase de dominio para el mapeo de peticiones que habitualmente se llamará RequestMap. Este parámetro es opcional puesto que este mapeo podemos hacerlo directamente en el archivo de configuración Config.groovy
Nosotros ejecutaremos el comando grails s2-quickstart es.ua.expertojava.todo Person Role RequestMap que creará cuatro nuevas clases de dominio en nuestra aplicación en el paquete es.ua.expertojava.todo que son las siguientes:
-
Person
-
PersonRole
-
Role
-
RequestMap
7.2. Configuración del plugin
Toda la configuración del plugin se añade automáticamente en el archivo de configuración Config.groovy y básicamente lo que se indica en esta configuración es el nombre de cada una de las clases necesarias para el plugin así como el tipo de seguridad implementada.
// Added by the Spring Security Core plugin:
grails.plugin.springsecurity.userLookup.userDomainClassName = 'es.ua.expertojava.todo.Person'
grails.plugin.springsecurity.userLookup.authorityJoinClassName = 'es.ua.expertojava.todo.PersonRole'
grails.plugin.springsecurity.authority.className = 'es.ua.expertojava.todo.Role'
grails.plugin.springsecurity.requestMap.className = 'es.ua.expertojava.todo.RequestMap'
grails.plugin.springsecurity.securityConfigType = 'Requestmap'
grails.plugin.springsecurity.controllerAnnotations.staticRules = [
'/': ['permitAll'],
'/index': ['permitAll'],
'/index.gsp': ['permitAll'],
'/assets/**': ['permitAll'],
'/**/js/**': ['permitAll'],
'/**/css/**': ['permitAll'],
'/**/images/**': ['permitAll'],
'/**/favicon.ico': ['permitAll']
]
Como te habrás dado cuenta, todos los parámetros de configuración empiezan por grails.plugin.springsecurity. De esta forma podemos especificar por ejemplo que tipo de algoritmo queremos para encriptar la contraseña de los usuarios de la siguiente forma
grails.plugin.springsecurity.password.algorithm='SHA-512'
7.3. Clases de dominio
Como comentábamos anteriormente, cuatro son las clases que se han creado al ejecutar el comando grails s2-quickstart, tres obligatorias (Person, Role y PersonRole) y una opcional (RequestMap). La clase PersonRole es una clase que relaciona que implementa la relación muchos-a-muchos entre la clase Person y la clase Role. Veamos cada una de estas clases en detalle:
7.3.1. Clase de dominio Person
La clase de dominio Person es la clase encargada de almacenar los datos de todos los usuarios de la aplicación. Estos datos son los típicos que desearíamos almacenar de un usuario como son el nombre de usuario y la contraseña. Además, también tiene una serie de propiedades que nos indicará si el usuario está activo o no, si la cuenta ha expirado, si la cuenta está bloqueada o si la contraseña ha expirado.
package es.ua.expertojava.todo
class Person {
transient springSecurityService
String username
String password
boolean enabled = true
boolean accountExpired
boolean accountLocked
boolean passwordExpired
static transients = ['springSecurityService']
static constraints = {
username blank: false, unique: true
password blank: false
}
static mapping = {
password column: '`password`'
}
Set<Role> getAuthorities() {
PersonRole.findAllByPerson(this).collect { it.role }
}
def beforeInsert() {
encodePassword()
}
def beforeUpdate() {
if (isDirty('password')) {
encodePassword()
}
}
protected void encodePassword() {
password = springSecurityService?.passwordEncoder ? springSecurityService.encodePassword(password) : password
}
}
Además, también se implementa un método llamado getAuthorities() que nos devolverá todos los roles que el usuario tenga asignados. Este método podíamos decir que es análogo a la definición static hasMany = [authorities: Authority] de la forma tradicional.
Esta clase podrá ser modificada a nuestro gusto para añadirle tantas propiedades como necesitemos, como por ejemplo, nombre, apellidos o email. Sin embargo, es más conveniente dejar esta clase tal cual está y extenderla utilizando una clase llamada por ejemplo User.
package es.ua.expertojava.todo
class User extends Person{
String name
String surnames
String confirmPassword
String email
Date dateOfBirth
String description
static hasMany = [todos:Todo]
static constraints = {
name(blank:false)
surnames(blank:false)
confirmPassword(blank:false, password:true)
email(blank:false, email:true)
dateOfBirth(nullable:true, validator: {
if (it?.compareTo(new Date()) < 0)
return true
return false
})
description(maxSize:1000,nullable:true)
}
static transients = ["confirmPassword"]
String toString(){
"@${username}"
}
}
Date cuenta de la relación que hemos establecido entre los usuarios y las tareas con static hasMany = [todos:Todo] con lo que habrá también que modificar la clase de domninio Todo para establecer la otra parte de la relación.
package es.ua.expertojava.todo
class Todo {
...
User user
...
}
Para facilitar la generación de usuarios desde la propia aplicación, vamos a generar las vistas y los controladores referentes a los usuarios con
grails generate-all es.ua.expertojava.todo.User
7.3.2. Clase de dominio Role
El plugin spring-security-core necesita de una clase que implemente los diferentes roles que pueden coexistir en nuestra aplicación. Habitualmente, esta clase se encarga de restringir el acceso a determinadas URLs a aquellos usuarios que tengan los permisos adecuados. Un usuario puede tener varios roles asignados que indiquen diferentes niveles de acceso a la aplicación y por lo normal, cada usuario debería tener al menos un rol asignado.
También es posible que un usuario no tenga ningún rol asignado (aunque no es muy habitual). En estos casos, cuando un usuario sin ningún rol asignado se identifica en la aplicación, automáticamente se le asigna un rol virtual llamado ROLE_NO_ROLES. Este usuario, sólo podrá acceder a aquellos recursos que no estén asegurados.
La clase Role tiene una única propiedad llamada authority de tipo String que será el nombre dado al rol. Esta propiedad debe ser única, tal y como vemos en las restricciones de la clase.
package es.ua.expertojava.todo
class Role {
String authority
static mapping = {
cache true
}
static constraints = {
authority blank: false, unique: true
}
}
7.3.3. Clase de dominio PersonRole
Para especificar la relación de muchos-a-muchos entre las clases de dominio Person y Role utilizamos una nueva clase llamada PersonRole. Esta clase tiene únicamente dos propiedades que serán las referidas a la clase de dominio Person y a la clase de dominio Role. Además, le especificaremos que la clave primaria de la clase será la composición de ambas propiedades y que no necesitamos almacenar la versión actual del objeto.
package es.ua.expertojava.todo
import org.apache.commons.lang.builder.HashCodeBuilder
class PersonRole implements Serializable {
private static final long serialVersionUID = 1
Person person
Role role
boolean equals(other) {
if (!(other instanceof PersonRole)) {
return false
}
other.person?.id == person?.id &&
other.role?.id == role?.id
}
int hashCode() {
def builder = new HashCodeBuilder()
if (person) builder.append(person.id)
if (role) builder.append(role.id)
builder.toHashCode()
}
static PersonRole get(long personId, long roleId) {
PersonRole.where {
person == Person.load(personId) &&
role == Role.load(roleId)
}.get()
}
static boolean exists(long personId, long roleId) {
PersonRole.where {
person == Person.load(personId) &&
role == Role.load(roleId)
}.count() > 0
}
static PersonRole create(Person person, Role role, boolean flush = false) {
def instance = new PersonRole(person: person, role: role)
instance.save(flush: flush, insert: true)
instance
}
static boolean remove(Person u, Role r, boolean flush = false) {
if (u == null || r == null) return false
int rowCount = PersonRole.where {
person == Person.load(u.id) &&
role == Role.load(r.id)
}.deleteAll()
if (flush) { PersonRole.withSession { it.flush() } }
rowCount > 0
}
static void removeAll(Person u, boolean flush = false) {
if (u == null) return
PersonRole.where {
person == Person.load(u.id)
}.deleteAll()
if (flush) { PersonRole.withSession { it.flush() } }
}
static void removeAll(Role r, boolean flush = false) {
if (r == null) return
PersonRole.where {
role == Role.load(r.id)
}.deleteAll()
if (flush) { PersonRole.withSession { it.flush() } }
}
static constraints = {
role validator: { Role r, PersonRole ur ->
if (ur.person == null) return
boolean existing = false
PersonRole.withNewSession {
existing = PersonRole.exists(ur.person.id, r.id)
}
if (existing) {
return 'userRole.exists'
}
}
}
static mapping = {
id composite: ['role', 'person']
version false
}
}
También se han creado una serie de métodos que facilitan la asignación de roles a usuario así como la revocación. Con esto, suponiendo que ya tenemos creados un rol y un usuario, podemos asignar o revocar un rol a un usuario de la siguiente forma:
User user = ...
Role role = ...
//Asignación de rol a un usuario
UserRole.create user, role
//Asignación de rol a un usuario indicándole el atributo flush
UserRole.create user, role, true
//Revocación de un rol a un usuario
User user = ...
Role role = ...
UserRole.remove user, role
//Revocación de un rol a un usuario indicándole el atributo flush
UserRole.remove user, role, true
7.3.4. Clase de dominio RequestMap
La clase de dominio RequestMap es una clase opcional que nos permite registrar que peticiones queremos asegurar introduciéndolas en la base de datos en lugar de tener que especificarlo en el archivo de configuración Config.groovy o mediante anotaciones (lo veremos a continuación).
Esta opción nos permite configurar estas entradas en tiempo de ejecución y podremos añadir, modificar o eliminar entradas sin tener que reiniciar la aplicación. Esta es la clase generada:
package es.ua.expertojava.todo
import org.springframework.http.HttpMethod
class RequestMap {
String url
String configAttribute
HttpMethod httpMethod
static mapping = {
cache true
}
static constraints = {
url blank: false, unique: 'httpMethod'
configAttribute blank: false
httpMethod nullable: true
}
}
El campo url será la dirección accedida a asegurar mientras que el campo configAttributeField indicará los roles permitidos para acceder a esa dirección url. También podemos especificar que método HTTP vamos a permitir. Si no lo asignamos, significará que podemos utilizar cualquier método.
7.4. Asegurar peticiones
En esta sección vamos a ver las diversas formas que nos permite el plugin de Spring Security para configurar las diferentes peticiones que necesitemos asegurar en nuestra aplicación. Básicamente, tenemos cuatro formas de hacer esto, de las cuales debemos elegir sólo una para espeficar que direcciones queremos asegurar en nuestra aplicación:
-
Anotaciones
-
Archivo de configuración Config.groovy
-
Instancias de la clase RequestMap
-
Expresiones específicas
Pero antes de ver cada una de estas formas, veamos unos cuantos aspectos interesantes. Habitualmente, las aplicaciones web son públicas con algunas páginas privadas que son las que aseguraremos. Sin embargo, si la mayor parte de nuestra aplicación es privada, podemos usar la aproximación pesimista para denegar el acceso a todas las urls que no tengan una regla configurada. Esto lo podemos hacer especificando en el archivo de configuración Config.groovy el siguiente valor:
grails.plugin.springsecurity.rejectIfNoRule = true
De esta forma, cualquier url que no tenga una regla especificada, será denegada para cualquier usuario.
Cualquier dirección que necesitemos asegurar debe tener especificada una regla. Por ejemplo, podremos proteger la url /todo/tag/** para que sólo los usuarios con el rol ROLE_ADMIN puedan crear etiquetas. Pero además, estas entradas podemos combinarlas con una serie de tokens predefinidos:
-
IS_AUTHENTICATED_ANONYMOUSLY, indica que cualquier usuario podría acceder a esa url.
-
IS_AUTHENTICATED_REMEMBERED, indica que el usuario se ha autenticado en la aplicación con la opción de remember me.
-
IS_AUTHENTICATED_FULLY, indica que el usuario se ha identificado completando el formulario de autenticación.
7.4.1. Anotaciones
Si queremos utilizar este método en nuestra aplicación, debemos especificar el siguiente parámetro en el archivo de configuración Config.groovy
grails.plugin.springsecurity.securityConfigType = "Annotation"
Podemos especificar anotaciones tanto a nivel de clase como a nivel de método. En caso de que se especifiquen reglas tanto a nivel de clase como a nivel de método, serán estas últimas las que se apliquen.
En el siguiente ejemplo especificamos la seguridad a nivel de método:
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'
}
}
Pero también podemos especificarlo a nivel de clase:
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'
}
}
En ocasiones, necesitaremos incluso definir reglas para proteger el acceso a determinados directorios, como por ejemplo, los directorios de imágenes o los archivos javascript. Para esto, debemos especificar reglas estáticas en el archivo de configuración Config.groovy:
grails.plugin.springsecurity.controllerAnnotations.staticRules = [
'/js/admin/**': ['ROLE_ADMIN'],
'/images/**': ['ROLE_ADMIN']
]
7.4.2. Archivo de configuración Config.groovy
Para poder utilizar este método, debemos especificar en el archivo de configuración Config.groovy el siguiente parámetro:
grails.plugin.springsecurity.securityConfigType = "InterceptUrlMap"
Posteriormente, lo único que debemos hacer es especificar en el archivo Config.groovy todas las reglas que necesitemos de la siguiente forma:
grails.plugin.springsecurity.interceptUrlMap = [
'/': ['permitAll'],
'/index': ['permitAll'],
'/index.gsp': ['permitAll'],
'/assets/**': ['permitAll'],
'/**/js/**': ['permitAll'],
'/**/css/**': ['permitAll'],
'/**/images/**': ['permitAll'],
'/**/favicon.ico': ['permitAll'],
'/login/**': ['permitAll'],
'/logout/**': ['permitAll'],
'/secure/**': ['ROLE_ADMIN'],
'/finance/**': ['ROLE_FINANCE', 'isFullyAuthenticated()'],
]
En este listado, es importante que especifiquemos las reglas en el orden correcto y esto se consigue siendo más específico para pasar posteriormente a ser más general. El siguiente ejemplo sería incorrecto, puesto que estaríamos dando permisos a los usuarios de tipo ROLE_ADMIN a las direcciones /secure/reallysecure/**:
'/secure/**': ['ROLE_ADMIN', 'ROLE_SUPERUSER'],
'/secure/reallysecure/**': ['ROLE_SUPERUSER']
Si un usuario de tipo ROLE_ADMIN intentase acceder a la dirección /secure/reallysecure/list se le daría acceso sin ningún problema puesto que casaría con la primera regla encontrada. La solución correcta sería la siguiente:
'/secure/reallysecure/**': ['ROLE_SUPERUSER']
'/secure/**': ['ROLE_ADMIN', 'ROLE_SUPERUSER']
7.4.3. Instancias de la clase RequestMap
Con esta forma, almacenaremos en la base de datos todas las urls que queremos proteger junto con la regla que debe cumplir, de forma muy similar a como hemos visto en el anterior apartado.
Lo primero que debemos hacer es especificar que utilizaremos este método en el archivo de configuración Config.groovy.
grails.plugin.springsecurity.securityConfigType = "Requestmap"
Posteriormente, podemos especificar estas urls junto con sus reglas en el archivo BootStrap.groovy.
for (String url in [
'/', '/index', '/index.gsp', '/**/favicon.ico',
'/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: '/j_spring_security_switch_user',
configAttribute: 'ROLE_SWITCH_USER,isFullyAuthenticated()').save()
A diferencia del método anterior, en esta ocasión no es necesario que tengamos en cuenta el orden de las reglas, ya que será el propio plugin quien se encargue de calcular que regla es la más específica.
Por defecto, las reglas especificadas de esta forma son cacheadas en la aplicación, con lo que debemos tener en cuenta que cuando creemos, modifiquemos o eliminemos una regla, deberemos actualizar la caché para que se tengan en cuenta los cambios introducidos. Esto lo podemos hacer con el método clearCachedRequestmaps() del servicio springSecurityService, tal y como vemos en el siguiente ejemplo.
class RequestmapController {
def springSecurityService
...
def save() {
def requestmapInstance = new Requestmap(params)
if (!requestmapInstance.save(flush: true)) {
render view: 'create', model: [requestmapInstance: requestmapInstance]
return
}
springSecurityService.clearCachedRequestmaps()
flash.message = "${message(code: 'default.created.message', args: [message(code: 'requestmap.label', default: 'Requestmap'), requestmapInstance.id])}"
redirect action: 'show', id: requestmapInstance.id
}
}
7.4.4. Expresiones específicas
En Spring Security podemos utilizar también Spring Expression Language (SpEL) para especificar las reglas de acceso a determinadas partes de nuestra aplicación. Mediante este lenguaje, vamos a poder ser algo más específicos. Veamos el mismo ejemplo utilizando cualquiera de los métodos que acabamos de ver. Empecemos por las anotaciones:
package com.yourcompany.yourapp
import grails.plugin.springsecurity.annotation.Secured
class SecureController {
@Secured(["hasRole('ROLE_ADMIN')"])
def someAction() {
...
}
@Secured(["authentication.name == 'ralph'"])
def someOtherAction() {
...
}
}
Sigamos con el método de almacenamiento de instancias de la clase RequestMap.
new Requestmap(url: "/secure/someAction",
configAttribute: "hasRole('ROLE_ADMIN')").save()
new Requestmap(url: "/secure/someOtherAction",
configAttribute: "authentication.name == 'ralph'").save()
Terminemos por la modificación del archivo de configuración Config.groovy:
grails.plugin.springsecurity.interceptUrlMap = [
'/secure/someAction': ["hasRole('ROLE_ADMIN')"],
'/secure/someOtherAction': ["authentication.name == 'ralph'"]
]
Aquí tienes una tabla con las expresiones que podemos utilizar:
Expresión | Descripción |
---|---|
hasRole(role) |
Devuelve cierto si el usuario tiene asignado este rol |
hasAnyRole([role1,role2]) |
Devuelve cierto si el usuario tiene asignado cualquiera de los roles pasados por parámetro |
principal |
Devuelve los datos del usuario registrado |
authentication |
Devuelve todos los datos relativos a la autenticación |
permitAll |
Siempre devuelve cierto |
denyAll |
Siempre devuelve falso |
isAnonymous() |
Devuelve cierto si el usuario actual es anónimo |
isRememberMe() |
Devuelve cierto si el usuario ha entrado con la opción remember me |
isAuthenticated() |
Devuelve cierto si el usuario no es anónimo |
isFullyAuthenticated() |
Devuelve cierto si el usuario no es anónimo o no ha entrado con la opción remember me |
request |
La petición HTTP, permitiendo expresiones como isFullyAuthenticated() && request.getMethod().equals('OPTIONS') |
La siguiente tabla muestra las equivalencias si lo hacemos de forma tradicional o mediante las expresiones SpEL.
Modo tradicional | Expresión |
---|---|
ROLE_ADMIN |
hasRole('ROLE_ADMIN') |
ROLE_USER, ROLE_ADMIN |
hasAnyRole('ROLE_USER','ROLE_ADMIN') |
ROLE_ADMIN, IS_AUTHENTICATED_FULLY |
hasRole('ROLE_ADMIN') and isFullyAuthenticated() |
IS_AUTHENTICATED_ANONYMOUSLY |
permitAll |
IS_AUTHENTICATED_REMEMBERED |
isAnonymous() or isRememberMe() |
IS_AUTHENTICATED_FULLY |
isFullyAuthenticated() |
7.4.5. Aspectos importantes
Como veremos más adelante, el plugin utiliza dos controladores para gestionar el proceso de login y logout del sistema. Estos controladores se llaman LoginController y LogoutController. Cualquier método de estos dos controladores debe ser accesible por cualquer usuario. Además, también debemos de dar acceso global a determinados recursos de nuestra aplicación.
Teniendo esto en cuenta, en función del método escogido para proteger nuestra aplicación, deberemos añadir una serie de reglas en nuestro archivo de configuración Config.groovy.
Mediante anotaciones
grails.plugin.springsecurity.securityConfigType = 'Annotation'
grails.plugin.springsecurity.controllerAnnotations.staticRules = [
'/': ['permitAll'],
'/index': ['permitAll'],
'/index.gsp': ['permitAll'],
'/assets/**': ['permitAll'],
'/**/js/**': ['permitAll'],
'/**/css/**': ['permitAll'],
'/**/images/**': ['permitAll'],
'/**/favicon.ico': ['permitAll'],
'/login/**': ['permitAll'],
'/logout/**': ['permitAll'],
'/dbconsole/**': ['permitAll']
]
Mediante instancias de la clase de dominio RequestMap
grails.plugin.springsecurity.securityConfigType = 'Requestmap'
/* Esto debe ir en el archivo BootStrap.groovy */
for (String url in [
'/', '/index', '/index.gsp', '/assets/**',
'/**/js/**', '/**/css/**', '/**/images/**',
'/**/favicon.ico', '/login/**', '/logout/**', '/dbconsole/**']) {
new RequestMap(url: url, configAttribute: 'permitAll').save()
}
Mediante reglas en el archivo Config.groovy con InterceptUrlMap
grails.plugin.springsecurity.securityConfigType = 'InterceptUrlMap'
grails.plugin.springsecurity.interceptUrlMap = [
'/': ['permitAll'],
'/index': ['permitAll'],
'/index.gsp': ['permitAll'],
'/assets/**': ['permitAll'],
'/**/js/**': ['permitAll'],
'/**/css/**': ['permitAll'],
'/**/images/**': ['permitAll'],
'/**/favicon.ico': ['permitAll'],
'/login/**': ['permitAll'],
'/logout/**': ['permitAll'],
'/dbconsole/**': ['permitAll']
]
7.5. Controladores
Con el plugin de Grails tendremos acceso a un par de controladores, que aunque en un principio no vamos a tener que modificar, está bien que conozcamos de su existencia. Estos dos controladores son LoginController y LogoutController y este es su contenido:
package grails.plugin.springsecurity
import grails.converters.JSON
import javax.servlet.http.HttpServletResponse
import org.springframework.security.access.annotation.Secured
import org.springframework.security.authentication.AccountExpiredException
import org.springframework.security.authentication.CredentialsExpiredException
import org.springframework.security.authentication.DisabledException
import org.springframework.security.authentication.LockedException
import org.springframework.security.core.context.SecurityContextHolder as SCH
import org.springframework.security.web.WebAttributes
@Secured('permitAll')
class LoginController {
/**
* Dependency injection for the authenticationTrustResolver.
*/
def authenticationTrustResolver
/**
* Dependency injection for the springSecurityService.
*/
def springSecurityService
/**
* Default action; redirects to 'defaultTargetUrl' if logged in, /login/auth otherwise.
*/
def index() {
if (springSecurityService.isLoggedIn()) {
redirect uri: SpringSecurityUtils.securityConfig.successHandler.defaultTargetUrl
}
else {
redirect action: 'auth', params: params
}
}
/**
* Show the login page.
*/
def auth() {
def config = SpringSecurityUtils.securityConfig
if (springSecurityService.isLoggedIn()) {
redirect uri: config.successHandler.defaultTargetUrl
return
}
String view = 'auth'
String postUrl = "${request.contextPath}${config.apf.filterProcessesUrl}"
render view: view, model: [postUrl: postUrl,
rememberMeParameter: config.rememberMe.parameter]
}
/**
* The redirect action for Ajax requests.
*/
def authAjax() {
response.setHeader 'Location', SpringSecurityUtils.securityConfig.auth.ajaxLoginFormUrl
response.sendError HttpServletResponse.SC_UNAUTHORIZED
}
/**
* Show denied page.
*/
def denied() {
if (springSecurityService.isLoggedIn() &&
authenticationTrustResolver.isRememberMe(SCH.context?.authentication)) {
// have cookie but the page is guarded with IS_AUTHENTICATED_FULLY
redirect action: 'full', params: params
}
}
/**
* Login page for users with a remember-me cookie but accessing a IS_AUTHENTICATED_FULLY page.
*/
def full() {
def config = SpringSecurityUtils.securityConfig
render view: 'auth', params: params,
model: [hasCookie: authenticationTrustResolver.isRememberMe(SCH.context?.authentication),
postUrl: "${request.contextPath}${config.apf.filterProcessesUrl}"]
}
/**
* Callback after a failed login. Redirects to the auth page with a warning message.
*/
def authfail() {
String msg = ''
def exception = session[WebAttributes.AUTHENTICATION_EXCEPTION]
if (exception) {
if (exception instanceof AccountExpiredException) {
msg = g.message(code: "springSecurity.errors.login.expired")
}
else if (exception instanceof CredentialsExpiredException) {
msg = g.message(code: "springSecurity.errors.login.passwordExpired")
}
else if (exception instanceof DisabledException) {
msg = g.message(code: "springSecurity.errors.login.disabled")
}
else if (exception instanceof LockedException) {
msg = g.message(code: "springSecurity.errors.login.locked")
}
else {
msg = g.message(code: "springSecurity.errors.login.fail")
}
}
if (springSecurityService.isAjax(request)) {
render([error: msg] as JSON)
}
else {
flash.message = msg
redirect action: 'auth', params: params
}
}
/**
* The Ajax success redirect url.
*/
def ajaxSuccess() {
render([success: true, username: springSecurityService.authentication.name] as JSON)
}
/**
* The Ajax denied redirect url.
*/
def ajaxDenied() {
render([error: 'access denied'] as JSON)
}
}
package grails.plugin.springsecurity
import javax.servlet.http.HttpServletResponse
import org.springframework.security.access.annotation.Secured
@Secured('permitAll')
class LogoutController {
/**
* Index action. Redirects to the Spring security logout uri.
*/
def index() {
if (!request.post && SpringSecurityUtils.getSecurityConfig().logout.postOnly) {
response.sendError HttpServletResponse.SC_METHOD_NOT_ALLOWED // 405
return
}
// TODO put any pre-logout code here
redirect uri: SpringSecurityUtils.securityConfig.logout.filterProcessesUrl // '/j_spring_security_logout'
}
}
7.6. Librerías adicionales
El plugin de Spring Security viene además con una serie de librerías accesibles en forma de etiquetas o servicio. Veamos cada uno de ellos.
7.6.1. Librería de etiquetas: SecurityTagLib
Con esta librería podremos insertar en nuestros archivos GSPs comprobaciones del tipo, ¿el usuario está identificado? o ¿tiene los permisos necesarios para ver esto?
ifLoggedIn
Muestra el contenido del cuerpo que pongamos dentro de esta etiqueta siempre que el usuario esté autenticado en el sistema.
<sec:ifLoggedIn>
Welcome Back!
</sec:ifLoggedIn>
ifNotLoggedIn
Muestra el contenido del cuerpo que pongamos dentro de esta etiqueta siempre que el usuario no esté autenticado en el sistema.
<sec:ifNotLoggedIn>
<g:link controller='login' action='auth'>
Login
</g:link>
</sec:ifNotLoggedIn>
ifAllGranted
Muestra el contenido del cuerpo que pongamos dentro de esta etiqueta siempre que el usuario tenga todos los roles pasados por parámetro.
<sec:ifAllGranted roles="ROLE_ADMIN,ROLE_SUPERVISOR">secure stuff here</sec:ifAllGranted>
ifAnyGranted
Muestra el contenido del cuerpo que pongamos dentro de esta etiqueta siempre que el usuario tenga al menos uno de los roles pasados por parámetro.
<sec:ifAnyGranted roles="ROLE_ADMIN,ROLE_SUPERVISOR">secure stuff here</sec:ifAnyGranted>
ifNotGranted
Muestra el contenido del cuerpo que pongamos dentro de esta etiqueta siempre que el usuario no tenga asignado ninguno de los roles pasados por parámetro.
<sec:ifNotGranted roles="ROLE_USER">non-user stuff here</sec:ifNotGranted>
loggedInUserInfo
Muestra el valor del campo especificado por parámetro del usuario identificado en el sistema.
<sec:loggedInUserInfo field="username"/>
username
Muestra el nombre de usuario del usuario autenticado en el sistema
<sec:ifLoggedIn>
Welcome Back <sec:username/>!
</sec:ifLoggedIn>
<sec:ifNotLoggedIn>
<g:link controller='login' action='auth'>Login</g:link>
</sec:ifNotLoggedIn>
access
Muestra el contenido del cuerpo que pongamos dentro de esta etiqueta siempre que la expresión se evalúe a cierto o el usuario tenga acceso a la url pasada por parámetro.
<sec:access expression="hasRole('ROLE_USER')">
You're a user
</sec:access>
<sec:access url="/admin/user">
<g:link controller='admin' action='user'>Manage Users</g:link>
</sec:access>
Incluso podemos especificar el controlador y la acción que queremos referenciar:
<sec:access controller='admin' action='user'>
<g:link controller='admin' action='user'>
Manage Users
</g:link>
</sec:access>
noAccess
Muestra el contenido del cuerpo que pongamos dentro de esta etiqueta siempre que la expresión se evalúe a falso o el usuario no tenga acceso a la url pasada por parámetro.
<sec:noAccess expression="hasRole('ROLE_USER')">
You're not a user
</sec:noAccess>
7.6.2. Servicio: SpringSecurityService
El servicio grails.plugin.springsecurity.SpringSecurityService ofrece algunos métodos que podremos utilizar en los controladores u otros servicios simplemente inyectándolos debidamente.
def springSecurityService
getCurrentUser()
Obtiene una instancia de las clase de dominio correspondiente al usuario autenticado en el sistema.
class SomeController {
def springSecurityService
def someAction() {
def user = springSecurityService.currentUser
...
}
}
isLoggedIn()
Comprueba si el usuario está autenticado en el sistema
class SomeController {
def springSecurityService
def someAction() {
if (springSecurityService.isLoggedIn()) {
...
}
else {
...
}
}
}
getAuthentication()
Obtiene los datos del usuario autenticado en el sistema
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
...
}
}
getPrincipal()
Obtiene los datos del usuario autenticado en el sistema
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
…
}
}
encodePassword()
Codifica la contraseña a partir del esquema de encriptación configurado. Por defecto, se utiliza el método SHA-256, pero éste se puede modificar en el archivo de configuración Config.groovy especificando el parámetro grails.plugin.springsecurity.password.algorithm, pudiendo utilizar cualquiera de los valores comentados en esta página de Java: http://docs.oracle.com/javase/7/docs/technotes/guides/security/crypto/CryptoSpec.html#Architecture.
class PersonController {
def springSecurityService
def updateAction() {
def person = Person.get(params.id)
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
}
}
updateRole()
Actualiza un rol, y en caso de que estemos utilizando el método RequestMap, también actualizará el nombre del rol en todas las instancias de la base de datos.
class RoleController {
def springSecurityService
def update() {
def roleInstance = Role.get(params.id)
if (!springSecurityService.updateRole(roleInstance, params)) {
render view: 'edit', model: [roleInstance: roleInstance]
return
}
flash.message = "The role was updated"
redirect action: show, id: roleInstance.id
}
}
deleteRole()
Elimina un rol, y en caso de que estemos utilizando el método Requestmap, también lo eliminará de aquellas instancias donde estuviera especificado.
class RoleController {
def springSecurityService
def delete() {
def roleInstance = Role.get(params.id)
try {
springSecurityService.deleteRole (roleInstance
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
}
}
}
clearCachedRequestmaps()
Este método fuerza una actualización de toda la caché referente al sistema de autenticación de nuestra aplicación. Siempre que creemos, modifiquemos o eliminemos una instancia de la clase RequestMap debemos invocar este método. En los métodos updateRole() y deleteRole() ya se llama automáticamente al método clearCachedRequestmaps() para que nosotros no tengamos que preocuparnos.
class RequestmapController {
def springSecurityService
def save() {
def requestmapInstance = new Requestmap(params)
if (!requestmapInstance.save(flush: true)) {
render view: 'create', model: [requestmapInstance: requestmapInstance]
return
}
springSecurityService.clearCachedRequestmaps()
flash.message = "Requestmap created"
redirect action: show, id: requestmapInstance.id
}
}
reauthenticate()
Este método renueva los datos de la autenticación del usuario. Esto se debe hacer después de que se actualicen los roles de un usuario.
class UserController {
def springSecurityService
def update() {
def userInstance = User.get(params.id)
params.salt = person.salt
if (params.password) {
params.password = springSecurityService.encodePassword(params.password, salt)
def salt = … // e.g. randomly generated using some utility method
params.salt = salt
}
userInstance.properties = params
if (!userInstance.save(flush: true)) {
render view: 'edit', model: [userInstance: userInstance]
return
}
if (springSecurityService.loggedIn &&
springSecurityService.principal.username == userInstance.username) {
springSecurityService.reauthenticate userInstance.username
}
flash.message = "The user was updated"
redirect action: show, id: userInstance.id
}
}
7.7. Otros aspectos interesantes de Spring Security
El plugin de Spring Security es sin duda uno de los más completos de Grails y en la documentación oficial encontrarás otros detalles del mismo. Sin embargo, aquí vamos a resaltar un par de ellos como son las restricciones por IP o la internacionalización del plugin.
7.7.1. Restricciones por IP
En ocasiones nos puede interesar proporcionar una serie de restricciones por IP de tal forma que únicamente las peticiones que vengan de esta IP podrán acceder al recurso. Esto lo debemos hacer en el archivo Config.groovy y este sería un ejemplo:
grails.plugin.springsecurity.ipRestrictions = [
'/pattern1/**': '123.234.345.456',
'/pattern2/**': '10.0.0.0/8',
'/pattern3/**': ['10.10.200.42', '10.10.200.63']
]
7.7.2. Internacionalización
El plugin de Spring Security viene traducido al inglés y al francés (entre otros idiomas), así que si necesitamos traducirlo al castellano por ejemplo, debemos crear las siguientes propiedades en el archivo de literales correspondiente:
Propiedad | Valor por defecto en inglés |
---|---|
springSecurity.errors.login.expired |
Sorry, your account has expired |
springSecurity.errors.login.passwordExpired |
Sorry, your password has expired. |
springSecurity.errors.login.disabled |
Sorry, your password 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. |
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. |
7.8. Ejercicios
7.8.1. Roles y usuarios (0.50 puntos)
En nuestra aplicación vamos a introducir dos tipos de usuarios: los administradores y los usuarios básicos. Los administradores serán capaces de crear nuevos usuarios, etiquetas y categorías mientras que los usuarios básicos simplemente podrán gestionar sus propias tareas.
Para ello introduce en el Bootstrap.groovy la creación de:
-
Dos roles (ROLE_ADMIN, ROLE_BASIC)
-
Un usuario administrador con nombre de usuario y contraseña "admin"
-
Un par de usuarios básicos con nombres de usuario y contraseñas "usuario1" y "usuario2"
-
Asigna varias tareas a ambos usuarios básicos
7.8.2. Modificar plantillas y vistas (0.25 puntos)
En su momento creábamos una plantilla llamada header.gsp que se renderiza en la parte superior de la aplicación y que básicamente pinta un enlace para que el usuario se pueda identificar en el sistema.
En este ejercicio vamos a modificar esta plantilla para especificar los enlaces correctos /login/auth para identificarse en el sistema y /logout/index para abandonar el mismo. Una cosa que debes tener en cuenta es que para abandonar el sistema, el plugin Spring Security sólo acepta el método POST, con lo que tendrás que crear un botón dentro de un formulario para enviar esta petición utilizando el método adecuado. No olvides también imprimir en la cabecera el nombre de usuario de la persona identificada en el sistema.
Por otro lado, vamos a modificar la pantalla inicial de la aplicación para mostrar el contenido de la misma en función del tipo de usuario identificado, es decir, los administradores verán varios enlaces para mantener usuarios, etiquetas y categorías mientras que los usuarios básicos sólo podrán mantener sus tareas.
7.8.3. Diferente rol, diferentes reglas (0.25 punto)
Por último, ahora que la aplicación soporta varios roles, deberemos distinguir que rol tiene acceso a que partes de la aplicación. Como comentábamos en el primer ejercicio, los administradores serán los encargados de gestionar usuarios, etiquetas y categorías, mientras que los usuarios básicos sólo podrán acceder a sus tareas. No olvides añadir reglas para los endpoints que añadíamos cuando hablamos sobre servicios REST.
Tendremos que realizar una serie de cambios en nuestra aplicación para que al crear una tarea, ésta sea asignada automáticamente al usuario identificado en el sistema. Deberemos proceder de la misma forma con el listado de las tareas para que sólo se muestren al usuario sus propias tareas.
Además, deberemos eliminar cualquier enlace presente en la aplicación que enlace a alguna parte que el usuario no deba tener acceso, como por ejemplo, la posibilidad de crear etiquetas desde la página de creación de tareas.
7.8.4. Modificar los tests unitarios (0.25 puntos)
Si volvemos a ejecutar los tests unitarios, nos daremos cuenta de que acabamos de romper aquellos que creaban tareas y ésto ha sido provocado por la introducción de la relación entre las tareas y los usuarios.
Soluciona estos problemas en los tests TodoSpec.groovy, TodoServiceSpec.groovy y TodoControllerSpec.groovy.