Third party library to Bundle OSGi on Liferay DPX/7

Technical Blogs December 20, 2017 By Ignacio Roncero Bazarra

Short explanation

If you want to convert a third party library of your portlets (modules) into a Bundle OSGi to be used by all modules, you could create a new module with your third party library and dependences and be used by your modules.

To explain this we will use JasperReport third party library. Supose that you have a module that uses JasperReport to generate PDF files. Probably you have these dependencies in the build.gradle file of your module:

/* POI */

compile group: 'org.apache.poi', name: 'poi', version: '3.15'

/* JasperReport */

compile group: 'net.sf.jasperreports', name: 'jasperreports', version: '5.6.1'

compile group: 'commons-digester', name: 'commons-digester', version: '2.1'

compile group: 'org.codehaus.groovy', name: 'groovy-all', version: '2.0.1'

compile group: 'com.lowagie', name: 'itext', version: '2.1.7.js2'
 
And you may have this configuration in the bnd.bnd file of your module:
Import-Package: \

*;resolution:=optional

Bundle-ClassPath:\

  .,\

  lib/poi-3.15.jar,\

  lib/jasperreports-5.6.1.jar,\

  lib/commons-digester-2.1.jar,\

  lib/groovy-all-2.0.1.jar,\

  lib/itext-2.1.7.js2.jar

Include-Resource:\

  lib/poi-3.15.jar=poi-3.15.jar,\

  lib/jasperreports-5.6.1.jar=jasperreports-5.6.1.jar,\

  lib/commons-digester-2.1.jar=commons-digester-2.1.jar,\

  lib/groovy-all-2.0.1.jar=groovy-all-2.0.1.jar,\

  lib/itext-2.1.7.js2.jar=itext-2.1.7.js2.jar

-metatype: *
 
Then you could create a new blank module with this build.gradle file:
repositories {

mavenCentral()

maven{url "http://jasperreports.sourceforge.net/maven2/"}

    maven{url "http://jaspersoft.artifactoryonline.com/jaspersoft/third-party-ce-artifacts/"}

}

configurations {

    jasperreports {

        transitive = true

    }

}

dependencies {

  compileOnly group: "org.osgi", name: "org.osgi.core", version: "6.0.0"

  compileOnly group: "org.osgi", name: "org.osgi.service.component.annotations", version: "1.3.0"

  /* JasperReport */

  compile group: 'net.sf.jasperreports', name: 'jasperreports', version: '5.6.1'

  compile group: 'commons-digester', name: 'commons-digester', version: '2.1'

  compile group: 'org.codehaus.groovy', name: 'groovy-all', version: '2.0.1'

  compile group: 'com.lowagie', name: 'itext', version: '2.1.7.js2'

  compile group: 'org.apache.poi', name: 'poi', version: '3.7'

}
 
And this bnd.bnd file:
Bundle-Name: jasperreports

Bundle-SymbolicName: jasperreports

Bundle-Version: 1.0.0

Export-Package: *;-split-package:=merge-last;-noimport:=true

Import-Package: \

*;resolution:=optional

Bundle-ClassPath:\

  .,\

  lib/poi-3.7.jar,\

  lib/jasperreports-5.6.1.jar,\

  lib/commons-digester-2.1.jar,\

  lib/groovy-all-2.0.1.jar,\

  lib/itext-2.1.7.js2.jar

Include-Resource:\

  lib/poi-3.7.jar=poi-3.7.jar,\

  lib/jasperreports-5.6.1.jar=jasperreports-5.6.1.jar,\

  lib/commons-digester-2.1.jar=commons-digester-2.1.jar,\

  lib/groovy-all-2.0.1.jar=groovy-all-2.0.1.jar,\

  lib/itext-2.1.7.js2.jar=itext-2.1.7.js2.jar

-metatype: *
 
When you build and deploy your new module you will deploy a new module that exports all public class for JasperReport use.
You would only need add dependency of your new module in all modules you want to add JasperReport funcionality:
compileOnly project(":modules:jasperreports")
 

Long explanation

A usually problem on Liferay 6.x was where you can put your third party library. If you put your third party library into your portlets, you will have very heavy war files (too dificult to manage for your CI System), and other problems. If you decide to put your third party library into your Tomcat lib/ext folder, you will have light war files but you will need to restart your Tomcat Server allways you add a new library or change a library version. 
With the new OSGi paradigm, a new world have been opened in front of us. We can put our third party library into modules (as jar files or as unzipped in the module classpath), on our lib/ext Tomcat folder (as in the old version) or into a module witch are used as module of dependencies.
The last option (as module) is very interesting because your third party library will be grouped in a module (then your modules may be more ligth) and you won't need to restart your Tomcat Server when you add a new library.
Follow these steps to convert your third party library to a new OSGi module:
  • Pass your dependencies configuration from build.gradle file of your module to build.gradle file of the new library module. This will be necesary for download your third party library on your new library module.
repositories {

mavenCentral()

maven{url "http://jasperreports.sourceforge.net/maven2/"}

    maven{url "http://jaspersoft.artifactoryonline.com/jaspersoft/third-party-ce-artifacts/"}

}

configurations {

    jasperreports {

        transitive = true

    }

}

dependencies {

  compileOnly group: "org.osgi", name: "org.osgi.core", version: "6.0.0"

  compileOnly group: "org.osgi", name: "org.osgi.service.component.annotations", version: "1.3.0"

  /* JasperReport */

  compile group: 'net.sf.jasperreports', name: 'jasperreports', version: '5.6.1'

  compile group: 'commons-digester', name: 'commons-digester', version: '2.1'

  compile group: 'org.codehaus.groovy', name: 'groovy-all', version: '2.0.1'

  compile group: 'com.lowagie', name: 'itext', version: '2.1.7.js2'

  compile group: 'org.apache.poi', name: 'poi', version: '3.7'

}
  • Pass your dependencies configuration from bnd.bnd file of your module to bnd.bnd file of the new library module. This will be necesary for load your third party library on your new library module jar file.
Bundle-Name: jasperreports

Bundle-SymbolicName: jasperreports

Bundle-Version: 1.0.0

Import-Package: \

*;resolution:=optional

Bundle-ClassPath:\

  .,\

  lib/poi-3.7.jar,\

  lib/jasperreports-5.6.1.jar,\

  lib/commons-digester-2.1.jar,\

  lib/groovy-all-2.0.1.jar,\

  lib/itext-2.1.7.js2.jar

Include-Resource:\

  lib/poi-3.7.jar=poi-3.7.jar,\

  lib/jasperreports-5.6.1.jar=jasperreports-5.6.1.jar,\

  lib/commons-digester-2.1.jar=commons-digester-2.1.jar,\

  lib/groovy-all-2.0.1.jar=groovy-all-2.0.1.jar,\

  lib/itext-2.1.7.js2.jar=itext-2.1.7.js2.jar

-metatype: *
  • Then you will need to add a little configuration to Export-Package parameter.
Export-Package: *;-split-package:=merge-last;-noimport:=true

-split-package:=merge-last directive on Export-Package allows fine grained control over what should be done with split packages, merging split packages but overwriting resources that come earlier in the classpath. That is, the last resource wins.

-noimport:=true directive on Export-Package disables automatically import packages by Export-Package directive.

When you build your new library module you will create a jar file with your third party library unzipped into your module and the configuration you need on MANIFEST.MF to use your library on other modules.

Then you will need add the dependency of your new library module into other module these use it:

compileOnly project(":modules:jasperreports")

