Skip to content

CAS 反向代理三层架构实战:从 Spring Boot 转发策略到 Tomcat HTTP 双连接器的跨版本演进

作者: 必码 | bima.cc


前言

在企业级身份认证基础设施中,Apereo CAS(Central Authentication Service)作为成熟的开源单点登录解决方案,被广泛应用于各类组织的统一认证场景。然而,CAS 在生产环境中几乎从不以"裸奔"方式直接暴露给终端用户——它总是部署在 Nginx、Apache HTTPD、HAProxy 等反向代理之后。这种部署架构带来了 SSL/TLS 卸载、负载均衡、统一入口等显著优势,但同时也引入了一系列棘手的协议头透传、端口映射和地址重写问题。这些问题如果处理不当,将直接导致 CAS 的票据验证失败、重定向循环、OIDC Issuer 匹配异常等严重故障。

本文基于 cas-overlay 项目的三个重要版本——CAS 5.3(基于 Spring Boot 1.x)、CAS 6.6(基于 Spring Boot 2.x)和 CAS 7.3(基于 Spring Boot 3.x),系统性地剖析反向代理场景下的三层配置架构。这三层架构分别是:第一层 Spring Boot 转发头策略(Forward Headers Strategy),负责在框架层面识别和信任代理转发的请求头;第二层 Tomcat RemoteIpValve 精细配置,负责在 Servlet 容器层面解析 X-Forwarded-* 头并重写请求的协议、端口和远程地址;第三层 HTTP 双连接器配置,负责在 Tomcat 连接器层面同时启用 HTTP 和 HTTPS 端口,并通过 proxyPort 属性实现端口映射。三层架构各司其职、层层递进,共同构成了 CAS 反向代理适配的完整解决方案。

本文的读者对象包括:正在或即将在生产环境中部署 CAS 的架构师和运维工程师;需要从 CAS 5.3 升级到 6.6 或 7.3 的技术人员;对 Spring Boot 嵌入式容器配置机制感兴趣的开发者;以及需要为 CAS 配置 OIDC 协议并解决反向代理下 Issuer 匹配问题的安全工程师。阅读本文需要具备 Spring Boot 基础配置知识、Tomcat 容器基本概念以及 HTTP 协议头相关的基础了解。


第一章 反向代理在 CAS 部署中的核心地位

导读: 本章从企业级部署架构的全局视角出发,阐述反向代理在 CAS 部署中的必然性,分析反向代理引入的核心技术挑战,并引出本文将要深入探讨的三层配置架构。

1.1 企业级部署架构概述

在典型的企业级 CAS 部署架构中,CAS 服务器很少直接面向互联网暴露。现代企业 IT 基础设施通常采用分层架构,CAS 作为核心认证服务被部署在内网,前端由反向代理层负责流量接入、SSL 终止和请求分发。这种架构不仅是最佳实践,更是企业安全合规的刚性需求。

下面是一个典型的 CAS 反向代理部署架构图:

                         ┌─────────────────────────────────────────────────┐
                         │              互联网 / 内网用户                    │
                         └────────────────────┬────────────────────────────┘
                                              │ HTTPS (443)

                         ┌─────────────────────────────────────────────────┐
                         │            反向代理层 (Nginx/HAProxy)            │
                         │                                                 │
                         │  ┌──────────────────────────────────────────┐   │
                         │  │  SSL/TLS 终止                              │   │
                         │  │  X-Forwarded-Proto: https                 │   │
                         │  │  X-Forwarded-Port: 443                    │   │
                         │  │  X-Forwarded-For: 客户端真实IP              │   │
                         │  └──────────────────────────────────────────┘   │
                         │                                                 │
                         │  ┌──────────────────────────────────────────┐   │
                         │  │  负载均衡 / 健康检查                        │   │
                         │  └──────────────────────────────────────────┘   │
                         └────────────────────┬────────────────────────────┘
                                              │ HTTP (8080) / HTTPS (8443)

                         ┌─────────────────────────────────────────────────┐
                         │          CAS 服务器集群 (Spring Boot)            │
                         │                                                 │
                         │  ┌──────────────┐  ┌──────────────┐            │
                         │  │  CAS 实例 A   │  │  CAS 实例 B   │            │
                         │  │  :8080/:8443  │  │  :8080/:8443  │            │
                         │  └──────┬───────┘  └──────┬───────┘            │
                         │         │                  │                    │
                         │         └────────┬─────────┘                    │
                         │                  │                              │
                         │         ┌────────▼─────────┐                    │
                         │         │   Redis 集群      │                    │
                         │         │  (票据/会话存储)   │                    │
                         │         └──────────────────┘                    │
                         └─────────────────────────────────────────────────┘

在这个架构中,反向代理承担了以下关键职责:

SSL/TLS 终止(SSL Offloading): 反向代理负责处理所有的 TLS 握手和加解密操作,CAS 服务器可以使用 HTTP 协议在内网中运行。这种方式极大地降低了 CAS 服务器的 CPU 负担,同时将证书管理集中到代理层。在实际部署中,CAS 的 HTTPS 端口(8443)通常只在内网可达,而外部用户通过代理的 443 端口访问。

统一入口与域名管理: 反向代理为所有服务提供统一的入口域名(如 cas.example.com),后端 CAS 实例无需关心外部域名映射。代理层负责将请求正确路由到后端实例,并处理路径重写。

负载均衡与高可用: 当 CAS 部署多实例时,反向代理负责请求分发和故障转移。配合健康检查机制,代理可以自动剔除不可用的后端实例。

安全防护: 反向代理层可以配置速率限制、IP 黑名单、WAF 规则等安全策略,为 CAS 提供额外的安全防护层。

静态资源缓存: CAS 的静态资源(JS、CSS、图片等)可以由反向代理直接缓存和提供,减轻 CAS 服务器的负载。

1.2 反向代理带来的核心挑战

尽管反向代理架构带来了诸多优势,但它也引入了一系列必须妥善处理的技术挑战。这些挑战的核心本质是:当请求经过反向代理转发后,CAS 服务器接收到的请求信息与客户端发起的原始请求之间存在信息偏差。

挑战一:协议信息丢失

当客户端通过 HTTPS(443端口)访问反向代理,代理再通过 HTTP(8080端口)转发到 CAS 时,CAS 服务器看到的请求协议是 HTTP 而非 HTTPS。这会导致 CAS 生成的所有重定向 URL 使用 http:// 而非 https://,进而引发浏览器安全警告或混合内容错误。

客户端请求:  https://cas.example.com:443/cas/login


反向代理转发:  http://cas-backend:8080/cas/login


CAS 感知到的:  协议=HTTP, 端口=8080, 地址=cas-backend

CAS 生成重定向: http://cas-backend:8080/cas/login?service=...

                    错误!应该是 https://cas.example.com/cas/login?service=...

挑战二:端口映射不一致

CAS 服务器监听的端口(如 8080)与外部访问端口(如 80 或 443)不一致。当 CAS 需要生成回调 URL 或重定向地址时,如果使用了内部端口号,将导致客户端无法正确访问。

挑战三:客户端真实 IP 丢失

经过反向代理转发后,CAS 服务器在 TCP 层看到的源 IP 地址是反向代理的 IP,而非客户端的真实 IP。这对于审计日志、IP 白名单认证、地理定位等功能都会产生影响。

挑战四:OIDC Issuer 匹配失败

在 OIDC 协议中,Issuer 是一个必须精确匹配的标识符。当 CAS 配置的 Issuer 为 https://cas.example.com/cas/oidc,但实际请求到达 CAS 时协议被识别为 HTTP,则 CAS 会认为 Issuer 不匹配而拒绝请求。这个问题在 CAS 6.6 和 7.3 版本中尤为突出。

挑战五:Cookie 安全属性冲突

CAS 生成的 TGT Cookie(Ticket Granting Ticket)带有 Secure 属性时,只能通过 HTTPS 传输。但在反向代理终止 SSL 的场景下,CAS 与代理之间使用 HTTP 通信,导致 Cookie 无法正确传递。

挑战六:Service URL 注册与回调不匹配

CAS 协议中,每个接入的应用(Service)都需要在 CAS 服务端预先注册其回调 URL。当反向代理改变了请求的协议或端口后,CAS 生成的回调 URL 可能与注册的 Service URL 不一致。例如,应用注册的 Service URL 为 https://app.example.com/callback,但 CAS 在生成验证票据的重定向时使用了 http://app.example.com:8080/callback,这将导致票据验证失败。这个问题在多应用接入的复杂环境中尤为突出,因为每个应用都可能有自己的反向代理配置。

挑战七:WebSocket 长连接的代理兼容性

CAS 6.6 和 7.3 版本引入了 WebSocket 支持用于推送通知和实时事件。WebSocket 连接在建立后是一个持久的长连接,反向代理需要正确处理 WebSocket 的升级请求(HTTP 101 Switching Protocols)。如果代理配置不当,WebSocket 连接可能在建立后很快被超时断开,或者代理缓冲了消息导致实时性丧失。

为了系统性地解决以上核心挑战,CAS 的反向代理配置需要从三个层面协同工作:

┌─────────────────────────────────────────────────────────────┐
│                    第一层:Spring Boot 转发头策略              │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐    │
│  │  use-forward-headers / forward-headers-strategy      │    │
│  │  决定框架是否信任并处理 X-Forwarded-* 请求头           │    │
│  └─────────────────────────────────────────────────────┘    │
├─────────────────────────────────────────────────────────────┤
│                    第二层:Tomcat RemoteIpValve              │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐    │
│  │  protocol-header / port-header / remote-ip-header    │    │
│  │  在 Servlet 容器层面解析转发头,重写请求的协议/端口/IP   │    │
│  └─────────────────────────────────────────────────────┘    │
├─────────────────────────────────────────────────────────────┤
│                    第三层:HTTP 双连接器配置                   │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐    │
│  │  HTTP Connector + HTTPS Connector + proxyPort        │    │
│  │  在连接器层面配置双端口监听和端口映射                   │    │
│  └─────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────┘

在接下来的三个章节中,我们将逐层深入,详细解析每一层的配置原理、跨版本差异和最佳实践。


第二章 第一层:Spring Boot 转发头策略

导读: Spring Boot 的转发头策略是反向代理适配的第一道防线。本章将从 CAS 5.3 的 use-forward-headers 开始,逐步演进到 CAS 6.6 的 NATIVE 策略,再到 CAS 7.3 的双重保障策略,深入解析三种策略的底层原理差异。

2.1 CAS 5.3:use-forward-headers 时代

CAS 5.3 基于 Spring Boot 1.5.x 构建,其转发头处理机制相对简单直接。在这个版本中,开发者通过一个布尔类型的配置项来控制是否启用转发头处理。

核心配置:

yaml
server:
  use-forward-headers: true

这行配置看似简单,但其背后触发了一系列复杂的框架行为。当 use-forward-headers 被设置为 true 时,Spring Boot 会自动向嵌入式 Servlet 容器注册一个 ForwardedHeaderFilter。这个 Filter 实现了 Servlet 规范中的 ForwardedHeaderFilter 功能,负责解析标准的 Forwarded 头以及常见的代理头(如 X-Forwarded-HostX-Forwarded-PortX-Forwarded-Proto)。

工作原理:

请求到达 CAS 5.3(use-forward-headers: true)

