Archive for the ‘Web Services’ category

WS-Security no CXF

September 24th, 2011

Completando o post anterior (bom, agora no sentido “que veio algum momento antes”), vamos ver algo bem mais útil: como criar um web service utilizando username token para autenticação. Suponho que ficou claro como criar um Web Service e um client para ele usando CFX + Spring e não vou subestimar a inteligência de vocês, vou mostrar só o que é preciso adicionar/alterar:

<beans xmlns="http://www.springframework.org/schema/beans"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xmlns:jaxws="http://cxf.apache.org/jaxws"
   xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://cxf.apache.org/jaxws http://cxf.apache.org/schemas/jaxws.xsd">
 
   <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-extension-http-jetty.xml" />
 
   <jaxws:endpoint name="testService"
      implementor="net.rafaelliu.services.TestImpl"
      address="http://localhost:9000/test">
 
      <!-- Enable WS-Addressing -->
       <jaxws:inInterceptors>
         <!-- Enable WS-Security -->
         <bean class="org.apache.cxf.ws.security.wss4j.WSS4JInInterceptor">
            <constructor-arg>
               <map>
                  <entry key="action" value="UsernameToken" />
                  <entry key="passwordType" value="PasswordText" />
                  <entry key="passwordCallbackClass" value="net.rafaelliu.callbacks.ServerPasswordCallback"/>
               </map>
            </constructor-arg>
         </bean>
       </jaxws:inInterceptors>
   </jaxws:endpoint>
 
 
   <jaxws:client name="testClient"
      address="http://localhost:9000/test"
      serviceClass="net.rafaelliu.services.Test">
 
      <jaxws:outInterceptors>
         <bean class="org.apache.cxf.ws.security.wss4j.WSS4JOutInterceptor">
            <constructor-arg>
               <map>
                  <entry key="action" value="UsernameToken" />
                  <entry key="passwordType" value="PasswordText" />
                  <entry key="user" value="rafaelliu" />
                  <entry key="passwordCallbackClass" value="net.rafaelliu.callbacks.ClientPasswordCallback" />
               </map>
            </constructor-arg>
         </bean>
      </jaxws:outInterceptors>
 
   </jaxws:client>
 
</beans>

É possível ver que no client definimos um outInterceptor que irá adicionar os headers com o token Username (um dos possíveis tokens como havia dito no post anterior, poderia ser Kerberos, SAML ou algum certificado) e no serviço definimos um inInterceptor que irá vazer a validação do envelope SOAP que chegar, verificando no header as credenciais.

Ambos os interceptors utilizam Callbacks para recuperar a senha a partir do nome do usuário. No client o callback será utilizado para setar a senha e no serviço será utilizado para recuperar a senha a partir do nome do usuário presente no header, que será comparada com a senha que veio junto do header.

A implementação dos Callbacks é Java puro, provavelmente utilizaria um DB ou LDAP, e é bastante simples:

public class ClientPasswordCallback implements CallbackHandler  {
 
   @Override
   public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
        WSPasswordCallback pc = (WSPasswordCallback) callbacks[0];
 
        if (pc.getIdentifier().equals("rafaelliu")) {
          pc.setPassword("abc123");
        }
   }
 
}

E para o serviço:

public class ServerPasswordCallback implements CallbackHandler  {
 
   @Override
   public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
        WSPasswordCallback pc = (WSPasswordCallback) callbacks[0];
 
        String password = null;
        if (pc.getIdentifier().equals("rafaelliu")) {
           password = "abc123";
        }
        if (password == null || !password.equals(pc.getPassword())) {
           throw new IOException("wrong password");
        }
   }
 
}

Uma nota importante é que o callback do serviço é responsável por lançar uma exceção caso o a senha do usuário não seja validade. Isso se deve à implementação do WSS4J, framework também da ASF que o CXF usa para WS-Security. Não testei, mas parece que na versão 2.4 do CXF, ou mais precisamente no WSS4J 1.6, esse comportamento mudou e o callback realmente ficou só um callback.

Bonito hein?

Criando um Web Service com WS-Addressing (Apache CXF)

August 16th, 2011

