lunes, octubre 26, 2009

Receta CXF, WSS4J y Spring Security


Como utilizar la seguridad de Spring Security para autenticar un usuario/contraseña de un servicio web implementado con CXF mediante WSS4J (Java WS-Security de Apache):

En el aplication context de spring, primero definimos los namespaces, importamos los xml de CXF y definimos el "Interceptor" que gestionará la seguridad:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
    xmlns:cxf="http://cxf.apache.org/core"
    xmlns:util="http://www.springframework.org/schema/util"
    xmlns:jaxws="http://cxf.apache.org/jaxws"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd
        http://cxf.apache.org/core http://cxf.apache.org/schemas/core.xsd
        http://cxf.apache.org/jaxws http://cxf.apache.org/schemas/jaxws.xsd
        http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-2.5.xsd"
    >

    <!-- Load CXF modules from cxf.jar -->
    <import resource="classpath:META-INF/cxf/cxf.xml" />
    <import resource="classpath:META-INF/cxf/cxf-extension-soap.xml" />
    <import resource="classpath:META-INF/cxf/cxf-servlet.xml" />

    <bean id="WSS4JInInterceptor" class="org.apache.cxf.ws.security.wss4j.WSS4JInInterceptor">
     <property name="properties">
      <map>
          <entry key="action" value="UsernameToken Timestamp"/>
          <entry key="passwordType" value="PasswordDigest"/>
          <entry key="passwordCallbackRef">
           <bean class="my.company.SecurityInPasswordHandler"/>
          </entry>
      </map>
     </property>
   </bean>
</beans>

Y luego agregamos el interceptor a los webservices que necesiten seguridad:
<jaxws:endpoint id="salaWebService" implementor="#salaService" address="/salas">
    <jaxws:inInterceptors>
        <ref bean="WSS4JInInterceptor"/>
    </jaxws:inInterceptors>
</jaxws:endpoint>

Asumiendo que tenemos el siguiente servicio:
@WebService
@SOAPBinding
public interface SalaService {
    @WebResult(name = "sala")
    public abstract Sala get(@WebParam(name = "id") Long id);
}

@Service("salaService")
@WebService(serviceName = "SalaService", portName = "SalaPort", endpointInterface = "my.company.service.SalaService")
public class SalaServiceImpl implements SalaService {
    public Sala get(Long id) {
        return (Sala) executor.execute(SalaBusinessOperations.GET, new Long(id));
    }
}

Y que CXF está configurado en el web.xml de la siguiente forma:
<servlet>
    <servlet-name>CXFServlet</servlet-name>
    <servlet-class>org.apache.cxf.transport.servlet.CXFServlet</servlet-class>
</servlet>    
<servlet-mapping>
    <servlet-name>CXFServlet</servlet-name>
    <url-pattern>/services/*</url-pattern>
</servlet-mapping>

La seguridad de spring debería estar configurada, por ejemplo, con lo siguiente:
<http >
    <intercept-url pattern="/services/**" access="ROLE_ANONYMOUS" />
    <intercept-url pattern="/**" access="ROLE_USER" />
    <logout/>
    <anonymous/>
</http>

Finalmente podemos dedicarnos a la clase que gestionará la seguridad:
public class SecurityInPasswordHandler implements CallbackHandler {
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private UserDetailsService userService;

    public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException, AuthenticationException {

        WSPasswordCallback pwdCallback = (WSPasswordCallback) callbacks[0];

        int usage = pwdCallback.getUsage();
        if ((usage == WSPasswordCallback.USERNAME_TOKEN) || (usage == WSPasswordCallback.USERNAME_TOKEN_UNKNOWN)) {
            String password = pwdCallback.getPassword();
            if (usage == WSPasswordCallback.USERNAME_TOKEN) {
                UserDetails userDetails = userService.loadUserByUsername(pwdCallback.getIdentifier());
                password = userDetails.getPassword();
            }
            Authentication authentication = new UsernamePasswordAuthenticationToken(pwdCallback.getIdentifier(), password);
            authentication = authenticationManager.authenticate(authentication); //throws AuthenticationException
            SecurityContextHolder.getContext().setAuthentication(authentication);
            // Return the password to the caller
            pwdCallback.setPassword(password);
        }
    }
}

Con esto logramos gestionar passwords enviadas en plano (PasswordText) y encriptadas (PasswordDigest). En ambos casos creando un SecurityContext para que la petición tenga los roles del usuario y puedan utilizarse las anotaciones "@Secured" de Spring Security.

2 comentarios:

美麗新世界 dijo...

Judge not of men and things at first sight...................................................

Barba dijo...

Hola Niko, encontré tu blog buscando cosas sobre CXF+Spring en la web. Es un post tuyo del año pasado, pero aún así comento, con la esperanza de recibir respuesta!!.

Estaba buscando hacer justamente lo que describís acá pero usando certificados X509. Partiendo de lo tuyo, pensaba aplicar la misma estrategia, pero sentite libre de corregirme si me equivoco en cuanto a la aproximación.

Entonces, la idea sería generar un X509AuthenticationToken en lugar de tu UNAuth Token. El código quedaría algo así:

if ((usage == WSPasswordCallback.SIGNATURE) )
{
UserDetails userDetails = userService.loadUserByUsername(pc.getIdentifer());
String password = userDetails.getPassword();
Authentication authentication = new X509AuthenticationToken(...);
authentication = authenticationManager.authenticate(authentication); //throws AuthenticationException
SecurityContextHolder.getContext().setAuthentication(authentication);
// Return the password to the caller
pc.setPassword(password);
}

El tema es que para generar el new X509AuthenticationToken necesito un X509Certificate que no se de donde obtener..

Alguna idea al respecto?, se te ocurre alguna aproximación alternativa?.
Muchas gracias desde ya!!