【发布时间】:2008-12-18 13:51:39
【问题描述】:
我正在实现一个将由外部 Java 和 PHP 客户端使用的 JAX-WS Web 服务。
客户端必须使用存储在每个客户端数据库中的用户名和密码进行身份验证。
最好使用哪种身份验证机制来确保其他客户端可以使用它?
【问题讨论】:
标签: java web-services security
我正在实现一个将由外部 Java 和 PHP 客户端使用的 JAX-WS Web 服务。
客户端必须使用存储在每个客户端数据库中的用户名和密码进行身份验证。
最好使用哪种身份验证机制来确保其他客户端可以使用它?
【问题讨论】:
标签: java web-services security
对于我们的 Web 服务身份验证,我们采用双重方法,以确保具有不同先决条件的客户端能够进行身份验证。
请注意,我们网络服务的所有流量都通过 SSL 安全连接进行路由。因此,嗅探密码是不可能的。当然,也可以选择带摘要的 HTTP 身份验证 - 有关更多信息,请参阅 this interesting site。
回到我们的例子:
//First, try authenticating against two predefined parameters in the HTTP
//Request Header: 'Username' and 'Password'.
public static String authenticate(MessageContext mctx) {
String s = "Login failed. Please provide a valid 'Username' and 'Password' in the HTTP header.";
// Get username and password from the HTTP Header
Map httpHeaders = (Map) mctx.get(MessageContext.HTTP_REQUEST_HEADERS);
String username = null;
String password = null;
List userList = (List) httpHeaders.get("Username");
List passList = (List) httpHeaders.get("Password");
// first try our username/password header authentication
if (CollectionUtils.isNotEmpty(userList)
&& CollectionUtils.isNotEmpty(passList)) {
username = userList.get(0).toString();
password = passList.get(0).toString();
}
// No username found - try HTTP basic authentication
if (username == null) {
List auth = (List) httpHeaders.get("Authorization");
if (CollectionUtils.isNotEmpty(auth)) {
String[] authArray = authorizeBasic(auth.get(0).toString());
if (authArray != null) {
username = authArray[0];
password = authArray[1];
}
}
}
if (username != null && password != null) {
try {
// Perform the authentication - e.g. against credentials from a DB, Realm or other
return authenticate(username, password);
} catch (Exception e) {
LOG.error(e);
return s;
}
}
return s;
}
/**
* return username and password for basic authentication
*
* @param authorizeString
* @return
*/
public static String[] authorizeBasic(String authorizeString) {
if (authorizeString != null) {
StringTokenizer st = new StringTokenizer(authorizeString);
if (st.hasMoreTokens()) {
String basic = st.nextToken();
if (basic.equalsIgnoreCase("Basic")) {
String credentials = st.nextToken();
String userPass = new String(
Base64.decodeBase64(credentials.getBytes()));
String[] userPassArray = userPass.split(":");
if (userPassArray != null && userPassArray.length == 2) {
String userId = userPassArray[0];
String userPassword = userPassArray[1];
return new String[] { userId, userPassword };
}
}
}
}
return null;
}
使用我们预定义的“用户名”和“密码”参数的第一次身份验证对于使用SOAP-UI 的集成测试人员特别有用(尽管我不完全确定是否无法使用 HTTP 使用 SOAP-UI基本身份验证)。然后第二次身份验证尝试使用 HTTP Basic 身份验证提供的参数。
为了拦截对 Web 服务的每次调用,我们在每个端点上定义了一个处理程序:
@HandlerChain(file = "../../../../../handlers.xml")
@SchemaValidation(handler = SchemaValidationErrorHandler.class)
public class DeliveryEndpointImpl implements DeliveryEndpoint {
handler.xml 看起来像:
<handler-chains xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee">
<handler-chain>
<handler>
<handler-name>AuthenticationHandler</handler-name>
<handler-class>mywebservice.handler.AuthenticationHandler</handler-class>
</handler>
</handler-chain>
</handler-chains>
如您所见,处理程序指向一个 AuthenticationHandler,它拦截对 Web 服务端点的每次调用。这是身份验证处理程序:
public class AuthenticationHandler implements SOAPHandler<SOAPMessageContext> {
/**
* Logger
*/
public static final Log log = LogFactory
.getLog(AuthenticationHandler.class);
/**
* The method is used to handle all incoming messages and to authenticate
* the user
*
* @param context
* The message context which is used to retrieve the username and
* the password
* @return True if the method was successfully handled and if the request
* may be forwarded to the respective handling methods. False if the
* request may not be further processed.
*/
@Override
public boolean handleMessage(SOAPMessageContext context) {
// Only inbound messages must be authenticated
boolean isOutbound = (Boolean) context
.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY);
if (!isOutbound) {
// Authenticate the call
String s = EbsUtils.authenticate(context);
if (s != null) {
log.info("Call to Web Service operation failed due to wrong user credentials. Error details: "
+ s);
// Return a fault with an access denied error code (101)
generateSOAPErrMessage(
context.getMessage(),
ServiceErrorCodes.ACCESS_DENIED,
ServiceErrorCodes
.getErrorCodeDescription(ServiceErrorCodes.ACCESS_DENIED),
s);
return false;
}
}
return true;
}
/**
* Generate a SOAP error message
*
* @param msg
* The SOAP message
* @param code
* The error code
* @param reason
* The reason for the error
*/
private void generateSOAPErrMessage(SOAPMessage msg, String code,
String reason, String detail) {
try {
SOAPBody soapBody = msg.getSOAPPart().getEnvelope().getBody();
SOAPFault soapFault = soapBody.addFault();
soapFault.setFaultCode(code);
soapFault.setFaultString(reason);
// Manually crate a failure element in order to guarentee that this
// authentication handler returns the same type of soap fault as the
// rest
// of the application
QName failureElement = new QName(
"http://yournamespacehere.com", "Failure", "ns3");
QName codeElement = new QName("Code");
QName reasonElement = new QName("Reason");
QName detailElement = new QName("Detail");
soapFault.addDetail().addDetailEntry(failureElement)
.addChildElement(codeElement).addTextNode(code)
.getParentElement().addChildElement(reasonElement)
.addTextNode(reason).getParentElement()
.addChildElement(detailElement).addTextNode(detail);
throw new SOAPFaultException(soapFault);
} catch (SOAPException e) {
}
}
/**
* Handles faults
*/
@Override
public boolean handleFault(SOAPMessageContext context) {
// do nothing
return false;
}
/**
* Close - not used
*/
@Override
public void close(MessageContext context) {
// do nothing
}
/**
* Get headers - not used
*/
@Override
public Set<QName> getHeaders() {
return null;
}
}
在 AuthenticationHandler 中,我们调用了上面进一步定义的 authenticate() 方法。请注意,我们会创建一个名为“Failure”的手动 SOAP 错误,以防身份验证出现问题。
【讨论】:
Basic WS-Security 可以与插入 JAAS 的 Java 和 PHP 客户端(以及其他客户端)一起使用,以提供数据库后端。如何实现这种取决于您的容器。使用 @RolesAllowed 注释来注释您的 Web 服务方法,以控制调用用户必须具有的角色。所有 J2EE 容器都将提供一些机制来指定应该针对哪些 JAAS 领域用户进行身份验证。例如,在 Glassfish 中,您可以使用管理控制台来管理领域、用户和组。然后在您的 application.xml 中指定领域和组到角色的映射。
Here 是有关如何在 Glassfish 上实现此目的的一些详细信息
在 JBoss 中使用 JBoss WS,会更容易。
您使用的是什么 JAX-WS 实现以及在哪个容器中?
【讨论】:
有没有办法独立于当前容器?我想定义哪个类负责授权。该类可以调用数据库或在其他地方有密码。
【讨论】: