Files
Qiniu/technical/sso/sso-cas.md
T

6.3 KiB
Raw Blame History

tags, create time
tags create time
sso
cas
auth
2026-07-13 10:03

CAS 协议

概述

CAS(Central Authentication Service)是由耶鲁大学于 2000 年开发的 SSO 协议,后由 Apereo 基金会维护。它是最早的 SSO 标准之一,设计简洁,在高校和企业内部门户中仍有广泛应用。当前版本为 CAS 3.0(CAS Protocol Specification)。

[!info] CAS 的定位 相比 SAML 的重量级 XML 和 OIDC 的 JWT 生态,CAS 的核心优势是简单。整个协议可以用几段 HTTP 请求描述清楚,实现成本极低。如果你只需要企业内部几个 Web 应用的 SSO,CAS 是最省事的选择。

核心流程

CAS 的核心思路是 Ticket 机制:用户在 CAS Server 登录后拿到一个一次性的 Service Ticket,业务应用用这个 Ticket 去 CAS Server 换取用户身份。

sequenceDiagram
    participant U as 用户 (浏览器)
    participant App as 业务应用 (CAS Client)
    participant CAS as CAS Server

    U->>App: 1. 访问受保护页面
    App->>U: 302 → CAS Server
    Note right of App: service=https://app.example.com/callback
    U->>CAS: 2. 重定向到 CAS 登录页
    CAS->>U: 3. 展示登录表单
    U->>CAS: 4. 提交凭证
    CAS->>CAS: 5. 验证凭证
    CAS->>U: 302 → service URL + Ticket
    Note left of CAS: ?ticket=ST-12345-xxxxx
    U->>App: 6. 携带 Ticket 回到应用
    App->>CAS: 7. 后端验证 Ticket
    Note right of App: GET /serviceValidate?ticket=ST-xxx&service=...
    CAS-->>App: 8. 返回用户信息 (XML)
    App-->>U: 9. 建立本地 Session,登录成功

Ticket 类型

CAS 协议定义了多种 Ticket,各有不同用途:

Ticket 前缀 生命周期 用途
TGT TGT- 用户会话级 CAS Server 的登录凭证,存在 Server 端
ST ST- 一次性,< 10s SP 验证用户身份用,用后即毁
PT PT- 一次性 Proxy Ticket,代理场景用
PGT PGT- 较长 Proxy Granting Ticket
PGTIOU PGTIOU- 短 PGT 和 ST 的关联标识

[!tip] 简化理解 对于 90% 的场景,你只需要关心 TGT(用户在 CAS Server 的登录态)和 ST(一次性验证凭证)。其他 Ticket 是为代理认证设计的,大多数接入方用不到。

Ticket 流转关系

graph TD
    A[用户登录] -->|成功| B[CAS Server 颁发 TGT]
    B -->|写入 Cookie| C[TGT 存在 CAS Server]
    C -->|SP 请求| D[生成 ST]
    D -->|一次性验证| E[SP 后端验证 ST]
    E -->|返回用户信息| F[SP 建立本地 Session]

关键接口

CAS 协议定义的核心端点:

端点 说明
/login 登录端点,支持 service 参数
/logout 登出端点,支持 service 参数回跳
/serviceValidate ST 验证端点(CAS 2.0)
/p3/serviceValidate ST 验证端点(CAS 3.0,返回更多用户属性)
/proxyValidate PT 验证端点
/proxy 获取 PGT

Ticket 验证响应示例

CAS 2.0:

<cas:serviceResponse>
  <cas:authenticationSuccess>
    <cas:user>zhangsan</cas:user>
  </cas:authenticationSuccess>
</cas:serviceResponse>

CAS 3.0(支持属性释放):

<cas:serviceResponse>
  <cas:authenticationSuccess>
    <cas:user>zhangsan</cas:user>
    <cas:attributes>
      <cas:email>zhangsan@example.com</cas:email>
      <cas:displayName>张三</cas:displayName>
      <cas:department>engineering</cas:department>
    </cas:attributes>
  </cas:authenticationSuccess>
</cas:serviceResponse>

Go 接入示例

CAS 接入相对简单,核心逻辑就是拦截请求 + 验证 Ticket:

// CAS Client 核心逻辑
func CASServerURL = "https://cas.example.com"
func ServiceURL = "https://app.example.com"

func CASMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // 1. 检查本地 Session
        if session, _ := store.Get(r, "session"); session.Values["user"] != nil {
            next.ServeHTTP(w, r)
            return
        }

        // 2. 检查 URL 中的 Ticket
        ticket := r.URL.Query().Get("ticket")
        if ticket == "" {
            // 3. 没有 Ticket → 重定向到 CAS
            loginURL := CASServerURL + "/login?service=" + url.QueryEscape(ServiceURL+r.URL.Path)
            http.Redirect(w, r, loginURL, http.StatusFound)
            return
        }

        // 4. 后端验证 Ticket
        validateURL := fmt.Sprintf(
            "%s/p3/serviceValidate?ticket=%s&service=%s",
            CASServerURL, ticket, url.QueryEscape(ServiceURL),
        )
        resp, err := http.Get(validateURL)
        if err != nil {
            http.Error(w, "CAS validation failed", 500)
            return
        }
        defer resp.Body.Close()

        // 5. 解析 XML 响应,提取用户名
        // ... 解析 logic ...

        // 6. 建立本地 Session
        session, _ := store.Get(r, "session")
        session.Values["user"] = parsedUser
        session.Save(r, w)

        // 7. 重定向去掉 Ticket 参数
        cleanURL := strings.Split(r.URL.String(), "?")[0]
        http.Redirect(w, r, cleanURL, http.StatusFound)
    })
}

常见陷阱与最佳实践

Ticket 不能重复验证

  • ST 验证一次后即失效,不要缓存 Ticket 验证结果
  • 用户刷新页面时 Ticket 已经失效,应该走 Session 而不是重新验证

service 参数必须严格匹配

  • CAS Server 会校验 service 参数是否在注册列表中
  • 不要拼接用户可控的内容到 service 参数

登出的局限性

  • CAS 的 /logout 只能清除 TGT(CAS Server 端的登录态)
  • 各 SP 的本地 Session 需要各自处理,CAS 通过回调通知 SP(Back-channel 或 Front-channel)
  • 实际部署中,很多 SP 不实现登出回调,导致用户以为登出了但 SP Session 仍有效

CAS vs OIDC 的选择

  • 如果你的系统只需要内网 Web 应用 SSO,CAS 足够
  • 如果需要支持移动端、SPA、第三方集成,直接上 OIDC
  • 很多现代 CAS Server(如 Apereo CAS 6.x)同时支持 CAS 协议和 OIDC/SAML

[!tip] Apereo CAS Server 的现代化 Apereo CAS Server 6.x+ 已经不仅仅是 CAS 协议服务器了。它同时支持 CAS / OIDC / SAML / REST API,可以作为统一身份网关使用。如果你需要一个开源自建的 SSO 平台,值得考虑。