新上线的服务出问题了,调用第三方的接口出现服务端响应状态码401,
赶紧查询 HTTP Code 401代表啥意思,于是找到了这篇文章 http常见的状态码,400,401,403状态码分别代表什么?
401 unauthorized,表示发送的请求需要有通过 HTTP 认证的认证信息
401是服务端响应的状态码,根据接口文档在请求header中添加 X_API_KEY用于接口验证,代码中也确实这么实现的。
而且同样的接口本地发送请求没有问题,这里是使用Hutool(3.3.2版本)工具包发送HTTP请求,调用第三方接口抓取数据。
于是想到了抓包看看,项目代码是部署在Linux服务器(IP是192.168.0.211)上的,无法使用Wireshark之类图形化工具,于是使用tcpdump命令去抓包。
tcpdump -i eth0 tcp port 80 and host 192.168.67.206 -w /tmp/httptool-206.pcap
-i eth0 指定网卡
tcp 指定协议
port 80 指定端口
host 192.168.67.206 指定ip,表示抓取192.168.67.206的主机收到和发出的数据包
-w 将抓包信息写入文件
将抓的数据包传输到本地使用Wireshark打开如下
第4行就是发出的HTTP GET请求,注意下这里发出的请求header中携带了cookie信息,而代码中并没有去设置cookie,那么这个cookie是怎么来的呢?于是先将这个cookie在本地代码中显示设置,在本地调试下,果然出现了 401 Unauthorized 异常,可能就是这个cookie导致的问题。
决定看下Hutool工具包中HttpRequest类实现源码是如何自动设置cookie的。
我们的业务代码
String result = HttpRequest.get(url) // 设置请求url
.header(X_API_KEY, apiKey) // 设置header
.timeout(TIME_OUT) // 设置超时时间
.execute().body();
上面都是设置请求需要的参数,看下HttpRequest中的execute() 方法
/**
* 执行Reuqest请求
*
* @return this
*/
public HttpResponse execute() {
return this.execute(false);
}
继续跟踪
/**
* 执行Reuqest请求
*
* @param isAsync 是否异步
* @return this
*/
public HttpResponse execute(boolean isAsync) {
//初始化URL
urlWithParamIfGet();
// 初始化 connection
initConnecton();
// 发送请求
send();
//手动实现重定向
HttpResponse httpResponse = sendRedirectIfPosible();
// 获取响应
if(null == httpResponse){
httpResponse = new HttpResponse(this.httpConnection, this.charset, isAsync, isIgnoreResponseBody());
}
return httpResponse;
}
进入到 initConnecton() 方法
/**
* 初始化网络连接
*/
private void initConnecton(){
// 初始化 connection
this.httpConnection = HttpConnection
.create(this.url, this.method, this.hostnameVerifier, this.ssf, this.timeout, this.proxy)
.header(this.headers, true); // 覆盖默认Header
//自定义Cookie
if(null != this.cookie){
this.httpConnection.setCookie(this.cookie);
}
//是否禁用缓存
if(this.isDisableCache){
this.httpConnection.disableCache();
}
//定义转发
this.httpConnection.setInstanceFollowRedirects(maxRedirectCount > 0 ? true : false);
}
this.httpConnection.setCookie(this.cookie); 可以看到如果我们显示指定了cookie,这里会通过 HttpConnection 中的 setCookie 方法进行设置
/**
* 设置Cookie
*
* @param cookie Cookie
* @return this
*/
public HttpConnection setCookie(String cookie) {
if (cookie != null) {
log.debug("Cookie: {}", cookie);
header(Header.COOKIE, cookie, true);
}
return this;
}
这里我们在代码中并没有指定cookie,那么代码中是否在其他地方调动了这个方法呢。
于是在setCookie 方法中打个断点,运行代码调试下看看,从IDEA中的Frames窗口中可以定位到调用setCookie 方法的地方,果然在 HttpConnection 的 initConn 方法中会调用setCookie 方法,从 CookiePool 中根据url里的host获取cookie。
我们看下 CookiePool 这个类,该类内部为了一个静态的Map,key是host, value是cookies字符串,CookiePool 用于模拟浏览器的Cookie,当访问后站点,记录Cookie,下次再访问这个站点时,一并提交Cookie到站点。也就是说以后的请求都会携带这个cookie。
package com.xiaoleilu.hutool.http;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
*Cookie池。此池针对所有HTTP请求可用。<br>
*此Cookie池用于模拟浏览器的Cookie,当访问后站点,记录Cookie,下次再访问这个站点时,一并提交Cookie到站点。
* @author Looly
*
*/
public class CookiePool {
//key: host, value: cookies字符串
private static Map<String, String> cookies = new ConcurrentHashMap<String, String>();
/**
* 获得某个网站的Cookie信息
* @param host 网站Host
* @return Cookie字符串
*/
public static String get(String host) {
return cookies.get(host);
}
/**
* 将某个网站的Cookie放入Cookie池
* @param host 网站Host
* @param cookie Cookie字符串
*/
public static void put(String host, String cookie) {
cookies.put(host, cookie);
}
/**
* 清空Cookie
* @since 3.0.7
*/
public static void clear(){
cookies.clear();
}
}
那么这个cookie是从哪里来的呢?继续看下 CookiePool 中的 put 方法在哪些地方被调用了,在 HttpConnection 类中找到了 storeCookie 方法
/**
* 存储服务器返回的Cookie到本地
*/
private void storeCookie() {
final String setCookie = header(Header.SET_COOKIE);
if (StrUtil.isBlank(setCookie) == false) {
log.debug("Set cookie: [{}]", setCookie);
CookiePool.put(url.getHost(), setCookie);
}
}
在HttpRequest中的 execute() 方法发送请求之后,获取响应数据的时候会调用 httpConnection.getInputStream(),获取服务端返回的信息时,从响应头中提取Set-Cookie字段的值,保存到CookiePool中。
原来是我们大部分的接口都是根据第三方接口,通过在请求header中添加 X_API_KEY用于接口验证,而有一个接口第三方并没有提供,于是我们通过模拟登录的方式登录到网站来抓取数据,就是在调动登录接口的时候,第三方服务端在响应中返回了 Set-Cookie 信息,而Hutool工具会从响应中提取 Set-Cookie信息保存在CookiePool 中,并在后续请求中携带这个cookie。
第三方网站登录接口返回的cookie
至此,问题已经定位到了,既然我们不想要这个cookie,那么可以在模拟登录调用第三方接口之后,调用CookiePool 中的put方法将host对应的cookie重置为null,这样同一个host的其他请求就不会携带cookie了。
// 清除cookie
CookiePool.put(ip, null);
总结,本文通过tcpdump抓包工具,查看完整的HTTP请求,分析了Hutool工具发送HTTP请求过程的源码,最终定位并解决了问题。
更多精彩内容请关注公众号 geekymv,喜欢请分享给更多的朋友哦」