1. 客户端发送请求:
   GET https://cas.example.com/cas/login
   Host: cas.example.com

2. Nginx 转发请求:
   GET http://cas-backend:8080/cas/login
   Host: cas-backend:8080
   X-Forwarded-Proto: https
   X-Forwarded-Port: 443
   X-Forwarded-Host: cas.example.com
   X-Forwarded-For: 10.0.0.1

3. ForwardedHeaderFilter 处理:
   - 读取 X-Forwarded-Proto → 重写 request.scheme = "https"
   - 读取 X-Forwarded-Port → 重写 request.serverPort = 443
   - 读取 X-Forwarded-Host → 重写 request.serverName = "cas.example.com"
   - 读取 X-Forwarded-For → 更新 remoteAddr

4. CAS 感知到的请求:
   协议=https, 端口=443, 主机=cas.example.com ✓

CAS 5.3 完整的反向代理相关配置示例:

yaml
server:
  port: 8443
  context-path: /cas
  # 启用转发头处理(适配反向代理)
  use-forward-headers: true

  ssl:
    enabled: true
    key-store: classpath:etc/ssl/keystore.p12
    key-store-password: changeit
    key-password: changeit
    keyStoreType: PKCS12
    protocol: TLS
    port: 8443

  tomcat:
    remoteip:
      enabled: true
      protocol-header: X-Forwarded-Proto
      port-header: X-Forwarded-Port
      remote-ip-header: X-FORWARDED-FOR
      protocol-header-https-value: https
      internal-proxies: 10\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|192\\.168\\.\\d{1,3}\\.\\d{1,3}|...

cas:
  server:
    name: https://cas.example.com
    prefix: ${cas.server.name}/cas
    http:
      enabled: true
      port: 8080
      protocol: org.apache.coyote.http11.Http11NioProtocol
      attributes:
        proxyPort: "80"
        address: "0.0.0.0"

局限性分析:

CAS 5.3 的 use-forward-headers 方案存在几个明显的局限性。首先,它是一个全局开关,缺乏细粒度的控制能力——要么全部信任转发头,要么完全不信任,无法针对不同的请求头设置不同的信任策略。其次,ForwardedHeaderFilter 作为 Servlet Filter 工作,在请求处理的早期阶段就修改了请求对象,这可能与某些需要原始请求信息的组件产生冲突。最后,在 Spring Boot 1.x 中,这个配置项的实现方式在不同嵌入式容器(Tomcat、Jetty、Undertow)之间可能存在行为差异,增加了跨容器部署的不确定性。

ForwardedHeaderFilter 的内部实现原理:

ForwardedHeaderFilter 是 Spring Framework 提供的一个 OncePerRequestFilter,其核心逻辑封装在 ForwardedHeaderExtractingRequest 包装类中。当 Filter 拦截到请求后,它会按照以下优先级解析转发头:

  1. 首先检查标准的 Forwarded 头(RFC 7239 定义的标准格式)
  2. 如果 Forwarded 头不存在,则依次检查 X-Forwarded-HostX-Forwarded-PortX-Forwarded-Proto 等非标准头
  3. 使用解析到的值创建 ForwardedHeaderExtractingRequest 包装对象

这个包装对象重写了 HttpServletRequest 的以下方法:

  • getScheme() — 返回转发头中的协议(http/https)
  • isSecure() — 根据协议返回 true/false
  • getServerName() — 返回转发头中的主机名
  • getServerPort() — 返回转发头中的端口号
  • getRequestURI()getRequestURL() — 基于修正后的信息重建 URL
  • getRemoteAddr() — 返回转发头中的客户端 IP

值得注意的是,ForwardedHeaderFilter 在重写 URL 时会移除敏感的查询参数(如 JWT Token),以防止 Token 通过 HTTP Referer 头泄露。这个安全特性在反向代理场景下尤为重要。

2.2 CAS 6.6:NATIVE 策略的引入

CAS 6.6 基于 Spring Boot 2.7.x 构建,Spring Boot 2.x 对转发头处理机制进行了重大重构。最显著的变化是引入了 ForwardHeadersStrategy 枚举类型,取代了原来的布尔开关。

核心配置:

yaml
server:
  forward-headers-strategy: NATIVE

ForwardHeadersStrategy 提供了三种策略选项:

策略说明行为
NONE不处理转发头完全忽略所有代理头
NATIVE使用容器原生机制利用 Tomcat RemoteIpValve、Jetty ForwardedRequestCustomizer 等容器原生组件
FRAMEWORK使用框架 Filter注册 ForwardedHeaderFilter(类似 Spring Boot 1.x 的行为)

NATIVE 策略的工作机制:

当选择 NATIVE 策略时,Spring Boot 不再注册 ForwardedHeaderFilter,而是直接配置嵌入式容器的原生代理处理组件。对于 Tomcat 而言,这意味着自动配置 RemoteIpValve

CAS 6.6(forward-headers-strategy: NATIVE)

┌──────────────────────────────────────────────────────┐
│                  Spring Boot 启动流程                  │
│                                                      │
│  1. 检测 forward-headers-strategy = NATIVE            │
│                                                      │
│  2. 跳过 ForwardedHeaderFilter 注册                    │
│     (与 FRAMEWORK 策略的关键区别)                       │
│                                                      │
│  3. 调用 TomcatServletWebServerFactory                 │
│     → addEngineValves()                              │
│     → 注册 RemoteIpValve 到 Engine 级别               │
│                                                      │
│  4. RemoteIpValve 在请求到达 Servlet 之前处理转发头     │
└──────────────────────────────────────────────────────┘

NATIVE 策略相比旧的 use-forward-headers 有几个重要优势:

性能优势: RemoteIpValve 作为 Tomcat Engine 级别的 Valve 工作,在请求处理的更早阶段介入,避免了 Filter 链的额外开销。Valve 在 Tomcat 的 Pipeline 架构中处于非常底层的位置,其执行效率高于 Servlet Filter。

配置灵活性: RemoteIpValve 提供了丰富的配置选项(如 internal-proxiesprotocol-headerport-header 等),允许精细控制哪些代理 IP 被信任、使用哪些请求头来确定协议和端口。

容器一致性: 使用容器原生机制确保了行为的一致性,因为 Tomcat 的 RemoteIpValve 是经过长期验证的成熟组件,其行为在不同版本的 Tomcat 中保持稳定。

CAS 6.6 完整的反向代理相关配置示例:

yaml
server:
  port: 8443
  servlet:
    context-path: /cas
  # 使用 NATIVE 策略处理转发头
  forward-headers-strategy: NATIVE

  ssl:
    enabled: true
    key-store: ${user.dir}/src/main/resources/etc/ssl/keystore.jks
    key-store-password: changeit
    key-password: changeit
    key-store-type: JKS
    protocol: TLS

  tomcat:
    remoteip:
      enabled: true
      protocol-header: X-Forwarded-Proto
      port-header: X-Forwarded-Port
      remote-ip-header: X-FORWARDED-FOR
      protocol-header-https-value: https
      internal-proxies: 10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|192\\.168\\.\\d{1,3}\\.\\d{1,3}|...

cas:
  server:
    name: https://cas.example.com
    prefix: ${cas.server.name}/cas
    tomcat:
      http:
        - enabled: true
          port: 8080
          scheme: http
          secure: false
          protocol: org.apache.coyote.http11.Http11NioProtocol
          attributes:
            proxyPort: "80"
            address: "0.0.0.0"

2.3 CAS 7.3:双重保障策略

CAS 7.3 基于 Spring Boot 3.x 构建,在转发头处理方面采用了一种"双重保障"的配置方式,同时启用了两种转发头处理机制。

核心配置:

yaml
server:
  use-forward-headers: true
  forward-headers-strategy: native

这种配置方式在表面上看似冗余——同时启用了两种机制来处理同一件事。但在实际场景中,这种双重保障策略有其特定的设计考量:

兼容性保障: CAS 7.3 基于 Spring Boot 3.x,其配置模型和自动装配机制发生了较大变化。同时配置两个选项可以确保在不同的自动装配路径下,至少有一种转发头处理机制能够生效。这在对配置行为不完全确定的新版本中是一种防御性编程策略。

覆盖面最大化: use-forward-headers: true 在 Spring Boot 3.x 中实际上被映射为 forward-headers-strategy: FRAMEWORK(如果 forward-headers-strategy 未显式设置的话)。当两者同时存在时,forward-headers-strategy 的优先级更高,但 use-forward-headers 的设置可以作为兜底保障。

CAS 7.3 完整的反向代理相关配置示例:

yaml
server:
  port: 8443
  # 双重保障:同时启用两种转发头处理机制
  use-forward-headers: true
  forward-headers-strategy: native
  servlet:
    context-path: /cas

  ssl:
    enabled: true
    key-store: classpath:keystore.jks
    key-store-password: changeit
    key-password: changeit
    key-store-type: JKS
    protocol: TLS

  tomcat:
    remoteip:
      enabled: true
      protocol-header: X-Forwarded-Proto
      port-header: X-Forwarded-Port
      remote-ip-header: X-Forwarded-For
      protocol-header-https-value: https
      internal-proxies: 10\.\d{1,3}\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3}|...

cas:
  server:
    name: https://cas.example.com
    prefix: ${cas.server.name}/cas
    tomcat:
      http:
        - enabled: true
          port: 8080
          scheme: http
          secure: false
          protocol: org.apache.coyote.http11.Http11NioProtocol
          attributes:
            proxyPort: "80"
            address: "0.0.0.0"

2.4 三种策略的底层原理对比

为了更清晰地理解三个版本中转发头策略的差异,我们从多个维度进行对比分析。

版本对比总览表:

维度CAS 5.3 (Spring Boot 1.x)CAS 6.6 (Spring Boot 2.x)CAS 7.3 (Spring Boot 3.x)
配置方式use-forward-headers: trueforward-headers-strategy: NATIVEuse-forward-headers: true + forward-headers-strategy: native
处理机制ForwardedHeaderFilter (Servlet Filter)RemoteIpValve (Tomcat Valve)RemoteIpValve + ForwardedHeaderFilter 兜底
处理层级Servlet Filter 链Tomcat Engine ValveTomcat Engine Valve + Servlet Filter
配置粒度全局布尔开关枚举策略 (NONE/NATIVE/FRAMEWORK)枚举策略 + 布尔兜底
性能影响中等(Filter 链开销)较低(Valve 直接处理)较低(以 Valve 为主)
安全控制无 IP 信任控制internal-proxies IP 白名单internal-proxies IP 白名单
Spring Boot 版本1.5.x2.7.x3.x
Java 版本要求Java 8+Java 11+Java 17+

处理层级对比图:

CAS 5.3 请求处理链:

  客户端请求


  ┌──────────────────┐
  │  Tomcat Connector │  ← 连接器层
  └────────┬─────────┘


  ┌──────────────────┐
  │  Tomcat Engine    │  ← 引擎层(无特殊处理)
  └────────┬─────────┘


  ┌──────────────────┐
  │  Tomcat Pipeline  │  ← 管道层
  └────────┬─────────┘


  ┌──────────────────┐
  │  Servlet Filter   │  ← ForwardedHeaderFilter 在此处理转发头
  └────────┬─────────┘


  ┌──────────────────┐
  │  CAS Dispatcher   │  ← CAS 处理请求
  └──────────────────┘


