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.