Spring WS:如何将拦截器应用于特定端点
Spring WS: How to apply Interceptor to a specific endpoint
我在 Spring 应用程序上有多个工作的 SOAP Web 服务,使用 httpBasic 身份验证,我需要在其中之一上使用 WS-Security 以允许使用以下 Soap Header 进行身份验证。
<soap:Header><wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" soap:mustUnderstand="1">
<wsse:UsernameToken xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" wsu:Id="UsernameToken-1">
<wsse:Username>username</wsse:Username>
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">password</wsse:Password>
</wsse:UsernameToken>
</wsse:Security></soap:Header>
当前的 WSConfiguration 是根据 https://github.com/spring-projects/spring-boot/blob/master/spring-boot-samples/spring-boot-sample-ws/ 给出的
@EnableWs
@Configuration
public class WebServiceConfig extends WsConfigurerAdapter {
@Bean
public ServletRegistrationBean dispatcherServlet(ApplicationContext applicationContext) {
MessageDispatcherServlet servlet = new MessageDispatcherServlet();
servlet.setApplicationContext(applicationContext);
return new ServletRegistrationBean(servlet, "/services/*");
}
@Bean(name = "SOAP1")
public DefaultWsdl11Definition defaultWsdl11Definition(XsdSchema soap1) {
DefaultWsdl11Definition wsdl11Definition = new DefaultWsdl11Definition();
wsdl11Definition.setPortTypeName("Soap1");
wsdl11Definition.setLocationUri("/soap1/");
wsdl11Definition.setTargetNamespace("http://mycompany.com/hr/definitions");
wsdl11Definition.setSchema(soap1);
return wsdl11Definition;
}
@Bean
public XsdSchema soap1() {
return new SimpleXsdSchema(new ClassPathResource("META-INF/schemas/hr.xsd"));
}
}
根据 http://spring.io/blog/2013/07/03/spring-security-java-config-preview-web-security/ 的网络安全看起来像这样
@EnableWebSecurity
@Configuration
public class CustomWebSecurityConfigurerAdapter extends
WebSecurityConfigurerAdapter {
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) {
auth
.inMemoryAuthentication()
.withUser("user1")
.password("password")
.roles("SOAP1")
.and()
.withUser("user2")
.password("password")
.roles("SOAP2");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeUrls()
.antMatchers("/soap/soap1").hasRole("SOAP1")
.antMatchers("/soap/soap2").hasRole("SOAP2")
.anyRequest().authenticated()
.and().httpBasic();
}
}
经过一番搜索,我发现Wss4J提供了一个UsernameToken认证,但是不知道怎么用。我想做的是以下
https://sites.google.com/site/ddmwsst/ws-security-impl/ws-security-with-usernametoken
但没有 XML 个带有 bean 定义的文件。
我打算做什么:
- 创建回调处理程序。
- 创建一个Wss4jSecurityInterceptor,将“
setValidationActions
”设置为"UsernameToken",将“setValidationCallbackHandler
”设置为我的回调处理程序,然后通过覆盖[=47]添加它=] addInterceptors
在我的 WebServiceConfig 上。
(我尝试过类似的方法,但我才意识到我的回调使用的是已弃用的方法)
问题:即使它有效,它也会应用于我在 "WebServiceConfig" 上的所有网络服务。
更新:
实现确实有效,但正如预期的那样,它应用于我的所有 Web 服务。我如何才能将我的拦截器仅添加到 1 个 Web 服务?
以下是我在WebServiceConfig中添加的代码
@Bean
public Wss4jSecurityInterceptor wss4jSecurityInterceptor() throws IOException, Exception{
Wss4jSecurityInterceptor interceptor = new Wss4jSecurityInterceptor();
interceptor.setValidationActions("UsernameToken");
interceptor.setValidationCallbackHandler(new Wss4jSecurityCallbackImpl());
return interceptor;
}
@Override
public void addInterceptors(List<EndpointInterceptor> interceptors) {
try {
interceptors.add(wss4jSecurityInterceptor());
} catch (Exception e) {
e.printStackTrace();
}
}
抱歉,我完全忘了回答这个问题,但万一它对某人有帮助:
我们通过创建一个新的 SmartEndpointInterceptor 并将其仅应用于我们的端点来使其工作:
public class CustomSmartEndpointInterceptor extends Wss4jSecurityInterceptor implements SmartEndpointInterceptor {
//CustomEndpoint is your @Endpoint class
@Override
public boolean shouldIntercept(MessageContext messageContext, Object endpoint) {
if (endpoint instanceof MethodEndpoint) {
MethodEndpoint methodEndpoint = (MethodEndpoint)endpoint;
return methodEndpoint.getMethod().getDeclaringClass() == CustomEndpoint.class;
}
return false;
}
}
我们没有将 wss4j bean 添加到 WebServiceConfig,而是添加了 SmartEndpointInterceptor:
@Configuration
public class SoapWebServiceConfig extends WsConfigurationSupport {
//Wss4jSecurityCallbackImpl refers to an implementation of https://sites.google.com/site/ddmwsst/ws-security-impl/ws-security-with-usernametoken
@Bean
public CustomSmartEndpointInterceptor customSmartEndpointInterceptor() {
CustomSmartEndpointInterceptor customSmartEndpointInterceptor = new CustomSmartEndpointInterceptor();
customSmartEndpointInterceptor.setValidationActions("UsernameToken");
customSmartEndpointInterceptor.setValidationCallbackHandler(new Wss4jSecurityCallbackImpl(login, pwd));
return customSmartEndpointInterceptor;
}
[...]
}
希望这已经够清楚了:)
值得注意的是,无论shouldIntercept方法的结果是什么,程序都会执行handleRequest方法。
这可能很危险,例如,在登录过程中。
在我正在开发的项目中,我们只有两个端点:
- 用户登录端点
- SomeGeneralEndpoint
登录将仅为登录目的而调用,并将生成一个令牌,我必须以某种方式从请求中解析该令牌(这是通过拦截器完成的,这是我们在应用程序中唯一需要的拦截器)。
假设我们有以下拦截器,就像 Christophe Douy 提出的那样,我们 class 感兴趣的是 UserLoginEndpoint.class
public class CustomSmartEndpointInterceptor extends Wss4jSecurityInterceptor implements SmartEndpointInterceptor {
//CustomEndpoint is your @Endpoint class
@Override
public boolean shouldIntercept(MessageContext messageContext, Object endpoint) {
if (endpoint instanceof MethodEndpoint) {
MethodEndpoint methodEndpoint = (MethodEndpoint)endpoint;
return methodEndpoint.getMethod().getDeclaringClass() == UserLoginEndpoint.class;
}
return false;
}
如果 return 为真,无论如何,那很好,将执行 handleRequest 方法中定义的逻辑。
但我的问题在哪里?
对于我的具体问题,我正在编写一个拦截器,只有在用户已经登录时才应该妨碍它。这意味着前面的代码片段应该如下
public class CustomSmartEndpointInterceptor extends Wss4jSecurityInterceptor implements SmartEndpointInterceptor {
//CustomEndpoint is your @Endpoint class
@Override
public boolean shouldIntercept(MessageContext messageContext, Object endpoint) {
if (endpoint instanceof MethodEndpoint) {
MethodEndpoint methodEndpoint = (MethodEndpoint)endpoint;
return methodEndpoint.getMethod().getDeclaringClass() != UserLoginEndpoint.class;
}
return false;
}
如果是这样,将执行 handleRequest 方法(我的实现如下)
@Override
public boolean handleRequest(MessageContext messageContext, Object endpoint) throws Exception {
System.out.println("### SOAP REQUEST ###");
InputStream is = null;
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
Document doc = null;
try {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
messageContext.getRequest().writeTo(buffer);
String payload = buffer.toString(java.nio.charset.StandardCharsets.UTF_8.name());
System.out.println(payload);
is = new ByteArrayInputStream(payload.getBytes());
DocumentBuilder builder = factory.newDocumentBuilder();
doc = builder.parse(is);
} catch (IOException ex) {
ex.printStackTrace();
return false;
}
XPath xpath = XPathFactory.newInstance().newXPath();
xpath.setNamespaceContext(new NamespaceContext() {
@Override
public String getNamespaceURI(String prefix) {
switch(prefix) {
case "soapenv":
return "http://schemas.xmlsoap.org/soap/envelope/";
case "it":
return "some.fancy.ws";
default:
return null;
}
}
@Override
public String getPrefix(String namespaceURI) {
return null;
}
@Override
public Iterator getPrefixes(String namespaceURI) {
return null;
}
});
XPathExpression expr = xpath.compile("//*//it:accessToken//text()");
Object result = expr.evaluate(doc, XPathConstants.NODE);
Node node = (Node) result;
String token = node.getNodeValue();
return authUtility.checkTokenIsValid(token);
}
但是如果 shouldIntercept returns false 会发生什么?如果 handleRequest 方法,如果你 "implements" SmartPointEndPointInterceptor 必须执行,returns true,调用链将继续;但如果它 return 为假,它将停在那里:我在第二种情况下,但是 handleRequest 仍然被执行 。
我发现的唯一解决方法是在 MessageContext 中添加一个 属性,它具有任意键和一个对应的值,该值是来自 shouldIntercept 方法的 return。
然后在 handleRequest 实现的第一行中否定该值以强制 return 为真并使调用链
if (!(Boolean)messageContext.getProperty("shouldFollow")) {
return true;
}
当然,这适用于只需要一个拦截器的项目(即,在我的例子中,只是为了验证用户是否真的登录了),还有许多其他因素可能会影响一切,但我觉得值得分享这个话题。
如果我在这里回答错误而不是打开一个新问题,我提前道歉
我在 Spring 应用程序上有多个工作的 SOAP Web 服务,使用 httpBasic 身份验证,我需要在其中之一上使用 WS-Security 以允许使用以下 Soap Header 进行身份验证。
<soap:Header><wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" soap:mustUnderstand="1">
<wsse:UsernameToken xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" wsu:Id="UsernameToken-1">
<wsse:Username>username</wsse:Username>
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">password</wsse:Password>
</wsse:UsernameToken>
</wsse:Security></soap:Header>
当前的 WSConfiguration 是根据 https://github.com/spring-projects/spring-boot/blob/master/spring-boot-samples/spring-boot-sample-ws/ 给出的
@EnableWs
@Configuration
public class WebServiceConfig extends WsConfigurerAdapter {
@Bean
public ServletRegistrationBean dispatcherServlet(ApplicationContext applicationContext) {
MessageDispatcherServlet servlet = new MessageDispatcherServlet();
servlet.setApplicationContext(applicationContext);
return new ServletRegistrationBean(servlet, "/services/*");
}
@Bean(name = "SOAP1")
public DefaultWsdl11Definition defaultWsdl11Definition(XsdSchema soap1) {
DefaultWsdl11Definition wsdl11Definition = new DefaultWsdl11Definition();
wsdl11Definition.setPortTypeName("Soap1");
wsdl11Definition.setLocationUri("/soap1/");
wsdl11Definition.setTargetNamespace("http://mycompany.com/hr/definitions");
wsdl11Definition.setSchema(soap1);
return wsdl11Definition;
}
@Bean
public XsdSchema soap1() {
return new SimpleXsdSchema(new ClassPathResource("META-INF/schemas/hr.xsd"));
}
}
根据 http://spring.io/blog/2013/07/03/spring-security-java-config-preview-web-security/ 的网络安全看起来像这样
@EnableWebSecurity
@Configuration
public class CustomWebSecurityConfigurerAdapter extends
WebSecurityConfigurerAdapter {
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) {
auth
.inMemoryAuthentication()
.withUser("user1")
.password("password")
.roles("SOAP1")
.and()
.withUser("user2")
.password("password")
.roles("SOAP2");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeUrls()
.antMatchers("/soap/soap1").hasRole("SOAP1")
.antMatchers("/soap/soap2").hasRole("SOAP2")
.anyRequest().authenticated()
.and().httpBasic();
}
}
经过一番搜索,我发现Wss4J提供了一个UsernameToken认证,但是不知道怎么用。我想做的是以下 https://sites.google.com/site/ddmwsst/ws-security-impl/ws-security-with-usernametoken 但没有 XML 个带有 bean 定义的文件。
我打算做什么:
- 创建回调处理程序。
- 创建一个Wss4jSecurityInterceptor,将“
setValidationActions
”设置为"UsernameToken",将“setValidationCallbackHandler
”设置为我的回调处理程序,然后通过覆盖[=47]添加它=]addInterceptors
在我的 WebServiceConfig 上。
(我尝试过类似的方法,但我才意识到我的回调使用的是已弃用的方法)
问题:即使它有效,它也会应用于我在 "WebServiceConfig" 上的所有网络服务。
更新:
实现确实有效,但正如预期的那样,它应用于我的所有 Web 服务。我如何才能将我的拦截器仅添加到 1 个 Web 服务?
以下是我在WebServiceConfig中添加的代码
@Bean
public Wss4jSecurityInterceptor wss4jSecurityInterceptor() throws IOException, Exception{
Wss4jSecurityInterceptor interceptor = new Wss4jSecurityInterceptor();
interceptor.setValidationActions("UsernameToken");
interceptor.setValidationCallbackHandler(new Wss4jSecurityCallbackImpl());
return interceptor;
}
@Override
public void addInterceptors(List<EndpointInterceptor> interceptors) {
try {
interceptors.add(wss4jSecurityInterceptor());
} catch (Exception e) {
e.printStackTrace();
}
}
抱歉,我完全忘了回答这个问题,但万一它对某人有帮助:
我们通过创建一个新的 SmartEndpointInterceptor 并将其仅应用于我们的端点来使其工作:
public class CustomSmartEndpointInterceptor extends Wss4jSecurityInterceptor implements SmartEndpointInterceptor {
//CustomEndpoint is your @Endpoint class
@Override
public boolean shouldIntercept(MessageContext messageContext, Object endpoint) {
if (endpoint instanceof MethodEndpoint) {
MethodEndpoint methodEndpoint = (MethodEndpoint)endpoint;
return methodEndpoint.getMethod().getDeclaringClass() == CustomEndpoint.class;
}
return false;
}
}
我们没有将 wss4j bean 添加到 WebServiceConfig,而是添加了 SmartEndpointInterceptor:
@Configuration
public class SoapWebServiceConfig extends WsConfigurationSupport {
//Wss4jSecurityCallbackImpl refers to an implementation of https://sites.google.com/site/ddmwsst/ws-security-impl/ws-security-with-usernametoken
@Bean
public CustomSmartEndpointInterceptor customSmartEndpointInterceptor() {
CustomSmartEndpointInterceptor customSmartEndpointInterceptor = new CustomSmartEndpointInterceptor();
customSmartEndpointInterceptor.setValidationActions("UsernameToken");
customSmartEndpointInterceptor.setValidationCallbackHandler(new Wss4jSecurityCallbackImpl(login, pwd));
return customSmartEndpointInterceptor;
}
[...]
}
希望这已经够清楚了:)
值得注意的是,无论shouldIntercept方法的结果是什么,程序都会执行handleRequest方法。
这可能很危险,例如,在登录过程中。
在我正在开发的项目中,我们只有两个端点:
- 用户登录端点
- SomeGeneralEndpoint
登录将仅为登录目的而调用,并将生成一个令牌,我必须以某种方式从请求中解析该令牌(这是通过拦截器完成的,这是我们在应用程序中唯一需要的拦截器)。
假设我们有以下拦截器,就像 Christophe Douy 提出的那样,我们 class 感兴趣的是 UserLoginEndpoint.class
public class CustomSmartEndpointInterceptor extends Wss4jSecurityInterceptor implements SmartEndpointInterceptor {
//CustomEndpoint is your @Endpoint class
@Override
public boolean shouldIntercept(MessageContext messageContext, Object endpoint) {
if (endpoint instanceof MethodEndpoint) {
MethodEndpoint methodEndpoint = (MethodEndpoint)endpoint;
return methodEndpoint.getMethod().getDeclaringClass() == UserLoginEndpoint.class;
}
return false;
}
如果 return 为真,无论如何,那很好,将执行 handleRequest 方法中定义的逻辑。
但我的问题在哪里?
对于我的具体问题,我正在编写一个拦截器,只有在用户已经登录时才应该妨碍它。这意味着前面的代码片段应该如下
public class CustomSmartEndpointInterceptor extends Wss4jSecurityInterceptor implements SmartEndpointInterceptor {
//CustomEndpoint is your @Endpoint class
@Override
public boolean shouldIntercept(MessageContext messageContext, Object endpoint) {
if (endpoint instanceof MethodEndpoint) {
MethodEndpoint methodEndpoint = (MethodEndpoint)endpoint;
return methodEndpoint.getMethod().getDeclaringClass() != UserLoginEndpoint.class;
}
return false;
}
如果是这样,将执行 handleRequest 方法(我的实现如下)
@Override
public boolean handleRequest(MessageContext messageContext, Object endpoint) throws Exception {
System.out.println("### SOAP REQUEST ###");
InputStream is = null;
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
Document doc = null;
try {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
messageContext.getRequest().writeTo(buffer);
String payload = buffer.toString(java.nio.charset.StandardCharsets.UTF_8.name());
System.out.println(payload);
is = new ByteArrayInputStream(payload.getBytes());
DocumentBuilder builder = factory.newDocumentBuilder();
doc = builder.parse(is);
} catch (IOException ex) {
ex.printStackTrace();
return false;
}
XPath xpath = XPathFactory.newInstance().newXPath();
xpath.setNamespaceContext(new NamespaceContext() {
@Override
public String getNamespaceURI(String prefix) {
switch(prefix) {
case "soapenv":
return "http://schemas.xmlsoap.org/soap/envelope/";
case "it":
return "some.fancy.ws";
default:
return null;
}
}
@Override
public String getPrefix(String namespaceURI) {
return null;
}
@Override
public Iterator getPrefixes(String namespaceURI) {
return null;
}
});
XPathExpression expr = xpath.compile("//*//it:accessToken//text()");
Object result = expr.evaluate(doc, XPathConstants.NODE);
Node node = (Node) result;
String token = node.getNodeValue();
return authUtility.checkTokenIsValid(token);
}
但是如果 shouldIntercept returns false 会发生什么?如果 handleRequest 方法,如果你 "implements" SmartPointEndPointInterceptor 必须执行,returns true,调用链将继续;但如果它 return 为假,它将停在那里:我在第二种情况下,但是 handleRequest 仍然被执行 。
我发现的唯一解决方法是在 MessageContext 中添加一个 属性,它具有任意键和一个对应的值,该值是来自 shouldIntercept 方法的 return。
然后在 handleRequest 实现的第一行中否定该值以强制 return 为真并使调用链
if (!(Boolean)messageContext.getProperty("shouldFollow")) {
return true;
}
当然,这适用于只需要一个拦截器的项目(即,在我的例子中,只是为了验证用户是否真的登录了),还有许多其他因素可能会影响一切,但我觉得值得分享这个话题。
如果我在这里回答错误而不是打开一个新问题,我提前道歉