CAS 6.6/7.3 请求处理链 (NATIVE 策略):

  客户端请求


  ┌──────────────────┐
  │  Tomcat Connector │  ← 连接器层
  └────────┬─────────┘


  ┌──────────────────┐
  │  Tomcat Engine    │  ← RemoteIpValve 在此处理转发头 ✓
  └────────┬─────────┘


  ┌──────────────────┐
  │  Tomcat Pipeline  │  ← 管道层(请求已被修正)
  └────────┬─────────┘


  ┌──────────────────┐
  │  Servlet Filter   │  ← 无 ForwardedHeaderFilter
  └────────┬─────────┘


  ┌──────────────────┐
  │  CAS Dispatcher   │  ← CAS 处理请求(已获取正确的协议/端口)
  └──────────────────┘

安全模型差异:

CAS 5.3 的 use-forward-headers: true 存在一个潜在的安全风险:它会无条件信任所有传入的 X-Forwarded-* 头。这意味着恶意客户端可以直接在请求中伪造这些头,欺骗 CAS 认为请求是通过 HTTPS 发起的,或者伪造客户端 IP 地址。

CAS 6.6 和 7.3 的 NATIVE 策略通过 internal-proxies 配置引入了 IP 信任模型。只有来自受信任代理 IP 地址的请求中的转发头才会被处理。来自非信任 IP 的请求中的转发头将被忽略,从而有效防止了头伪造攻击。

安全模型对比:

CAS 5.3:
  任意请求 → 信任 X-Forwarded-* → 可能被伪造 ✗

CAS 6.6/7.3:
  来自 10.x.x.x 的请求 → 信任 X-Forwarded-* → 安全 ✓
  来自 192.168.x.x 的请求 → 信任 X-Forwarded-* → 安全 ✓
  来自 203.0.113.x 的请求 → 忽略 X-Forwarded-* → 安全 ✓

迁移建议: 如果从 CAS 5.3 升级到 6.6 或 7.3,建议将 use-forward-headers: true 替换为 forward-headers-strategy: NATIVE,并确保 internal-proxies 正则表达式覆盖所有反向代理的 IP 地址。在 CAS 7.3 中,可以保留双重配置作为过渡期的兼容性保障,但长期建议只使用 forward-headers-strategy: native

FRAMEWORK 策略的适用场景:

虽然本文主要推荐使用 NATIVE 策略,但 FRAMEWORK 策略在某些特殊场景下仍有其价值。当使用 Jetty 或 Undertow 作为嵌入式容器时,FRAMEWORK 策略可以提供与 Tomcat RemoteIpValve 等价的功能,因为 ForwardedHeaderFilter 是容器无关的。此外,如果需要在 Filter 层面进行自定义的转发头处理逻辑(例如添加额外的安全校验),FRAMEWORK 策略提供了更好的扩展点。

三种策略的选择决策树:

选择转发头策略的决策流程:

                    ┌──────────────────────┐
                    │  使用什么容器?        │
                    └──────────┬───────────┘

                 ┌─────────────┼─────────────┐
                 │             │             │
                 ▼             ▼             ▼
            ┌────────┐   ┌────────┐   ┌──────────┐
            │ Tomcat │   │ Jetty  │   │ Undertow │
            └───┬────┘   └───┬────┘   └────┬─────┘
                │            │             │
                ▼            │             │
        ┌──────────────┐     │             │
        │ 需要 IP 信任  │     │             │
        │ 控制吗?      │     │             │
        └──┬───────┬───┘     │             │
           │       │         │             │
          是      否         │             │
           │       │         │             │
           ▼       │         ▼             ▼
      ┌────────┐   │   ┌──────────┐  ┌──────────┐
      │ NATIVE │   │   │ FRAMEWORK│  │ FRAMEWORK│
      └────────┘   │   └──────────┘  └──────────┘
           │       │
           ▼       ▼
      ┌────────────────┐
      │ NATIVE (推荐)  │
      └────────────────┘

第三章 第二层:Tomcat RemoteIpValve 精细配置

导读: RemoteIpValve 是 Tomcat 提供的用于处理反向代理场景的核心组件。本章将深入解析 RemoteIpValve 的工作原理、各项配置参数的含义、internal-proxies 正则表达式的编写规则,以及三个版本之间的配置差异。

3.1 RemoteIpValve 工作原理

RemoteIpValve 是 Tomcat 中专门用于处理反向代理场景的 Valve 组件。Valve 在 Tomcat 的架构中类似于 Servlet Filter,但工作在更底层的容器级别。RemoteIpValve 的核心职责是:解析反向代理添加的协议转发头,并据此修改请求对象的协议、端口、服务器名称和远程地址等属性。

RemoteIpValve 的处理流程:

RemoteIpValve 内部处理流程:

┌────────────────────────────────────────────────────────────┐
│                    invoke(Request, Response)                │
│                                                            │
│  1. 检查请求来源 IP 是否匹配 internal-proxies               │
│     ├── 匹配 → 继续处理转发头                               │
│     └── 不匹配 → 直接放行,不修改请求                        │
│                                                            │
│  2. 解析 remote-ip-header (如 X-Forwarded-For)             │
│     ├── 提取最右侧(或最左侧)的非信任代理 IP               │
│     └── 设置 request.remoteAddr / request.remoteHost        │
│                                                            │
│  3. 解析 protocol-header (如 X-Forwarded-Proto)            │
│     ├── 读取值 (http / https)                              │
│     └── 设置 request.scheme / request.isSecure              │
│                                                            │
│  4. 解析 port-header (如 X-Forwarded-Port)                 │
│     ├── 读取端口号                                         │
│     └── 设置 request.serverPort                             │
│                                                            │
│  5. 解析 hosts-header (如 X-Forwarded-Host)                │
│     ├── 读取主机名和端口                                    │
│     └── 设置 request.serverName / request.serverPort        │
│                                                            │
│  6. 将修改后的请求传递给下一个 Valve 或 Pipeline             │
└────────────────────────────────────────────────────────────┘

关键实现细节:

RemoteIpValve 在处理 X-Forwarded-For 头时,会按照代理链的顺序逐级剥离信任代理的 IP。例如,当请求经过两级代理时:

X-Forwarded-For: 客户端IP, 一级代理IP, 二级代理IP

如果 internal-proxies 匹配一级代理IP和二级代理IP:
  → request.remoteAddr = 客户端IP

如果 internal-proxies 只匹配二级代理IP:
  → request.remoteAddr = 一级代理IP

这种逐级剥离的机制确保了在多级代理场景下,CAS 能够获取到真实的客户端 IP 地址。

3.2 协议头与端口头配置

RemoteIpValve 的配置通过 Spring Boot 的 server.tomcat.remoteip.* 命名空间暴露。以下是各项配置的详细说明。

protocol-header(协议头):

yaml
server:
  tomcat:
    remoteip:
      protocol-header: X-Forwarded-Proto

protocol-header 指定了用于确定原始请求协议的 HTTP 头名称。当此头存在且其值为 protocol-header-https-value(默认为 https)时,RemoteIpValve 会将请求的 scheme 设置为 https,将 isSecure 设置为 true,并将 serverPort 设置为 443(除非 port-header 指定了其他端口)。

这是反向代理适配中最关键的配置之一。没有正确的协议头配置,CAS 将无法感知到原始请求使用的是 HTTPS 协议,从而生成错误的回调 URL。

port-header(端口头):

yaml
server:
  tomcat:
    remoteip:
      port-header: X-Forwarded-Port

port-header 指定了用于确定原始请求端口的 HTTP 头名称。当此头存在时,RemoteIpValve 会将其值解析为整数,并设置为请求的 serverPort

在典型的部署中,外部 HTTPS 端口为 443,HTTP 端口为 80。反向代理会在转发时添加 X-Forwarded-Port: 443X-Forwarded-Port: 80,使得 CAS 能够生成包含正确端口的 URL。

remote-ip-header(远程 IP 头):

yaml
server:
  tomcat:
    remoteip:
      remote-ip-header: X-Forwarded-For

remote-ip-header 指定了用于确定客户端真实 IP 地址的 HTTP 头名称。RemoteIpValve 会解析此头的值,提取出最右侧的非信任代理 IP 作为客户端的真实 IP。

protocol-header-https-value(HTTPS 协议值):

yaml
server:
  tomcat:
    remoteip:
      protocol-header-https-value: https

此配置定义了 protocol-header 中表示 HTTPS 的值。默认值为 https。在某些特殊的代理配置中,可能使用不同的值来表示 HTTPS 协议,此时需要相应调整此配置。

完整配置示例与请求处理过程:

yaml
server:
  tomcat:
    remoteip:
      enabled: true
      protocol-header: X-Forwarded-Proto
      port-header: X-Forwarded-Port
      remote-ip-header: X-Forwarded-For
      protocol-header-https-value: https
请求处理示例:

输入请求:
  GET /cas/login HTTP/1.1
  Host: cas-backend:8080
  X-Forwarded-Proto: https
  X-Forwarded-Port: 443
  X-Forwarded-For: 203.0.113.50
  X-Forwarded-Host: cas.example.com
  来源IP: 192.168.1.10 (匹配 internal-proxies)

RemoteIpValve 处理后:
  request.scheme = "https"        (从 X-Forwarded-Proto)
  request.serverPort = 443        (从 X-Forwarded-Port)
  request.serverName = "cas.example.com" (从 X-Forwarded-Host)
  request.remoteAddr = "203.0.113.50"    (从 X-Forwarded-For)
  request.isSecure = true         (因为协议为 https)

3.3 internal-proxies 正则表达式解析

internal-proxies 是 RemoteIpValve 安全模型的核心配置。它定义了哪些 IP 地址被视为"受信任的内部代理",只有来自这些 IP 地址的请求中的转发头才会被处理。

默认值:

Tomcat 的 RemoteIpValve 默认的 internal-proxies 正则表达式为:

10\.\d{1,3}\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3}|169\.254\.\d{1,3}\.\d{1,3}|127\.\d{1,3}\.\d{1,3}\.\d{1,3}|172\.1[6-9]\.\d{1,3}\.\d{1,3}|172\.2[0-9]\.\d{1,3}\.\d{1,3}|172\.3[0-1]\.\d{1,3}\.\d{1,3}|0:0:0:0:0:0:0:1|::1

这个默认值覆盖了以下私有 IP 地址范围:

正则片段匹配的 IP 范围说明
10\.\d{1,3}\.\d{1,3}\.\d{1,3}10.0.0.0 - 10.255.255.255A 类私有地址
192\.168\.\d{1,3}\.\d{1,3}192.168.0.0 - 192.168.255.255C 类私有地址
172\.1[6-9]\.\d{1,3}\.\d{1,3}172.16.0.0 - 172.19.255.255B 类私有地址(上段)
172\.2[0-9]\.\d{1,3}\.\d{1,3}172.20.0.0 - 172.29.255.255B 类私有地址(中段)
172\.3[0-1]\.\d{1,3}\.\d{1,3}172.30.0.0 - 172.31.255.255B 类私有地址(下段)
127\.\d{1,3}\.\d{1,3}\.\d{1,3}127.0.0.0 - 127.255.255.255本地回环地址
0:0:0:0:0:0:0:1 / ::1::1IPv6 本地回环地址

