Nginx HTTPS端口接收HTTP请求的400错误:原理、排查与解决方案

📅 2026/7/31 9:36:11 👁️ 阅读次数 📝 编程学习
Nginx HTTPS端口接收HTTP请求的400错误:原理、排查与解决方案

1. 问题现场:当HTTP请求撞上HTTPS端口

如果你在配置Nginx反向代理时,在浏览器里访问一个本应走HTTPS的地址,却突然蹦出来一个“400 Bad Request”的错误页面,并且Nginx的错误日志里赫然写着The plain HTTP request was sent to HTTPS port,别慌,这几乎是每个运维和开发在搭建HTTPS服务时都会踩的“经典坑”。这个错误直白得有点可爱:一个明文的HTTP请求,被发送到了专门处理加密HTTPS流量的端口上。想象一下,你拿着普通信封(HTTP)想去寄挂号信(HTTPS)的柜台,柜员当然会拒绝你。

这个问题看似简单,但其背后的原因和解决方案却涉及到Nginx配置的核心逻辑、网络协议的本质区别,以及我们在架构设计时容易忽略的细节。它不仅仅是一个配置错误,更是一个理解客户端、代理服务器、上游服务三者之间通信协议的绝佳切入点。今天,我们就来彻底拆解这个报错,从原理到实操,从根因到多种解决方案,让你不仅能快速修复问题,更能深刻理解Nginx作为反向代理在处理HTTP/HTTPS混合流量时的行为模式。

2. 核心原理:HTTP与HTTPS的端口“隔离墙”

要解决问题,必须先理解问题背后的协议逻辑。这个报错的根源在于协议与端口的严格绑定关系,以及Nginx监听端口的处理机制

2.1 端口与协议的默认约定

在网络世界中,端口号就像大楼里的房间号,而协议(HTTP/HTTPS)则规定了进入房间后使用的“语言”或“通信规则”。有一些端口号被IANA(互联网号码分配机构)赋予了默认的协议:

  • 80端口: 默认用于HTTP协议。这是一种明文传输协议,数据在传输过程中如同明信片,可以被中间网络设备轻易查看和篡改。
  • 443端口: 默认用于HTTPS协议。这是在HTTP之下加入了SSL/TLS加密层的安全协议,数据被加密传输,如同密封的挂号信,保证了机密性和完整性。

当客户端(如浏览器)向服务器的443端口发起连接时,它预期服务器端已经准备好了SSL/TLS加密环境。连接建立后,客户端会立即开始SSL/TLS握手过程(发送Client Hello消息)。反之,如果客户端向80端口发起连接,它预期进行的是普通的明文HTTP对话。

2.2 Nginx的“先入为主”判断

Nginx作为一个高性能的Web服务器和反向代理,它在监听一个端口(例如443)时,需要决定如何处理进入这个端口的流量。Nginx的判断逻辑是基于连接建立后的最初几个字节

  1. 监听配置: 你在nginx.conf中写下了listen 443 ssl;。这行配置告诉Nginx:“请在443端口上监听,并且准备好进行SSL/TLS解密工作。”
  2. 连接进入: 一个TCP连接到达服务器的443端口。
  3. 协议探测: Nginx会读取该连接发送过来的第一个数据包。它期待看到的是SSL/TLS握手的开头(例如字节0x16表示“握手”,0x03表示TLS版本等)。
  4. 错误发生: 如果Nginx发现客户端发来的第一个数据包根本不是SSL/TLS握手报文,而是一个明文的HTTP请求(例如GET / HTTP/1.1\r\nHost: ...),它就会立刻断定:“这是一个普通的HTTP请求,但它走错了门,来到了HTTPS的端口。”于是,Nginx直接返回400 Bad Request,并在错误日志中记录The plain HTTP request was sent to HTTPS port

关键点: 这个错误是Nginx在应用层判断出来的,而不是TCP/IP层。连接本身已经成功建立(TCP三次握手完成),问题出在后续的应用层协议不匹配。

2.3 反向代理场景下的典型诱因

在简单的静态网站中,很少直接遇到此问题。但在反向代理场景下,它变得非常普遍,主要有以下几个触发场景:

  1. 上游服务重定向: 这是最常见的原因。你配置Nginx将https://your-domain.com的请求代理到上游一个HTTP服务(如http://192.168.1.100:8080)。如果这个上游服务在它的响应中(例如因为登录验证、错误处理)返回了一个301/302重定向,并且这个重定向的Location头是HTTP地址(如http://192.168.1.100:8080/new-path),那么浏览器就会直接根据这个地址发起新的请求。如果这个新请求的地址恰好指向了Nginx的443端口(例如因为DNS解析或配置指向),就会触发上述错误。
  2. 客户端错误拼接URL: 用户或前端代码手动拼接了一个错误的URL,例如本应是https://example.com/api,却写成了http://example.com:443/api。显式指定了http协议却使用了443端口。
  3. 代理配置不一致: 在复杂的多层代理架构中,中间的某个代理错误地修改了请求的协议头(如X-Forwarded-Proto),导致最终到达Nginx的请求信息混乱。
  4. 后端应用生成错误链接: 某些Web框架或应用在生成绝对链接时,未能正确感知到前端是通过HTTPS访问的,仍然生成了HTTP协议的链接。

3. 解决方案全景图:从应急到根治

面对这个报错,我们可以根据不同的场景和根本原因,采取从临时规避到彻底根治的不同层级的解决方案。下图梳理了核心的解决思路:

flowchart TD A[报错: The plain HTTP request was sent to HTTPS port] --> B{排查根因}; B --> C[“场景1: 上游服务返回HTTP重定向”]; B --> D[“场景2: 客户端错误请求<br>(如 http://domain:443)”]; B --> E[“场景3: 配置错误或端口冲突”]; C --> F[“方案A: 修正上游服务<br>(推荐)”]; C --> G[“方案B: Nginx代理重写响应头<br>(proxy_redirect)”]; C --> H[“方案C: 启用HTTP/HTTPS兼容监听<br>(listen 443 ssl http2)”]; D --> I[“方案D: 强制HTTPS跳转<br>(return 301 https://)”]; D --> J[“方案E: 捕获并修正错误请求”]; E --> K[“方案F: 检查并修正Nginx配置”]; E --> L[“方案G: 检查端口占用与防火墙”]; F & G & H & I & J & K & L --> M[问题解决];

接下来,我们将对每一种方案进行详细的拆解和实操演示。

3.1 方案A:修正上游服务(治本之策)

这是最根本、最推荐的解决方案。确保你的上游应用(如Tomcat, Spring Boot, Node.js, Django应用)能够感知到它正在被一个HTTPS反向代理保护,并据此生成正确的HTTPS链接。

核心原理: 反向代理服务器(Nginx)会在将客户端请求转发给上游时,添加一些特殊的HTTP头来传递原始请求的信息。上游服务需要读取这些头来重建原始的请求URL。

关键HTTP头

  • X-Forwarded-Proto: 告知上游服务,原始客户端请求使用的协议是http还是https
  • X-Forwarded-Host: 告知原始请求的Host头。
  • X-Forwarded-Port: 告知原始请求的端口。

Nginx配置示例(传递关键头信息)

location /yourapp/ { proxy_pass http://upstream_server:8080; # 传递客户端原始协议、主机名和端口 proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Port $server_port; # 通常也会传递客户端真实IP proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }

上游服务适配(以Spring Boot为例): Spring Boot应用需要在application.propertiesapplication.yml中配置,以信任这些来自反向代理的头信息,并自动用于链接生成。

# application.yml server: # 使用X-Forwarded-*头来覆盖请求信息 forward-headers-strategy: native # 或者 framework tomcat: # 内部重定向也使用X-Forwarded-Proto use-relative-redirects: false # 解码URL中的斜杠 relaxed-path-chars: '|' relaxed-query-chars: '|'

对于其他框架:

  • Node.js (Express): 使用app.set('trust proxy', true)app.set('trust proxy', 'loopback')
  • Python (Django): 在设置文件中配置SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')并确保USE_X_FORWARDED_HOST = True
  • PHP: 需要检查$_SERVER['HTTP_X_FORWARDED_PROTO']并在代码逻辑中手动处理。

实操心得: 在微服务或容器化环境中,确保所有服务镜像都正确配置了代理头信任。这应该作为基础镜像或部署规范的一部分。我曾在一个K8s环境中排查了半天,最后发现是一个服务的Docker镜像没有更新信任代理的配置,导致其生成的内部跳转链接全是HTTP,引发连锁报错。

3.2 方案B:Nginx代理重写响应头(快速拦截)

如果上游服务暂时无法修改,或者它是一个你无法控制的第三方服务,那么可以在Nginx层面拦截并修正上游返回的重定向响应。这是最常用、最有效的临时或中期解决方案。

核心指令proxy_redirect这个指令用于重写上游服务响应头中的LocationRefresh字段。

场景模拟: 上游服务http://192.168.1.100:8080返回了一个重定向:Location: http://192.168.1.100:8080/login我们需要将其改为:Location: https://your-domain.com/login

Nginx配置示例

server { listen 443 ssl; server_name your-domain.com; ssl_certificate /path/to/cert.pem; ssl_certificate_key /path/to/key.pem; location / { proxy_pass http://192.168.1.100:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # 核心:重写上游返回的重定向URL # 格式:proxy_redirect [需要被替换的上游默认URL] [替换成的目标URL]; proxy_redirect http://192.168.1.100:8080 https://your-domain.com; # 更通用的写法,替换任何以http开头的Location头 # proxy_redirect http:// $scheme://; } }

配置解析

  • proxy_redirect http://192.168.1.100:8080 https://your-domain.com;这行配置是精确匹配。它会扫描上游响应头,如果LocationRefresh头以http://192.168.1.100:8080开头,就将其替换为https://your-domain.com
  • proxy_redirect http:// $scheme://;这是一种更“暴力”但通用的方法。它会将所有以http://开头的重定向URL,替换为当前请求使用的协议($scheme变量,在这里是https)开头。这在开发或测试环境中非常方便,但生产环境建议使用更精确的匹配,避免意外修改。

注意事项proxy_redirect默认是开启的,其默认值来源于proxy_pass指令后的URL。但默认行为通常不足以处理HTTP到HTTPS的协议转换,因此需要显式配置。另外,如果上游服务使用了相对路径进行重定向(如Location: /login),则不会触发proxy_redirect的重写,这是安全的,因为浏览器会基于当前页面的基础URL(已经是HTTPS)来补全。

3.3 方案C:启用HTTP/HTTPS兼容监听(非常规方案)

这是一个比较特殊且需要谨慎使用的方案。通过修改Nginx的监听指令,使其在同一个端口上既能处理HTTPS,又能“降级”处理明文HTTP请求。

修改监听配置: 将listen 443 ssl;改为listen 443 ssl http2;。注意,这里的关键不是http2,而是这种写法在某些Nginx版本或编译参数下,可能隐含了更宽松的协议检测。但更标准的做法是使用两个listen指令:

server { # 标准HTTPS监听 listen 443 ssl; # 添加一个“非标准”的HTTP监听在同一端口(不推荐) # listen 443; server_name your-domain.com; ... }

强烈警告在生产环境中,强烈不建议在443端口同时监听明文HTTP。这样做存在严重的安全风险:

  1. 中间人攻击: 攻击者可以拦截客户端到服务器的连接,并尝试降级为HTTP通信,从而窃取或篡改敏感数据。
  2. 协议混淆: 破坏了端口与协议的约定,可能导致客户端或中间设备(如CDN、WAF)行为异常。
  3. SSL剥离攻击: 更容易受到SSL剥离攻击,用户以为自己访问的是HTTPS,实际连接已被劫持为HTTP。

这个方案仅在某些极端调试场景下临时使用,例如上游应用极其陈旧且无法修改,同时内部网络环境绝对可信。一旦调试结束,必须恢复为仅listen 443 ssl;

3.4 方案D:强制HTTPS跳转(根除HTTP访问)

如果错误是由于用户或客户端直接访问了http://your-domain.com:443引起的,最好的办法是从源头杜绝HTTP访问。我们可以在服务器的80端口设置一个全局重定向,将所有HTTP流量强制跳转到HTTPS。

标准配置

# HTTP 服务器块,监听80端口 server { listen 80; server_name your-domain.com www.your-domain.com; # 永久重定向(301)到HTTPS版本 return 301 https://$server_name$request_uri; # 或者使用rewrite指令(效果相同) # rewrite ^(.*)$ https://$server_name$1 permanent; } # HTTPS 服务器块,监听443端口 server { listen 443 ssl; server_name your-domain.com www.your-domain.com; ssl_certificate /path/to/cert.pem; ssl_certificate_key /path/to/key.pem; ... # 其他HTTPS配置 }

工作原理: 当用户访问http://your-domain.com或错误的http://your-domain.com:443(如果DNS指向正确,访问443端口的HTTP请求会先被80端口的配置捕获吗?不一定,这取决于连接目标端口。对于显式指定:443的HTTP请求,它直接到达443端口,不会被80端口的配置处理。因此,此方案主要解决的是用户访问http://your-domain.com(无端口,默认80)的情况。

3.5 方案E:捕获并修正错误请求(精准处理)

对于直接访问http://your-domain.com:443这种“顽固”的错误请求,我们可以在443端口的server块中,通过判断$scheme变量来捕获并处理。

server { listen 443 ssl; server_name your-domain.com; ssl_certificate /path/to/cert.pem; ssl_certificate_key /path/to/key.pem; # 如果请求协议是HTTP(说明是明文请求发到了443端口) if ($scheme = "http") { # 记录一条特殊日志以便排查 access_log /var/log/nginx/http_on_443.log combined; # 强制重定向到正确的HTTPS URL return 301 https://$host$request_uri; # 注意:由于此时SSL握手未完成,这个301响应也是明文的。 # 更常见的做法是直接返回一个400或444,但重定向对用户更友好。 } ... # 正常的HTTPS处理逻辑 }

注意: 使用if指令需要小心,在Nginx中if是“邪恶的”,因为它可能破坏请求处理的某些阶段。在这个特定场景下(在server块内判断$scheme),通常是安全的。但更好的实践是使用单独的server块来监听80端口并重定向,如方案D所示。

3.6 方案F:检查并修正Nginx配置

有时候,问题就出在配置文件的笔误或逻辑冲突上。请系统性地检查你的Nginx配置:

  1. 检查监听指令: 确认你的HTTPS server块使用的是listen 443 ssl;,而不是listen 443;(缺少ssl参数)。
  2. 检查SSL证书路径: 确保ssl_certificatessl_certificate_key指令指向的文件路径正确且Nginx进程有读取权限。可以使用nginx -t测试配置,但更建议sudo nginx -T | grep -A5 -B5 \"listen 443\"来查看完整相关配置。
  3. 检查配置包含关系: 确保没有在其他地方(如/etc/nginx/conf.d/sites-enabled/)存在重复或冲突的server块定义,它们可能也监听了443端口但配置不同。
  4. 检查默认服务器: 如果有多个server块监听443端口,Nginx会根据server_name来匹配。如果没有匹配的,会使用标记为default_server的那个。检查你的默认服务器配置是否正确。

3.7 方案G:检查端口占用与防火墙

在极少数情况下,问题可能不在Nginx配置本身。

  1. 端口占用: 使用sudo netstat -tlnp | grep :443sudo ss -tlnp | grep :443命令,确认确实是Nginx进程在监听443端口,而不是Apache、Docker容器或其他程序。
  2. 防火墙/安全组: 确认云服务器安全组或本地防火墙(如firewalldufw)已放行443端口的入站流量。有时防火墙可能会干扰或修改数据包。
  3. 负载均衡器/CDN: 如果你前面有云负载均衡器(如AWS ALB、阿里云SLB)或CDN,检查它们的配置。确保它们正确地终止了HTTPS(即SSL卸载),并以HTTP协议向后端(你的Nginx)发送流量。在这种情况下,你的Nginx可能只需要监听80端口,而负载均衡器负责处理443端口。如果配置错误,负载均衡器可能将未解密的HTTPS流量或错误的HTTP流量转发到了你的后端端口。

4. 实战排查:从日志到修复的完整流程

当遇到“The plain HTTP request was sent to HTTPS port”报错时,不要盲目尝试各种方案。遵循一个系统的排查流程,可以更快地定位问题根源。

4.1 第一步:收集关键信息

  1. 完整的错误页面: 浏览器显示什么?是Nginx的默认400页面,还是上游应用的自定义错误页?
  2. Nginx错误日志: 这是最重要的线索。查看Nginx错误日志(通常位于/var/log/nginx/error.log),找到对应时间戳和客户端IP的报错记录。它通常会伴随client: [客户端IP]server: [你的域名]信息。
  3. Nginx访问日志: 查看访问日志(如/var/log/nginx/access.log),看是否有对应的请求记录。注意查看$scheme$status字段。一个发往443端口的HTTP请求,在访问日志中$scheme可能记录为http,状态码是400
  4. 浏览器开发者工具
    • 网络(Network)标签: 查看失败请求的详细信息。重点是Request URL(它是不是http://...:443?),以及响应头。
    • 查看重定向: 在请求列表中,检查是否在报400错误之前,有一个301/302重定向。点击这个重定向请求,查看其响应头中的Location字段,这个字段的值很可能就是罪魁祸首。

4.2 第二步:模拟与复现

使用命令行工具如curl来复现问题,可以排除浏览器缓存、插件等干扰。

# 模拟一个直接向443端口发送HTTP明文请求的错误场景 curl -v http://your-domain.com:443/ # 模拟正常HTTPS请求 curl -v https://your-domain.com/ # 如果怀疑是上游重定向,可以只获取响应头,并跟随重定向 curl -I -L http://your-domain.com/possible-redirect-path # 注意观察最终重定向到了哪个URL

curl -v的输出会详细显示整个HTTP对话过程,包括发送的请求头和接收的响应头,这对于诊断重定向问题至关重要。

4.3 第三步:针对性验证与修复

根据收集到的信息,匹配到前述的某个场景,然后应用对应的解决方案。

案例诊断示例: 假设你在访问https://your-domain.com/app时遇到400错误。

  1. curl -I -L https://your-domain.com/app显示,首先收到了一个302 Found响应,其Location头为http://backend-internal-ip:8080/app/login
  2. 浏览器或curl跟随这个重定向,向http://backend-internal-ip:8080/app/login发起请求,但由于DNS或网络配置,这个请求实际上又被发送到了你的Nginx服务器的443端口。
  3. Nginx在443端口收到了一个明文HTTP请求,于是报错。

根因: 上游服务(backend-internal-ip:8080)在未感知HTTPS的情况下,生成了HTTP的重定向。解决方案: 采用方案B,在Nginx的location /app/配置块中添加proxy_redirect http://backend-internal-ip:8080 https://your-domain.com;。或者采用方案A,修复上游服务,使其正确读取X-Forwarded-Proto头。

4.4 第四步:测试与监控

修复配置后,执行sudo nginx -t测试配置语法,然后sudo nginx -s reload重载配置。

进行全面的测试:

  1. 使用浏览器无痕模式访问主要功能页面。
  2. 测试登录、登出等会触发重定向的流程。
  3. 使用curlPostman测试API接口。
  4. 再次检查Nginx错误日志,确认The plain HTTP request was sent to HTTPS port错误是否消失。

在监控系统中,可以针对Nginx的400状态码设置告警,特别是当请求URL中包含:443时,这能帮助你提前发现配置问题或异常客户端行为。

5. 深度避坑与进阶技巧

在解决了基本问题之后,还有一些更深层次的坑和优化技巧值得了解。

5.1 关于proxy_redirect的陷阱

  • 默认值陷阱proxy_redirect的默认值是default,其行为是使用proxy_pass指令后的URL(不含路径)来重写Location头。例如proxy_pass http://backend/old/;会默认将Location: http://backend/new重写为Location: /new(相对路径)。这通常不是你想要的,尤其是在HTTPS场景下。因此,显式配置proxy_redirect是好习惯。
  • 关闭重写: 如果你确信上游服务返回的重定向已经是正确的绝对HTTPS URL,可以使用proxy_redirect off;来关闭Nginx的重写功能,减少不必要的处理开销。
  • 复杂路径替换: 当路径发生变化时,需要更精细的配置。
    # 上游返回: Location: http://old-host:8080/v1/api/login # 目标重写为: Location: https://new-domain.com/v2/api/login proxy_redirect http://old-host:8080/v1/ https://new-domain.com/v2/;

5.2 混合内容(Mixed Content)问题

即使Nginx反向代理工作正常,你的网站也可能因为“混合内容”问题而在浏览器控制台看到警告或错误。这是因为网页(通过HTTPS加载)中引用了HTTP协议的资源(如图片、JS、CSS)。

解决方案

  1. 内容安全策略(CSP): 在Nginx或应用响应头中添加Content-Security-Policy: upgrade-insecure-requests,这会告诉浏览器自动将页面内所有的HTTP请求升级为HTTPS。
    add_header Content-Security-Policy "upgrade-insecure-requests";
  2. 相对协议URL: 确保前端代码、模板或CMS生成资源链接时使用相对协议,例如//example.com/static/img.jpg,浏览器会根据当前页面协议自动补全。
  3. Nginx Sub_filter模块: 对于无法修改的静态HTML,可以使用Nginx的ngx_http_sub_module模块动态替换响应体中的文本。
    location / { proxy_pass http://backend; sub_filter 'http://your-old-domain.com' 'https://your-new-domain.com'; sub_filter_once off; # 全局替换 }

5.3 在Docker/Kubernetes环境中的特殊考量

在容器化部署中,网络拓扑变得更加复杂。

  • 容器间通信: 在Docker Compose或K8s集群内部,服务间通信通常使用HTTP。Nginx容器代理上游服务时,proxy_pass通常指向内部服务名和HTTP端口(如http://app-service:8080)。关键在于,必须确保从Nginx到上游服务的X-Forwarded-Proto头被正确设置为https,因为上游服务感知到的直接请求来自Nginx容器,协议是HTTP。
  • Ingress Controller: 在K8s中使用Ingress(如Nginx Ingress Controller)时,SSL/TLS终止通常在Ingress层面完成。Ingress Controller的配置注解(annotations)就变得至关重要。例如,对于Nginx Ingress,你需要确保配置了正确的注解来传递协议头:
    apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: my-ingress annotations: nginx.ingress.kubernetes.io/backend-protocol: "HTTPS" # 如果后端也需要HTTPS nginx.ingress.kubernetes.io/configuration-snippet: | proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Port $server_port;
  • 服务网格(Service Mesh): 在Istio等服务网格中,协议检测和转发可能由Sidecar代理(Envoy)处理。你需要查阅特定服务网格的文档,了解如何配置使其正确处理HTTP/HTTPS的转换和头信息传递。

5.4 性能与安全加固

  1. SSL/TLS优化: 使用强加密套件,启用HTTP/2(listen 443 ssl http2;),设置合理的SSL会话缓存和会话票证,以提升HTTPS性能。
  2. HSTS(HTTP Strict Transport Security): 在HTTPS server块中添加HSTS头,强制浏览器在未来一段时间内只能通过HTTPS访问该站点,有效防止SSL剥离攻击。
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

    警告: 在确认你的HTTPS配置完全正确且稳定之前,不要轻易添加includeSubDomainspreload指令,否则一旦配置出错,用户将在很长时间内无法访问你的网站。

  3. 错误页面定制: 为400、404、500等错误定制友好的错误页面,提升用户体验,同时可以隐藏服务器信息。
    error_page 400 /custom_400.html; location = /custom_400.html { root /usr/share/nginx/html; internal; }

处理“The plain HTTP request was sent to HTTPS port”报错的过程,是一次深入理解Web架构中协议、代理与安全之间关系的实践。从最基础的端口协议认知,到Nginx的配置细节,再到上游应用的适配,每一步都环环相扣。记住,最优雅的解决方案永远是让上游应用感知代理(方案A),其次是利用Nginx的proxy_redirect进行拦截修正(方案B)。强制跳转(方案D)是保障最终用户访问体验的基石。避免使用危险的兼容监听方案(方案C)。在复杂的云原生环境中,更要关注配置在每一层(负载均衡、Ingress、Nginx、应用)的传递与一致性。