Seguridad
Llegados a este punto, debemos dar un paso adelante para mejorar nuestra aplicación y que mejor que añadiéndole seguridad desde varios puntos de vista, ya que hasta el momento, nuestro método para identificar a los usuarios era demasiado simple y totalmente inseguro.
Uno de los objetivos de la seguridad en una aplicación es establecer una forma efectiva de identificar a los usuarios (autenticación), ya que se necesita saber de alguna forma que el usuario que está tratando de entrar a nuestra aplicación, es quien verdaderamente está diciéndonos. Esto se suele hacer típicamente especificando un nombre de usuario y una contraseña.
Además, otro de los objetivos hablando en términos de seguridad es saber que usuarios tienen acceso a cada una de las partes de la aplicación (control de acceso). Por ejemplo, un usuario de tipo profesor o socio no debe tener acceso a la gestión de otros usuarios.
En esta sesión, veremos como implementar la seguridad de nuestra aplicación de varias formas. En primer lugar, utilizaremos un método propio desde cero y posteriormente veremos como utilizar el plugin JSecurity.
Autenticación
El proceso de autenticación en una aplicación consiste en la identificación por parte de los usuarios para comprobar que realmente son quienes dicen ser. En este proceso también podemos incluir la parte en la que el usuario abandona la aplicación.
Hasta el momento ese proceso en nuestra aplicación consiste simplemente en seleccionar uno de los usuarios listados en un seleccionable, lo cual no es nada seguro. En primer lugar vamos a modificar esta forma de entrar en la aplicación por otra en la cual se nos solicite el nombre de usuario (login) y la contraseña (password).
En primer lugar debemos cambiar la vista correspondiente (grails-app/views/usuario/login.gsp) para indicarle la nueva forma de acceso a nuestra aplicación, que ahora quedaría así:
<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> <meta name="layout" content="main"/> <title>Login</title> </head> <body> <div class="body"> <g:if test="${flash.message}"> <div class="message"> ${flash.message} </div> </g:if> <p> Bienvenido a la aplicación Biblioteca. Por favor, identifícate. </p> <form> <br/> <label for="login">Nombre de usuario</label><br/><input type="text" maxlength="20" id="login" name="login" value="${fieldValue(bean:usuarioInstance,field:'login')}"/> <br/> <label for="login">Contraseña</label><br/><input type="password" maxlength="20" id="password" name="password"/> <br/> <div class="buttons"> <span class="button"><g:actionSubmit value="Login" action="handleLogin"/></span> </div> </form> </div> </body> </html>
Y por supuesto, también debemos modificar el controlador de la clase de dominio Usuario para que ahora se compruebe no sólo el nombre de usuario escogido sino también la contraseña. El método handleLogin() quedaría así:
def handleLogin = { def usuario = Usuario.findByLoginAndPassword(params.login,params.password) if (!usuario) { flash.message = "El usuario ${params.login} no existe" redirect(controller: 'usuario', action:'login') return } else { session.usuario = usuario redirect(controller:'operacion') } }
Si te fijas, comprobarás como simplemente hemos cambiado una línea en el controlador para que ahora se compruebe tanto el nombre de usuario como la contraseña. Sin embargo, todavía queda algo por hacer y es referente a la forma con la que la contraseña de los usuarios se almacena en la base de datos.
Hasta este momento, la contraseña de los usuarios se almacena en forma de texto plano en la tabla correspondiente de la base de datos, lo cual se puede considerar como una falta muy grave en temas de seguridad y de protección de datos, ya que muchos usuarios utilizan siempre la misma contraseña para diferentes sistemas de autenticación, con lo que si nuestro sistema es intervenido por algún agente externo, éste tendrá acceso a estas contraseñas.
Para solucionar esto, debemos encriptar la contraseña antes de insertar los datos del usuarios, lo cual habitualmente se consigue mediante técnicas hash. En Grails vamos a utilizar una técnica llamada DigestUtils que utiliza el paquete org.apache.commons.codec.digest.DigestUtils. Esta clase contiene una serie de métodos para producir diversos tipos de cadenas encriptadas:
- DigestUtils.md5(java.lang.String cadena), que produce una cadena de 16 elementos de tipo byte[]
- DigestUtils.md5Hex(java.lang.String cadena), utiliza también MD5, pero en este caso creará una cadena de 32 caracteres hexadecimales
- DigestUtils.shaHex(java.lang.String cadena), también devuelve una cadena de 32 caracteres hexadecimales pero utiliza SHA-1 como método de encriptación
Para encriptar la contraseña de los usuarios, en primer lugar debemos modificar la clase de dominio, puesto que estamos indicando que la longitud de la cadena no puede ser superior a 20 caracteres, lo que una vez encriptada la contraseña será insuficiente. Vamos a modificar este tamaño a 255.
A continuación, modificaremos los métodos correspondientes en el controlador de la clase Usuario para que a partir de ahora encripte la contraseña utilizando el método DigestUtils.md5Hex(pwd). El método save() quedaría así:
def save = { params.password = DigestUtils.md5Hex(params.password) def u = usuarioService.altaUsuario(params) if(!u.hasErrors()) { flash.message = "Usuario ${u.nombre} ${u.apellidos} created" redirect(action:show,id:u.id) } else { render(view:'create',model:[usuarioInstance:u]) } }
La modificación de la contraseña para que sea encriptada debe ser realizada también en el método update(). No debemos olvidar también importar el paquete org.apache.commons.codec.digest.DigestUtils.
Debemos modificar también el fichero BootStrap.groovy para que la contraseña sea también encriptada antes de que sea insertada en la base de datos.
def usuario1 = new Usuario(login:'frangarcia', password:DigestUtils.md5Hex('mipassword'), nombre:'Francisco José', apellidos:'García Rico',tipo:'administrador')
Por último, debemos actualizar el método handleLogin() para comprobar los datos del usuario que intenta identificarse para que la contraseña sea encriptada antes de realizar dicha comprobación. Modificamos la línea correspondiente por esta otra def usuario = Usuario.findByLoginAndPassword(params.login, DigestUtils.md5Hex(params.password)).
Ahora sí, nuestra aplicación empieza a tener mejor forma en cuanto a lo relativo a la seguridad de la misma, ya que las contraseñas de los usuarios no aparecen en texto plano.
Registro de usuarios con CAPTCHAS
El siguiente paso en nuestra aplicación va a ser añadir la posibilidad de que sean los propios usuarios quienes se registren en la aplicación. Por supuesto, este registro no permitirá a la persona que quiera registrarse seleccionar el tipo de usuario, de tal forma que los nuevos usuarios serán automáticamente del tipo socio. Además, veremos como instalar un plugin a nuestra aplicación para añadir a este registro de nuevos usuarios la utilización de la técnica de CAPTCHA para evitar el registro automatizado de nuevos usuarios.
La típica prueba consiste en que el usuario introduzca un conjunto de caracteres que se muestran en una imagen distorsionada que aparece en pantalla, que supuestamente una máquina no puede entender y que sólo un humano podrá escribir la secuencia correcta. Últimamente se han añadido otro tipo de pruebas como archivos de audio, preguntas de cálculo matemático, o de cualquier otro tipo.
Fuente: Artículo de la Wikipedia en Español
Empecemos entonces instalando un plugin que nos va a permitir realizar este tipo de comprobaciones a la hora de registrar nuevos usuarios. Grails dispone de un potente y eficaz sistema de plugins realizado en su mayoría por la, cada vez más extensa, comunidad de usuarios que facilita la ampliación de las aplicaciones.
El plugin que vamos a instalar se llama jcaptcha y para ello debemos escribir en línea de comandos grails install-plugin jcaptcha. Este plugin creará automáticamente un controlador y un servicio que posteriormente podremos utilizar para realizar las comprobaciones correspondientes.
La instalación de este plugin requiere también algunas modificaciones en el archivo de configuración grails-app/conf/Config.groovy. En este archivo debemos añadir el siguiente código para la generación de las imágenes correspondientes a los captchas.
import java.awt.Font import java.awt.Color import com.octo.captcha.service.multitype.GenericManageableCaptchaService import com.octo.captcha.engine.GenericCaptchaEngine import com.octo.captcha.image.gimpy.GimpyFactory import com.octo.captcha.component.word.wordgenerator.RandomWordGenerator import com.octo.captcha.component.image.wordtoimage.ComposedWordToImage import com.octo.captcha.component.image.fontgenerator.RandomFontGenerator import com.octo.captcha.component.image.backgroundgenerator.GradientBackgroundGenerator import com.octo.captcha.component.image.color.SingleColorGenerator import com.octo.captcha.component.image.textpaster.NonLinearTextPaster ....... jcaptchas { image = new GenericManageableCaptchaService( new GenericCaptchaEngine( new GimpyFactory( new RandomWordGenerator( "abcdefghijklmnopqrstuvwxyz1234567890" ), new ComposedWordToImage( new RandomFontGenerator( 20, // min font size 30, // max font size [new Font("Arial", 0, 10)] as Font[] ), new GradientBackgroundGenerator( 140, // width 35, // height new SingleColorGenerator(new Color(0, 60, 0)), new SingleColorGenerator(new Color(20, 20, 20)) ), new NonLinearTextPaster( 6, // minimal length of text 6, // maximal length of text new Color(0, 255, 0) ) ) ) ), 180, // minGuarantedStorageDelayInSeconds 180000 // maxCaptchaStoreSize ) }
Una vez ya tenemos instalado el plugin jcaptcha, lo siguiente que vamos a hacer es crear la vista correspondiente para el registro de los nuevos usuarios. Este registro será prácticamente idéntico a cuando un administrador añade usuarios, con lo que vamos a copiar el archivo grails-app/views/usuario/create.gsp en un nuevo archivo llamado grails-app/views/usuario/register.gsp.
En el registro de los nuevos usuarios, además del captcha correspondiente, vamos a pedirles también a quienes deseen registrarse en nuestro sistema que introduzcan una confirmación de la contraseña, algo que también es típico en este tipo de sistemas. El siguiente será el código de la parte del formulario del archivo register.gsp.
<g:form action="handleRegistration" method="post" > <div class="dialog"> <table> <tbody> <tr class="prop"> <td valign="top" class="name"> <label for="login">Login:</label> </td> <td valign="top" class="value ${hasErrors(bean:usuarioInstance,field:'login','errors')}"> <input type="text" maxlength="20" id="login" name="login" value="${fieldValue(bean:usuarioInstance,field:'login')}"/> </td> </tr> <tr class="prop"> <td valign="top" class="name"> <label for="password">Password:</label> </td> <td valign="top" class="value ${hasErrors(bean:usuarioInstance,field:'password','errors')}"> <input type="password" maxlength="20" id="password" name="password"/> </td> </tr> <tr class="prop"> <td valign="top" class="name"> <label for="password">Confirm Password:</label> </td> <td valign="top"> <input type="password" maxlength="20" id="confirm" name="confirm"/> </td> </tr> <tr class="prop"> <td valign="top" class="name"> <label for="nombre">Nombre:</label> </td> <td valign="top" class="value ${hasErrors(bean:usuarioInstance,field:'nombre','errors')}"> <input type="text" id="nombre" name="nombre" value="${fieldValue(bean:usuarioInstance,field:'nombre')}"/> </td> </tr> <tr class="prop"> <td valign="top" class="name"> <label for="apellidos">Apellidos:</label> </td> <td valign="top" class="value ${hasErrors(bean:usuarioInstance,field:'apellidos','errors')}"> <input type="text" id="apellidos" name="apellidos" value="${fieldValue(bean:usuarioInstance,field:'apellidos')}"/> </td> </tr> <tr class="prop"> <td valign="top" class="name"> <label for="tipo">Captcha:</label> </td> <td valign="top" class="value ${hasErrors(bean:usuarioInstance,field:'responseCaptcha','errors')}"> <jcaptcha:jpeg name="image" /><br> <g:textField name="responseCaptcha" value="" /><br> </td> </tr> </tbody> </table> </div> <div class="buttons"> <span class="button"><input class="save" type="submit" value="Create" /></span> </div> </g:form>
Lo siguiente que debemos hacer es añadir los correspondientes métodos en el controlador de la clase Usuario, en este caso los métodos register() y handleRegister(). Este último será quien se encargue de comprobar que la pregunta referente al captcha ha sido correctamente contestada y que la confirmación de la contraseña coincida con la contraseña. También debemos crear una variable llamada jcaptchaService en la parte superior del controlador, que será quien se encargue de comprobar la validez de la respuesta. Aquí volvemos a encontrarnos con el paradigma de Grails que indica convención sobre configuración, puesto que nombrando esta variable como jcaptchaService, nos olvidamos de más configuración. El código de estos métodos quedaría así:
def jcaptchaService ........... def register = { def usuarioInstance = new Usuario() usuarioInstance.properties = params return ['usuarioInstance':usuarioInstance] } def handleRegistration = { def usuarioInstance = new Usuario() //Proceso el captcha if (!jcaptchaService.validateResponse("image", session.id, params.responseCaptcha)){ flash.message = "El captcha no es correcto" redirect(controller:'usuario', action:'register') } else{ //Compruebo la confirmación de la contraseña if(params.password != params.confirm) { flash.message = "La confirmación de la contraseña no coincide con la contraseña" redirect(action:register) } else{ params.password = DigestUtils.md5Hex(params.password) params.tipo = "socio" usuarioInstance.properties = params if(usuarioInstance.save()) { session.usuario = usuarioInstance redirect(controller:'operacion') } else { render(view:'register',model:[usuarioInstance:usuarioInstance]) } } } }
En primer lugar comprobamos que la respuesta introducida al desafío del captcha es correcta para posteriormente chequear que la confirmación a la contraseña se ha introducido correctamente. En caso de pasar ambos tests, se guarda la información del usuario en la base de datos y se autentica directamente al usuario en la aplicación para que pueda empezar a operar sobre ella. Podemos comprobar el funcionamiento de este registro de usuarios accediendo a http://localhost:8080/biblioteca/usuario/register.
Por último, simplemente debemos mostrar a los usuarios de nuestra aplicación algún enlace donde poder registrarse. Esto se suele poner junto al enlace correspondiente a la identificación, es decir, en el encabezado de nuestra aplicación, con lo que vamos a modificar el archivo grails-app/views/common/_header.gsp para que junto a la palabra Identificarse, aparezca también un enlace para que un usuario pueda registrarse en la aplicación.
<div id="menu"> <nobr> <g:if test="${session.usuario}"> <b>${session.usuario?.nombre} ${session.usuario?.apellidos}</b> | <g:link controller="usuario" action="logout"><g:message code="encabezado.logout"/></g:link> </g:if> <g:else> <g:link controller="usuario" action="login"><g:message code="encabezado.login"/></g:link> | <g:link controller="usuario" action="register">Registrarse</g:link> </g:else> </nobr> </div>
En la siguiente imagen podemos ver como quedaría nuestra aplicación cuando un usuario desea registrarse en ella.
Control de acceso: Filtros
La segunda parte de cualquier sistema de autenticación, es saber quien puede hacer que en una aplicación. Por ejemplo, un usuario de tipo socio nunca va a poder crear nuevos usuarios en el sistema. En Grails podemos conseguir este tipo de restricciones gracias a los filtros de los controladores.
Ya en la sesión 7 vimos una primera introducción a los filtros cuando indicábamos que determinadas operaciones sólo estaban permitidas a los usuarios de tipo administrador. Ahora vamos a ampliar esta seguridad que ya añadimos en su momento.
En nuestra aplicación, sólo hay 3 lugares donde los usuarios no identificados no van a tener ningún problema en acceder. Estos lugares son la página inicio de la aplicación, la página donde se pueden registrar y por último, la página donde se deben identificar. El resto, deberán estar protegidas ante el acceso de usuarios no autorizados.
Tal y como vimos en la sesión 7, los filtros deben crearse en el directorio grails-app/conf y el nombre del filtro debe terminar por la palabra Filters. Nuestro filtro se va a llamar, por razones obvias, SecurityFilters. Este será el esqueleto de los filtros que creemos en la aplicación.
class SecurityFilters { def filters = { ...... } }
Los filtros pueden ser definidos de dos formas diferentes:
- Especificando el controlador y la acción
- Especificando la URI
Además, también vamos a poder especificar el instante en el que queremos que se ejecute el filtro:
- before, el filtro se ejecuta antes que la acción dada
- after, el filtro se ejecuta después de la acción dada
- afterView, el filtro se ejecuta después de que la vista sea renderizada
Vamos a ver como quedaría el filtro que protege del acceso no permitido a nuestra aplicación.
class SecurityFilters { def filters = { bibliotecaFilter(controller:'*', action:'*') { before = { if (!session.usuario && (controllerName.equals('usuario') && !actionName.equals('login')) && (controllerName.equals('usuario') && !actionName.equals('register')) && !controllerName.equals('jcaptcha') ){ redirect(controller:'usuario', action:'login') return false } } } } }
Con este filtro, estamos protegiendo completamente nuestra aplicación del acceso de usuarios no identificados y salvo en los casos expuestos en el filtro, el usuario será redirigido a la pantalla de identificación.
Para realizar estas comprobaciones, podemos hacer uso de las variables controllerName que nos indica el controlador al que estamos accediendo y actionName que utilizaremos para referirnos a la acción del controlador referida.
También podemos especificar filtros especificando la URI. Un ejemplo podría ser el siguiente, en donde se redirije al usuario a un nuevo proceso de registro.
otroFilter(uri:'/usuario/register') { before = { redirect(controller:'usuario', action:'registernew') } }
Aquí Grails inyecta una serie de propiedades para hacerlas accesibles en los filtros como son:
- request
- response
- session
- servletContext
- applicationContext
- params
Sin embargo, esta solución puede no ser suficiente cuando estamos desarrollando sistemas con varios perfiles de usuario y cada uno de ellos tiene una serie de operaciones permitidas y prohibidas. Por ejemplo, en nuestra aplicación un usuario de tipo administrador no debería ser capaz de realizar ninguna operación y simplemente encargarse de la gestión de los usuarios.
En la siguiente sección, vamos a ver como utilizar el plugin JSecurity para implementar un sistema de seguridad con varios perfiles, cada uno de ellos con unos permisos determinados.
JSecurity
Si necesitamos ayuda con nuestra aplicación para la creación de usuarios con determinados permisos, posiblemente la mejor solución nos la proporcione el plugin JSecurity. Este plugin implementa una solución perfecta para la creación de usuarios, permisos y perfiles y las relaciones entre estos.
En primer lugar, vamos a realizar la instalación del plugin. Para ello ejecutamos en línea de comandos grails install-plugin jsecurity y posteriormente el comando grails quick-start. Este segundo comando copiará una serie de clases de dominio, controladores y vistas en nuestra aplicación que nos servirán para realizar el proceso de autenticación en la aplicación.
Clases de dominio de JSecurity
El plugin JSecurity utilizar seis clases de dominio para realizar la implementación de su sistema de seguridad. Estas seis clases de dominio son las siguientes:
JsecUser
Esta clase es la que se encarga de la gestión de los usuarios del sistema. Los atributos de esta clase son:
Atributo | Descripción |
---|---|
username | Nombre de usuario |
passwordHash | Contraseña |
JsecRole
Esta clase especifica los roles de la aplicación. En nuestro caso necesitaríamos cuatro roles: administrador, bibliotecario, profesor y socio.
Atributo | Descripción |
---|---|
name | Nombre del rol o perfil |
JsecPermission
Esta clase se utiliza para establecer grupos de permisos. Los atributos de esta clase son:
Atributo | Descripción |
---|---|
type | Nombre para el permiso |
possibleActions | Listado de acciones posibles para el permiso separadas por comas |
JsecRolePermissionRel
Esta clase se utiliza para establecer las relaciones entre los permisos y los roles del sistema. Los atributos de esta clase son:
Atributo | Descripción |
---|---|
role | JsecRole relacionado |
permission | JsecPermission relacionado |
target | Controlador asociado con la relación |
actions | Acciones objetivo de los permisos |
JsecUserRoleRel
Esta clase se utiliza para establecer las relaciones entre los usuarios y los roles del sistema. Los atributos de esta clase son:
Atributo | Descripción |
---|---|
user | JsecUser relacionado |
role | JsecRole relacionado |
JsecUserPermissionRel
Esta clase se utiliza para establecer las relaciones entre los permisos y los usuarios del sistema. Los atributos de esta clase son:
Atributo | Descripción |
---|---|
user | JsecUser relacionado |
permission | JsecPermission relacionado |
target | Controlador asociado con la relación |
actions | Acciones objetivo de los permisos |
Carga de datos
Una vez ya conocemos las diferentes clases incluidas en la instalación del plugin JSecurity, vamos a crear algunos datos de ejemplo en la aplicación y modificar el sistema de registro para comprobar la potencia de este plugin.
En el ejemplo que vamos a desarrollar, vamos a utilizar dos tipos de usuarios, los administradores y los profesores. Para ello, vamos a utilizar el archivo BootStrap.groovy para crear algunos datos de ejemplo.
def admin = new JsecUser(username:'admin', passwordHash:DigestUtils.shaHex('password')).save() def profe = new JsecUser(username:'profesor', passwordHash:DigestUtils.shaHex('password')).save() def adminRole = new JsecRole(name:'Administrador').save() def profeRole = new JsecRole(name:'Profesor').save() def perm = new JsecPermission(type:'BasicPermission', possibleActions:'create,delete,update').save() new JsecUserRoleRel(user:admin, role:adminRole).save() new JsecUserRoleRel(user:profe, role:profeRole).save()
Con el código anterior, hemos creado dos usuarios cada uno con un rol diferente. Podemos comprobar como el sistema nos autentica correctamente si accedemos a la dirección http://localhost:8080/biblioteca/auth e intentamos entrar con los usuarios recién creados.
Lo que vamos a hacer a continuación es definir mediante filtros, que operaciones vamos a poder realizar con cada uno de esos roles. Para ello, vamos a modificar el filtro que hemos creado en el apartado anterior (SecurityFilters) para añadir nuevos filtros y eliminar los creados anteriormente. El filtro quedaría de la siguiente forma:
auth(controller: "*", action: "*") { before = { // Necesitamos acceder a estas acciones para identificarnos if (controllerName=="auth" && (actionName == "login") || (actionName=="signin")) return true // Para cualquier otra operación el usuario debe estar identificado accessControl { true } } } usuarioControl(controller: "usuario", action: "(create|edit|save|update|delete|show|list)") { before = { accessControl { role("Administrador") } } } operacionControl(controller: "operacion", action: "(create|edit|save|update|delete|show|list)") { before = { accessControl { role("Profesor") } } } libroControl(controller: "libro", action: "(create|edit|save|update|delete|show|list)") { before = { accessControl { role("Administrador") } } }
Mediante estos filtros, estamos indicando que los administradores controlarán la gestión de los usuarios y de los libros, mientras que los profesores tendrán la posibilidad de realizar operaciones. Hemos utilizado los roles creados para la asignación de los permisos necesarios.
Etiquetas GSP
El plugin JSecurity permite la utilización de una serie de etiquetas para determinadas operaciones. Estas etiquetas son las siguientes:
- isLoggedIn: comprueba si el usuario está identificado en el sistema
- isNotLoggedIn: comprueba si el usuario no está identificado en el sistema
- authenticated: etiqueta sinónima a isLoggedIn
- notAuthenticated: etiqueta sinónima a isNotLoggedIn
- user: sólo se muestra el contenido si el usuario es reconocido con la utilización de la opción Remember me
- principal: muestra el nombre de usuario del usuario registrado
- hasRole: sólo se muestra el contenido si el usuario está autenticado y además tiene un rol asignado
- lacksRole: el método inverso a hasRole
- hasPermission: sólo se muestra el contenido si el usuario está autenticado y además tiene permisos asignados
- lacksPermission: el método inverso a hasPermission
Podemos modificar el contenido de la plantilla _header.gsp para mostrar un enlace para identificarse en el sistema o indicar el nombre del usuario junto con un enlace para abandonar la aplicación. El nuevo encabezado quedaría así:
<div id="menu"> <nobr> <jsec:isLoggedIn> <div><jsec:principal/> (<g:link controller="auth" action="signOut"><g:message code="encabezado.logout"/></g:link>)</div> </jsec:isLoggedIn> <jsec:isNotLoggedIn> <div><g:link controller="auth" action="login"><g:message code="encabezado.login"/></g:link>)</div> </jsec:isNotLoggedIn> </nobr> </div>