项目中的实际配置:

在 cas-overlay 项目中,internal-proxies 的配置根据版本不同略有差异,但核心匹配规则一致:

yaml
# CAS 5.3 (YAML 格式,双反斜杠转义)
internal-proxies: 10\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|192\\.168\\.\\d{1,3}\\.\\d{1,3}|172\\.1[6-9]{1}\\.\\d{1,3}\\.\\d{1,3}|172\\.2[0-9]{1}\\.\\d{1,3}\\.\\d{1,3}|172\\.3[0-1]{1}\\.\\d{1,3}\\.\\d{1,3}|0:0:0:0:0:0:0:1|127\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}

# CAS 6.6 (YAML 格式,双反斜杠转义)
internal-proxies: 10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|192\\.168\\.\\d{1,3}\\.\\d{1,3}|172\\.1[6-9]{1}\\.\\d{1,3}\\.\\d{1,3}|172\\.2[0-9]{1}\\.\\d{1,3}\\.\\d{1,3}|172\\.3[0-1]{1}\\.\\d{1,3}\\.\\d{1,3}|0:0:0:0:0:0:0:1|127\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}

# CAS 7.3 (YAML 格式,单反斜杠)
internal-proxies: 10\.\d{1,3}\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3}|172\.1[6-9]\.\d{1,3}\.\d{1,3}|172\.2[0-9]\.\d{1,3}\.\d{1,3}|172\.3[0-1]\.\d{1,3}\.\d{1,3}|0:0:0:0:0:0:0:1|127\.\d{1,3}\.\d{1,3}\.\d{1,3}

YAML 转义注意事项:

在 YAML 文件中编写正则表达式时,反斜杠的转义规则需要特别注意。YAML 中的双引号字符串和单引号字符串对反斜杠的处理方式不同:

  • 双引号字符串中,\\ 表示一个字面反斜杠 \
  • 单引号字符串中,\\ 表示两个字面反斜杠 \\

因此,在 YAML 中表示正则表达式 \d 时:

  • 双引号字符串中应写为 "\\d"
  • 单引号字符串中应写为 '\\d''\d'

在 CAS 5.3 和 6.6 中使用双反斜杠(\\d)是因为 YAML 解析器将 \\ 转义为 \,最终传递给 Tomcat 的正则表达式为 \d。在 CAS 7.3 中使用单反斜杠(\d),说明该版本的 YAML 解析配置或上下文可能有所不同。

实际配置中的陷阱与注意事项:

在编写 internal-proxies 正则表达式时,有几个常见的陷阱需要避免:

陷阱一:量词过于宽松。 使用 \d{1,3} 匹配 IP 地址的每个八位组时,虽然理论上允许 0-999 的范围(超出了 0-255 的有效范围),但在实际场景中这通常不是问题,因为代理 IP 地址总是有效的。然而,如果对安全性有极高要求,可以使用更精确的匹配模式。

陷阱二:遗漏 IPv6 地址。 如果环境中使用了 IPv6,需要确保 internal-proxies 中包含 IPv6 地址的匹配规则。默认配置中的 0:0:0:0:0:0:0:1::1 仅覆盖了 IPv6 本地回环地址,如果代理使用其他 IPv6 地址(如 fe80:: 链路本地地址或 fd00:: 唯一本地地址),需要额外添加。

陷阱三:正则表达式中的管道符 | 与 YAML 列表的混淆。 在 YAML 中,| 是多行字符串的指示符。如果正则表达式以 | 开头或包含裸露的 |,可能导致 YAML 解析错误。解决方案是将整个正则表达式用引号包裹,或确保 | 出现在正确的上下文中。

自定义 internal-proxies:

如果你的反向代理使用了非私有 IP 地址(如公有云的负载均衡器 IP),需要将其添加到 internal-proxies 中:

yaml
server:
  tomcat:
    remoteip:
      # 添加云服务商的内部 IP 段
      internal-proxies: 10\.\d{1,3}\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3}|100\.64\.\d{1,3}\.\d{1,3}|172\.1[6-9]\.\d{1,3}\.\d{1,3}|172\.2[0-9]\.\d{1,3}\.\d{1,3}|172\.3[0-1]\.\d{1,3}\.\d{1,3}|0:0:0:0:0:0:0:1|127\.\d{1,3}\.\d{1,3}\.\d{1,3}

注意: 100.64.0.0/10(100.64.0.0 - 100.127.255.255)是 RFC 6598 定义的运营商级 NAT 地址段,部分云服务商的内部负载均衡器使用此网段。

3.4 跨版本配置差异

三个 CAS 版本在 RemoteIpValve 配置方面的差异主要体现在配置格式和转义规则上,核心功能保持一致。

配置格式对比表:

配置项CAS 5.3CAS 6.6CAS 7.3
enabledtruetruetrue
protocol-headerX-Forwarded-ProtoX-Forwarded-ProtoX-Forwarded-Proto
port-headerX-Forwarded-PortX-Forwarded-PortX-Forwarded-Port
remote-ip-headerX-FORWARDED-FORX-FORWARDED-FORX-Forwarded-For
protocol-header-https-valuehttpshttpshttps
internal-proxies 转义\\d (双反斜杠)\\d (双反斜杠)\d (单反斜杠)
配置文件格式YAMLYAMLYAML

remote-ip-header 大小写差异:

值得注意的是,CAS 5.3 和 6.6 中使用的是大写的 X-FORWARDED-FOR,而 CAS 7.3 中使用的是标准大小写的 X-Forwarded-For。实际上,HTTP 头名称是不区分大小写的(根据 RFC 2616 规范),Tomcat 在匹配时会进行大小写不敏感的比较,因此两种写法在功能上完全等价。但从代码规范角度,推荐使用标准的大小写格式 X-Forwarded-For

配置验证方法:

为了验证 RemoteIpValve 是否正确工作,可以通过以下方式进行检查:

bash
# 发送带有转发头的请求,观察 CAS 的响应
curl -v \
  -H "X-Forwarded-Proto: https" \
  -H "X-Forwarded-Port: 443" \
  -H "X-Forwarded-For: 203.0.113.50" \
  -H "X-Forwarded-Host: cas.example.com" \
  http://localhost:8080/cas/login

# 检查响应中的 Location 头是否使用了正确的协议和端口
# 正确: Location: https://cas.example.com/cas/login
# 错误: Location: http://localhost:8080/cas/login

启用 Tomcat 访问日志也是验证配置的有效手段:

yaml
server:
  tomcat:
    accesslog:
      enabled: true
      pattern: '%t %a "%r" %s (%D ms)'

在访问日志中,%a 显示的是经过 RemoteIpValve 处理后的远程 IP 地址。如果配置正确,日志中应该显示客户端的真实 IP 而非代理 IP。


第四章 第三层:HTTP 双连接器配置

导读: Tomcat 的连接器(Connector)是处理网络连接的核心组件。在反向代理场景下,CAS 通常需要同时监听 HTTP 和 HTTPS 两个端口。本章将深入解析三个版本中 HTTP 连接器的配置方式演进,包括编程式创建、YAML 声明式配置和 WebServerFactoryCustomizer 的跨版本变化。

4.1 CAS 5.3:编程式连接器创建

CAS 5.3 基于 Spring Boot 1.x,其嵌入式容器配置 API 使用的是 EmbeddedServletContainerCustomizer 接口。在这个版本中,HTTP 连接器的创建完全通过 Java 代码实现。

核心 Java 配置类:

java
package com.example.cas.config;

import org.apache.catalina.connector.Connector;
import org.apache.coyote.http11.Http11NioProtocol;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.annotation.Configuration;

/**
 * Tomcat HTTP 连接器配置类
 * <p>
 * 在 Spring Boot 1.x 中,通过 EmbeddedServletContainerCustomizer
 * 向嵌入式 Tomcat 添加额外的 HTTP 连接器。
 * </p>
 */
@Configuration
public class TomcatHttpConnectorConfig implements EmbeddedServletContainerCustomizer {

    private static final Logger LOGGER = LoggerFactory.getLogger(TomcatHttpConnectorConfig.class);

    // 注入 CAS 配置属性
    @Autowired
    private CasConfigurationProperties casProperties;

    @Override
    public void customize(ConfigurableEmbeddedServletContainer container) {
        // 确认容器类型为 Tomcat
        if (container instanceof TomcatEmbeddedServletContainerFactory) {
            final TomcatEmbeddedServletContainerFactory tomcat =
                (TomcatEmbeddedServletContainerFactory) container;
            configureHttpConnector(tomcat);
        } else {
            LOGGER.error("当前容器不支持 Tomcat 配置: [{}]", container);
        }
    }

    /**
     * 配置 HTTP 连接器
     * 核心步骤:创建 Connector → 设置端口 → 设置属性 → 注册到工厂
     */
    private void configureHttpConnector(TomcatEmbeddedServletContainerFactory tomcat) {
        // 从 CAS 配置中读取 HTTP 连接器属性
        CasEmbeddedApacheTomcatHttpProperties httpProps =
            casProperties.getServer().getHttp();

        if (httpProps.isEnabled()) {
            LOGGER.debug("正在为嵌入式 Tomcat 创建 HTTP 连接器...");

            // 步骤1: 创建新的 Connector 实例
            final Connector connector = new Connector(httpProps.getProtocol());

            // 步骤2: 设置监听端口
            int port = httpProps.getPort();
            if (port <= 0) {
                LOGGER.warn("未配置显式端口,正在扫描可用端口...");
                port = SocketUtils.findAvailableTcpPort();
            }
            connector.setPort(port);
            LOGGER.info("HTTP 连接器已激活,监听端口: [{}]", port);

            // 步骤3: 将配置中的属性(如 proxyPort、address)设置到连接器
            httpProps.getAttributes().forEach(connector::setAttribute);

            // 步骤4: 将连接器注册到 Tomcat 工厂
            tomcat.addAdditionalTomcatConnectors(connector);
        }
    }
}

对应的 YAML 配置:

yaml
cas:
  server:
    http:
      enabled: true
      port: 8080
      protocol: org.apache.coyote.http11.Http11NioProtocol
      attributes:
        proxyPort: "80"
        address: "0.0.0.0"

工作原理详解:

CAS 5.3 的连接器创建过程可以分解为以下几个关键步骤:

CAS 5.3 HTTP 连接器创建流程:

┌─────────────────────────────────────────────────────────────┐
│  1. Spring Boot 启动                                        │
│     └── 扫描到 TomcatHttpConnectorConfig 配置类              │
│         └── 注册为 EmbeddedServletContainerCustomizer Bean   │
├─────────────────────────────────────────────────────────────┤
│  2. 创建嵌入式 Tomcat 容器                                   │
│     └── TomcatEmbeddedServletContainerFactory                │
│         ├── 默认创建 HTTPS Connector (8443)                  │
│         └── 调用所有 Customizer 的 customize() 方法           │
├─────────────────────────────────────────────────────────────┤
│  3. TomcatHttpConnectorConfig.customize() 执行              │
│     ├── 读取 cas.server.http 配置                           │
│     ├── 创建新的 Connector(protocol=Http11NioProtocol)       │
│     ├── 设置 port=8080                                      │
│     ├── 设置属性: proxyPort=80, address=0.0.0.0             │
│     └── 调用 tomcat.addAdditionalTomcatConnectors()         │
├─────────────────────────────────────────────────────────────┤
│  4. Tomcat 启动                                             │
│     ├── HTTPS Connector 监听 0.0.0.0:8443                  │
│     └── HTTP Connector 监听 0.0.0.0:8080                   │
└─────────────────────────────────────────────────────────────┘

