Thanks for the response, Dave. After much trial and error, I got it working, but it was close-of-business Friday and I hadn't gotten around to posting my working version yet.
It turns out there are a lot of things I didn't understand about how CXF and WSS4J operate (especially with respect to the fact that each interceptor in the chain modifies the SoapMessage object).
Here's what my version ended up looking like.
The jaxws:endpoint declaration
Code:
<jaxws:endpoint id="mySecureService" implementor="#mySecureServiceImpl" address="/mySecureService">
<jaxws:serviceFactory>
<ref bean="ServiceFactoryBean" />
</jaxws:serviceFactory>
<jaxws:inInterceptors>
<ref bean="wsAuthenticationInterceptor" />
</jaxws:inInterceptors>
</jaxws:endpoint>
The authentication interceptor declaration:
Code:
<bean id="wsAuthenticationInterceptor" class="com.mycompany.springsupport.cxf.security.auth.WSAuthenticationInInterceptor">
<constructor-arg index="0">
<map key-type="java.lang.String" value-type="java.lang.Object">
<entry key="action" value="UsernameToken" />
<entry key="passwordType" value="PasswordText" />
<entry key="passwordCallbackClass" value="com.mycompany.springsupport.cxf.security.auth.ServerPasswordCallback" />
</map>
</constructor-arg>
<property name="authenticationManager" ref="authenticationManager"/>
</bean>
The authentication interceptor definition... notice that this is a bit different from yours in that it extends org.apache.cxf.ws.security.wss4j.WSS4JInIntercepto r instead of AbstractPhaseInterceptor
Code:
package com.mycompany.springsupport.cxf.security.auth;
import java.util.Map;
import java.util.Vector;
import org.apache.cxf.binding.soap.SoapMessage;
import org.apache.cxf.interceptor.Fault;
import org.apache.cxf.ws.security.wss4j.WSS4JInInterceptor;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.apache.ws.security.WSConstants;
import org.apache.ws.security.WSSecurityEngineResult;
import org.apache.ws.security.WSUsernameTokenPrincipal;
import org.apache.ws.security.handler.WSHandlerConstants;
import org.apache.ws.security.handler.WSHandlerResult;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.Authentication;
import org.springframework.security.AuthenticationManager;
import org.springframework.security.context.SecurityContextHolder;
import org.springframework.security.providers.UsernamePasswordAuthenticationToken;
import org.springframework.util.Assert;
public class WSAuthenticationInInterceptor extends WSS4JInInterceptor implements InitializingBean{
private AuthenticationManager authenticationManager;
public WSAuthenticationInInterceptor(){
super();
}
public WSAuthenticationInInterceptor(Map<String,Object> properties){
super(properties);
}
@Override
public void afterPropertiesSet() throws Exception {
Assert.notNull(getAuthenticationManager(), "Authentication manager must be set");
Assert.notNull(getProperties(),"Interceptor properties must be set, even if empty");
}
/**
* @return the authenticationManager
*/
public AuthenticationManager getAuthenticationManager() {
return authenticationManager;
}
/**
* @param authenticationManager the authenticationManager to set
*/
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
@Override
public void handleMessage(SoapMessage message) throws Fault {
try {
super.handleMessage(message);
Vector<WSHandlerResult> result = (Vector<WSHandlerResult>) message.getContextualProperty(WSHandlerConstants.RECV_RESULTS);
if (result != null && !result.isEmpty()) {
for (WSHandlerResult res : result) {
// loop through security engine results
for (WSSecurityEngineResult securityResult : (Vector<WSSecurityEngineResult>) res.getResults()) {
int action = (Integer) securityResult.get(WSSecurityEngineResult.TAG_ACTION);
// determine if the action was a username token
if ((action & WSConstants.UT) > 0) {
// get the principal object
WSUsernameTokenPrincipal principal = (WSUsernameTokenPrincipal) securityResult.get(WSSecurityEngineResult.TAG_PRINCIPAL);
if (principal.getPassword()==null){
principal.setPassword("");
}
Authentication authentication = new UsernamePasswordAuthenticationToken(principal.getName(), principal.getPassword());
authentication = authenticationManager.authenticate(authentication);
if (!authentication.isAuthenticated()) {
System.out.println("This user is not authentic.");
//throw new AuthenticationException( "This user is not authentic." );
}
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
}
} catch (RuntimeException ex) {
Logger.getLogger(WSAuthenticationInterceptor.class).log(Level.ERROR, ex.getMessage(), ex);
throw ex;
}
}
}
What's really important is the call to
Code:
super.handleMessage(message)
at the beginning of my handleMessage() implementation. It turns out that org.apache.cxf.ws.security.wss4j.WSS4JInIntercepto r.handleMessage() is what populates the RECV_RESULTS contextual property (remember how I said earlier that the interceptor chain modifies the message?) With that call in place, I was able to generate an authentication token and from there, my access decision manager and voters did their thing and the whole authentication system fell into place 
Tested and verified with SoapUI for a user who had permissions and a user who did not. Both worked as expected.