Está en la página 1de 9

1

Usando JWT (JSON Web Token) para el control de


acceso con ExtJS y Spring MVC.
Requisitos previos
Para poder hacer uso de JWT en java existe la siguiente librera que ha de ser
agregado al proyecto en Spring MVC. En el repositorio, elegir la librera ms
reciente y con ms uso. https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt
Una vez agregada esta librera podemos empezar a hacer uso de este estndar
en seguridad. Si desea obtener ms informacin sobre su uso visitar el siguiente
link: https://github.com/jwtk/jjwt
Como funciona
JWT es un formato de representacin compacto de peticiones o reclamaciones
(claims), destinado a entornos con limitaciones de espacio como: HTTP
Authorization y parmetros de consulta URI, que hace uso de una URL segura
para representar peticiones a ser transferidas entre dos partes. Las peticiones en
JWT son codificadas como un objeto JSON que es usado como la carga principal
de una estructura JWS (JSON Web Signature) o como el texto plano de una
estructura JWE (JSON WEB Encrytion), permitiendo que la peticin este firmada
digitalmente o ntegramente protegida con un cdigo de autenticacin de mensaje
(MAC) o cifrada. Si desea realizar una mayor profundizacin consultar el siguiente
enlace: https://tools.ietf.org/html/rfc7519.
El proceso para autenticar las peticiones ocurre de la siguiente manera: Un cliente
cuando desea realizar una peticin debe contar con un JWT. Para poder obtener
este JWT, el usuario debe haberse autenticado inicialmente, de lo contrario todas
las peticiones realizadas posteriormente que lleguen al servidor y no cuente con
este no sern atendidas.

Ventajas
1. JWT adems ofrece soluciones a la escalabilidad de la aplicacin al evitar
almacenar datos de sesin en el servidor, que generalmente sobrecargan la
memoria y el disco.
2. La informacin puede ser accedida y validad desde cualquier aplicacin.
3. CSRF Cross-site request forgery (CSRF) es un tipo vulnerabilidad que
afecta a aplicaciones web y que se basa en explotar la confianza que los
sitios web tienen con sus usuarios. Con autenticacin tradicional, a no ser
que utilicemos tcnicas para prevenirlo, estamos expuestos a este tipo de
ataques. Con la autenticacin basada en tokens, evitamos este tipo de
problemas.
Implementacin JWT en Spring MVC.
Lo primero que se debe hacer es interceptar todas las peticiones que lleguen al
servidor por medio de un filtro. La implementacin de este filtro se encuentra en el
siguiente repositorio: https://github.com/victacora/FilterSpringMVC.
En el archivo de configuracin web.xml copiar las siguientes lneas:
<filter>
<filter-name>
org.seratic.enterprise.tgestiona.web.filter.SecurityFilter
</filter-name>
<filter-class> filter.SecurityFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>SecurityFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

Estas lneas corresponden a la configuracin del filtro, aqu se define que clase lo
implementa, y cules son las URL que deben ser interceptadas por l, en este
caso, todas. Cabe aclarar que no es posible excluir una URL en especfica
haciendo uso de este archivo de configuracin, mas adelant expondr en qu
forma se puede solucionar esta necesidad.
Al descargar el paquete del repositorio, se puede ver los siguientes archivos:

SecurityFilter.java es el ms representativo y corresponde a la implementacin de


la clase que implementa el filtro. Las dems clases son auxiliares. Esta clase es la
encargada de determinar cmo se debe obtener el token enviado dentro de los
parmetros de la peticin, y validar si este es vlido. Si este es vlido se atiende la
peticin, o se retorna un error en formato JSON para las peticiones AJAX o se
redirige al iniciar sesin. Esta clase tambin cuenta con el mtodo:
verificarRutasExcluida. Mtodo que permite excluir ciertas rutas, archivos o
acciones del proceso de validacin.
Generacin y validacin del Token JWT
Las siguientes lneas son las encargadas de validar el token enviado en los
parmetros de la peticin, y forman parte de la lgica del filtro por lo que no se
debe modificar. nicamente es mencionado para aclarar detalles al momento de
generar el token.
String idUsuario = Jwts.parser().setSigningKey(Constantes.KEY)
.parseClaimsJws(token).getBody().getSubject();

Constantes.KEY es la llave usada para firmar el token, y tambin es la misma que


se usa para su verificacin. El valor usado en este caso para Constantes.KEY es:
a3jyO5WBfiLz31MS3oYHbUP6VlF94JgE43qJR2E4Np39TSrd4f49PbAhz4FUzZbn

El valor de este key es aleatorio y puede estar conformado por cualquier valor. Si
desea
generar
una
clave
puede
utilizarse
este
enlace:
https://www.grc.com/passwords.htm
Las siguientes lneas corresponde a la generacin del token una vez el usuario ha
sido debidamente autenticado, este token es enviado al cliente y este debe
almacenarlo para poder ejecutar cualquier peticin posteriormente al servidor. El
token generado en este caso fue firmado haciendo uso del algoritmo HS256, se
estableci como subject el id del usuario, la fecha de entregado con la fecha
actual, y fecha de vencimiento del token un dia despus de haber sido generado.
El valor de Constantes.TIEMPO_EXPIRACION_TOKEN, corresponde a 20
minutos expresado en milisegundos 1200000. Este valor se recomienda que este
entre 20 minutos y 1 hora y que antes de expirar se renueve el token y as aadir
un mayor nivel de seguridad, tambin se puede ir almacenado para crear una lista
negra y evitar que se reutilice.
String token =
Jwts.builder().setSubject(String.valueOf(retorno.getCodigo())).signWith(Signatur
eAlgorithm.HS256, Constantes.KEY).setIssuedAt(new Date()).setExpiration(new
Date(Calendar.getInstance().getTimeInMillis() +
Constantes.TIEMPO_EXPIRACION_TOKEN)).compact();

Como realizar peticiones desde ExtJS


Para hacer peticiones desde ExtJS se implementaron dos listeners, estos
capturan todas las peticiones generadas desde la aplicacin hacia el servidor
antes de ser enviadas y despus de haberse completado. El primer listeners
obtiene la peticin antes de ser enviada al servidor y recupera los datos del
usuario que inicio sesin incluyendo el token que se obtuvo tras haber validado
exitosamente el usuario. Este se aade a los parmetros y se ejecuta la peticin,
cabe aclarar que aadir el token en los parmetros es una opcin, la informacin
revisada tambin sugiere su envi en el header de la peticin en el atributo
authorization, sin embargo esta aproximacin presento problemas al momento de
realizar la descarga y envi de archivos con el tipo de contenido multipart/formdata, para obtener ms detalles de este inconveniente revizar el siguiente enlace,
en File uploads:
http://cdn.sencha.com/ext/gpl/4.1.1/docs/index.html#!/api/Ext.data.Connection
Aqu estn las lneas para aadir por defecto el token en el header de todas las
peticiones.
Ext.Ajax.defaultHeaders = {
'Authorization': token
};

Una vez se ha completado la peticin, el otro listeners verifica si no ha ocurrido


algn error desde el lado del servidor relacionado con la validacin del JWT y la
sesin, en caso de haber ocurrido se informa al usuario y se le pide que inicie
sesin nuevamente.
Listerners ejecutados una vez se autentica el usuario
//Listeneres
Ext.Ajax.on('beforerequest',agregarToken);
Ext.Ajax.on('requestcomplete',verificarRespuestaServicio);

Implementacin de los mtodos asociados a los listerners


//funciones para el manejo de autenticacion de peticiones
agregarToken=function(conn, opts)
{
try
{
var store = Ext.data.StoreManager.lookup("Store_Login");
if(typeof store !=='undefined'&&store.getCount()>0){
opts.params=opts.params || {};
opts.params.token = store.getAt(0).get('token');
}

5
}
catch(e) {
//la cadena no contiene un formato valido json
console.log('Error inesperado: '+e.message);
}
};
verificarRespuestaServicio=function(conn, req, opts)
{
try{
var json = Ext.JSON.decode(req.responseText);
if (typeof json
!=='undefined'&&json.metaData!==undefined&&json.metaData===666){
showErrorAuthentication(json.message);
}
}
catch(e) {
//la cadena no contiene un formato valido json
console.log('Error inesperado: '+e.message);
}
return true;
};

