13 de Marzo de 2013, artículo original de Ian Tsai, Engineer, Potix Corporation
Spring Security es una solución bastante común para cubrir las necesidades de seguridad de una aplicación web Java, está muy extendido y probado. A pesar de esto, debido a su manera de proteger los recursos mediante la definición de patrones de URLs, no resulta intuitivo de aplicar para un desarrollador, por ejemplo sobre peticiones Ajax específicas y el mecanismo que estas conllevan.
En este artículo introduciremos cómo integrar Spring Security con ZK sin problemas mediante la construcción de una aplicación simple de ejemplo (Un sistema de ediciión y publicación de artículos simple).
Hemos utilizado Git como sistema de control de versiones y el código de este ejemplo está en Github, puedes conseguir aquí
El proyecto está basado en Mave, por lo tanto si quieres probarlo en otra versión de ZK o Spring, únicamente tienes que cambiar el numero de versión en el fichero pom.xml
Esta aplicación de ejemplo es simplemente un sistema para publicar y editar artículos que permite 3 tipos diferentes de accesos:
El siguiente artículo se basa en los requisitos de implementación de la aplicación expuesta previamente para demostrar la integración de Spring Security y ZK
Primero, veamos cómo configurar nuestro proyecto. Para usar Spring Security, tenemos que añadir una declaración de Listener y otra de Filtro en el fichero web.xml
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
/WEB-INF/applicationContext.xml
/WEB-INF/applicationContext-security.xml
</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<listener>
<listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
</listener>
<listener>
<listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class>
</listener>
<filter><!-- the filter-name must be preserved, do not change it! -->
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
Como puedes ver, a pesar de los normales Spring Context Listeners (RequestContextListener y ContextLoaderListener), también hemos declarado (HttpSessionEventPublisher y springSecurityFilterChain de Spring Security. Aquí HttpSessionEventPublisher es opcional y está diseñado para que Spring Security lleve un control detallado de las sesiones concurrentes, springSecurityFilterChain es principal gancho para que toda la funcionalidad de Spring Security funcione en nuestra aplicación, es un parámetro requerido y tiene que llamarse springSecurityFilterChain
En este proyecto, hems separado el fichero de Spring ApplicationContext.xml en 2 ficheros, el original ApplicationContext.xml es para la declaración del backend bean y service bean, y el adicional applicationContext-security.xml es únicamente para la configuración de Spring Security.
In applicationContext-security.xml, hay 2 elementos principales que tenemos que configurar, que son el <http> y el <authentication-manager>.
El elemento http del fichero de configuración le dice a Spring qué clase de resources necesitan ser securizados, qué puertos será usado por el contender para las conexiones http y https, y qué tipo de log-in utilizará nuestra aplicación web
<http auto-config="true">
<port-mappings>
<port-mapping http="8080" https="8443"/>
</port-mappings>
<intercept-url pattern="/zkau/**" access="IS_AUTHENTICATED_ANONYMOUSLY" requires-channel="any"/>
<intercept-url pattern="/login.zul" access="IS_AUTHENTICATED_ANONYMOUSLY" requires-channel="https" />
<intercept-url pattern="/newArticle.zul" access="ROLE_USER" requires-channel="https" />
<intercept-url pattern="/j_spring_security_check" access="IS_AUTHENTICATED_ANONYMOUSLY" requires-channel="https" />
<intercept-url pattern="/**" access="IS_AUTHENTICATED_ANONYMOUSLY" requires-channel="any" />
<session-management session-fixation-protection="none" />
<form-login login-page="/login.zul"
authentication-failure-url="/login.zul?login_error=1"
login-processing-url="/j_spring_security_check"/>
<logout logout-success-url="/index.zul" invalidate-session="true" />
</http>
La declaración del elemento authentication-manager está diseñada para administrar authentication-provider que es el que provee la instancia de autenticación de Spring y realizará el trabajo de autenticar. Puedes declarar multiples elementos authentication-provider para soportar diferentes orígenes de usuarios como por ejemplo un servidor remoto LDAP (LDAPAuthenProvider) o un servicio online OpenID (openidAuthenProvider), en este proyecto hemos extendido UserDetailsService del authentication-provider por defecto
<authentication-manager>
<authentication-provider user-service-ref="myUserDetailsService">
<password-encoder hash="md5" />
</authentication-provider>
</authentication-manager>
<beans:bean id="myUserDetailsService"
class="org.zkoss.demo.springsec.model.MyUserDetailsService"/>
Después de configurar Spring Security, veamos como lo usamos para proteger una llamada a una página ZUL.
En nuestro proyecto de ejemplo, NewArticle.zul es un recurso restringido que al que solo puede accer un usuario quien ya se ha logeado, es lo normal puesto que para poder postear un nuevo artículo, tenemos que saber quien es el autor. En Spring Security, para restringir un recurso solo para que acceda cierto usuario (que tiene suficientes permisos) es muy fácil, declaramos el elemento <intercept-url> dentro de <http> en applicationContext-security-xml:
<intercept-url pattern="/newArticle.zul" access="ROLE_USER" requires-channel="https" />
En este punto, el atributo pattern se utiliza para identificar la request sobre la que estamos aplicando la restricción (newArticle.zul), y si este patrón coincide con una URL, el atributo access será usado para checkear los permisos del usuario.
Como podemos ver, la petición por request mediante Spring Security es muy sencilla e intuitiva, está basada en comparar un patrón definido sobre una petición request.
Ahora, después de haber securizado newArticle.zul, la pantalla de log-in (login.zul) que se requiere para el proceso de autenticación tiene que ser implementada para que los usuarios puedan hacer log-in. A continuación veamos como implementar una página de log-in en ZUL:
<html:form id="f" name="f" action="j_spring_security_check" method="POST"
xmlns:html="native">
<grid>
<rows>
<row>User: <textbox id="u" name="j_username"/></row>
<row>Password: <textbox id="p" type="password" name="j_password"/></row>
<row spans="2">
<hbox>
<html:input type="reset" value="Reset"/>
<html:input type="submit" value="Submit Query"/>
<button type="submit" label="Accedi_trendy" mold="trendy"/>
<button type="submit" label="Accedi_os" />
</hbox>
</row>
</rows>
</grid>
</html:form>
Si un usuario anónimo pulsa el botón de nuevo artículo en la página principal, le redireccionaremos a la pantalla de login.zul
Para securizar una parte concreta de una página web, Spring Security tiene su propia taglib que provee del soporte básico para acceder a información securizada y aplicar reglas en páginas JSPs (Puedes encontrar esta implementación propia de Spring en el código fuente de spring-security-taglibs-3.1.2.RELEASE.jar por ejemplo). Y en nuestro caso, pata securizar partes de código en páginas ZUL, ZK dispone de enfoques para definir una nueva taglib. En nuestra aplicación de ejemplo, para securizar con Spring Security mediante EL hemos creado nuestra propia tablib.
Aunque ZK Developer Reference provee de una información muy detallada de lo que podemos hacer con nuestra propia taglib de ZK, aquí nos centraremos únicamente en lo que necesitamos para Spring Security.
Primero, creamos un fichero security.tld dentro de /WEB-INF/. A pesar d eque para un proyecto más serio que un ejemplo, puedes usar el classpath:metainfo/tld7config.xml como se menciona en la ZK Developer Reference
Segundo, definimos las expresiones EL en el fichero que acabamos de crear /WEB-INF/security.tld, por ejemplo:
<taglib>
<uri>http://www.zkoss.org/demo/integration/security</uri>
<description>
Methods and actions for ZK + Spring Security
</description>
<function>
<name>isAllGranted</name>
<function-class>org.zkoss.demo.springsec.SecurityUtil</function-class>
<function-signature>
boolean isAllGranted(java.lang.String authorities) {
</function-signature>
<description>
Return true if the authenticated principal is granted authorities
of ALL the specified roles.
</description>
</function>
...
Como podemos ver, el ficlero .tld neceista empezar con un elemento root: taglib con un primer elemento uri a su vez. Entonces ya puedes declarar funciones. En la declaración de una funcion, hemos mapeado un método isAllGranted() de org.zkoss.demo.springsec.SecurityUtil a nuestra función EL isAllGranted, mediante SecurityUtil, que basa su implementación en Spring Security SecurityContextHolder
Ahora, veamos como utilizar nuestro taglib personalizado basado en Spring Security en nuestro fichero ZUL
En nuestro proyecto demo, decuardo con los casos de uso anteriores, tenemos una función de "borrar artículo", que solo puede ejecutar el usuario con el rol: ROLE_EDITOR. Ahora, con nuestras taglibs personalizadas para Spring Security, podemos configurar algunos límites como estos:
<?taglib uri="/WEB-INF/security.tld" prefix="sec"?>
...
<button id="deleteBtn" label="Delete"
if="${sec:isAllGranted('ROLE_USER')}"
disabled="${not sec:isAllGranted('ROLE_EDITOR')}"/>
...
En el ejemplo de arriba, hemos usado expresiones EL personalizadas junto con el atributo disbled de ZK para proteger nos del clic del usuario, pero ZK también dispone de otros atributos que pueden aplicarse como limite de seguridad como visible y readonly(para comboboxes). Esos atributos basados en los efectos html son muy interesantes para satisfacer futuras necesidades de crecimiento en cuanto a nuevos resquisitos de seguridad o funcionales del interfaz de usuario.
Para saber más sobre cómo ecurizar la parte servidor del componente ZK, revisa el artículo Block Request for Inaccessible Widgets
En un framework Ajax como es ZK, hay 2 tipos de peticiones (requests). Hablemos pues sobre la más dificl de esta integración, es decir ¿cómo manejamos las peticiones Ajax de ZK mediante Spring Security?
Primero, podemos echar un ojo a ¿cómo proteger una petición a una página mediante programación?
A veces, podemos necesitar comprobar la seguridad desde Java y no desde el fichero ZUL, por ejemplo en nuestro accessDeniedExTest.zul, tenemos un Initiator que realiza el chequeo de seguridad en el métodopublic void doInit(Page page, Map<String, Object> args) throws Exception:
public class AccessDeniedExInit extends GenericInitiator {
public void doInit(Page page, Map<String, Object> args) throws Exception {
if(SecurityUtil.isNoneGranted("ROLE_EDITOR")){
throw new AccessDeniedException("this is a test of AccessDeniedException!");
}
}
}
La excepción AccessDeniedException es la clave para que la cadena de filtros de Spring Security lo manegen, y todo funcione correctamente durante la petición de la página (request), pero ¿qué pasa si lanzamos una Excepción en una acción (evento) de ZK y levanta un EventListener?
Sin tocar más el código, lanzar una AccessDeniedException
Para superar este tema, convertimos la petición ZK Ajax en una petición normal (page request), por lo tanto Spring Security ya es capaz de manejar la excepción AccessDeniedException como una petición de página normal. Implementar todo esto implica 3 pasos:
Primero, tenemos que decirle a ZK que queremos un control de errores personalizado para la excepción AccessDeniedException. Hacer esto es muy simple, en el fichero zk.xml añadimos el siguiente código:
<error-page>
<exception-type>org.springframework.security.access.AccessDeniedException</exception-type>
<location>security_process.zul</location>
</error-page>
El manejador de errores de ZK es muy simple, mapeamos un error en concreto a unan página .zul, desde entonces esa página ZUL será procesada según el contexto en el que suceda ese error. En nuestro fichero security_process.zul simplemente tenemos una directiva init
<?init class="org.zkoss.demo.springsec.ui.error.SpringSecurityHandleInit"?>
<zk>
<!-- DO NOTHING! -->
</zk>
Entonces en SpringSecurtyHandleInit, podemos chequear si es una petición Ajax, guardar toda la información necesaria en la sesión e indicar al client engine que nos redirija/devuelva a nuestra página ZUL security_process.zul otra vez
if(exec.isAsyncUpdate(null) ){
//STEP 1: convert Ajax Request to Page Request(Error Handling Page Request)
System.out.println(">>>> Security Process: STEP 1");
if(ex instanceof AccessDeniedException){
sess.setAttribute(VAR_DESKTOP_REQ_URI, getOriginalDesktopUri());// for login-success-url
sess.setAttribute(VAR_SPRING_SECURITY_ERROR, ex);
Executions.sendRedirect(toSecurityProcessUrl((AccessDeniedException) ex));// GOTO STEP 2 by redirection.
}else{
throw new IllegalArgumentException(
"How come an unexpected Exception type will be mapped to this handler? please correct it in your zk.xml");
}
La redirección volverá al fichero ZUL security_process.zul y como esta vez estamos dentro de una petición de página (no ajax), podemos recoger la excepción de HttpSesion y lanzarla para que la cadena de seguridad de Spring pueda capturarla y procesarla.
Exception err = (Exception) sess.getAttribute(VAR_SPRING_SECURITY_ERROR);Anora, Spring Security chequeará el origen de la petición para autenticar y autorizar o no al usuario. En el proceso de autenticación, el usuario será redireccionado a la pantalla de login.zul como hemos configurado previamente, y si consigue logearse satisfactoriamente, el usuario será redirigido a la ubicación original, donde sucedió la excepción, y en nuestro caso será security_process.zul, el cual no es la ubicación original donde se realizó la petición Ajax original que causó el error. Por lo tanto en este caso necesitamos un tercer paso para manejar correctamente esta situación.
String dtPath = (String) sess.getAttribute(VAR_DESKTOP_REQ_URI);
if(err!=null){
//STEP 2: throw Error in Error Handling Page Request.
System.out.println(">>>> Security Process: STEP 2");
sess.removeAttribute(VAR_SPRING_SECURITY_ERROR);
throw err;// we suppose Spring Security Error Filter Chain will handle this properly.
}
Volvamos a revisar el código del paso 1
sess.setAttribute(VAR_DESKTOP_REQ_URI, getOriginalDesktopUri());
En el paso 1 guardabamos la url de la petición original que se generaba, y por lo tanto podemos implementar un método de cómo recibir la url original (getOriginalDesktopUri() del siguiente modo:
private static String getOriginalDesktopUri(){
// developer may implement this part to adapt to PushState or any other Page based Framework, that might have interference to request URI.
String str = Executions.getCurrent().getDesktop().getRequestPath();
String qs = Executions.getCurrent().getDesktop().getQueryString();
System.out.println(">>>security Process: Desktop path= "+str);
return str+"?"+qs;
}
Ahora en el paso 3 lo único que hacemos es redireccinar de vuelta a la url original:
else if(dtPath!=null){
System.out.println(">>>> Security Process: STEP 3");
//STEP 3: if Spring Security Authentication was triggered at STEP 2,
//then we need STEP 3 to redirect back to original URI the very first desktop belongs to.
sess.removeAttribute(VAR_DESKTOP_REQ_URI);
exec.sendRedirect(dtPath);
}
Para más detalle, por favor echa un ojo al código fuente de la clase: SpringSecurityHandleInit
Despues de adaptar la petición Ajax de ZK a Spring Security, veamos cómo aplican los límites de seguridad a la acción del usuario en nuestro proyecto de ejemplo, primero, en uno de nuestros casos de uso donde al usuario se le permite editar un artículo si él es el autor o tiene el rol ROLE_EDITOR, si abres el botón de editar en ArticleContentViewCtrl para el fichero articleContent.zul, lo hemos implementado del siguiente modo:
@Listen("onClick=#openEditorBtn")
public void edit(){
//ownership & permission check.
if(!isOwner() && SecurityUtil.isNoneGranted("ROLE_EDITOR")){
throw new AccessDeniedException(
"The user is neither the author, nor a privileged user.");
}
ArticleEditor editor = new ArticleEditor();
editor.setParent(container);
editor.doHighlighted();
}
Como se muestra, lanzamos AccessDeniedException directamente en el event listener, y podemos hacer lo mismo en la acción del ZK View Model:
public class TestVModel {
...
@Command
@NotifyChange("fullName")
public void doChange(){
if(SecurityUtil.isNoneGranted("ROLE_EDITOR")){
throw new AccessDeniedException("you are not an editor!");
}
}
}
Si estas usando un bean de Spring con la anotación @Secured en algunos métodos, cuando el usuario no pase el chequeo de seguridad se lanzará la excepción y se manejará de la misma manera.
En este artículo, hemos visto como usar Spring Security en una aplicación web basada en ZK, recorriendo la mayoría de cosas que un desarrollador hace en este tipo de integraciones, y además un pequeño ejemplo de cómo adaptar una petición Ajax de ZK a la cadena de filtro de Spring Security (filter chain)
Este documento es un extracto de la documentación oficial del Framework ZK, traducido y ampliado por Francisco Ferri. Colaborador de Potix (creadores del Framework ZK). Si quieres contactar con él puedes hacerlo en franferri@gmail.com, en twitter @franciscoferri o en LinkedIn