You can use my example liferay workspace (https://github.com/ironcero/jasperreports-bundle-test).

Edited

As Miroslav said you could simplify this configuration changing the compile directives on your gradle file (build.gradle):

compileInclude group: 'org.apache.poi', name: 'poi', version: '3.7'

With compileInclude Gradle will copy the library jar file in lib folder inside the module jar file. I Built some diferent scenarios trying to understand the diferences between compileOnly, compile and compileInclude. In gradle for OSGi bundle I haven't found any diferences between compileOnly and compile:

For this build.gradle file:

repositories {

mavenCentral()

maven{url "http://jasperreports.sourceforge.net/maven2/"}

    maven{url "http://jaspersoft.artifactoryonline.com/jaspersoft/third-party-ce-artifacts/"}

}



configurations {

    jasperreports {

        transitive = true

    }

}





dependencies {

compileOnly group: "org.osgi", name: "org.osgi.core", version: "6.0.0"

compileOnly group: "org.osgi", name: "org.osgi.service.component.annotations", version: "1.3.0"

/* JasperReport */

compileOnly group: 'net.sf.jasperreports', name: 'jasperreports', version: '5.6.1'

compile group: 'commons-digester', name: 'commons-digester', version: '2.1'

compile group: 'org.codehaus.groovy', name: 'groovy-all', version: '2.0.1'

compile group: 'com.lowagie', name: 'itext', version: '2.1.7.js2'

compile group: 'org.apache.poi', name: 'poi', version: '3.7'



// https://mvnrepository.com/artifact/org.apache.commons/commons-math3

compileInclude group: 'org.apache.commons', name: 'commons-math3', version: '3.6.1'



}
 

Where net.sf.jasperreports.jasperreports library is configured as compileOnly; org.codehaus.groovy.groovy-all is configured as compile; and org.apache.commons.commons-math3 is configured as compileInclude.

To complete the example, bnd.bnd was:

Bundle-Name: jasperreports

Bundle-SymbolicName: jasperreports

Bundle-Version: 1.0.0

-metatype: *

Result was that only commons-math3 library was included on lib filder inside the bundle jar file (logical). And this was MANIFEST.MF file:

Manifest-Version: 1.0

Bnd-LastModified: 1516660109235

Bundle-ClassPath: .,lib/commons-math3-3.6.1.jar

Bundle-ManifestVersion: 2

Bundle-Name: jasperreports

Bundle-SymbolicName: jasperreports

Bundle-Version: 1.0.0

Created-By: 1.8.0_73 (Oracle Corporation)

Import-Package: net.sf.jasperreports.components.table,org.codehaus.gro

 ovy.ast.builder;version="[2.0,3)"

Javac-Debug: on

Javac-Deprecation: off

Javac-Encoding: Cp1252

Private-Package: jasperreports,jasperreports.api;version="1.0.0",lib,a

 ssets.org.apache.commons.math3.exception.util,assets.org.apache.commo

 ns.math3.random,org.apache.commons.math3,org.apache.commons.math3.ana

 lysis,org.apache.commons.math3.analysis.differentiation,org.apache.co

 mmons.math3.analysis.function,org.apache.commons.math3.analysis.integ

 ration,org.apache.commons.math3.analysis.integration.gauss,org.apache

 .commons.math3.analysis.interpolation,org.apache.commons.math3.analys

 is.polynomials,org.apache.commons.math3.analysis.solvers,org.apache.c

 ommons.math3.complex,org.apache.commons.math3.dfp,org.apache.commons.

 math3.distribution,org.apache.commons.math3.distribution.fitting,org.

 apache.commons.math3.exception,org.apache.commons.math3.exception.uti

 l,org.apache.commons.math3.filter,org.apache.commons.math3.fitting,or

 g.apache.commons.math3.fitting.leastsquares,org.apache.commons.math3.

 fraction,org.apache.commons.math3.genetics,org.apache.commons.math3.g

 eometry,org.apache.commons.math3.geometry.enclosing,org.apache.common

 s.math3.geometry.euclidean.oned,org.apache.commons.math3.geometry.euc

 lidean.threed,org.apache.commons.math3.geometry.euclidean.twod,org.ap

 ache.commons.math3.geometry.euclidean.twod.hull,org.apache.commons.ma

 th3.geometry.hull,org.apache.commons.math3.geometry.partitioning,org.

 apache.commons.math3.geometry.partitioning.utilities,org.apache.commo

 ns.math3.geometry.spherical.oned,org.apache.commons.math3.geometry.sp

 herical.twod,org.apache.commons.math3.linear,org.apache.commons.math3

 .ml.clustering,org.apache.commons.math3.ml.clustering.evaluation,org.

 apache.commons.math3.ml.distance,org.apache.commons.math3.ml.neuralne

 t,org.apache.commons.math3.ml.neuralnet.oned,org.apache.commons.math3

 .ml.neuralnet.sofm,org.apache.commons.math3.ml.neuralnet.sofm.util,or

 g.apache.commons.math3.ml.neuralnet.twod,org.apache.commons.math3.ml.

 neuralnet.twod.util,org.apache.commons.math3.ode,org.apache.commons.m

 ath3.ode.events,org.apache.commons.math3.ode.nonstiff,org.apache.comm

 ons.math3.ode.sampling,org.apache.commons.math3.optim,org.apache.comm

 ons.math3.optim.linear,org.apache.commons.math3.optim.nonlinear.scala

 r,org.apache.commons.math3.optim.nonlinear.scalar.gradient,org.apache

 .commons.math3.optim.nonlinear.scalar.noderiv,org.apache.commons.math

 3.optim.nonlinear.vector,org.apache.commons.math3.optim.nonlinear.vec

 tor.jacobian,org.apache.commons.math3.optim.univariate,org.apache.com

 mons.math3.optimization,org.apache.commons.math3.optimization.direct,

 org.apache.commons.math3.optimization.fitting,org.apache.commons.math

 3.optimization.general,org.apache.commons.math3.optimization.linear,o

 rg.apache.commons.math3.optimization.univariate,org.apache.commons.ma

 th3.primes,org.apache.commons.math3.random,org.apache.commons.math3.s

 pecial,org.apache.commons.math3.stat,org.apache.commons.math3.stat.cl

 ustering,org.apache.commons.math3.stat.correlation,org.apache.commons

 .math3.stat.descriptive,org.apache.commons.math3.stat.descriptive.mom

 ent,org.apache.commons.math3.stat.descriptive.rank,org.apache.commons

 .math3.stat.descriptive.summary,org.apache.commons.math3.stat.inferen

 ce,org.apache.commons.math3.stat.interval,org.apache.commons.math3.st

 at.ranking,org.apache.commons.math3.stat.regression,org.apache.common

 s.math3.transform,org.apache.commons.math3.util

Require-Capability: osgi.ee;filter:="(&(osgi.ee=JavaSE)(version=1.8))"

Tool: Bnd-3.2.0.201605172007

You can see that all packages of commons-math3 library were included on Private-package section. However, in Import-Package section only were included the packages used on module source files (net.sf.jasperreports.components.table,org.codehaus.groovy.ast.builder).

The problem come when we want to export these libraries to used outside our modules. Then you will need to add Export-Package section on bnd.bnd file. If you only includes some package in this section, then all classes of first package will be copied in the classpath of bundle jar file independently of compile directive. I think that bnd doesn't know how to build the MANIFEST.MF in this case. 

On the other hand, if you includes '*' in Export-Package section, then all classes of all packages includes on your bundle will be copied in the classpath of bundle jar file. If you includes '*' in Export-Package section and same library with compileInclude then all classes of library will be copied in classpath and in lib folder (duplicate).

Conclusion

At last I'm going to try to summarize all common scenarios:

  1. Add a third party library: If you only need to add one third party library to your bundle you can do easily adding the library with compileInclude in your build.gradle. You won't need anything else.
  2. Add a third party library that is already in OSGi container: If you only need to add one third party library that is exported by other bundle of your OSGi container you can do easily adding the library with compileOnly in your build.gradle. You won't need anything else.
  3. Add a third party library that you want to export to another bundle: If you need to include a library in your bundle to used in other bundle (to build a libraries bundle, for example), you will need to add the library to build.gradle with compileInclude and add all packages you need to Export-Package section on bnd.bnd. However if you need to export all package of third party libraries, I think that is better option include these library with compile in build.gradle and add '*' to Export-Package section on bnd.bnd.

CounterLocalService - Solución elegante a un problema habitual

Technical Blogs August 15, 2016 By Ignacio Roncero Bazarra

No te ha pasado nunca que usas algo habitualmente que te descuadra, que tiene algo que no entiendes del todo. Pues esa ha sido mi experiencia con el servicio de Counter de Liferay. Por ello me he sumergido en el código para entender el funcionamiento completo y así poder dormir tranquilo de nuevo wink

¿Para qué sirve?

El servicio Counter nos permite obtener el ID de forma automática para una nueva Entidad al guardarla en BBDD. Más concretamente al definir una nueva entidad con Service Builder en Liferay suele ser habitual no añadir ningún tipo de secuencia o autoincremento a la clave primaria de nuestra Entidad (esto es debido principalmente a que no todas las BBDD cuentan con todas estas opciones).

Al no tener autoincremente de ningún tipo en la clave primaria de nuestra Entidad necesitamos algún método para generar ese ID y no asignar el mismo número a dos entidades distintas.

Para ello se utiliza habitualmente el servicio Counter para obtener este número. Más concretamente se suele utilizar CounterLocalServiceUtil.increment(miClase.class.getName()). Este método devuelve un identificador distinto cada vez que se invoca y nunca se repite. Parece magia, su funcionamiento es bastante simple.

 ¿Que ves?

Una vez que conoces esto (y sobre todo para hacer pruebas) sueles buscar la tabla de BBDD donde se almacena el número por le que va para tu entidad. Rápidamente localizas la tabla counter que contiene las columnas name y currentId. En la columna name se almacena la clase para la que se guarda el número actual (en nuestro caso el nombre canónico de miClase) y la columna currenId almacena el número actual.

Ya aquí las cosas empiezan a no cuadrar, ya que ves que para tu clase el currentId vale 200 y la última entidad guardad tiene el ID 210.

Peor se pone cuando de forma (aparentemente aleatoria) los ID's pegan saltos de 100 números. 

De esta forma nunca estás seguro de que número asignar al currentId en BBDD cuando cargas entidades a mano o que ID poner en tus nuevas entidades insertadas manualmente.

Pues todo esto tiene un sentido y ahora verás cual es.

¿Cómo funciona?

Una vez que accedes al código fuente de la implementación del servicio y profundizas un poco en el descubres que el funcionamiento es mucho más simple del que parece.

Cuando arranca el servidor de Liferay este crea una clase singleton donde almacenará un mapa con todos los currentId utilizables (este mapa estará vacío en el arranque). En dicho mapa se almacena también el currentId base (que es el obtenido de BBDD) y el incremento (vamos a llamarlo) por salto.

Una vez que se llama al método CounterLocalServiceUtil.increment para nuestra clase se accede al mapa en busca del currentId para esa clase.

  • Si no se encuentra dicha clave el servicio accede a BBDD en busca el currentId para dicha clase en la tabla counter. Una vez localizado el currentId actual en BBDD se obtiene y se incrementa en función de la base del incremento (es un segundo parámetro que admite el método). Normalmente para un incremento base de 1 el currentId inicial se aumenta en 100. Una vez que se añade el nuevo currentId al mapa de la clase singleton se devuelve el valor actual.
  • Si se encuentra dicha clave se incrementa el currentId obtenido del mapa en función del incremento base y se devuelve. En este punto se comprueba si el nuevo currentId es mayor o igual que el currentId base + incremento por salto. En caso afirmativo se accede a BBDD y se actualiza el currentId de la fila en cuestión (asignando el actual currentId devuelto por el método).

Imagino que explicado de esta forma puede parecer un poco lioso. Veamos un ejemplo con números.

Supongamos que tenemos una entidad propia llamada miClase, para la que hay 120 entidades cuyos ID's van correlativos del 1 al 121. Como hemos utilizando previamente el servicio Counter en la tabla counter tendremos una fila con el nombre canónico de nuestra clase y el valor 100 en el currentId.

Al arrancar nuestro Liferay este crear el singleton con el mapa vacío y al llamar al incremento para nuestra clase (para almacenar una nueva entidad) este incrementa el currentId de BBDD de 100 a 200 y devuelve el ID 200 a nuestra nueva entidad.

Al llamar de nuevo al incremento para una nueva entidad, el método nos devolverá el siguiente ID (201) sin tocar la BBDD. Y eso ocurrirá así para todos los números hasta llegar al 300. En caso de solicitar el incremento y este llegar al 300, ese mismo número se almacenará el BBDD.

Así en caso de reiniciar el servidor de Liferay, este volverá a incrementar a 400 el currentId de nuestra clase en su primera petición.

Conclusiones

Con este diseño se ha conseguido un gran rendimiento en una operación que por su frecuencia de uso es crítica (influye en todas las operaciones de creación de objetos en BBDD). Ya que el valor del ID actual de cada clase lo mantiene en memoria (en la clase singleton) y solo accede a BBDD la primera vez (para cargarlo) y una vez de cada X (en función del incremento base y del salto) para actualizar la BBDD.

El único inconveniente de este sistema es que se pierden varios números cada vez que se reinicia el servidor.

Como reza el título, una solución muy elegante a un pequeño problema de rendimiento jeje.

Proceso Login (Español)

Technical Blogs October 21, 2015 By Ignacio Roncero Bazarra

Resumen

Liferay ofrece un complejo sistema de login que permite su personalización cubriendo prácticamente la totalidad de las necesidades que nos podamos encontrar. Para saber cómo personalizar correctamente el sistema es importante conocer a fondo este proceso de Login. Esta entrada (pese a no mostrar el detalle completo) pretende arrojar algo de luz sobre este tema.

Descripción a alto nivel

Por defecto el sistema de login de Liferay se basa en la validación de las credenciales enviadas por el usuario al sistema, utilizando la tabla User_ de la base datos de nuestro portal para ello.

En la configuración del portal podemos seleccionar cuáles serán los campos utilizables para dichas credenciales (por dirección de correo electrónico, por nombre de usuario o por identificador de usuario). En esta sección del panel de control también podemos configurar si queremos que se conecte con un LDAP para la autenticación y/o importación de usuarios, si queremos utilizar CAS, OpenSSO, NTLM o por URL, etc. Todas estas configuraciones tienen su explicación en el proceso de login de Liferay.

Para ilustrarlo vamos a ver un diagrama (ref: Wiki Liferay Authentation Process) con un resumen del proceso básico de login:

En este proceso podemos ver como cuando un usuario acceder al portal y le envía sus credenciales el portal llama a un método de la clase LoginAction. En dicho método llamará a otro método de la clase LoginUtil y así sucesivamente hasta completar el diagrama anterior. Más adelante profundizaremos en los métodos a los que se llama y su cometido, pues será de gran utilidad a la hora de modificar el comportamiento del sistema (mediante un Hook o, esperemos que no :P, un plugin-ext).

Lo que nos indica el diagrama anterior es, básicamente, que Liferay antes de realizar la autenticación de las credenciales del usuario contra los datos del mismo almacenados en la base de datos ejecutará una serie de métodos de las clases configuradas en la propiedad auth.pipeline.pre del portal.properties.

Estas clases tendrán que implementar com.liferay.portal.security.auth.Authenticator y por lo tanto sobrescribir los métodos authenticateByEmailAddress, authenticateByScreenName y authenticateByUserId. Estos métodos devolverán SUCCESS (1), FAILURE (-1) o DNE (0) en función del resultado de la autenticación de las credenciales del usuario para cada filtro. Es importante saber, que para que el login del usuario sea correcto es necesario que todos los filtros devuelvan SUCCESS, ya que si alguno devuelve algún error el proceso de login parará y no permitirá el acceso del usuario.

Una vez pasado todos los filtros se comprobará contra la base de datos si el usuario existe y si la contraseña es la correcta (esta comprobación está supeditada a una propiedad llamada auth.pipeline.enable.liferay.check la cual si es false el sistema no comprobará a esta altura si la contraseña coincide, sino que solo revisará si el usuario existe y es válido). Si las credenciales pasan esta comprobación, el sistema volverá llamar a los filtros, pero esta vez basándose en la propiedad auth.pipeline.post. Si todos los métodos correspondientes del este post-proceso también devuelve SUCCESS entonces el sistema devolverá una autenticación correcta, dando acceso al usuario al recurso solicitado.

Por defecto, solo hay un filtro en el auth.pipeline.pre, el del LDAP (LDAPAuth). Y no hay ningún filtro en el auth.pipeline.post. Pero añadiendo en estas propiedades nuestras propias clases (en un Hook, por ejemplo) podríamos modificar el tratamiento de las credenciales fácilmente.

El uso de la pipeline hace que sea relativamente fácil modificar el comportamiento del Login del portal. Pero este sistema se basa en tratar unos datos que el usuario le ha enviado al portal. Hay muchas otras situaciones en las que se necesita interferir en la forma en la que el usuario envía dicha información al portal, como por ejemplo con CAS, OpenSSO o NTLM. Para estos casos Liferay utiliza un sistema de AutoLogin.

Este sistema se basa en la propiedad auto.login.hooks la indica las clases que serán llamadas por orden para obtener las credenciales del usuario. Estas clases tienen que implementar com.liferay.portal.security.auth.AutoLogin y sobrescribir el método login. Dicho método recibirá todos los datos del request (y del response) y devolverá un array de String que representarán las credenciales. Estas credenciales se enviarán al punto de entrada del proceso básico de login mostrado anteriormente. Con lo que ya no será necesario que el usuario introduzca sus credenciales en el portlet de login del portal, sino que este se podrá integrar con otros sistemas para obtener dichas credenciales.

La ejecución secuencial de estas clases de AutoLogin se parará en el momento en el que alguna de las clases devuelva unas credenciales válidas. Por defecto la propiedad viene con todos los AutoLogin que trae de serie el portal, pero los métodos antes de ejecutarse revisan si estos filtros están activos en la configuración del portal. De esta manera si queremos activar un filtro de AutoLogin no es necesario que modifiquemos la propiedad auto.login.hooks, solo tendremos que activar el filtro en sí, bien en el panel de control bien en el portal-ext.properties.

Resumen descripción a alto nivel

El proceso de login comienza cuando un usuario intenta acceder a un recurso privado del portal o cuando introduce usuario/contraseña en el portlet de Login. En el caso de acceder a un recurso privado del portal, este ejecuta primero una series de filtros de AutoLogin buscando las credenciales del usuario (NTLM por ejemplo busca las credenciales que debe proporcionar el navegador a partir del usuario del dominio de sistemas basados en Windows). Si fallan todos los filtros de AutoLogin el usuario es redirigido al portlet de login (normalmente en /c/login) para que el usuario introduzca sus credenciales. Con las credenciales del usuario (ya sea por AutoLogin o introducidas por el usuario) se llama a los métodos de Login de Liferay los cuales primero ejecuta los filtros de pipeline. Si algún filtro falla (no devuelve SUCCESS) es señal de que el usuario no ha introducido las credenciales correctamente y el login fallaría. Una vez pasados los filtros de auth.pipeline.pre se autenticaría el usuario contra la base de datos y si esta no falla se pasarían los filtros auth.pipeline.post.

Integración con LDAP

Antes de continuar, entrando en detalle en el proceso de login, nos puede surgir la duda de cómo funciona Liferay con el LDAP. Para el caso del LDAP Liferay tiene un filtro auth.pipeline.pre llamado LDAPAuth, el cual loga el usuario utilizando contra el LDAP (o un AD o sistema similar) y si el resultado es SUCCESS entonces importa el usuario desde el LDAP a la base de datos de Liferay antes de que este compruebe sus credenciales contra la base de datos. De esta forma en cada acceso de los usuarios estos se importan o actualizan en la base de datos de Liferay.

Descripción detallada del proceso de Login

En el siguiente diagrama muestra en detalle el funcionamiento del sistema de Login de Liferay:

Para descargar la imagen

Lo primero que hace Liferay cuando recibe la petición de un usuario que no está logado y está intentando acceder a un recurso privado (documento, página, etc) es ejecutar todos los filtros de tipo AutoLogin. Estos filtros con clases java que implementan la clase AutoLogin. Por lo tanto tienen que sobrescribir el método login. Dicho método recibe como parámetros el request y el response de la petición y devuelve un array de String. Si el método obtiene las credenciales del usuario debe devolverlas en dicho array de String. Recordemos que las clases de autologin se ejecutan en el orden que se indique en la propiedad auto.login.hook. Por lo tanto si quisiéramos añadir un filtro más solo tendríamos que crear un hook añadiendo un nuevo elemento a la propiedad auto.login.hook y crear la clase que implemente com.liferay.portal.security.auth.AutoLogin.

Si alguno de estos filtros devuelve credenciales entonces el proceso continúa llamando al método processAction de la clase LoginAction. Si ninguno de los filtros devuelve credenciales entonces el usuario es redirigido a una página del portal donde pueda introducir sus credenciales en el portlet de login. Estas credenciales se guardarían en el request y también se llamaría al método processAction de la clase LoginAction.

El método processAction comprueba si la propiedad auth.login.disabled está deshabilitada. En caso de estar habilitada redirige al usuario a la página de login desactivado. En caso contrario continúa con el proceso de login llamando al método login de la misma clase LoginAction.

El método login obtiene las credenciales que debe estar almacenadas en la request que se le ha pasado por parámetros. Con dichas credenciales llamaría al método login de la clase LoginUtil. Dicho método es el que realiza el login del usuario propiamente dicho. Una vez se ejecute este método se hacen una serie de comprobaciones propias de JaaS y finaliza el proceso de login permitiendo el acceso al recurso solicitado por el usuario.

El método login de la clase LoginUtil obtiene el userId utilizando el método getAuthenticatedUserId de la misma clase. Una vez obtiene el userId realiza una serie de comprobaciones y genera la sesión del usuario (guarda sus datos en sesión y en las cookies para el usuario).

El método getAuthenticatedUserId chequea primero si se trata de una petición a /api/login o /api/secure/login (pues se trataría de un login automático desde la api). En caso afirmativo llamaría al método authenticateForBasic del servicio UserLocalServiceUtil repetidas veces (cambiando el tipo de login: AUTH_TYPE_EA, AUTH_TYPE_SN y AUTH_TYPE_ID) hasta que localiza el id del usuario (utilizando las credenciales suministradas). En caso negativo llama al método de autenticación correspondiente del servicio UserLocalServiceUtil en función del tipo de autenticación que tiene configurada la instancia del portal:

  • AUTH_TYPE_EA: llama a authenticateByEmailAddress
  • AUTH_TYPE_SN: llama a authenticateByScreenName
  • AUTH_TYPE_ID: llama a authenticateByUserId

Estos métodos deben devolver el resultado del login (SUCCESS, FAILURE o DNE), y solo en caso de devolver SUCCESS entonces el método getAuthenticatedUserId devuelve el id del usuario logado. En caso de no devolver SUCCESS entonces lanzaría una excepción.

Cualquier de estos métodos indicados anteriormente (authenticateBy…) del servicio UserLocalServiceUtil solo realizan una llamada al método authenticate de la misma clase pasándole por parámetro el tipo de autenticación y el screenName, emailAddress o userId en función del tipo.

El método authenticate tiene una mayor complejidad. Primero comprueba si ha recibido las credenciales (en caso contrario lanza una excepción que acaba capturando la clase LoginAction en el método processAction). Una vez comprobado que tenemos las credenciales del usuario llamaríamos al método authenticateByEmailAddress, authenticateByScreenName o authenticateByUserId de la interfaz AuthPipeline de pre auth, que básicamente llama al mismo método de cada clase que implemente Authenticator y que esté configurada en la propiedad auth.pipeline.pre. Como se ha indicado antes, para que el login continúe todas estas llamadas tienen que devolver SUCCESS, ya que del contrario contaría la ejecución y la llamada del método de la interfaz AuthPipeline devolvería el error correspondiente.

Una vez pasado el auth.pipeline.pre comprueba si el usuario que se está intentando logar existe y si no es Guest ni está bloqueado. Ya que en tal caso la función se pararía y devolvería el error correspondiente (DNE si el usuario no existe y FAILURE si el usuario está bloqueado o fuese Guest). En caso que el usuario exista, no esté bloqueado ni sea Guest la función comprueba si el auth.pipeline.pre ha devuelto SUCCESS y si la propiedad auth.pipeline.enable.liferay.check está a true. En tal caso comprueba si el usuario y la contraseña coinciden con el usuario de base de datos. En caso contrario se salta esta comprobación.

Por último, y solo si hasta ahora todas las comprobaciones han sido SUCCESS llama al post-procesado (igual que en auth.pripeline.pre) de las clases configuradas en la propiedad auth.pipeline.post.

Finalmente si el resultado de todas las operaciones ha sido SUCCESS se sale del método y finaliza el login en processAction (LoginAction). En caso contrario ejecuta todos los métodos onFailureByEmailAddress, onFailureByScreenName y onFailureByUserId de las clases configuradas en auth.pipeline.post.

Ejemplos de modificación del proceso de Login

Ahora veremos algunos ejemplos de modificaciones que se le podrían hacer al login:

  1. Añadir un nuevo tipo de login (por ejemplo SAML):
    • Para ello habría que añadir un nuevo tipo de AutoLogin que obtuviese las credenciales del usuario siguiendo el protocolo en cuestión y no sería necesario tocar el resto del proceso (en principio).
  2. Importar usuario directamente en el login (por ejemplo desde el LDAP):
    • Esto ya lo hace por defecto Liferay si activamos el LDAP. Liferay lo hace añadiendo un Authenticator en el auth.pipeline.pre que valida las credenciales del usuario contra el LDAP y en caso de ser correctas, obtiene los datos del usuario desde el LDAP y añade el usuario (o lo actualiza si ya existe) a la base de datos de Liferay, para que posteriormente en el chequeo de Liferay las credenciales del usuario estén actualizadas.
  3. Modificar el comportamiento del Login:
    • En principio nunca se debería modificar el comportamiento del login, pero si es necesario se podría crear un wrapper del servicio UserLocalService con un Hook para cambiar la lógica de los métodos authenticateBy… Pero no es buena idea. Es mucho mejor, deshabilitar el chequeo de las credenciales contra la base de datos (utilizando la propiedad auth.pipeline.enable.liferay.check y añadiendo un Authenticator que haga lo que necesitemos.
Showing 3 results.