SOAP服务长期依赖WS-Security完成消息签名和加密,但面向第三方系统开放接口时,团队更希望沿用OAuth 2.0的令牌授权流程。OAuth 2.0本身没有规定必须使用JSON或REST,它的Bearer Token只要求通过HTTP Authorization头或表单参数传递。因此SOAP与OAuth整合的可行路径很清晰:把访问令牌放到HTTP层,由统一的过滤器校验,再在SOAP消息头中保留必要的客户端上下文。本文围绕服务端校验、客户端调用和常见错误展开,帮助旧式SOAP接口平滑补齐授权能力。

一、整合前必须明确的几个边界
SOAP消息包含Envelope、Header和Body。OAuth 2.0中常见的Bearer Token规范建议放在HTTP Authorization头中。如果SOAP服务端只解析SOAP消息而不接触HTTP请求对象,令牌就无法被读取。因此必须在Web容器、ESB或API网关层增加读取HTTP头的机制。
另外要区分两种令牌用途:一种是客户端身份认证,证明调用方是谁;另一种是资源授权,证明调用方有权限访问某个SOAP操作。OAuth 2.0的access token本质上属于后者。不要用access token替代WS-Security的数字签名,除非你的服务只依赖传输层TLS加密。
如果原系统已经启用WS-Security UsernameToken,可以保留UsernameToken用于消息完整性校验,同时增加OAuth令牌用于粗粒度鉴权。两者作用域不同,不会互相冲突。同理,SOAPAction仍然用于定位具体操作,OAuth scope则用于判断该操作是否被授权。
二、在HTTP层校验OAuth 2.0 Bearer Token
最直接的方案是在SOAP端点前增加一个Servlet Filter或WCF Authorization Manager,从Authorization头中提取Bearer令牌,并向授权服务器校验。校验结果可以放入请求属性,供后续业务逻辑读取用户身份。
Java平台的Filter示例:
public class OAuthTokenFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String auth = httpRequest.getHeader("Authorization");
if (auth == null || !auth.regionMatches(true, 0, "Bearer ", 0, 7)) {
((HttpServletResponse) response).sendError(401, "Missing bearer token");
return;
}
String token = auth.substring(7);
if (!tokenService.isValid(token)) {
((HttpServletResponse) response).sendError(401, "Invalid token");
return;
}
chain.doFilter(request, response);
}
}
从Authorization头解析时要特别注意大小写。按照RFC 6750,Bearer是大小写不敏感的,但实际很多客户端只会发送首字母大写。过滤器应该使用regionMatches忽略大小写,而不是直接调用startsWith("Bearer ")。
TokenValidator需要实现令牌校验,包括签名验证、过期时间检查、audience是否匹配当前SOAP服务。如果授权服务器使用JWT,可以在本地解析并验签,避免每次请求都远程调用introspection端点。如果使用不透明令牌,则必须通过introspection请求授权服务器。
C#端WCF服务可以在ServiceAuthorizationManager中做同样处理:
public class OAuthAuthorizationManager : ServiceAuthorizationManager
{
protected override bool CheckAccessCore(OperationContext operationContext)
{
var requestProperty = (HttpRequestMessageProperty)operationContext.IncomingMessageProperties[HttpRequestMessageProperty.Name];
var auth = requestProperty.Headers[HttpRequestHeader.Authorization];
if (string.IsNullOrEmpty(auth) || !auth.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
{
return false;
}
var token = auth.Substring(7);
return TokenValidator.Validate(token);
}
}
这里没有使用WebOperationContext,因为传统SOAP服务并不走REST端点。通过HttpRequestMessageProperty读取HTTP头,能够兼容WCF的SOAP绑定。
如果服务部署在网关之后,网关可能已经完成令牌校验并剥离Authorization头。此时服务端需要信任网关转发的身份信息,通常在反向代理层设置内部请求头,例如X-User-Id。服务端应拒绝来自非网关网段的直接请求,防止攻击者伪造身份头。
三、通过SOAP Header传递OAuth上下文
有些场景下HTTP头不够用,例如消息经过多层中间件后HTTP头可能被丢弃,或需要把令牌随SOAP消息持久化到日志、审计系统。此时可以将OAuth令牌放进SOAP Header的自定义元素中。
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:auth="http://ipipp.com/oauth">
<soapenv:Header>
<auth:OAuthToken>ACCESS_TOKEN_VALUE</auth:OAuthToken>
<auth:ClientId>billing-system</auth:ClientId>
</soapenv:Header>
<soapenv:Body>
<getUserRequest>
<userId>12345</userId>
</getUserRequest>
</soapenv:Body>
</soapenv:Envelope>
服务端可以在SOAPHandler中读取这个头,校验后将ClientId和Token写入消息上下文。JAX-WS提供SOAPHandler接口,能够拦截所有入站和出站消息。下面是一个简化实现。
public class OAuthSoapHandler implements SOAPHandler<SOAPMessageContext> {
@Override
public boolean handleMessage(SOAPMessageContext context) {
Boolean outbound = (Boolean) context.get(SOAPMessageContext.MESSAGE_OUTBOUND_PROPERTY);
if (Boolean.TRUE.equals(outbound)) {
return true;
}
try {
SOAPMessage message = context.getMessage();
SOAPHeader header = message.getSOAPPart().getEnvelope().getHeader();
if (header == null) {
throw new SOAPException("Missing SOAP header");
}
// 查找OAuthToken节点并校验
String token = extractOAuthToken(header);
if (!tokenService.isValid(token)) {
return false;
}
context.put("oauth.token", token);
context.setScope("oauth.token", MessageContext.Scope.APPLICATION);
return true;
} catch (Exception e) {
return false;
}
}
}
但是让业务服务自己写SOAPHandler会带来重复代码。更好的是把解析逻辑下沉到公共库,所有SOAP服务只依赖一个starter或中间件。服务编排平台如Mule、Apache Camel也支持在流程中读取SOAP Header并调用OAuth校验服务。
如果坚持标准路线,也可以把OAuth Token打包成WS-Security BinarySecurityToken。但二进制令牌的ValueType需要自定义,且客户端库支持度不高。实际项目更常用自定义头,简单直接。
四、客户端调用SOAP并附带OAuth令牌
客户端首先要从授权服务器获取access token。对于服务到服务的SOAP调用,推荐使用client_credentials授权模式。HTTP客户端库需要同时设置Content-Type为text/xml或application/soap+xml,以及Authorization头。
Java原生HttpURLConnection示例:
URL url = new URL("http://127.0.0.1:8080/soap/userService");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "text/xml; charset=utf-8");
conn.setRequestProperty("Authorization", "Bearer " + accessToken);
conn.setDoOutput(true);
try (OutputStream os = conn.getOutputStream()) {
os.write(soapEnvelope.getBytes(StandardCharsets.UTF_8));
}
如果使用HttpClient或Spring WebClient,可以通过拦截器统一添加Authorization头,避免每个调用点手动拼写。Python端可以用requests的headers参数。
import requests
url = "http://127.0.0.1:8080/soap/userService"
headers = {
"Content-Type": "text/xml; charset=utf-8",
"Authorization": f"Bearer {access_token}"
}
response = requests.post(url, data=soap_body, headers=headers)
客户端还需要处理401响应。当access token过期或无效时,服务端应返回401 Unauthorized,并附带错误描述。客户端识别到401后可以尝试刷新令牌并重试一次。注意不要无限重试,避免令牌刷新后仍失败造成循环。
对于旧式SOAP客户端代码生成工具,例如wsimport生成的Service类,通常没有直接暴露HTTP头的方法。此时需要通过BindingProvider的RequestContext设置Authorization头。
BindingProvider provider = (BindingProvider) port;
Map<String, Object> requestContext = provider.getRequestContext();
Map<String, List<String>> headers = new HashMap<>();
headers.put("Authorization", Collections.singletonList("Bearer " + accessToken));
requestContext.put(MessageContext.HTTP_REQUEST_HEADERS, headers);
五、错误响应与安全加固
当令牌缺失或无效时,服务端应返回HTTP 401而不是200包着SOAP Fault。如果统一过滤器返回401,它不会生成SOAP Fault,而是直接输出普通HTTP错误,这符合OAuth 2.0 Bearer Token规范。但有些SOAP客户端只检查SOAP Fault,会误以为网络异常。可以在网关上同时返回一个极简SOAP Fault和401状态码,提高兼容性。
另一种错误是403 Forbidden,表示令牌有效但无权调用当前SOAP操作。这需要在业务层或策略引擎中判断scopes。OAuth 2.0的scope值可能与SOAP操作名不对应,建议在授权服务器中配置operation:getUser形式的scope,服务端校验时与当前SOAPAction映射。
安全方面,Bearer Token必须依赖TLS传输。SOAP服务如果使用HTTP明文传输,Authorization头会被中间人截获。不要把access token写进URL查询串,也不要写入WSDL的endpoint地址中。日志记录时应脱敏Authorization头和SOAP Header中的令牌值。
最后还要考虑令牌撤销。对于持续几分钟的短期令牌影响不大;对于长期令牌,服务端应在每次请求时通过introspection或本地撤销列表确认令牌未被吊销。使用JWT时,无法直接撤销单个JWT,除非引入短过期时间和refresh token流程。