Apache CXF é o projeto da ASF para criação de Web Services utilizando JAX-WS e JAX-RS. Entre outros padrões WS-*, ele dá suporte a WS-Addressing e WS-Security.

O WS-Addressing busca independência da camada de transporte enviando informações de transporte no envelope SOAP. Embora na maioria dos casos seja utilizado HTTP que já supre as necessidades mais básicas como endereços de origem e destino, Web Services podem ser utilizado sobre outros protocolos. Imagine ter acesso a um envelope SOAP solto. Da onde ele veio? Pra onde vai? O servidor já recebeu esse envelope? Sem o WS-Addressing essas informações podem ser descobertas apenas analisando a camada de transporte. E se o protocolo de transporte utilizado não armazenar a origem do consumidor? Como o servidor irá eventualmente responder uma requisição? Com o WS-Addressing é possível utilizar protocolos muito mais simples que, a princípio são “incompletos”.

O WS-Security é o padrão para autenticação, assinatura e encriptação de envelopes SOAP. É um padrão bastante interessante, amplo e flexível. Trabalha com o conceito de tokens. Token são utilizados para a autenticação de usuários e assinatura de dados. O interessante desse padrão é que a utilização de tokens torna o padrão extensível, sendo possível implementar tokens proprietários. O problema é que tanto o cliente quanto o servidor precisam entender a forma do token, portanto a especificação definiu alguns tokens padrão como Username (usuário/senha), X.509, SAML e Kerberos. A utilização de tokens permite a encriptação de mensagens (ou trechos de mensagens) e sua utilização junto com um algoritmo de hash (que também é configurável) permite a assinatura de mensagens (ou trechos de mensagens).

Criando o Web Service

O primeiro passo é criar um Web Service, o que é ridículo muito simples utilizando JAX-WS:

@WebService
public class MeuServico {
 
	@WebMethod
	public void falar() {
		System.out.println("Blah");
	}
 
}

E é isso. O web service está pronto, agora precisamos publicar ele. De novo, a especificação torna isso muito fácil:

Endpoint.publish("http://localhost:9000/MeuServico", new MeuServico());

Basta acessar http://localhost:9000/MeuServico?wsdl e ver o WSDL gerado. Utilizando o SoapUI é possível testar nosso web service.

Utilizando Spring com o CXF

Embora tenha sido muito simples publicar nosso web service utilizando a classe Endpoint, é possível também utilizar o Spring para isso. A utilização do Spring trás algumas vantagens como poder mapear nosso web service no web.xml de uma webapp, poder centralizar as configurações em um XML e poder definir beans que podem ser reutilizados e injetados de forma declarativa. O CXF implementa os padrões WS-* através de interceptors. Vamos utilizar dois interceptors, um para WS-Addressing e outro para WS-Security, que vamos definir no XML do Spring:

<beans xmlns="http://www.springframework.org/schema/beans"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xmlns:jaxws="http://cxf.apache.org/jaxws"
   xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://cxf.apache.org/jaxws http://cxf.apache.org/schemas/jaxws.xsd">
 
   <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-extension-http-jetty.xml" />
 
   <jaxws:endpoint name="testService"
      implementor="net.rafaelliu.services.TestImpl"
      address="http://localhost:9000/test">
 
      <!-- Enable WS-Addressing -->
      <jaxws:features>
         <wsa:addressing xmlns:wsa="http://cxf.apache.org/ws/addressing" />
      </jaxws:features>
 
      <!-- Optional, enables logging for inbound -->
      <jaxws:inInterceptors>
         <bean class="org.apache.cxf.interceptor.LoggingInInterceptor"/>
      </jaxws:inInterceptors>
 
      <!-- Optional, enables logging for outbound -->
      <jaxws:outInterceptors>
         <bean class="org.apache.cxf.interceptor.LoggingOutInterceptor"/>
      </jaxws:outInterceptors>
   </jaxws:endpoint>
 
 
   <jaxws:client name="testClient"
      address="http://localhost:9000/test"
      serviceClass="net.rafaelliu.services.Test">
 
      <jaxws:features>
         <wsa:addressing xmlns:wsa="http://cxf.apache.org/ws/addressing" />
      </jaxws:features>
 
   </jaxws:client>
 
