WS-Security con CXF

De ChuWiki


Algunos conceptos[editar]

Cuando creamos un Web Service, podemos querer que sólo puedan usarlo usuarios autentificados, es decir, con un usuario y password válidos. Vamos a ver aquí un ejemplo de cómo hacerlo con CXF.

En primer lugar, desarrollamos tanto nuestro web service como nuestro cliente de web service de una forma normal, sin tener en cuenta para nada este tema de seguridad. Puedes seguir el ejemplo de Ejemplo sencillo de web service con CXF

En los web services, tanto cliente como servidor, tenemos posibilidad de añadir "callbacks" que serán llamados cada vez que el cliente o el servidor envíen o reciban un mensaje. Estos callbacks nos permiten de forma fácil añadir cosas o analizar el contenido del mensaje SOAP/XML que se envía o recibe antes de enviarlo o antes de que sea tratado. Es aquí donde debemos añadir la información de usuario/password o analizar la información de usuario/password para denegar o no el acceso.

Existen dos tipos de callbacks, los de entrada y los de salida. Los de entrada nos permiten analizar un mensaje que nos llega. Sería el caso del servidor del web service, que debe analizar la petición del cliente para ver si el usuario/password que hay en esa petición es o no válido. En los de salida podemos añadir "cosas" a ese mensaje. Es el caso del cliente que necesita añadir al mensaje su usuario y password.

Afortunadamente, para los callbacks más habituales como este de seguridad y de autentificación de usuario/password, CXF nos proporciona clases y soporte para poder desarrollar estos callbacks de forma muy sencilla. En concreto, las clases de CXF WSS4JInInterceptor y WSS4JOutInterceptor se encargan de todo esto y sólo nos llamarán en el punto concreto donde tenemos que poner cual es la password del usuario (bien en el cliente porque necesita enviarla, bien en el servidor porque necesita saber cual es la password válida para ver si coincide con la enviada por el cliente).


Parte del servidor[editar]

Hemos visto que en el servidor debemos añadir un callback de entrada, de forma que analice el usuario y password que llega del cliente. Vamos primero a hacer el callback, que sería algo como esto

package com.chuidiang.ejemplos.ws_security;

import java.io.IOException;

import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;

import org.apache.ws.security.WSPasswordCallback;

public class ServerPasswordCallback implements CallbackHandler {

   @Override
   public void handle(Callback[] callbacks) throws IOException,
         UnsupportedCallbackException {
      WSPasswordCallback pc = (WSPasswordCallback) callbacks[0];

      System.out.println("usuario recibido : " + pc.getIdentifier());
      if (pc.getIdentifier().equals("joe")) {
         // Ponemos cual sería la password valida para este usuario.
         // El framework CXF se encargará de verificar que coincide
         // con la enviada por el cliente.
         pc.setPassword("password");
      }

   }

}

Simplemente es una clase que implementa CallbackHandler. En el método, en la posición 0 del array callbacks, recibiremos un WSPassswordCallback en el que podemos consultar la información de usuario que ha enviado el cliente. Solo tenemos que meter ahí con el método setPassword() cual sería la password válida para ese usuario. Será el framework de CXF el que se encargue de comprobar que coincide con la enviada por el cliente.

Ahora sólo debemos añadir esa Callback a nuestro Web Service. Lo hacemos de esta forma

package com.chuidiang.ejemplos.ws_security;

import com.chuidiang.ejemplos.ws.UnWebService;

import java.util.HashMap;
import java.util.Map;

import org.apache.cxf.endpoint.Endpoint;
import org.apache.cxf.jaxws.EndpointImpl;
import org.apache.cxf.ws.security.wss4j.WSS4JInInterceptor;

public class Prueba {

   public static void main(String[] args) {

      // La clase UnWebService es la implementación del web service.
      EndpointImpl jaxWsEndpoint = (EndpointImpl) EndpointImpl
            .publish(
                  "http://localhost:8080/UnWebService",
                  new UnWebService());

      // Aqui obtenemos la clase a la que debemos añadir el callback
      // para nuestro web service.
      Endpoint cxfEndpoint = jaxWsEndpoint.getServer().getEndpoint();

      // Propiedades para la clase WSS4JInInterceptor, entre otras
      // esta nuestra clase Callback.
      Map<String, Object> inProps = new HashMap<String, Object>();
      inProps.put("action", "UsernameToken");
      inProps.put("passwordType", "PasswordText");
      inProps.put("passwordCallbackRef", new ServerPasswordCallback());

      WSS4JInInterceptor wssIn = new WSS4JInInterceptor(inProps);

      // Le pasamos el interceptor
      cxfEndpoint.getInInterceptors().add(wssIn);

   }
}

En la primera línea publicamos nuestro WebService con EndpointImpl.publish(). Esto es más o menos lo que haríamos habitualmente independientemente de que queramos o no autentificación de usuarios.

En la segunda linea obtenemos el Endpoint de nuestro web service que tiene el método adecuado para añadirle los callback o interceptores.

Como comentamos anteriormente, para el tema de seguridad usuario/password, CXF nos proporciona clases que nos facilitan la tarea. Una de ellas es WSS4JInInterceptor, capaz de analizar los mensajes de entrada para comprobar usuario/password. Tenemos entonces que instanciar esa clase, pero pasándole la configuración necesaria. La configuración es en este caso el Map de propiedades. En él estamos indicando

  • El tipo de validación que vamos a querer en "action". El valor "UsernameToken" indica que usaremos el nombre de usuario y password. Hay varias opciones disponibles que podemos deducir de las constantes definidas en WSHandlerConstants. De hecho, podemos usar esas constantes en vez de el String "UsernameToken" a pelo.
  • El tipo de password a usar, puede ser, entre otras, texto plano "PasswordText" o encriptada "PasswordDigest".
  • En "passwordCallbackRef" pasamos una instancia de nuestra clase ServerPasswordCallback.

Ahora sólo queda instanciar WSS4JInInterceptor pasándole estas propiedades en el constructor y pasársea a nuestro cxfEntpoint tal cual se ve en el código. Desde este momento, cada vez que un cliente llame a nuestro web service, se llamará a nuestro callback donde sólo debemos indicar cual es la clave válida para el usuario concreto.


Parte del cliente[editar]

En el lado del cliente debemos hacer más o menos lo mismo, con la diferencia de que en vez de Callback de entrada, sería Callback de salida, ya que queremos poner la password en el envío del mensaje.

Hacemos nuestra clase Callback

package com.chuidiang.ejemplos.ws_security;

import java.io.IOException;

import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;

import org.apache.ws.security.WSPasswordCallback;

public class ClientePasswordCallback implements CallbackHandler {

   @Override
   public void handle(Callback[] callbacks) throws IOException,
         UnsupportedCallbackException {
      WSPasswordCallback pc = (WSPasswordCallback) callbacks[0];
      // set the password for our message.
      pc.setPassword("password");

      // En caso de que el cliente sea multiusuario, 
      // con pc.getIdentifier() podriamos obtener el nombre de usuario
      // para poner la password que corresponda a ese usuario.
   }
}

El nombre de usuario vemos más adelante dónde se rellena. En este callback sólo tenemos que poner la password de dicho usuario, que normalmente le habremos pedido al mismo usuario en algún momento.

Ahora sólo hay que añadir el Callback a nuestra clase cliente

package com.chuidiang.ejemplos.ws_security;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;

import org.apache.cxf.frontend.ClientProxy;
import org.apache.cxf.ws.security.wss4j.WSS4JOutInterceptor;
import org.apache.ws.security.WSConstants;
import org.apache.ws.security.handler.WSHandlerConstants;

public class PruebaCliente {

   /**
    * @param args
    */
   public static void main(String[] args) {
      try {
         UnWebServiceService cliente = new UnWebServiceService(
               new URL(
                     "http://localhost:8080/UnWebService?wsdl"));

         UnWebServiceImpl servicio = cliente
               .getUnWebServicePort();

         org.apache.cxf.endpoint.Client client = ClientProxy
               .getClient(servicio);
         org.apache.cxf.endpoint.Endpoint cxfEndpoint = client.getEndpoint();

         Map<String, Object> outProps = new HashMap<String, Object>();
         outProps.put(WSHandlerConstants.ACTION,
               WSHandlerConstants.USERNAME_TOKEN);
         // Specify our username
         outProps.put(WSHandlerConstants.USER, "joe");
         // Password type : plain text
         outProps.put(WSHandlerConstants.PASSWORD_TYPE, WSConstants.PW_TEXT);
         // Callback used to retrieve password for given user.
         outProps.put(WSHandlerConstants.PW_CALLBACK_CLASS,
               ClientePasswordCallback.class.getName());

         WSS4JOutInterceptor wsOut = new WSS4JOutInterceptor(outProps);
         cxfEndpoint.getOutInterceptors().add(wsOut);

         Object resultado = servicio.unMetodoDelWebService(parametrosDelWebService);

      } catch (MalformedURLException e) {
         e.printStackTrace();
      }
   }
}

Primero instanciamos las clases del cliente UnWebServiceService y UnWebServiceImpl, normalmente generadas automáticamente por el script wsdl2java de CXF.

Después instanciamos WSS4JOutInterceptor que necesita en el constructor un Map de propiedades que creamos y rellenamos previamente. Estas propiedades son (esta vez usamos las constantes en vez de los textos a piñón fijo como hicimos en el servidor).

  • En WSHandlerConstants.ACTION ponemos que vamos a usar usuario/password usando la constante WSHandlerConstants.USERNAME_TOKEN). En el caso del servidor habiamos puesto "action" y "UsernameToken", que es lo mismo.
  • En WSHandlerConstants.USER ponemos el nombre de usuario "joe"
  • En WSHandlerConstants.PASSWORD_TYPE ponemos que la clave ira en texto plano WSConstants.PW_TEXT
  • En WSHandlerConstants.PW_CALLBACK_CLASS pondremos el nombre completo de la clase que hace de Callback ClientePasswordCallback.class.getName()). En el caso del servidor habiamos usado "passwordCallbackRef", correspondiente a la constante WSHandlerConstants.PW_CALLBACK_REF, por lo que en el servidor pasamos una instancia de la clase mientras que aquí nos basta pasar el nombre de la clase.

Ahora sólo nos queda instanciar WSS4JOutInterceptor pasándole las propiedades en cuestión y añadir el callback a nuestro cxfEndpoint.

WSS4JOutInterceptor wsOut = new WSS4JOutInterceptor(outProps);
cxfEndpoint.getOutInterceptors().add(wsOut);

A partir de ahora, cada vez que usemos la clase del web service llamando a un método, se pondrá automáticamente el nombre de usuario y se llamará a nuestro callback para que pongamos la password de dicho usuario.