关键 API 说明:

  • EmbeddedServletContainerCustomizer:Spring Boot 1.x 中用于自定义嵌入式 Servlet 容器的回调接口
  • TomcatEmbeddedServletContainerFactory:Spring Boot 1.x 中创建嵌入式 Tomcat 的工厂类
  • ConfigurableEmbeddedServletContainer:可配置的嵌入式容器接口
  • Connector:Tomcat 的连接器类,代表一个独立的网络端点
  • addAdditionalTomcatConnectors():向 Tomcat 工厂添加额外的连接器

4.2 CAS 6.6/7.3:YAML 声明式配置

从 CAS 6.6 开始,HTTP 连接器的配置方式发生了根本性变化。Spring Boot 2.x 引入了全新的嵌入式容器配置 API,同时 CAS 框架本身也增强了对 YAML 声明式连接器配置的支持。

CAS 6.6 YAML 配置:

yaml
cas:
  server:
    tomcat:
      http:
        - enabled: true
          port: 8080
          scheme: http
          secure: false
          protocol: org.apache.coyote.http11.Http11NioProtocol
          attributes:
            proxyPort: "80"
            address: "0.0.0.0"

CAS 7.3 YAML 配置:

yaml
cas:
  server:
    tomcat:
      http:
        - enabled: true
          port: 8080
          scheme: http
          secure: false
          protocol: org.apache.coyote.http11.Http11NioProtocol
          attributes:
            proxyPort: "80"
            address: "0.0.0.0"

两个版本的 YAML 配置几乎完全一致,都采用了数组形式(http: 下的 - 列表项),支持配置多个 HTTP 连接器。每个连接器可以独立配置端口、协议、安全属性和自定义属性。

YAML 配置与 Java 配置的对比:

维度CAS 5.3 (Java 编程式)CAS 6.6/7.3 (YAML 声明式)
配置方式Java 代码创建 ConnectorYAML 声明式配置
多连接器支持需要编写循环逻辑YAML 数组原生支持
属性设置connector.setAttribute()attributes: YAML 节点
地址绑定通过 attributes 设置通过 attributes 设置
协议指定通过 http.getProtocol()通过 protocol: 字段
维护成本较高(代码变更需重新编译)较低(修改配置即可生效)
灵活性高(可编写任意逻辑)中(受限于配置 schema)

声明式配置的处理流程:

CAS 6.6/7.3 YAML 声明式连接器处理流程:

┌─────────────────────────────────────────────────────────────┐
│  1. Spring Boot 启动                                        │
│     └── 加载 application.yml                                │
│         └── 解析 cas.server.tomcat.http[] 配置项             │
├─────────────────────────────────────────────────────────────┤
│  2. CAS 自动装配                                            │
│     └── CasEmbeddedTomcatServletWebServerFactory            │
│         ├── 读取 YAML 中的 http[] 数组配置                   │
│         ├── 为每个数组项创建 Connector 实例                  │
│         ├── 设置 port / scheme / secure / protocol          │
│         ├── 设置 attributes (proxyPort, address)            │
│         └── 注册 Connector 到 Tomcat                        │
├─────────────────────────────────────────────────────────────┤
│  3. TomcatHttpConnectorConfig.customize() 执行              │
│     └── 遍历所有 Connector                                  │
│         └── 对 HTTP Connector 设置 address = "0.0.0.0"      │
├─────────────────────────────────────────────────────────────┤
│  4. Tomcat 启动                                             │
│     ├── HTTPS Connector 监听 0.0.0.0:8443                  │
│     └── HTTP Connector 监听 0.0.0.0:8080                   │
└─────────────────────────────────────────────────────────────┘

4.3 WebServerFactoryCustomizer 跨版本演进

从 Spring Boot 1.x 到 2.x 再到 3.x,嵌入式容器的自定义接口经历了两次重大重构。CAS 的 TomcatHttpConnectorConfig 类也随之演进。

API 演进对比:

Spring Boot 版本自定义接口工厂类CAS 版本
1.xEmbeddedServletContainerCustomizerTomcatEmbeddedServletContainerFactoryCAS 5.3
2.xWebServerFactoryCustomizer<T>TomcatServletWebServerFactoryCAS 6.6
3.xWebServerFactoryCustomizer<T>TomcatServletWebServerFactoryCAS 7.3

CAS 5.3 (Spring Boot 1.x):

java
// Spring Boot 1.x API
public class TomcatHttpConnectorConfig
    implements EmbeddedServletContainerCustomizer {

    @Override
    public void customize(
        ConfigurableEmbeddedServletContainer container) {
        // 类型检查并转换
        if (container instanceof TomcatEmbeddedServletContainerFactory) {
            TomcatEmbeddedServletContainerFactory tomcat =
                (TomcatEmbeddedServletContainerFactory) container;
            // 创建并添加 HTTP 连接器
            Connector connector = new Connector(protocol);
            connector.setPort(port);
            attributes.forEach(connector::setAttribute);
            tomcat.addAdditionalTomcatConnectors(connector);
        }
    }
}

CAS 6.6 (Spring Boot 2.x):

java
// Spring Boot 2.x API - 泛型化接口
public class TomcatHttpConnectorConfig
    implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {

    @Override
    public void customize(TomcatServletWebServerFactory factory) {
        // 使用 Lambda 表达式添加连接器定制器
        factory.addConnectorCustomizers(connector -> {
            // 仅处理 HTTP 连接器
            if ("HTTP/1.1".equals(connector.getProtocol())
                || connector.getProtocol().contains("Http11")) {
                connector.setAttribute("address", "0.0.0.0");
            }
        });
    }
}

CAS 7.3 (Spring Boot 3.x):

java
// Spring Boot 3.x API - 接口签名不变,但底层实现有变化
public class TomcatHttpConnectorConfig
    implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {

    @Override
    public void customize(TomcatServletWebServerFactory factory) {
        factory.addConnectorCustomizers(connector -> {
            if ("HTTP/1.1".equals(connector.getProtocol())
                || connector.getProtocol().contains("Http11")) {
                // 注意:CAS 7.3 使用 setProperty 替代 setAttribute
                connector.setProperty("address", "0.0.0.0");
            }
        });
    }
}

setAttribute vs setProperty 的差异:

CAS 6.6 使用 connector.setAttribute("address", "0.0.0.0"),而 CAS 7.3 使用 connector.setProperty("address", "0.0.0.0")。这两个方法在 Tomcat 的 Connector 类中有不同的语义:

  • setAttribute(String name, Object value):设置连接器的通用属性,值类型为 Object,内部通过 IntrospectionUtils.setProperty() 实现
  • setProperty(String name, String value):设置连接器的字符串属性,值类型为 String,直接调用 IntrospectionUtils.setProperty()

在功能上,两者都能实现将 address 属性设置为 "0.0.0.0" 的目的。但从 API 语义上看,setProperty 更精确地表达了"设置字符串属性"的意图。在 Spring Boot 3.x / Tomcat 10.x 的版本中,推荐使用 setProperty 方法。

演进趋势分析:

从 CAS 5.3 到 7.3,TomcatHttpConnectorConfig 的职责发生了显著变化:

职责演进:

CAS 5.3:
  ├── 创建 HTTP Connector (编程式)
  ├── 设置端口
  ├── 设置所有属性 (proxyPort, address 等)
  └── 注册 Connector 到 Tomcat 工厂
  → 职责:连接器的完整生命周期管理

CAS 6.6/7.3:
  ├── Connector 由 CAS 框架根据 YAML 配置自动创建
  └── 仅补充设置 address 属性
  → 职责:连接器的属性微调

这种演进趋势反映了 Spring Boot 和 CAS 框架的设计理念变化:从"通过代码控制一切"转向"约定优于配置"的声明式风格。CAS 6.6 和 7.3 中,连接器的核心配置(端口、协议、proxyPort 等)都通过 YAML 声明,Java 配置类仅负责那些无法通过 YAML 表达的精细调整。

CAS 5.3 到 6.6/7.3 的升级迁移指南:

当从 CAS 5.3 升级到 6.6 或 7.3 时,TomcatHttpConnectorConfig 的迁移需要特别注意以下几点:

步骤一:修改接口声明。EmbeddedServletContainerCustomizer 替换为 WebServerFactoryCustomizer<TomcatServletWebServerFactory>,并更新相应的 import 语句。

步骤二:简化连接器创建逻辑。 由于 CAS 6.6/7.3 通过 YAML 自动创建连接器,Java 代码中不再需要手动创建 Connector 实例、设置端口和注册到工厂。只需要保留对连接器属性的微调逻辑。

步骤三:更新配置文件格式。cas.server.http.* 配置迁移为 cas.server.tomcat.http[] 数组格式,注意新增的 schemesecure 字段。

步骤四:验证连接器行为。 升级后需要验证以下行为是否正常:

  • HTTP 端口是否正确监听
  • HTTPS 端口是否正常工作
  • proxyPort 是否生效(通过检查生成的重定向 URL)
  • address 是否正确绑定到 0.0.0.0

完整的迁移前后对比:

java
// ===== 升级前 (CAS 5.3) =====
@Configuration
public class TomcatHttpConnectorConfig
    implements EmbeddedServletContainerCustomizer {

    @Autowired
    private CasConfigurationProperties casProperties;

    @Override
    public void customize(ConfigurableEmbeddedServletContainer container) {
        TomcatEmbeddedServletContainerFactory tomcat =
            (TomcatEmbeddedServletContainerFactory) container;
        CasEmbeddedApacheTomcatHttpProperties http =
            casProperties.getServer().getHttp();
        if (http.isEnabled()) {
            Connector connector = new Connector(http.getProtocol());
            connector.setPort(http.getPort());
            http.getAttributes().forEach(connector::setAttribute);
            tomcat.addAdditionalTomcatConnectors(connector);
        }
    }
}

// ===== 升级后 (CAS 6.6/7.3) =====
@Configuration
public class TomcatHttpConnectorConfig
    implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {

    @Override
    public void customize(TomcatServletWebServerFactory factory) {
        factory.addConnectorCustomizers(connector -> {
            if ("HTTP/1.1".equals(connector.getProtocol())
                || connector.getProtocol().contains("Http11")) {
                connector.setProperty("address", "0.0.0.0");
            }
        });
    }
}

4.4 proxyPort 与地址绑定

在反向代理场景中,proxyPortaddress 是两个至关重要的连接器属性。

proxyPort 属性:

proxyPort 告诉 Tomcat:虽然这个连接器实际监听的是内部端口(如 8080),但对外表现为另一个端口(如 80)。当 Tomcat 需要生成包含端口号的 URL 时(例如重定向),会使用 proxyPort 的值而非实际监听端口。

proxyPort 工作原理:

