这几天在作一个功能,其实很简单。就是调用几个外部的API,返回数据后进行组装而后成为新的接口。其中一个API是一个很奇葩的API,虽然是基于HTTP的,但既没有基于SOAP规范,也不是Restful风格的接口。还好使用它也没有复杂的场景。只是构造出URL,发送一个HTTP的get请求,而后给我返回一个XML结构的数据。html
我使用了Spring MVC中的RestTemplate做为客户端,而后引入了Jackson-dataformat-xml做为xml映射为对象的工具库。因为集成外部API的事情已经作了不少次了,集成这个API也是轻车熟路,三下五除二就完成了。java
接下来为了验证连通性,我先在SoapUI里配置了该外部API的某个测试环境,尝试发送了一个Get请求,成功收到了Response。而后我把本身的程序运行起来,尝试经过本身的程序调用该API,结果返回了HTTP 500错误,即“internal server error”。spring
这可奇了怪了。我第一反应是程序中对外部API的配置和SoapUI中的配置不同。我仔细对比了发送请求的URL,须要的HTTP header以及用做验证的username和password都是彻底一致的。这个问题被排除。apache
接下来我想再仔细看看Response,可否找到什么蛛丝马迹。仔细查看了Response的header和body,发现header一切正常,body是个空的body,没有提供任何的可用信息。服务器
而后我能想到的另外一个解决方案就是联系该外部API的团队,让他们帮忙看看我发送了请求以后,为何服务器会返回500。但惋惜这是一个很老的服务了,找到该团队的人而且排期帮我看log至少要花好几天的时间了。并且既然SoapUI能调用成功,而应用程序却调用不成功,问题多半仍是出在咱们这。app
接下来我想既然问题有可能出在咱们这,那么确定是request有差别。因为我发的是一个Get请求,没有body实体,URL又彻底同样,那么问题极可能出在request的header上。这个API须要request中包含两个自定义的header,而我在SoapUI以及本身的程序中都已经配置了。那问题会在哪里哪?less
既然在SoapUI里没法重现这个问题,我就使用了Chrome插件版的POSTMAN,经过它配置了该API的调用。而后奇迹出现了,我居然在POSTMAN中重现了这个问题。当我看到在POSTMAN也返回了500 error后,我思考了5秒钟,猜到了缘由。问题极可能是出在了Authentication这个header上面。工具
要说这个问题,还要从HTTP的Basic Authentication提及。Basic Authentication是HTTP实现访问控制的最简单的一种技术。HTTP Client端会将用户名和密码组合后使用Base64加密,生成key为‘Authentication’,value为‘Basic BASE64CODE’的HTTP header,发送给服务器端以便进行Basic认证方式。测试
但这个经典的Basic Authentication是要经历两步的。第一步,客户端发送不带Authentication header的HTTP请求,服务器检查后发现受访的资源须要认证,就会返回HTTP Status 401,表示未受权,客户端发现服务器端返回401后,会再构造一个新的请求,此次包含了Authentication header,服务器接收后验证经过,返回资源。加密
那么我在本身的应用程序和POSTMAN中调用返回500 internal server error的缘由是当第一次给Server发送不带Authentication header的HTTP请求时,Server居然返回了HTTP Status 500。其实它应该返回401,这样HTTP Client会再发一个包含了Authentication的新请求。因为它返回了500,HTTP Client认为服务器有问题,就中止处理了。
那为何在SoapUI中调用能够成功那?那是由于SoapUI使用的Http client在发第一次请求时就已经设置了Authentication header,因此就没有问题。这样能够避免重复发请求的现象。这种行为叫作‘preemptive authentication’(抢先验证),在SoapUI中你能够选择是否启用该行为。具体能够参见How To Authenticate SOAP Requests in SoapUI。
因此问题的根源在于该外部API在实现Basic Authentication时没有彻底遵循规范,这锅咱们不背。
解决方案有两种。第一种是让该外部API遵循Basic Authentication的规范,若是请求未受权应该返回401而不是500。不过我说过这是一个很古老的API了,让它们改要等到猴年马月了。
第二种就是个人应用程序在给该外部API发送请求时,第一次就设置Authentication header。咱们用的是RestTemplate,而RestTemplate底层使用的是Apache Http Client 4.0+版本。要注入这个header很简单,在实例化RestTemplate后,给其多加一个Intecepter。
1 2 |
restTemplate.getInterceptors().add( new BasicAuthorizationInterceptor("username", "password")); |
加上这一行代码后,运行程序,顺利的获得了Response,世界清静了。
最后一个问题,为何Http Client当配置了用户名和密码后,不主动的启用‘preemptive authentication’那?毕竟能够少发不少请求啊。这是Apache官方给出的缘由:
HttpClient does not support preemptive authentication out of the box, because if misused or used incorrectly the preemptive authentication can lead to significant security issues, such as sending user credentials in clear text to an unauthorized third party. Therefore, users are expected to evaluate potential benefits of preemptive authentication versus security risks in the context of their specific application environment. Nonetheless one can configure HttpClient to authenticate preemptively by prepopulating the authentication data cache.
扩展阅读: