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

180 lines
6.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [sso, cas, auth]
create time: 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 换取用户身份。
```mermaid
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 流转关系
```mermaid
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:**
```xml
<cas:serviceResponse>
<cas:authenticationSuccess>
<cas:user>zhangsan</cas:user>
</cas:authenticationSuccess>
</cas:serviceResponse>
```
**CAS 3.0(支持属性释放):**
```xml
<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:
```go
// 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 平台,值得考虑。