┌──────────────────────────────────────────────────────────────┐
│                                                              │
│  Tomcat HTTP Connector                                       │
│  ├── port = 8080 (实际监听端口)                               │
│  └── proxyPort = 80 (对外表现端口)                            │
│                                                              │
│  当 CAS 需要生成重定向 URL 时:                                 │
│                                                              │
│  无 proxyPort:  http://cas.example.com:8080/cas/login        │
│  有 proxyPort:  http://cas.example.com:80/cas/login          │
│                                                              │
│  反向代理期望的 URL: http://cas.example.com/cas/login         │
│  (80 是 HTTP 默认端口,可以省略)                               │
│                                                              │
└──────────────────────────────────────────────────────────────┘

配置示例:

yaml
cas:
  server:
    tomcat:
      http:
        - enabled: true
          port: 8080
          attributes:
            proxyPort: "80"    # 字符串类型,Tomcat 会自动转换

address 属性:

address 属性控制连接器绑定的网络接口地址。默认情况下,连接器绑定到所有可用网络接口(相当于 0.0.0.0)。但在某些容器化或虚拟化环境中,默认绑定地址可能不正确。

address 绑定示例:

address: "0.0.0.0"    → 监听所有网络接口 (推荐用于容器化部署)
address: "127.0.0.1"  → 仅监听本地回环接口 (仅本机访问)
address: "192.168.1.100" → 仅监听指定网卡 (多网卡环境)

在 CAS 三个版本中的配置方式:

yaml
# CAS 5.3 - 通过 attributes 配置
cas:
  server:
    http:
      enabled: true
      port: 8080
      attributes:
        proxyPort: "80"
        address: "0.0.0.0"

# CAS 6.6 - 同样通过 attributes 配置
cas:
  server:
    tomcat:
      http:
        - enabled: true
          port: 8080
          attributes:
            proxyPort: "80"
            address: "0.0.0.0"

# CAS 7.3 - 同样通过 attributes 配置
cas:
  server:
    tomcat:
      http:
        - enabled: true
          port: 8080
          attributes:
            proxyPort: "80"
            address: "0.0.0.0"

双连接器架构图:

CAS 服务器双连接器架构:

┌─────────────────────────────────────────────────────────────┐
│                    CAS (Spring Boot + Tomcat)                │
│                                                             │
│  ┌───────────────────────────────────────────────────────┐  │
│  │  HTTPS Connector (主连接器)                            │  │
│  │  ├── port: 8443                                       │  │
│  │  ├── scheme: https                                    │  │
│  │  ├── secure: true                                     │  │
│  │  ├── SSL/TLS 配置: keystore.jks                       │  │
│  │  └── 绑定地址: 0.0.0.0:8443                           │  │
│  └───────────────────────────────────────────────────────┘  │
│                                                             │
│  ┌───────────────────────────────────────────────────────┐  │
│  │  HTTP Connector (辅助连接器)                            │  │
│  │  ├── port: 8080                                       │  │
│  │  ├── scheme: http                                     │  │
│  │  ├── secure: false                                    │  │
│  │  ├── proxyPort: 80 (反向代理映射)                      │  │
│  │  └── 绑定地址: 0.0.0.0:8080                           │  │
│  └───────────────────────────────────────────────────────┘  │
│                                                             │
│  两个连接器共享同一个 Servlet Engine 和部署的应用             │
└─────────────────────────────────────────────────────────────┘
         ▲                    ▲
         │                    │
    HTTPS (8443)         HTTP (8080)
         │                    │
    ┌────┴────┐         ┌────┴────┐
    │ 直接访问  │         │ 反向代理 │
    │ (内网)   │         │ (Nginx) │
    └─────────┘         └─────────┘

第五章 OIDC Issuer 反向代理适配

导读: OIDC(OpenID Connect)协议中的 Issuer 标识符是身份验证的核心要素。在反向代理场景下,协议和端口的转换可能导致 Issuer 匹配失败。本章将深入分析 Issuer 匹配问题的根源,并讲解如何通过 acceptedIssuersPattern 配置实现协议无关的匹配策略。

5.1 Issuer 匹配问题的根源

在 OIDC 协议中,Issuer(签发者)是一个用于唯一标识 OIDC Provider 的 URL。根据 OpenID Connect Discovery 规范,客户端在发起认证请求时,会从 Discovery 文档中获取 Issuer 值,并在后续的 Token 验证等环节中严格匹配此值。

问题场景:

正常场景(无反向代理):

  CAS 配置:  issuer = https://cas.example.com/cas/oidc
  客户端请求: https://cas.example.com:8443/cas/oidc/.well-known/openid-configuration
  Discovery 返回:  { "issuer": "https://cas.example.com/cas/oidc" }
  Token 验证:      issuer 匹配 ✓


反向代理场景(协议不匹配):

  CAS 配置:  issuer = https://cas.example.com/cas/oidc
  客户端请求: https://cas.example.com/cas/oidc/.well-known/openid-configuration
  Nginx 转发: http://cas-backend:8080/cas/oidc/.well-known/openid-configuration
  CAS 感知:   scheme=http, host=cas-backend, port=8080
  Discovery 返回:  { "issuer": "http://cas-backend:8080/cas/oidc" }  ← 错误!
  Token 验证:      "http://cas-backend:8080/cas/oidc" ≠ "https://cas.example.com/cas/oidc"
  结果:            Issuer 匹配失败 ✗

问题根源分析:

Issuer 匹配失败的根源在于 CAS 构建 Issuer URL 时使用了从 HttpServletRequest 中获取的协议、主机名和端口信息。当转发头配置不正确时,CAS 获取到的是内部地址信息而非外部地址信息,导致生成的 Issuer URL 与客户端期望的不一致。

这个问题在 CAS 5.3 中相对容易解决,因为 Issuer 通常通过 cas.authn.oidc.issuer 配置项硬编码为 HTTPS URL。但在 CAS 6.6 和 7.3 中,OIDC 模块引入了更严格的 Issuer 验证机制,不仅要求 Discovery 文档中的 Issuer 正确,还要求请求中的 Issuer 必须匹配预定义的模式。

5.2 acceptedIssuersPattern 正则配置

为了解决反向代理场景下的 Issuer 匹配问题,CAS 6.6 和 7.3 引入了 acceptedIssuersPattern 配置项。这个配置项允许使用正则表达式定义可接受的 Issuer 模式,从而同时支持 HTTP 和 HTTPS 两种协议的 Issuer。

CAS 6.6 配置:

yaml
cas:
  authn:
    oidc:
      core:
        issuer: https://cas.example.com/cas/oidc
        # 使用正则表达式同时匹配 HTTP 和 HTTPS
        acceptedIssuersPattern: https?://(cas\.example\.com(:\d+)?|localhost(:\d+)?)/cas/oidc

CAS 7.3 配置:

yaml
cas:
  authn:
    oidc:
      core:
        issuer: https://cas.example.com/cas/oidc
        # 注意属性名使用 kebab-case
        accepted-issuers-pattern: https?://(cas\.example\.com(:8443)?|localhost(:\d+)?)/cas/oidc

正则表达式解析:

https?://(cas\.example\.com(:\d+)?|localhost(:\d+)?)/cas/oidc

拆解分析:
┌────────────┬────────────────────────────────────────────────┐
│ 片段         │ 含义                                          │
├────────────┼────────────────────────────────────────────────┤
│ https?     │ 匹配 "http" 或 "https"(? 表示 s 可选)          │
│ ://        │ 匹配协议分隔符                                  │
│ (          │ 开始捕获组(主机名部分)                          │
│   cas\.    │ 匹配 "cas."(\. 匹配字面点号)                   │
│   example\.│ 匹配 "example."                                │
│   com      │ 匹配 "com"                                     │
│   (:\d+)?  │ 可选的端口号(冒号后跟一个或多个数字)             │
│   |        │ 或(匹配 localhost)                            │
│   localhost│ 匹配 "localhost"                                │
│   (:\d+)?  │ 可选的端口号                                    │
│ )          │ 结束捕获组                                      │
│ /cas/oidc  │ 匹配路径 "/cas/oidc"                            │
└────────────┴────────────────────────────────────────────────┘

匹配示例:
  ✓ https://cas.example.com/cas/oidc
  ✓ http://cas.example.com/cas/oidc
  ✓ https://cas.example.com:8443/cas/oidc
  ✓ http://cas.example.com:8080/cas/oidc
  ✓ https://localhost/cas/oidc
  ✓ http://localhost:8080/cas/oidc
  ✗ https://evil.com/cas/oidc          (域名不匹配)
  ✗ https://cas.example.com/cas/oauth  (路径不匹配)

CAS 6.6 与 7.3 的配置差异:

维度CAS 6.6CAS 7.3
配置属性名acceptedIssuersPattern (camelCase)accepted-issuers-pattern (kebab-case)
端口匹配(:\d+)? 匹配任意端口(:8443)? 匹配特定端口
YAML 风格camelCaserelaxed binding (kebab-case)

CAS 7.3 使用 kebab-case 的属性名是 Spring Boot 的 relaxed binding 特性所推荐的命名风格。Spring Boot 的 relaxed binding 允许使用 kebab-case、camelCase、snake_case 等多种格式来引用同一个配置属性,但 kebab-case 是官方推荐的格式。

5.3 协议无关匹配策略

acceptedIssuersPattern 的核心设计思想是实现"协议无关"的 Issuer 匹配。在反向代理场景下,请求到达 CAS 时可能使用 HTTP 协议(代理终止 SSL 后转发),但 Issuer 配置为 HTTPS URL。通过正则表达式中的 https? 模式,两种协议的 Issuer 都能被接受。

完整的 OIDC 反向代理适配方案:

OIDC 反向代理适配三层保障:

┌─────────────────────────────────────────────────────────────┐
│  第一层:Spring Boot 转发头策略                               │
│  → 确保 CAS 感知到正确的协议和端口                             │
│  → Discovery 文档中的 Issuer 使用正确的 URL                   │
│  → forward-headers-strategy: NATIVE                         │
├─────────────────────────────────────────────────────────────┤
│  第二层:Tomcat RemoteIpValve                                │
│  → 在容器层面重写请求的协议、端口和主机名                       │
│  → 确保 HttpServletRequest 返回正确的 getScheme() 等值        │
│  → protocol-header: X-Forwarded-Proto                       │
├─────────────────────────────────────────────────────────────┤
│  第三层:acceptedIssuersPattern 兜底                         │
│  → 即使前两层配置不完美,也能通过正则匹配接受请求               │
│  → 同时支持 HTTP 和 HTTPS 两种 Issuer                        │
│  → accepted-issuers-pattern: https?://...                   │
└─────────────────────────────────────────────────────────────┘

调试 Issuer 匹配问题的方法:

当遇到 Issuer 匹配问题时,可以通过以下步骤进行排查:

bash
# 1. 获取 Discovery 文档,检查返回的 issuer 值
curl -k https://cas.example.com/cas/oidc/.well-known/openid-configuration | jq '.issuer'

# 2. 检查请求头是否正确传递
curl -v \
  -H "X-Forwarded-Proto: https" \
  -H "X-Forwarded-Port: 443" \
  -H "X-Forwarded-Host: cas.example.com" \
  http://localhost:8080/cas/oidc/.well-known/openid-configuration

# 3. 检查 CAS 日志中是否有 Issuer 匹配相关的错误
grep -i "issuer" /path/to/cas/logs/cas.log

深入理解 CAS 的 Issuer 构建机制:

CAS 在构建 OIDC Issuer URL 时,其内部逻辑会依次尝试以下数据源:

  1. 显式配置的 issuer:即 cas.authn.oidc.core.issuer 配置项的值。如果设置了此值,CAS 通常会直接使用它作为 Issuer。

  2. 基于请求动态构建:如果未显式配置 issuer,CAS 会根据当前请求的 schemeserverNameserverPort 和 context path 动态构建 Issuer URL。这就是为什么在反向代理场景下,如果转发头配置不正确,Issuer 会变成内部地址。

  3. Endpoint URL 推导:CAS 还会根据配置的 cas.server.namecas.server.prefix 来推导各种端点的 URL,这些 URL 的协议和端口也受到转发头配置的影响。

理解这个构建机制后,就能明白为什么三层配置架构中的每一层都对 OIDC Issuer 的正确性至关重要:第一层和第二层确保 CAS 感知到正确的协议和端口,从而正确构建 Issuer URL;第三层的 acceptedIssuersPattern 则作为最后一道防线,确保即使 Issuer URL 有微小偏差,也能被接受。

常见错误与解决方案:

错误现象可能原因解决方案
Issuer not matched转发头未正确配置检查第一层和第二层配置
Issuer not matched正则表达式不匹配检查 acceptedIssuersPattern 是否覆盖实际 URL
Discovery 返回 HTTP Issuerforward-headers-strategy 未配置设置为 NATIVE
Discovery 返回内部 IPX-Forwarded-Host 未设置在 Nginx 中添加 proxy_set_header Host

第六章 生产环境部署最佳实践

导读: 理论配置最终要落地到生产环境。本章将提供 Nginx 反向代理配置示例、HTTPS 终止与证书管理方案、健康检查与就绪检测配置,以及常见问题的排查方法。

6.1 Nginx 反向代理配置示例

以下是一个完整的 Nginx 反向代理配置示例,适用于 CAS 的生产环境部署。

Nginx 配置文件:

nginx
# /etc/nginx/conf.d/cas.conf

# HTTP → HTTPS 重定向
server {
    listen 80;
    server_name cas.example.com;

    # 将所有 HTTP 请求重定向到 HTTPS
    return 301 https://$host$request_uri;
}

# HTTPS 主配置
server {
    listen 443 ssl http2;
    server_name cas.example.com;

    # SSL/TLS 证书配置
    ssl_certificate     /etc/nginx/ssl/cas.example.com.crt;
    ssl_certificate_key /etc/nginx/ssl/cas.example.com.key;

    # TLS 协议版本和加密套件
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
    ssl_prefer_server_ciphers on;

    # SSL 会话缓存
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;

    # HSTS 安全头
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    # 请求体大小限制(CAS 登录表单可能较大)
    client_max_body_size 10m;

    # 代理超时配置
    proxy_connect_timeout 30s;
    proxy_read_timeout 300s;
    proxy_send_timeout 300s;

    # 代理转发头设置(关键配置)
    location /cas/ {
        proxy_pass http://cas_backend:8080;

        # 传递原始协议信息
        proxy_set_header X-Forwarded-Proto $scheme;

        # 传递原始端口信息
        proxy_set_header X-Forwarded-Port $server_port;

        # 传递原始主机名
        proxy_set_header X-Forwarded-Host $host;

        # 传递客户端真实 IP
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        # 传递原始 Host 头
        proxy_set_header Host $host;

        # WebSocket 支持(CAS 可能使用 WebSocket 推送)
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }

    # 健康检查端点(不记录访问日志)
    location /cas/status/health {
        proxy_pass http://cas_backend:8080;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Port $server_port;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $host;
        access_log off;
    }
}

# 上游服务器定义
upstream cas_backend {
    # CAS 实例列表(负载均衡)
    server 192.168.1.10:8080 max_fails=3 fail_timeout=30s;
    server 192.168.1.11:8080 max_fails=3 fail_timeout=30s;

    # 健康检查
    # 注意:开源版 Nginx 不支持主动健康检查
    # 可使用 nginx_upstream_check_module 或商业版 Nginx Plus
    keepalive 32;
}

配置要点解析:

Nginx 转发头设置与 CAS 三层架构的对应关系:

┌──────────────────────────────────────────────────────────────┐
│  Nginx 配置                    CAS 对应配置                   │
├──────────────────────────────────────────────────────────────┤
│  X-Forwarded-Proto: $scheme   → protocol-header              │
│  X-Forwarded-Port: $server_port → port-header                │
│  X-Forwarded-For: $proxy_add_x_forwarded_for                 │
│                                → remote-ip-header            │
│  X-Forwarded-Host: $host      → (由 RemoteIpValve 自动处理)   │
│  Host: $host                  → (由 RemoteIpValve 自动处理)   │
└──────────────────────────────────────────────────────────────┘

多实例负载均衡配置:

当部署多个 CAS 实例时,Nginx 的 upstream 配置需要特别注意会话亲和性(Session Affinity)。CAS 使用 TGT Cookie 维护登录状态,如果请求被分发到不同的 CAS 实例且未配置分布式会话存储,将导致用户被反复要求登录。

转发头传递的完整数据流:

理解转发头从 Nginx 到 CAS 的完整传递过程对于排查问题至关重要。下面展示一个完整的请求-响应数据流:

完整数据流示例:

1. 用户浏览器发送请求:
   GET https://cas.example.com/cas/login HTTP/2
   Host: cas.example.com
   User-Agent: Mozilla/5.0 ...

2. Nginx 接收请求并添加转发头:
   GET http://cas-backend:8080/cas/login HTTP/1.1
   Host: cas.example.com
   X-Forwarded-Proto: https
   X-Forwarded-Port: 443
   X-Forwarded-Host: cas.example.com
   X-Forwarded-For: 203.0.113.50
   Connection: keep-alive

3. CAS Tomcat 接收请求:
   RemoteIpValve 处理前:
     scheme=http, port=8080, remoteAddr=192.168.1.10
   RemoteIpValve 处理后:
     scheme=https, port=443, remoteAddr=203.0.113.50

