HTTP 重定向原理
HTTP 协议中,Server 端可以通过 HTTP 状态码 + Location 响应头 的方式告知 Client, 当前访问的地址被移除,请访问新的资源 ;如下所示:
- 301 状态码 告知 Client 当前资源被永久移除
- Location 响应头 告知 Client 当前访问的资源被移到了什么地方
可以看到,当前请求还是有响应结果的,Client 可以选择 显示响应结果 ,或者 跳转到新的资源地址
$ curl -v http://h2.kail.xyz/
> GET / HTTP/1.1
> ...
>
< HTTP/1.1 301 Moved Permanently
< Location: https://h2.kail.xyz/
< ...
<html>
<head><title>301 Moved Permanently</title></head>
<body>
<center><h1>301 Moved Permanently</h1></center>
<hr><center>openresty/1.19.9.1</center>
</body>
</html>
注意 1: 重定向是 针对 URL 资源的,不是针对域名 ;
- 比如:Server 端返回的 Location 值,完全可以是
Location: https://h2.kail.xyz/other-resource
- 即 访问的是
http://h2.kail.xyz/
,之后会跳转到https://h2.kail.xyz/other-resource
注意 2:一般重定向后的资源会转成 GET 请求,也可以通过其他状态码控制使用原始请求方式,后面会说明
- 假如原始请求是 POST,重定向后的资源会变成 GET 方式访问
HttpClient 中 Redirect 的默认行为
httpclient:4.5.6
我们一般这样创建一个默认的 HttpClient
final CloseableHttpClient httpClient = HttpClients.createDefault();
等同于
// 没有任何自定义参数
final CloseableHttpClient build = HttpClients.custom().build();
在 build()
时构造 CloseableHttpClient
,关于重定向的部分如下:
public CloseableHttpClient build() {
// ...
// 判断是否禁用重定向,redirectHandlingDisabled 默认 false,即默认支持重定向
if (!redirectHandlingDisabled) {
// 重定向策略,如果用户没有自定义,使用默认重定向策略
RedirectStrategy redirectStrategyCopy = this.redirectStrategy;
if (redirectStrategyCopy == null) {
redirectStrategyCopy = DefaultRedirectStrategy.INSTANCE;
}
// 在执行链中加入 RedirectExec 重定向执行器
execChain = new RedirectExec(execChain, routePlannerCopy, redirectStrategyCopy);
}
// ...
}
RedirectStrategy 重定向策略
重试策略使用的默认实现是 DefaultRedirectStrategy
,主要作用就是:
- 判断是否要重定向:响应头中包含
Location
,且 状态码是301
、302
、303
、307
其中之一,HttpClient5 支持308
- 获取重定向后的地址: 获取响应头中的
Location
值 - 转换请求类型 ,后面会说明(HttpClient5 转换逻辑放到
RedirectExec
中)
RedirectExec 重定向执行器
其核心逻辑是 根据是否重定向,进行循环请求 ,简化后的伪代码如下:
public CloseableHttpResponse execute(...){
// 获取配置的最大重定向次数
final int maxRedirects = config.getMaxRedirects() > 0 ? config.getMaxRedirects() : 50;
// 循环重定向
for (int redirectCount = 0;;redirectCount++) {
// 执行请求逻辑,拿到 Response
final CloseableHttpResponse response = requestExecutor.execute(...);
// 是否开启重定向 && 当前请求资源被重定向了
if (config.isRedirectsEnabled() && this.redirectStrategy.isRedirected(...)) {
// 限制重定向次数
if (redirectCount >= maxRedirects) {
throw new RedirectException("Maximum redirects ("+ maxRedirects + ") exceeded");
}
// 获取重定向后的地址,构造新的请求,进入下次循环,重新发起请求
final HttpRequest redirect = this.redirectStrategy.getRedirect(...);
} else {
// 如果重定向没有开启,直接返回
return response;
}
}
}
禁用重定向
重定向默认是开启,如果您需要禁用 HttpClient 的重定向功能,从上面 HttpClients.custom().build()
和 RedirectExec
的伪代码中可以看出,禁用重定向有两种方式:
- 实例级别禁用 :禁止 RedirectExec 的构建,在整个请求逻辑中,没有 Redirect 相关逻辑的代码
- 请求级别禁用: RedirectExec 仍在请求处理链中,但是不进行重定向,可以控制到指定的请求
实例级别禁用
CloseableHttpClient httpClient = HttpClients.custom()
.disableRedirectHandling() // ❤ 该 HttpClient 实例不支持重定向
.build();
请求级别禁用
RequestConfig requestConfig = RequestConfig.custom()
.setRedirectsEnabled(false) // ❤ 请求配置
.build();
CloseableHttpClient httpClient = HttpClients.custom()
.setDefaultRequestConfig(requestConfig) // ❤ 默认行为
.build();
HttpRequestBase request = new HttpGet("http://h2.kail.xyz");
request.setConfig(requestConfig); // ❤ 或 每次请求前进行设置
重定向状态码的语义
状态码 | 协议规范 | 作用 |
---|
永久 vs 临时
永久: 原始资源被永久移除, 当你发起请求时,应该直接访问重定向后资源 ,客户端会缓存 301 状态,下次直接跳到新的资源地址,不会产生真实的请求
临时: 原始资源可能还在,不确定什么时候恢复, 当你发起请求时,应该先访问原始资源
测试:http://httpbin.org/status/301,第二次会走磁盘缓存
测试:http://httpbin.org/status/302,第二次仍然发起请求
301/302 vs 308/307 状态
301/302
和308/307
对应的 永久 和 临时 语义是一样的- ❤
308/307
状态码 不允许 Client 将原本为 非 GET 的请求重定向到 GET 请求上 ,即 会保留原始的请求方式 - ❤ 而
301/302
会把 POST 请求 转为 GET 请求访问Location
的值 - 测试详见下方:「HttpClient 对 状态码 的处理方式」
303 状态
- 可以理解为,原始请求的资源 和 重定向后的资源 都可以访问,重定向到的资源并不是你所请求的资源,而是对你所请求资源的一些描述
- ❤ 与 302 类似,区别是 302 只会把 POST 转成 GET 方式访问 ,303 除了 GET 和 HEAD,其他都会被转成 GET
HttpClient 对 状态码 的处理方式
不区分永久和临时的语义
HttpClient 并 不区分永久和临时的语义 ,即 每次都会事先访问原始资源,再根据请求结果重定向,相当于每次访问会产生两次请求
这里配置 301 重定向,HTTP 重定向到 HTTPs
server {
listen 80;
server_name h2.kail.xyz;
return 301 https://$server_name$request_uri;
}
测试代码
final CloseableHttpClient httpClient = HttpClients.createDefault();
HttpGet get = new HttpGet("http://h2.kail.xyz");
for (int i = 0; i < 2; i++) {
try (CloseableHttpResponse response = httpClient.execute(get);) {
EntityUtils.consume(response.getEntity());
}
}
Nginx 日志,每次调用,两次请求
172.17.0.1 - - [26/Mar/2022:18:11:58 +0000] "GET / HTTP/1.1" 301 175 "-" "Apache-HttpClient/5.1.3 (Java/1.8.0_271)"
172.17.0.1 - - [26/Mar/2022:18:11:59 +0000] "GET / HTTP/1.1" 200 23 "-" "Apache-HttpClient/5.1.3 (Java/1.8.0_271)"
172.17.0.1 - - [26/Mar/2022:18:11:59 +0000] "GET / HTTP/1.1" 301 175 "-" "Apache-HttpClient/5.1.3 (Java/1.8.0_271)"
172.17.0.1 - - [26/Mar/2022:18:11:59 +0000] "GET / HTTP/1.1" 200 23 "-" "Apache-HttpClient/5.1.3 (Java/1.8.0_271)"
区分 301、308 状态
测试代码
HttpPost post = new HttpPost("http://h2.kail.xyz");
// 这里改成了 POST
try (CloseableHttpResponse response = httpClient.execute(post);) {
EntityUtils.consume(response.getEntity());
}
301
状态时的 Nginx 日志,POST
重定向后变为了 GET
172.17.0.1 - - [26/Mar/2022:18:18:43 +0000] "POST / HTTP/1.1" 301 175 "-" "Apache-HttpClient/5.1.3 (Java/1.8.0_271)"
172.17.0.1 - - [26/Mar/2022:18:18:44 +0000] "GET / HTTP/1.1" 200 23 "-" "Apache-HttpClient/5.1.3 (Java/1.8.0_271)"
308
状态时的 Nginx 日志,重定向后 仍然是 POST
(405 是因为 Nginx 不允许对静态资源发起 POST 请求)
172.17.0.1 - - [26/Mar/2022:18:21:52 +0000] "POST / HTTP/1.1" 308 177 "-" "Apache-HttpClient/5.1.3 (Java/1.8.0_271)"
172.17.0.1 - - [26/Mar/2022:18:21:52 +0000] "POST / HTTP/1.1" 405 163 "-" "Apache-HttpClient/5.1.3 (Java/1.8.0_271)"
HttpClient 对请求转换的伪代码
httpclient:5.1.3
支持重定向的状态码 @see DefaultRedirectStrategy
switch (statusCode) {
case HttpStatus.SC_MOVED_PERMANENTLY: // 301
case HttpStatus.SC_MOVED_TEMPORARILY: // 302
case HttpStatus.SC_SEE_OTHER: // 303
case HttpStatus.SC_TEMPORARY_REDIRECT: // 307
case HttpStatus.SC_PERMANENT_REDIRECT: // 308
return true;
default:
return false;
}
状态码请求方式转换 @see RedirectExec
switch (statusCode) {
case HttpStatus.SC_MOVED_PERMANENTLY: // 301
case HttpStatus.SC_MOVED_TEMPORARILY: // 302
// 只针对 POST 请求进行转换
if (Method.POST.isSame(request.getMethod())) {
// POST 转 GET
redirectBuilder = BasicRequestBuilder.get();
} else {
// 其他类型不转
redirectBuilder = BasicRequestBuilder.copy(scope.originalRequest);
}
break;
case HttpStatus.SC_SEE_OTHER: // 303
// 非 GET && 非 HEAD,即 GET 和 HEAD 不转,其他统一转成 GET
if (!Method.GET.isSame(request.getMethod()) && !Method.HEAD.isSame(request.getMethod())) {
redirectBuilder = BasicRequestBuilder.get();
} else {
// 其他类型不转
redirectBuilder = BasicRequestBuilder.copy(scope.originalRequest);
}
break;
default:
// 307、308 不转换请求类型
redirectBuilder = BasicRequestBuilder.copy(scope.originalRequest);
}