function reiniciar()
{
window.onbeforeunload = null;
var store = Ext.data.StoreManager.lookup("Store_Login");
if(typeof store !=='undefined'&&store.getCount()>0){
store.removeAll();
}
Ext.Ajax.request({
url: 'usuario/cerrarSesion.action',
method :'POST',
failure: function(response,action){
try{
var json = Ext.JSON.decode(response.responseText);
}catch(e){
Ext.Msg.alert('Informacin','Ha ocurrido un error, detalles:
'+json.message);
}
}
});
Ext.Ajax.un('beforerequest',agregarToken);
Ext.Ajax.un('requestcomplete',verificarRespuestaServicio);
window.location.href = window.location.href;
}

showErrorAuthentication = function(msj)
{
Ext.Msg.show({
title: 'Error',
msg: msj,
buttons: Ext.MessageBox.OK,

6
buttonText:{
ok: "Aceptar"
},
icon: Ext.MessageBox.INFO
});
setTimeout(reiniciar, 3000);
};

Al cerrar sesin o en el archivo app.js en el evento launch se controla que los


listeners no se estn ejecutando, se elimina los datos del usuario que inicio sesin
y se detiene el intervalo que se est ejecutando peridicamente renovando el
token, en la siguiente parte se dan ms detalles.
var store = Ext.data.StoreManager.lookup("Store_Login");
if(typeof store !=='undefined'&&store.getCount()>0){
store.removeAll();
}
if(intRenovarToken!==undefined)clearInterval(intRenovarToken);
Ext.Ajax.un('beforerequest',agregarToken);
Ext.Ajax.un('requestcomplete',verificarRespuestaServicio);

Renovacin del token JWT automticamente


Atendiendo las recomendaciones propuestas aqu:
1. https://auth0.com/docs/tokens/refresh-token
2. http://stackoverflow.com/questions/26739167/jwt-json-web-token-automaticprolongation-of-expiration
Se implement el siguiente mtodo el cual se ejecuta una sola vez antes de que
se venza el token, y lo renueva automticamente sin necesidad de volver a iniciar
sesin. Esto se hace para garantizar un mayor nivel de seguridad al hacer que el
token expire en un corto tiempo:
//renovacin del token para aadir un mayor nivel de seguridad
intRenovarToken=window.setInterval(ObtenerTokenNuevo,
store.getAt(0).get('tiempoRenovacion'));

Este mtodo se ejecuta en el momento en el que usuario ha iniciado sesin, y se


llama desde un intervalo, este se llama una sola vez y enva nicamente el token
que se obtuvo al iniciar sesin. Los datos de la sesin son almacenados en el
servidor y no compromete las credenciales de autenticacin del usuario. Una vez
el token es renovado se reinicia nuevamente el intervalo.
ObtenerTokenNuevo=function(){
try
{
var store = Ext.data.StoreManager.lookup("Store_Login");
if(typeof store !=='undefined'&&store.getCount()>0)
{

7
Ext.Ajax.request({
url: 'usuario/renovarToken.action',
method :'POST',
success: function(response,action){
var result = Ext.JSON.decode(response.responseText);
if (result.success)
{
store.getAt(0).set('token',result.data.token);
store.getAt(0).set('tiempoRenovacion',result.data.tiempoRenovacion);
clearInterval(intRenovarToken);
intRenovarToken=window.setInterval(ObtenerTokenNuevo,
result.data.tiempoRenovacion);
}
},
failure: function(response,action){
try{
var json = Ext.JSON.decode(response.responseText);
}catch(e){
Ext.Msg.alert('Informacin','Ha ocurrido un error,
detalles: '+json.message);
}
}
});
}
}
catch(e) {
//la cadena no contiene un formato valido json
console.log('Error inesperado: '+e.message);
}
};

Mtodos para controlar la sesin del lado del servidor


Una vez iniciada sesin se almacena los datos del usuario en el lado del servidor
por medio de HttpSession el cual se utiliza posteriormente para poder validar la
sesin y obtener un token nuevo. Tambin existe una constante que se puede
configurar para definir el tiempo en el que se renovara nuevamente el token y la
cual
debe
ser
menor
al
tiempo
de
vencimiento
del
mismo.
Constantes.TIEMPO_RENOVACION_TOKEN
tiene
un
valor
1020000
milisegundos y equivale a 17 minutos, tiempo en el que se ejecutara nuevamente
el mtodo renovarToken para obtener uno nuevo.
@RequestMapping(value = "/validar")
public @ResponseBody
Map<String, Object> validar(@RequestParam String usuario, @RequestParam
String clave, HttpServletRequest httpServletRequest) {
try {
UsuarioAutenticacionVO u = usuarioFacade.validar(usuario, clave);
if (u != null) {
HttpSession httpSession = httpServletRequest.getSession();
httpSession.setAttribute("usuario", u);

8
}
return ExtJSReturn.mapOK(u);
} catch (ErrorWebException e) {
return ExtJSReturn.mapError(e.getMessage(), 10);
} catch (Exception e) {
log.error("validar fallo", e);
return ExtJSReturn.mapError("Error validando usuario");
}
}
/**
*
* @param idUsuario
* @return
*/
@RequestMapping(value = "/renovarToken")
public @ResponseBody
Map<String, Object> renovarToken(HttpServletRequest httpServletRequest) {
try {
String codigo = (String)
httpServletRequest.getAttribute("idUsuario");
int idUsuario = codigo != null && !codigo.equals("") ?
Integer.parseInt(codigo) : -1;
HttpSession session = httpServletRequest.getSession();
UsuarioAutenticacionVO u = (UsuarioAutenticacionVO)
session.getAttribute("usuario");
if (u != null && idUsuario != -1 && u.getCodigo() == idUsuario) {
TokenVO token = new TokenVO();
String tokenNuevo =
Jwts.builder().setSubject(String.valueOf(idUsuario)).signWith(SignatureAlgorithm
.HS256, Constantes.KEY).setIssuedAt(new Date()).setExpiration(new
Date(Calendar.getInstance().getTimeInMillis() +
Constantes.TIEMPO_EXPIRACION_TOKEN)).compact();
token.setToken("Bearer " + tokenNuevo);
token.setTiempoRenovacion(Constantes.TIEMPO_RENOVACION_TOKEN);
return ExtJSReturn.mapOK(token);
} else {
return ExtJSReturn.mapError("Error al renovar token, no se pudo
validar la sesion correctamente.", 666);
}
} catch (Exception e) {
log.error("renovarToken fallo", e);
return ExtJSReturn.mapError("Error al renovar token, no se pudo
validar la sesion correctamente.", 666);
}
}
/**
*
* @param idUsuario
* @return
*/
@RequestMapping(value = "/cerrarSesion")
public @ResponseBody
Map<String, Object> cerrarSesion(HttpServletRequest httpServletRequest) {
try {

9
HttpSession session = httpServletRequest.getSession(false);
if (session != null) {
session.invalidate();
}
return ExtJSReturn.mapOK();
} catch (Exception e) {
log.error("cerrarSesion fallo", e);
return ExtJSReturn.mapError("Error al cerrar sesion.");
}
}

Pruebas
Para probar se capturaron algunas URL, junto con sus parmetros a travs de la
consola del navegador y se utiliz Postman, el cual es una utilidad que se instala
al navegador Chrome y permite realizar peticiones en las que es posible aadir el
token de forma manual.

Consideraciones adicionales
Para hacer que JWT se mucho ms seguro se recomienda:
1. cifrar la informacin si se va incluir ms datos sensibles dentro del payload
del JWT usando la funcin PUT.
2. Trasmitir los token sobre HTTPS para prevenir ataques man-in-the-middle.
3. Asignar un tiempo de expiracin bajo de 20 minutos a 1 hora, y renovarlos
automticamente una sola vez, cuando estos estn prximos a vencer.