4. CAS 生成响应:
   HTTP/1.1 200 OK
   Set-Cookie: TGT=...; Path=/cas; Secure; HttpOnly
   Content-Type: text/html
   (页面中的表单 action 使用 https://cas.example.com/cas/login)

5. Nginx 将响应返回给用户:
   HTTP/2 200 OK
   Set-Cookie: TGT=...; Path=/cas; Secure; HttpOnly
   Content-Type: text/html
nginx
# 方案一:IP Hash 会话亲和(简单但不精确)
upstream cas_backend {
    ip_hash;
    server 192.168.1.10:8080;
    server 192.168.1.11:8080;
}

# 方案二:使用 Redis 分布式会话(推荐)
# CAS 配置 Redis 票据存储后,无需会话亲和
upstream cas_backend {
    least_conn;
    server 192.168.1.10:8080;
    server 192.168.1.11:8080;
}

6.2 HTTPS 终止与证书管理

在企业环境中,HTTPS 终止策略的选择对安全性和性能都有重要影响。

HTTPS 终止方案对比:

方案说明优点缺点
Nginx 终止SSL 在 Nginx 层终止CAS 无需处理 SSL,性能好Nginx 到 CAS 之间为 HTTP
CAS 终止SSL 一直到 CAS端到端加密CAS 性能开销大
双向 SSLNginx 和 CAS 都有 SSL最大安全性配置复杂,性能开销最大
透传 SSLNginx 以 TCP 模式转发端到端加密Nginx 无法修改请求

推荐方案:Nginx 终止 + 内网 HTTP

这是最常见的部署方案,Nginx 负责 SSL 终止,CAS 通过 HTTP 在内网运行。配合本文描述的三层配置架构,可以确保 CAS 正确感知外部 HTTPS 协议。

证书管理建议:

bash
# 使用 Let's Encrypt 自动获取和续期证书
certbot --nginx -d cas.example.com

# 证书自动续期(certbot 会自动添加 cron 任务)
certbot renew --dry-run

# 证书文件路径
# /etc/letsencrypt/live/cas.example.com/fullchain.pem
# /etc/letsencrypt/live/cas.example.com/privkey.pem

CAS 端 SSL 配置(用于内网 HTTPS):

即使使用 Nginx 终止 SSL,CAS 仍然可以保留 HTTPS 端口用于内网直接访问(如管理操作):

yaml
server:
  ssl:
    enabled: true
    key-store: classpath:keystore.jks
    key-store-password: changeit
    key-store-type: JKS
    protocol: TLS

6.3 健康检查与就绪检测

在容器化和 Kubernetes 环境中,健康检查和就绪检测是确保服务可靠性的关键配置。

CAS 健康检查端点配置:

yaml
# 通用配置(适用于 CAS 6.6/7.3)
management:
  endpoints:
    web:
      exposure:
        include: health,info
      base-path: /status
  endpoint:
    health:
      enabled: true
      show-details: when_authorized
    info:
      enabled: true

健康检查 URL:

# 基础健康检查
GET /cas/status/health

# 详细健康信息(需要授权)
GET /cas/status/health?details=true

# 预期响应:
{
  "status": "UP",
  "components": {
    "redis": { "status": "UP" },
    "ldap": { "status": "UP" },
    "diskSpace": { "status": "UP" }
  }
}

Kubernetes 健康检查配置:

yaml
# Kubernetes Deployment 配置片段
spec:
  containers:
    - name: cas
      image: cas-overlay:latest
      ports:
        - containerPort: 8080
        - containerPort: 8443
      # 存活探针:检测 CAS 是否存活
      livenessProbe:
        httpGet:
          path: /cas/status/health
          port: 8080
        initialDelaySeconds: 120
        periodSeconds: 30
        timeoutSeconds: 10
        failureThreshold: 3
      # 就绪探针:检测 CAS 是否可以接收流量
      readinessProbe:
        httpGet:
          path: /cas/status/health
          port: 8080
        initialDelaySeconds: 60
        periodSeconds: 10
        timeoutSeconds: 5
        failureThreshold: 3
      # 启动探针:给 CAS 足够的启动时间
      startupProbe:
        httpGet:
          path: /cas/status/health
          port: 8080
        initialDelaySeconds: 30
        periodSeconds: 10
        failureThreshold: 30

Nginx 健康检查配置:

nginx
# Nginx 主动健康检查(需要 nginx_upstream_check_module)
upstream cas_backend {
    server 192.168.1.10:8080;
    server 192.168.1.11:8080;

    check interval=3000 rise=2 fall=3 timeout=5000 type=http;
    check_http_send "GET /cas/status/health HTTP/1.0\r\n\r\n";
    check_http_expect_alive http_2xx http_3xx;
}

6.4 常见问题排查

以下是 CAS 反向代理部署中常见的问题及其排查方法。

问题一:无限重定向循环

现象: 用户访问 CAS 登录页面时,浏览器报告"重定向次数过多"或"ERR_TOO_MANY_REDIRECTS"。

原因分析:

重定向循环的典型原因:

1. CAS 认为请求是 HTTP,生成 HTTP 重定向 URL
   → 浏览器访问 HTTP URL
   → Nginx 将 HTTP 重定向到 HTTPS
   → CAS 又生成 HTTP URL
   → 无限循环

2. X-Forwarded-Proto 未正确传递
   → CAS 无法感知原始 HTTPS 协议
   → 生成 http:// 的回调 URL

排查步骤:

bash
# 步骤1: 检查 Nginx 是否正确设置转发头
curl -v -k https://cas.example.com/cas/login 2>&1 | grep -i "x-forwarded"

# 步骤2: 直接访问 CAS 后端,模拟代理转发
curl -v \
  -H "X-Forwarded-Proto: https" \
  -H "X-Forwarded-Port: 443" \
  -H "X-Forwarded-Host: cas.example.com" \
  http://localhost:8080/cas/login 2>&1 | grep -i "location"

# 步骤3: 检查 CAS 配置
# 确认 forward-headers-strategy 或 use-forward-headers 已启用
# 确认 cas.server.name 使用 https:// 前缀

问题二:票据验证失败

现象: CAS 登录成功,但应用端验证票据时返回"票据验证失败"。

原因分析:

票据验证失败的常见原因:

1. 回调 URL 中的协议或端口不正确
   → CAS 生成的 service URL 与应用注册的 service URL 不匹配

2. proxyPort 未配置
   → CAS 使用内部端口 8080 生成回调 URL
   → 应用期望的 URL 使用端口 80 或 443

排查步骤:

bash
# 检查 CAS 生成的回调 URL
# 在 CAS 日志中搜索 service URL 相关信息
grep "service=" /path/to/cas/logs/cas.log | tail -20

# 检查应用注册的 service URL
# 确保与 CAS 生成的 URL 一致

问题三:OIDC Token 验证失败

现象: OIDC 客户端获取 Token 后验证失败,日志显示 Issuer 不匹配。

排查步骤:

bash
# 1. 获取 Discovery 文档中的 Issuer
curl -k https://cas.example.com/cas/oidc/.well-known/openid-configuration | jq '.issuer'

# 2. 对比 CAS 配置中的 Issuer
# cas.authn.oidc.core.issuer 应与 Discovery 返回值一致

# 3. 检查 acceptedIssuersPattern 是否覆盖实际 Issuer
# 确保正则表达式能匹配 Discovery 返回的 Issuer URL

问题四:Cookie 无法设置

现象: CAS 登录后无法保持会话,反复要求登录。

原因分析:

Cookie 问题的常见原因:

1. TGC Cookie 设置了 Secure 属性,但代理到 CAS 使用 HTTP
   → Cookie 无法通过 HTTP 传输

2. Cookie 的 Domain 或 Path 与实际请求不匹配

3. SameSite 属性导致跨站请求时 Cookie 被阻止

解决方案:

yaml
# CAS 配置:禁用 TGC Cookie 的 Secure 属性(代理终止 SSL 时需要)
cas:
  tgc:
    secure: false

# 或者确保代理到 CAS 也使用 HTTPS

问题排查速查表:

问题检查项命令/方法
重定向循环转发头是否传递curl -v 检查响应头
端口不正确proxyPort 是否配置检查 cas.server.tomcat.http[].attributes.proxyPort
协议不正确forward-headers-strategy检查 server.forward-headers-strategy
IP 获取错误internal-proxies检查 server.tomcat.remoteip.internal-proxies
Issuer 不匹配acceptedIssuersPattern检查 cas.authn.oidc.core.accepted-issuers-pattern
Cookie 丢失TGC secure 设置检查 cas.tgc.secure
连接被拒绝连接器是否启动检查 CAS 启动日志中的端口绑定信息

Docker 环境下的反向代理部署示例:

在容器化部署场景中,CAS 与 Nginx 通常运行在不同的容器中,通过 Docker 网络进行通信。以下是一个使用 Docker Compose 的完整部署示例:

yaml
# docker-compose.yml
version: '3.8'

services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d
      - ./nginx/ssl:/etc/nginx/ssl
    depends_on:
      - cas
    networks:
      - cas-network

  cas:
    image: cas-overlay:latest
    environment:
      - SPRING_PROFILES_ACTIVE=production
    ports:
      - "8080:8080"
      - "8443:8443"
    volumes:
      - ./cas/config:/etc/cas/config
      - ./cas/logs:/etc/cas/logs
    networks:
      - cas-network

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis-data:/data
    networks:
      - cas-network

networks:
  cas-network:
    driver: bridge

volumes:
  redis-data:

在 Docker 环境中,internal-proxies 的配置需要特别注意。Docker 网络通常使用 172.17.0.0/16 或 172.18.0.0/16 等网段,这些地址已经被默认的 internal-proxies 正则表达式覆盖(172.16.0.0 - 172.31.255.255)。但如果使用了自定义的 Docker 网络网段,需要确保该网段被包含在 internal-proxies 中。

CAS 启动验证清单:

部署完成后,建议按照以下清单逐项验证 CAS 的反向代理配置是否正确:

CAS 反向代理部署验证清单:

[ ] 1. 直接访问 CAS HTTP 端口
      curl -v http://localhost:8080/cas/login
      → 应返回 200 OK 和登录页面

[ ] 2. 直接访问 CAS HTTPS 端口
      curl -vk https://localhost:8443/cas/login
      → 应返回 200 OK 和登录页面

[ ] 3. 通过反向代理访问(带转发头)
      curl -v -H "X-Forwarded-Proto: https" \
               -H "X-Forwarded-Port: 443" \
               -H "X-Forwarded-Host: cas.example.com" \
               http://localhost:8080/cas/login
      → 检查响应中的 URL 是否使用 https://cas.example.com

[ ] 4. 健康检查端点
      curl http://localhost:8080/cas/status/health
      → 应返回 {"status": "UP"}

[ ] 5. OIDC Discovery 端点
      curl -v -H "X-Forwarded-Proto: https" \
               -H "X-Forwarded-Host: cas.example.com" \
               http://localhost:8080/cas/oidc/.well-known/openid-configuration
      → 检查 issuer 是否为 https://cas.example.com/cas/oidc

[ ] 6. 通过外部域名访问
      curl -v https://cas.example.com/cas/login
      → 应返回 200 OK,无重定向循环

[ ] 7. 完整登录流程
      在浏览器中访问 https://cas.example.com/cas/login
      → 输入凭据后应成功登录,无报错

总结与展望

本文系统性地剖析了 CAS 反向代理三层架构的配置原理和跨版本演进。通过对 CAS 5.3、6.6、7.3 三个版本的深入对比分析,我们可以总结出以下核心要点:

第一层——Spring Boot 转发头策略是反向代理适配的入口。从 CAS 5.3 的 use-forward-headers: true 到 CAS 6.6 的 forward-headers-strategy: NATIVE,再到 CAS 7.3 的双重保障策略,转发头处理机制从简单的布尔开关演进为精细的策略枚举,从 Servlet Filter 层面下沉到 Tomcat Valve 层面,在性能和安全性上都有显著提升。

第二层——Tomcat RemoteIpValve 精细配置是协议头解析的核心。通过 protocol-headerport-headerremote-ip-header 等配置项,RemoteIpValve 能够精确地将反向代理添加的转发头转换为请求对象的属性。internal-proxies 正则表达式提供了 IP 信任模型,有效防止了转发头伪造攻击。三个版本在此层的配置基本一致,差异主要在 YAML 转义规则上。

第三层——HTTP 双连接器配置是端口映射的关键。CAS 5.3 通过 EmbeddedServletContainerCustomizer 编程式创建 HTTP 连接器,CAS 6.6/7.3 则通过 YAML 声明式配置实现。proxyPort 属性解决了内部端口与外部端口不一致的问题,address 属性确保连接器在容器化环境中正确绑定网络接口。

OIDC Issuer 适配是反向代理场景下的特殊挑战。acceptedIssuersPattern 正则配置通过协议无关的匹配策略,同时支持 HTTP 和 HTTPS 两种 Issuer,为 OIDC 协议在反向代理环境下的正确工作提供了兜底保障。

展望未来,随着 CAS 版本的持续迭代和 Spring Boot 的不断升级,反向代理配置将继续朝着更加声明化、自动化的方向演进。Spring Boot 3.x 引入的 GraalVM Native Image 支持可能对嵌入式容器的配置方式产生新的影响。同时,随着云原生技术的普及,CAS 在 Kubernetes 环境下的 Service Mesh 集成(如 Istio 的 mTLS)也将成为新的配置挑战。建议读者在掌握本文所述的三层架构原理基础上,持续关注 CAS 和 Spring Boot 的版本更新,以便及时适配新的配置模型和最佳实践。

三层架构配置速查卡:

为了方便读者在日常工作中快速查阅,下面将三个版本的核心配置整理为一张速查卡:

┌───────────────────────────────────────────────────────────────────┐
│                    CAS 反向代理三层架构配置速查卡                    │
├───────────────────────────────────────────────────────────────────┤
│                                                                   │
│  第一层:Spring Boot 转发头策略                                    │
│  ┌──────────┬────────────────────────────────────────────┐        │
│  │ CAS 5.3  │ server.use-forward-headers: true           │        │
│  │ CAS 6.6  │ server.forward-headers-strategy: NATIVE    │        │
│  │ CAS 7.3  │ use-forward-headers: true + strategy: native│       │
│  └──────────┴────────────────────────────────────────────┘        │
│                                                                   │
│  第二层:Tomcat RemoteIpValve                                     │
│  ┌──────────────────────────────────────────────────────┐        │
│  │ server.tomcat.remoteip.enabled: true                 │        │
│  │ server.tomcat.remoteip.protocol-header: X-Forwarded-Proto    │
│  │ server.tomcat.remoteip.port-header: X-Forwarded-Port │        │
│  │ server.tomcat.remoteip.remote-ip-header: X-Forwarded-For     │
│  │ server.tomcat.remoteip.protocol-header-https-value: https    │
│  │ server.tomcat.remoteip.internal-proxies: (正则表达式)  │        │
│  └──────────────────────────────────────────────────────┘        │
│                                                                   │
│  第三层:HTTP 双连接器 + proxyPort                                 │
│  ┌──────────┬────────────────────────────────────────────┐        │
│  │ CAS 5.3  │ Java: EmbeddedServletContainerCustomizer   │        │
│  │          │ YAML: cas.server.http.port/attributes       │        │
│  │ CAS 6.6  │ Java: WebServerFactoryCustomizer            │        │
│  │          │ YAML: cas.server.tomcat.http[] 数组          │        │
│  │ CAS 7.3  │ Java: WebServerFactoryCustomizer            │        │
│  │          │ YAML: cas.server.tomcat.http[] 数组          │        │
│  │ 通用     │ attributes.proxyPort: "80"                  │        │
│  │          │ attributes.address: "0.0.0.0"               │        │
│  └──────────┴────────────────────────────────────────────┘        │
│                                                                   │
│  OIDC 适配:                                                      │
│  ┌──────────┬────────────────────────────────────────────┐        │
│  │ CAS 5.3  │ cas.authn.oidc.issuer (硬编码 HTTPS)       │        │
│  │ CAS 6.6  │ acceptedIssuersPattern (正则匹配)           │        │
│  │ CAS 7.3  │ accepted-issuers-pattern (正则匹配)         │        │
│  └──────────┴────────────────────────────────────────────┘        │
│                                                                   │
└───────────────────────────────────────────────────────────────────┘

版权声明: 本文为必码(bima.cc)原创技术文章,仅供学习交流。

本文内容基于实际项目源码解析整理,代码示例均为教学简化版本,仅供学习参考。

如需获取完整项目代码或技术支持,请访问 bima.cc