这几天在做一个功能, 其实很简单. 就是调用几个外部的 API, 返回数据后进行组装然后成为新的接口. 其中一个 API 是一个很奇葩的 API, 虽然是基于 HTTP 的, 但既没有基于 SOAP 规范, 也不是 Restful 风格的接口. 还好使用它也没有复杂的场景. 只是构造出 URL, 发送一个 HTTP 的 get 请求, 然后给我返回一个 XML 结构的数据.
我使用了 Spring MVC 中的 RestTemplate 作为客户端, 然后引入了 Jackson-dataformat-xml 作为 xml 映射为对象的工具库. 由于集成外部 API 的事情已经做了很多次了, 集成这个 API 也是轻车熟路, 三下五除二就完成了.
接下来为了验证连通性, 我先在 SoapUI 里配置了该外部 API 的某个测试环境, 尝试发送了一个 Get 请求, 成功收到了 Response. 然后我把自己的程序运行起来, 尝试通过自己的程序调用该 API, 结果返回了 HTTP 500 错误, 即 "internal server error".
这可奇了怪了. 我第一反应是程序中对外部 API 的配置和 SoapUI 中的配置不一样. 我仔细对比了发送请求的 URL, 需要的 HTTP header 以及用作验证的 username 和 password 都是完全一致的. 这个问题被排除.
接下来我想再仔细看看 Response, 能否找到什么蛛丝马迹. 仔细查看了 Response 的 header 和 body, 发现 header 一切正常, body 是个空的 body, 没有提供任何的可用信息.
然后我能想到的另一个解决方案就是联系该外部 API 的团队, 让他们帮忙看看我发送了请求之后, 为什么服务器会返回 500. 但可惜这是一个很老的服务了, 找到该团队的人并且排期帮我看 log 至少要花好几天的时间了. 而且既然 SoapUI 能调用成功, 而应用程序却调用不成功, 问题多半还是出在我们这.
接下来我想既然问题有可能出在我们这, 那么肯定是 request 有差异. 由于我发的是一个 Get 请求, 没有 body 实体, URL 又完全一样, 那么问题很可能出在 request 的 header 上. 这个 API 需要 request 中包含两个自定义的 header, 而我在 SoapUI 以及自己的程序中都已经配置了. 那问题会在哪里哪?
既然在 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.
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.
来源: https://www.cnblogs.com/huang0925/p/8406420.html