</beans>

Agora o Endpoint pegaremos do ApplicationContext do Spring:

ClassPathXmlApplicationContext appContext = new ClassPathXmlApplicationContext("./cxf-config.xml");
Endpoint ep = (Endpoint) appContext.getBean("testService");
ep.publish();

Nesse ponto temos um serviço utilizando WS-Addressing no ar. Vamos usar um proxy client do CXF para invocar o web service:

ClassPathXmlApplicationContext appContext = new ClassPathXmlApplicationContext("./cxf-config.xml");
Test client = (Test) appContext.getBean("testClient");
client.doTest();

Como ficou tudo transparente, é mais do que certo o leitor duvidar que realmente algo está acontecendo. Para tirar a dúvida é possível utilizar o SoapUI mencionado, mas fica como dever de casa descobrir como ele funciona com WS-Addressing :p (quer uma dica?)

Conclusão

O CXF está entre os frameworks que mais acho legais de mexer. Muito tem arquitetado, fácil de usar, extensível e just works. Ele não é o framework de web services mais completo em termos de suporte aos padrões WS-*, mas definitivamente vale uma olhada. Foi possível ver como utilizar o CXF para publicar e consumir web services e como fazer a configuração via Spring. O padrão WS-Addressing não é assim tão comum (ou mesmo “útil”), mas futuramente quero mostrar como usar WS-Security no CXF também, o que é um pouco mais complicado e tornaria o post massante.

setProperty must be overridden by all subclasses of SOAPMessage

May 8th, 2008

Tive recentemente que fazer uma prova de conceito, usando web services. Criei uma aplicação usando JAX-WS no JBoss 4.2.2 rodando na JDK 6 da Sun e me deparei um a seguinte exceção:

java.lang.UnsupportedOperationException: setProperty must be overridden by all subclasses of SOAPMessage
at javax.xml.soap.SOAPMessage.setProperty(SOAPMessage.java:424)
at org.jboss.ws.core.soap.SOAPMessageImpl.<init>(SOAPMessageImpl.java:83)
at org.jboss.ws.core.soap.MessageFactoryImpl.createMessage(MessageFactoryImpl.java:161)

Pesquisando um pouco achei vários relatos similares e até uma entrada no JIRA. A classe SOAPMessage é da biblioteca SAAJ, e o problema é entre o JBoss e a JDK 6. Até a versão 6, essa biblioteca era distruida separadamente, mas na nova versão ela já vem integrada. O problema é que o JBoss já possui uma implementação própria dessa biblioteca e ao se carregar o SOAPMessage a biblioteca da Sun é utilizada, a qual está implementada de forma lançar um UnsupportedOperationException.

Uma solução é usar o esquema de endorsed da JVM. Bibliotecas endorsed são carregadas antes, tendo prioridade então sobre quaisquer outras bibliotecas. Assim, basta adicionarmos o jboss-saaj.jar (em <CONFIG_HOME>/lib) ao <JBOSS_HOME>/lib/endorsed. O script run.sh define a variável JBOSS_ENDORSED_DIRS=”$JBOSS_HOME/lib/endorsed” e cuida de setar esse diretório na propriedade java.endorsed.dirs da JVM.

Mas eu estou rodando o JBoss para JDK 6! (EDIT)

Existe uma “peculiaridade” se você estiver rodando o JBoss 5 para JDK 6 no WTP do Eclipse. O JBoss 5 usa o $JBOSS_HOME/lib/endorsed como o java.endorsed.dirs, setando esse parâmetro quando executamos o $JBOSS_HOME/bin/run.sh. O problema quando executamos via WTP é que o eclipse não usar o run.sh, portanto não seta o diretório endorsed e caímos no mesmo problema do JBoss 4.

Para resolver o problema basta clicar no “launch configuration” do servidor e adicionar o seguinte parâmetro no “VM arguments”:

-Djava.endorsed.dirs=../lib/endorsed

IMPORTANTE: O JBoss não é oficialmente suportado na JDK 6 e outros imprevistos podem vir a ocorrer. Recomenda-se utilizar a JDK 5, ao invés.