Introduction
WSS4J can be used for securing web services deployed in virtually any application server, but it includes special support for Axis. WSS4J ships with handlers that can be used in Axis-based web services for an easy integration. These handlers can be added to the service deployment descriptor (wsdd file) to add a WS-Security layer to the web service. This is a step by step tutorial for deploying a simple service with Username Token.
Prereqs
To run this tutorial, you must install a JDK (of course). I suggest JDK 1.4.2_04 or 1.5.0. Then you need an application server. I’ve personally used version jakarta-tomcat-4.1.31. Then you need to download and install Axis (version 1.2) and WSS4J. Getting hold of WSS4J and the other jars you may need can be quite tricky. One way is to download Maven and checkout and build WSS4J through it. That’s what I did (not without problems though).
If you have problems getting the needed jar files let me know and I'll try to add them to this space for download. I've compiled the wss4j.jar package and made it available for download here.
You don’t really need a Java code editor, but it helps. Personally I use Eclipse and Lomboz (a J2EE plug-in for Eclipse).
Installing WSS4J
- Download the WSS4J binaries or build it from sources
- Copy the contents (the jar files) of the WSS4J lib directory to your Axis WEB-INF/lib directory. Many jar files will already exist. Most of them will already exist there but you can just overwrite them all.
- You may need to restart Tomcat unless you have automatic deployment/class loading turned on. Check the Axis Happiness Page (typically at http://localhost:8080/axis), make sure that the XML Security (xmlsec.jar) is listed under the "Optional Components" section.
Creating the service
- This tutorial will secure the StockQuoteService which ships with the sample code with Axis. If you deploy the sample web apps that ships with Axis you don’t need to do anything more. Look at the Axis docs on how to install it properly. Unless you have one already, create a deployment descriptor (deploy.wsdd) file with the following contents:
<deployment xmlns="http://xml.apache.org/axis/wsdd/" xmlns:java="http://xml.apache.org/axis/wsdd/providers/java">
<service name="stock-wss-01" provider="java:RPC" style="document" use="literal">
<parameter name="className" value="samples.stock.StockQuoteService"/>
<parameter name="allowedMethods" value="getQuote"/>
<parameter name="scope" value="application"/>
</service>
</deployment>
It doesn’t matter where you put this file.
- deploy the service (using AxisAdmin):
java org.apache.axis.client.AdminClient -lhttp://localhost:8080/axis/services/AdminService deploy.wsdd
The AdminClient class depends on a load of jar-files, so to deploy this I created a bat-file that looked like this:
setlocal
set CLASSPATH=%CLASSPATH%;C:\axis-1_2RC2\lib\axis.jar;C:\axis-1_2RC2\lib\jaxrpc.jar;C:\axis-1_2RC2\lib\commons-logging.jar;C:\axis-1_2RC2\lib\commons-discovery.jar;C:\axis-1_2RC2\lib\saaj.jar;
java org.apache.axis.client.AdminClient -lhttp://localhost:8080/axis/services/AdminService test-deploy.wsdd
endlocal
You have to change the bat-file to reflect where you’ve put your axis jar files naturally.
Creating the Client
- Use WSDL2Java to generate the client service bindings (a number of soap client classes):
java org.apache.axis.wsdl.WSDL2Java -o . -Nhttp://fox:8080/axis/services/stock-wss-01 samples.stock.client http://fox:8080/axis/services/stock-wss-01?wsdl
Again, the wsdl2java needs a number of jar files to work properly, so I created a new bat-file to help out with that. The bat-file looks like this:
setlocal
set CLASSPATH=%CLASSPATH%;C:\axis-1_2RC2\lib\axis.jar;C:\axis-1_2RC2\lib\jaxrpc.jar;C:\axis-1_2RC2\lib\commons-logging.jar;C:\axis-1_2RC2\lib\commons-discovery.jar;C:\axis-1_2RC2\lib\saaj.jar;C:\axis-1_2RC2\lib\wsdl4j.jar;
java org.apache.axis.wsdl.WSDL2Java -o . -Nhttp://localhost:8080/axis/services/stock-wss-01 samples.stock.client http://localhost:8080/axis/services/stock-wss-01?wsdl
endlocal
A bunch of java classes will be created under samples/stock/client, including the StockQuoteServiceServiceLocator. - Write a simple java console application that uses the generated service locator. For example:
package samples.stock.client;
import java.rmi.RemoteException;
import javax.xml.rpc.ServiceException;
public class StockServiceClient {
public StockServiceClient() {
}
public static void main(String[] args) throws ServiceException, RemoteException {
if (args.length == 0) {
System.out.println("Usage:\njava StockServiceClient [symbol]");
return;
}
StockQuoteServiceService locator = new StockQuoteServiceServiceLocator();
StockQuoteService service = locator.getStockWss01();
float quote = service.getQuote(args[0]);
System.out.println("stock quote service returned " + args[0] + ": " + quote);
}
} - run the client:
java samples.stock.client.StockServiceClient XXX
If all went well, you should get the result:
stock quote service returned IBM: 55.25
When using "XXX" as parameter, the service won't try to go out on the Internet to get the real quotes, but just returns a float with the value of 55.25.
What you’ve created so far is a very simple web service with a simple client that calls it. WSS4J has not been used yet, so this web service call is unprotected. Now it’s time to add a Username Token to the soap call.
Configuring the Service for Username Token
- Modify the deployment descriptor you created above to look like this:
<deployment xmlns="http://xml.apache.org/axis/wsdd/" xmlns:java="http://xml.apache.org/axis/wsdd/providers/java">
<service name="stock-wss-01" provider="java:RPC" style="document" use="literal">
<requestFlow>
<handler type="java:org.apache.ws.axis.security.WSDoAllReceiver">
<parameter name="passwordCallbackClass" value="PWCallback"/>
<parameter name="action" value="UsernameToken"/>
</handler>
</requestFlow>
<parameter name="className" value="samples.stock.StockQuoteService"/>
<parameter name="allowedMethods" value="getQuote"/>
<parameter name="scope" value="application"/>
</service>
</deployment>
WSDoAllReceiver is an Axis handler located in wss4j.jar package. This is the standard way to deploy an Axis handler. For more details please refer to the Axis handler for WSS4J documentation. - Create a class named PWCallback.java and compile it and put the resulting PWCallback.class file into your Axis WEB-INF/classes directory. In this example I used the default package for simplicity, but you might need to use the fully qualified class name (be consistent with the deployment descriptor).
The following code snippet shows a simple password callback class:
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 PWCallback implements CallbackHandler {
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
for (int i = 0; i < callbacks.length; i++) {
if (callbacks[i] instanceof WSPasswordCallback) {
WSPasswordCallback pc = (WSPasswordCallback)callbacks[i];
// set the password given a username
if ("wss4j".equals(pc.getIdentifer())) {
pc.setPassword("security");
}
} else {
throw new UnsupportedCallbackException(callbacks[i], "Unrecognized Callback");
}
}
}
} - Redeploy the service using the bat file you created earlier. Your service should now be expecting a WSS Username Token in the incoming soap request, and clients should send the username "wss4j" and password "security" to get through.
Configuring the Client for Username Token
- run the client we created again:
java samples.stock.client.StockServiceClient IBM
You should now get an error:
Exception in thread "main" AxisFault
faultCode: {http://schemas.xmlsoap.org/soap/envelope/}Server.generalException
faultSubcode:
faultString: WSDoAllReceiver: Request does not contain required Security header
This is because your client is not configured to send a Username Token yet, so the service is rejecting the request. To fix this, you need to create a callback class in the client, which adds the Username Token to the outgoing soap request. - Create a deployment descriptor file (client_deploy.wsdd) for the client:
<deployment xmlns="http://xml.apache.org/axis/wsdd/" xmlns:java="http://xml.apache.org/axis/wsdd/providers/java">
<transport name="http" pivot="java:org.apache.axis.transport.http.HTTPSender"/>
<globalConfiguration >
<requestFlow >
<handler type="java:org.apache.ws.axis.security.WSDoAllSender" >
<parameter name="action" value="UsernameToken"/>
<parameter name="user" value="wss4j"/>
<parameter name="passwordCallbackClass" value="samples.stock.client.PWCallback"/>
<parameter name="passwordType" value="PasswordDigest"/>
</handler>
</requestFlow >
</globalConfiguration >
</deployment> - Create the samples.stock.client.PWCallback class:
package samples.stock.client;
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;
/**
* PWCallback for the Client
*/
public class PWCallback implements CallbackHandler {
/**
* @see javax.security.auth.callback.CallbackHandler#handle(javax.security.auth.callback.Callback[])
*/
public void handle(Callback[] callbacks) throws IOException,
UnsupportedCallbackException {
for (int i = 0; i < callbacks.length; i++) {
if (callbacks[i] instanceof WSPasswordCallback) {
WSPasswordCallback pc = (WSPasswordCallback)callbacks[i];
// set the password given a username
if ("wss4j".equals(pc.getIdentifer())) {
pc.setPassword("security");
}
} else {
throw new UnsupportedCallbackException(callbacks[i], "Unrecognized Callback");
}
}
}
} - Define the system property axis.ClientConfigFile for your client:
java -Daxis.ClientConfigFile=client_deploy.wsdd -classpath $AXISCLASSPATH samples.stock.client.StockServiceClient
Make sure that your CLASSPATH includes the jar files under WEB-INF/lib.
Another way to do this is to specify the wsdd file in your StockServiceClient to the service locator programmatically:
...
import org.apache.axis.EngineConfiguration;
import org.apache.axis.configuration.FileProvider;
...
EngineConfiguration config = new FileProvider("client_deploy.wsdd");
StockQuoteServiceService locator = new StockQuoteServiceServiceLocator(config);
... - Run the client, you should get no errors:
stock quote service returned XXX: 55.25
Your client is now sending a Username Token in the wsse request header with the username "wss4j" (see client_deploy.wsdd) and password "security" (see the PWCallback implementation).
Another way to do this is to have the client application set the username and CallbackHandler implementation programmatically instead of using the client_deploy.wsdd file:
...
import org.apache.axis.client.Stub;
...
Remote remote = locator.getPort(StockQuoteService.class);
Stub axisPort = (Stub)remote;
axisPort._setProperty(UsernameToken.PASSWORD_TYPE, WSConstants.PASSWORD_DIGEST);
axisPort._setProperty(WSHandlerConstants.USER, "wss4j");
axisPort._setProperty(WSHandlerConstants.PW_CALLBACK_REF, pwCallback);
where "pwCallback" is a reference to a PWCallback implementation. See the Axis handler for WSS4J documentation for more details on this.
UPDATE: I've tried to set the callback using the techinque above, but without much success. I'll continue trying, and when I get it working I'll update this section again :)
UPDATE 2: After some testing and teaking and good ideas from people, I got the thing above working. It's all explained in another blog post. - Try modifying your client's PWCallback to return the wrong password, or send the wrong username. The service should reject your requests.
Nenhum comentário:
Postar um comentário