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

9.5 KiB
Raw Blame History

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

CAS 协议

概述

2000 年,耶鲁大学面临一个问题:校园里有图书馆系统、邮件系统、选课系统、成绩查询……每个系统都要单独登录。教授们抱怨「我记不住这么多密码」。

于是耶鲁开发了 CAS(Central Authentication Service,中央认证服务)——一个极简的 SSO 协议。核心思路只有一句话:用户在统一的登录页登录,拿到一张「一次性票据」,拿着票据去各个系统换通行证。

[!info] 类比:游乐园的手环 你在游乐园门口买票(登录),工作人员给你戴上一条手环(TGT)。之后你去每个游乐设施(SP),出示手环,工作人员给你一张一次性小票(ST),验证通过后就能玩。手环是你的登录凭证,小票是每个设施的访问凭证。

CAS 协议就是这个流程的标准化描述。

当前版本:CAS Protocol 3.0(2016 年发布,增加了属性释放能力)。

核心流程

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

    U->>App: 1. 访问受保护页面
    App->>U: 2. 「你还没登录」<br/>302 重定向到 CAS Server
    Note right of App: URL: /login?service=https://app.example.com
    U->>CAS: 3. 浏览器跳到 CAS 登录页
    CAS->>U: 4. 展示登录表单
    U->>CAS: 5. 输入账号密码
    CAS->>CAS: 6. 验证凭证,生成 Service Ticket (ST)
    CAS->>U: 7. 302 跳回业务应用,URL 带上 ST
    Note left of CAS: Location: https://app.example.com?ticket=ST-12345-xxxxx
    U->>App: 8. 浏览器跳回应用,ST 在 URL 里
    App->>CAS: 9. 应用后端拿着 ST 去 CAS 验证
    Note right of App: GET /p3/serviceValidate?ticket=ST-xxx&service=...
    CAS-->>App: 10. 返回用户信息(XML 格式)
    App-->>U: 11. 验证通过,建立本地 Session,登录成功

关键设计:

  • 步骤 7 的 service 参数告诉 CAS「验证完后跳回哪里」
  • 步骤 9 是后端直连 CAS,不经过浏览器——ST 不会暴露给用户
  • ST 是一次性的,验证一次就失效,有效期通常 < 10 秒

Ticket 类型

CAS 用 Ticket(票据)机制管理认证状态。有两种你必须理解:

核心 Ticket

Ticket 全称 一句话解释 生命周期
TGT Ticket Granting Ticket 登录凭证。用户在 CAS 登录成功后获得,相当于「游乐园手环」 用户会话级,存在 CAS Server 端
ST Service Ticket 访问凭证。每个 SP 访问时临时生成,相当于「游乐设施的小票」 一次性,验证即毁,有效期 < 10 秒

高级 Ticket(代理场景,大多数情况用不到)

Ticket 说明
PT Proxy Ticket,SP 代替用户去访问另一个 SP 时使用
PGT Proxy Granting Ticket,用于获取 PT 的凭证
PGTIOU PGT 和 ST 之间的关联标识

[!tip] 不需要全部理解 如果你的场景只是「用户登录 → 访问业务系统」,只需要理解 TGT 和 ST。Proxy 系列 Ticket 是为链式代理认证设计的,90% 的接入方用不到。

Ticket 流转图

graph TD
    A[用户在 CAS Server 登录] -->|成功| B[CAS Server 颁发 TGT]
    B -->|TGT 以 Cookie 形式<br/>存在 CAS 域名下| C[用户浏览器持有 TGT Cookie]
    C -->|用户访问 SP 1| D[CAS 生成 ST-1]
    C -->|用户访问 SP 2| E[CAS 生成 ST-2]
    D -->|SP 1 后端验证 ST-1| F[SP 1 建立本地 Session]
    E -->|SP 2 后端验证 ST-2| G[SP 2 建立本地 Session]

TGT vs ST 的关系:TGT 是「总的登录状态」(在 CAS Server 端),ST 是「给某个 SP 的一次性凭证」。用户登录一次获得一个 TGT,之后访问每个 SP 都会从 TGT 派生出一个新的 ST。

CAS 2.0 vs 3.0

版本 区别
CAS 2.0 /serviceValidate 端点,只返回用户名
CAS 3.0 /p3/serviceValidate 端点,除了用户名还返回用户属性(邮箱、角色等)

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>

接入时优先使用 CAS 3.0(/p3/serviceValidate),能拿到更多用户信息。

Go 接入示例

CAS 接入的核心逻辑:拦截请求 → 重定向登录 → 验证 Ticket → 建立 Session。

const (
    casServerURL = "https://cas.example.com"
    serviceURL   = "https://app.example.com"
)

// CASMiddleware 拦截所有请求,未登录则重定向到 CAS
func CASMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // 1. 已经登录过(本地 Session 存在)→ 直接放行
        session, _ := store.Get(r, "session")
        if user, ok := session.Values["user"].(string); ok && user != "" {
            next.ServeHTTP(w, r)
            return
        }

        // 2. 检查 URL 中是否有 Ticket
        ticket := r.URL.Query().Get("ticket")
        if ticket == "" {
            // 没有 Ticket → 重定向到 CAS 登录页
            // service 参数告诉 CAS 登录完成后跳回哪里
            loginURL := fmt.Sprintf("%s/login?service=%s",
                casServerURL,
                url.QueryEscape(serviceURL+r.URL.Path),
            )
            http.Redirect(w, r, loginURL, http.StatusFound)
            return
        }

        // 3. 有 Ticket → 后端去 CAS 验证(这一步是后端直连,不经过浏览器)
        validateURL := fmt.Sprintf(
            "%s/p3/serviceValidate?ticket=%s&service=%s",
            casServerURL,
            ticket,
            url.QueryEscape(serviceURL+r.URL.Path),
        )
        resp, err := http.Get(validateURL)
        if err != nil {
            http.Error(w, "CAS validation failed", http.StatusBadGateway)
            return
        }
        defer resp.Body.Close()

        // 4. 解析 CAS 返回的 XML,提取用户名
        body, _ := io.ReadAll(resp.Body)
        // 简化的 XML 解析(生产环境建议用 encoding/xml)
        var result struct {
            AuthenticationSuccess struct {
                User string `xml:"user"`
            } `xml:"authenticationSuccess"`
        }
        if err := xml.Unmarshal(body, &result); err != nil ||
            result.AuthenticationSuccess.User == "" {
            http.Error(w, "CAS authentication failed", http.StatusUnauthorized)
            return
        }

        // 5. 建立本地 Session(之后就不用每次都去 CAS 验证了)
        session.Values["user"] = result.AuthenticationSuccess.User
        session.Save(r, w)

        // 6. 重定向到去掉 ticket 参数的原始 URL
        //    这样用户刷新页面不会重复验证 Ticket(Ticket 已经失效了)
        cleanURL := r.URL.Path
        if r.URL.RawQuery != "" {
            // 保留非 ticket 的查询参数
            q := r.URL.Query()
            q.Del("ticket")
            if encoded := q.Encode(); encoded != "" {
                cleanURL += "?" + encoded
            }
        }
        http.Redirect(w, r, cleanURL, http.StatusFound)
    })
}

常见陷阱与最佳实践

Ticket 不能重复验证

Service Ticket 验证一次就失效了。如果用户点完登录后按了 F5 刷新页面,URL 里的 ST 已经用过了,再去验证会失败。

解决:步骤 6 中重定向到去掉 ticket 参数的 URL。用户刷新时 URL 里没有 ticket,走的是本地 Session(步骤 1),不会重复验证。

service 参数必须严格匹配

CAS Server 会校验 service 参数是否在预注册的白名单中。不要把用户可控的内容拼到 service 参数里。

必须用 HTTPS

CAS 的 Ticket 是通过 URL 传递的(?ticket=ST-xxx)。如果用 HTTP,网络上的任何人都能看到 Ticket 并冒充用户。CAS 必须全程 HTTPS。

登出的局限性

CAS 的 /logout 端点会清除 TGT(CAS Server 端的登录态),但它没法直接清除各个 SP 的本地 Session。

CAS 通过两种方式通知 SP:

  • Front-channel(前台通知):浏览器跳转到各个 SP 的登出 URL(通过 iframe 或重定向)
  • Back-channel(后台通知):CAS Server 直接调用 SP 的登出接口(服务器对服务器)

实际部署中,很多 SP 不实现登出回调。务实的做法:先清除本地 Session,CAS 端的 TGT 也清除,用户下次访问任何 SP 都会重新走登录流程。

TGT 过期后的体验

TGT 通常有较长的有效期(几小时到几天,取决于配置)。TGT 过期后,用户访问 SP 时会被重定向到 CAS 登录页重新输入密码。如果 CAS Server 配置了「记住我」功能,可能只需要重新点击确认而不需要输入密码。

[!tip] 现代 CAS Server 的进化 Apereo CAS Server 6.x+ 已经不只是一个 CAS 协议服务器了。它同时支持 CAS / OIDC / SAML 协议,可以作为统一身份网关——新系统用 OIDC 接入,老系统用 CAS 接入,一套 CAS Server 全搞定。如果你需要开源自建 SSO 平台,Apereo CAS 值得考虑。