HiHuo
首页
博客
手册
工具
关于
首页
博客
手册
工具
关于
  • SSO与身份认证

    • 单点登录(SSO)与身份认证教程
    • 第1章:认证与授权基础
    • 第2章:单点登录原理与实现
    • 第3章:OAuth 2.0与OpenID Connect
    • 第4章:企业级SSO方案
    • 第5章:微服务认证与安全

第2章:单点登录原理与实现

什么是单点登录

定义

Single Sign-On (SSO): 用户只需登录一次,就可以访问所有相互信任的应用系统,无需重复登录。

传统登录 vs SSO

传统方式(多次登录):

用户访问多个系统,需要多次登录:

┌─────────┐     登录1    ┌─────────┐
│  User   │────────────> │ 系统A    │
└─────────┘              └─────────┘
     │        登录2       ┌─────────┐
     ├───────────────────>│ 系统B    │
     │                    └─────────┘
     │        登录3       ┌─────────┐
     └───────────────────>│ 系统C    │
                          └─────────┘

问题:
 多次输入密码
 多个账号管理
 用户体验差

SSO方式(一次登录):

用户只需登录一次SSO,即可访问所有系统:

┌─────────┐    1.访问    ┌─────────┐
│  User   │───────────> │ 系统A    │
└─────────┘             └─────────┘
     │                       │ 2.重定向
     │                       ↓
     │                  ┌─────────┐
     │     3.登录       │   SSO   │
     ├─────────────────>│  服务器  │
     │                  └─────────┘
     │ 4.返回票据             │
     │<───────────────────────┤
     │                        │
     │ 5.访问系统B             │
     │──────────────────> ┌─────────┐
     │                    │ 系统B    │
     │                    └─────────┘
     │                        │ 6.验证票据
     │                        ↓
     │                   ┌─────────┐
     │                   │   SSO   │
     │                   │  服务器  │
     │                   └─────────┘
     │<──────────────────────┤ 7.验证成功
     │                        │
    已登录所有系统

优势:
 一次登录,全局有效
 统一身份管理
 提升用户体验

SSO核心流程

完整流程图

┌──────┐                    ┌─────────┐                    ┌─────────┐
│ User │                    │ 应用A    │                    │  SSO    │
└──────┘                    └─────────┘                    │ Server  │
   │                              │                        └─────────┘
   │ 1. 访问应用A                  │                             │
   ├─────────────────────────────>│                             │
   │                              │                             │
   │                              │ 2. 检查本地Session           │
   │                              │    (未登录)                │
   │                              │                             │
   │                              │ 3. 重定向到SSO登录页         │
   │<─────────────────────────────┤                             │
   │   Redirect:                  │                             │
   │   sso.com/login?service=appA │                             │
   │                              │                             │
   │ 4. 访问SSO登录页                                           │
   ├────────────────────────────────────────────────────────────>│
   │                              │                             │
   │                              │              5. 检查SSO Session
   │                              │                 (未登录)    │
   │                              │                             │
   │                              │              6. 显示登录页面  │
   │<────────────────────────────────────────────────────────────┤
   │                              │                             │
   │ 7. 输入用户名密码                                           │
   ├────────────────────────────────────────────────────────────>│
   │                              │                             │
   │                              │              8. 验证成功     │
   │                              │              9. 创建SSO Session
   │                              │              10. 生成票据(Ticket)
   │                              │                             │
   │ 11. 重定向回应用A,携带票据                                 │
   │<────────────────────────────────────────────────────────────┤
   │   Redirect:                  │                             │
   │   appA.com/login?ticket=ST-1 │                             │
   │                              │                             │
   │ 12. 访问应用A,携带票据        │                             │
   ├─────────────────────────────>│                             │
   │                              │                             │
   │                              │ 13. 验证票据                 │
   │                              ├───────────────────────────> │
   │                              │                             │
   │                              │              14. 票据有效    │
   │                              │<────────────────────────────┤
   │                              │              15. 返回用户信息 │
   │                              │                             │
   │                              │ 16. 创建本地Session          │
   │                              │                             │
   │ 17. 返回受保护资源            │                             │
   │<─────────────────────────────┤                             │
   │                              │                             │
   │                                                            │
   │ === 用户再访问应用B ===                                     │
   │                              │                             │
   │ 18. 访问应用B                                   ┌─────────┐│
   ├────────────────────────────────────────────────>│ 应用B    ││
   │                                                 └─────────┘│
   │                                                       │    │
   │                                    19. 检查本地Session │    │
   │                                        (未登录)      │    │
   │                                                       │    │
   │                                    20. 重定向到SSO    │    │
   │<──────────────────────────────────────────────────────┤    │
   │   Redirect: sso.com/login?service=appB               │    │
   │                                                       │    │
   │ 21. 访问SSO                                                │
   ├────────────────────────────────────────────────────────────>│
   │                                                            │
   │                                         22. 检查SSO Session│
   │                                             (已登录)      │
   │                                         23. 直接生成票据    │
   │                                                            │
   │ 24. 重定向回应用B,携带票据                                 │
   │<────────────────────────────────────────────────────────────┤
   │   Redirect: appB.com/login?ticket=ST-2                     │
   │                                                            │
   │ 25. 应用B验证票据,创建本地Session                           │
   ├────────────────────────────────────────────────────────────>│
   │                                                            │
   │ 26. 登录成功(无需再次输入密码)                             │
   │<────────────────────────────────────────────────────────────┤
   │                                                            │

CAS协议

CAS简介

CAS (Central Authentication Service): Yale大学开发的开源SSO协议。

CAS核心概念

1. TGT (Ticket Granting Ticket):
   - SSO服务器颁发给用户
   - 存储在SSO服务器的Session中
   - 用于生成ST

2. TGC (Ticket Granting Cookie):
   - 存储TGT的Cookie
   - 存储在用户浏览器中
   - 用于标识SSO Session

3. ST (Service Ticket):
   - 一次性票据
   - 用于应用系统验证用户身份
   - 验证后立即销毁

CAS登录流程(详细)

// 1. 用户访问应用A
GET https://app-a.com/protected-resource

// 2. 应用A检查本地Session(未登录)
//    重定向到CAS Server
HTTP/1.1 302 Found
Location: https://sso.example.com/cas/login?service=https://app-a.com/cas-callback

// 3. CAS Server检查TGC Cookie(首次访问,无TGC)
//    显示登录页面

// 4. 用户输入用户名密码
POST https://sso.example.com/cas/login
{
  "username": "john",
  "password": "password123",
  "service": "https://app-a.com/cas-callback"
}

// 5. CAS Server验证成功
//    - 生成TGT
//    - 设置TGC Cookie
//    - 生成ST
//    - 重定向回应用A
HTTP/1.1 302 Found
Set-Cookie: TGC=TGT-1-xxxx; Path=/cas; HttpOnly; Secure
Location: https://app-a.com/cas-callback?ticket=ST-1-yyyy

// 6. 应用A收到ST
//    向CAS Server验证ST
GET https://sso.example.com/cas/serviceValidate?ticket=ST-1-yyyy&service=https://app-a.com/cas-callback

// 7. CAS Server验证ST,返回用户信息
HTTP/1.1 200 OK
Content-Type: application/xml

<cas:serviceResponse>
  <cas:authenticationSuccess>
    <cas:user>john</cas:user>
    <cas:attributes>
      <cas:email>john@example.com</cas:email>
      <cas:name>John Doe</cas:name>
    </cas:attributes>
  </cas:authenticationSuccess>
</cas:serviceResponse>

// 8. 应用A创建本地Session
//    返回受保护资源

// 9. 用户访问应用B(已有TGC Cookie)
GET https://app-b.com/protected-resource

// 10. 应用B重定向到CAS Server
HTTP/1.1 302 Found
Location: https://sso.example.com/cas/login?service=https://app-b.com/cas-callback

// 11. CAS Server检查TGC Cookie(已存在)
//     直接生成ST,重定向回应用B(无需登录)
HTTP/1.1 302 Found
Location: https://app-b.com/cas-callback?ticket=ST-2-zzzz

// 12. 应用B验证ST,创建本地Session
//     用户无需再次登录

SAML协议

SAML简介

SAML (Security Assertion Markup Language): 基于XML的企业级SSO标准。

SAML核心概念

1. IdP (Identity Provider):
   身份提供者,负责认证用户

2. SP (Service Provider):
   服务提供者,业务应用系统

3. Assertion:
   断言,包含用户身份信息的XML文档

SAML流程(SP-Initiated)

┌──────┐                 ┌────┐                 ┌────┐
│ User │                 │ SP │                 │IdP │
└──────┘                 └────┘                 └────┘
   │                        │                      │
   │ 1. 访问SP               │                      │
   ├───────────────────────>│                      │
   │                        │                      │
   │                        │ 2. 生成SAML Request   │
   │                        │                      │
   │ 3. 重定向到IdP(携带SAMLRequest)               │
   │<───────────────────────┤                      │
   │                        │                      │
   │ 4. 访问IdP(携带SAMLRequest)                  │
   ├──────────────────────────────────────────────>│
   │                        │                      │
   │                        │         5. 解析请求   │
   │                        │         6. 验证用户   │
   │                        │         7. 生成Assertion
   │                        │                      │
   │ 8. 返回SAML Response(包含Assertion)          │
   │<──────────────────────────────────────────────┤
   │                        │                      │
   │ 9. 提交SAML Response到SP                      │
   ├───────────────────────>│                      │
   │                        │                      │
   │                        │ 10. 验证Assertion     │
   │                        │ 11. 创建Session      │
   │                        │                      │
   │ 12. 返回受保护资源      │                      │
   │<───────────────────────┤                      │
   │                        │                      │

JWT-based SSO

原理

使用JWT作为SSO票据,实现跨域单点登录

优势:
 无状态(无需服务端存储)
 跨域友好
 易于实现

流程:
1. 用户登录SSO,获取JWT
2. JWT包含用户信息
3. 各应用验证JWT即可

实现方案

方案1:共享JWT密钥

┌─────────┐                    ┌─────────┐
│  SSO    │<──共享密钥──────────>│ 应用A/B  │
└─────────┘                    └─────────┘

所有应用使用相同密钥验证JWT

优点:简单
缺点:密钥泄露风险

方案2:公钥/私钥

┌─────────┐                    ┌─────────┐
│  SSO    │──私钥签名 JWT────────>│ 应用A/B  │
└─────────┘                    └─────────┘
                                   │
                                   │ 公钥验证JWT
                                   ↓

优点:更安全
缺点:需要分发公钥

Go实现SSO

完整SSO服务器实现

sso_server.go:

package main

import (
	"crypto/rand"
	"encoding/hex"
	"encoding/json"
	"html/template"
	"net/http"
	"sync"
	"time"
)

// TGT (Ticket Granting Ticket)
type TGT struct {
	ID        string
	UserID    int
	Username  string
	CreatedAt time.Time
	ExpiresAt time.Time
}

// ST (Service Ticket)
type ST struct {
	ID        string
	TGTID     string
	Service   string
	UserID    int
	Username  string
	CreatedAt time.Time
	Used      bool
}

// SSOServer SSO服务器
type SSOServer struct {
	tgts map[string]*TGT
	sts  map[string]*ST
	mu   sync.RWMutex
}

func NewSSOServer() *SSOServer {
	return &SSOServer{
		tgts: make(map[string]*TGT),
		sts:  make(map[string]*ST),
	}
}

// Login 登录页面
func (s *SSOServer) LoginPage(w http.ResponseWriter, r *http.Request) {
	service := r.URL.Query().Get("service")

	// 检查TGC Cookie(是否已登录)
	cookie, err := r.Cookie("TGC")
	if err == nil {
		// 已登录,直接生成ST并重定向
		tgt, exists := s.getTGT(cookie.Value)
		if exists {
			st := s.createST(tgt, service)
			http.Redirect(w, r, service+"?ticket="+st.ID, http.StatusFound)
			return
		}
	}

	// 未登录,显示登录页面
	tmpl := `
	<html>
	<body>
		<h2>SSO Login</h2>
		<form method="POST" action="/sso/login">
			<input type="hidden" name="service" value="{{.Service}}">
			<input type="text" name="username" placeholder="Username" required><br>
			<input type="password" name="password" placeholder="Password" required><br>
			<button type="submit">Login</button>
		</form>
	</body>
	</html>
	`
	t := template.Must(template.New("login").Parse(tmpl))
	t.Execute(w, map[string]string{"Service": service})
}

// Login 处理登录
func (s *SSOServer) Login(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}

	username := r.FormValue("username")
	password := r.FormValue("password")
	service := r.FormValue("service")

	// 验证用户名密码(简化示例)
	if !s.authenticate(username, password) {
		http.Error(w, "Invalid credentials", http.StatusUnauthorized)
		return
	}

	// 创建TGT
	tgt := s.createTGT(1, username)

	// 设置TGC Cookie
	http.SetCookie(w, &http.Cookie{
		Name:     "TGC",
		Value:    tgt.ID,
		Path:     "/sso",
		HttpOnly: true,
		Secure:   true,
		MaxAge:   7200, // 2小时
	})

	// 生成ST
	st := s.createST(tgt, service)

	// 重定向回应用
	http.Redirect(w, r, service+"?ticket="+st.ID, http.StatusFound)
}

// ValidateTicket 验证票据
func (s *SSOServer) ValidateTicket(w http.ResponseWriter, r *http.Request) {
	ticket := r.URL.Query().Get("ticket")
	service := r.URL.Query().Get("service")

	st, exists := s.getST(ticket)
	if !exists {
		w.WriteHeader(http.StatusUnauthorized)
		json.NewEncoder(w).Encode(map[string]string{
			"error": "Invalid ticket",
		})
		return
	}

	// 验证Service
	if st.Service != service {
		w.WriteHeader(http.StatusUnauthorized)
		json.NewEncoder(w).Encode(map[string]string{
			"error": "Service mismatch",
		})
		return
	}

	// 验证票据是否已使用
	if st.Used {
		w.WriteHeader(http.StatusUnauthorized)
		json.NewEncoder(w).Encode(map[string]string{
			"error": "Ticket already used",
		})
		return
	}

	// 标记为已使用
	s.markSTAsUsed(ticket)

	// 返回用户信息
	json.NewEncoder(w).Encode(map[string]interface{}{
		"success": true,
		"user": map[string]interface{}{
			"id":       st.UserID,
			"username": st.Username,
		},
	})
}

// Logout 登出
func (s *SSOServer) Logout(w http.ResponseWriter, r *http.Request) {
	cookie, err := r.Cookie("TGC")
	if err == nil {
		s.deleteTGT(cookie.Value)
	}

	// 清除Cookie
	http.SetCookie(w, &http.Cookie{
		Name:     "TGC",
		Value:    "",
		Path:     "/sso",
		MaxAge:   -1,
		HttpOnly: true,
		Secure:   true,
	})

	w.Write([]byte("Logged out successfully"))
}

// 创建TGT
func (s *SSOServer) createTGT(userID int, username string) *TGT {
	tgtID := "TGT-" + generateID()
	tgt := &TGT{
		ID:        tgtID,
		UserID:    userID,
		Username:  username,
		CreatedAt: time.Now(),
		ExpiresAt: time.Now().Add(2 * time.Hour),
	}

	s.mu.Lock()
	s.tgts[tgtID] = tgt
	s.mu.Unlock()

	return tgt
}

// 创建ST
func (s *SSOServer) createST(tgt *TGT, service string) *ST {
	stID := "ST-" + generateID()
	st := &ST{
		ID:        stID,
		TGTID:     tgt.ID,
		Service:   service,
		UserID:    tgt.UserID,
		Username:  tgt.Username,
		CreatedAt: time.Now(),
		Used:      false,
	}

	s.mu.Lock()
	s.sts[stID] = st
	s.mu.Unlock()

	return st
}

// 获取TGT
func (s *SSOServer) getTGT(tgtID string) (*TGT, bool) {
	s.mu.RLock()
	defer s.mu.RUnlock()

	tgt, exists := s.tgts[tgtID]
	if !exists || time.Now().After(tgt.ExpiresAt) {
		return nil, false
	}

	return tgt, true
}

// 获取ST
func (s *SSOServer) getST(stID string) (*ST, bool) {
	s.mu.RLock()
	defer s.mu.RUnlock()

	st, exists := s.sts[stID]
	return st, exists
}

// 标记ST为已使用
func (s *SSOServer) markSTAsUsed(stID string) {
	s.mu.Lock()
	if st, exists := s.sts[stID]; exists {
		st.Used = true
	}
	s.mu.Unlock()
}

// 删除TGT
func (s *SSOServer) deleteTGT(tgtID string) {
	s.mu.Lock()
	delete(s.tgts, tgtID)
	s.mu.Unlock()
}

// 生成随机ID
func generateID() string {
	bytes := make([]byte, 16)
	rand.Read(bytes)
	return hex.EncodeToString(bytes)
}

// 验证用户(简化示例)
func (s *SSOServer) authenticate(username, password string) bool {
	// 实际应该查询数据库
	return username == "admin" && password == "password"
}

func main() {
	sso := NewSSOServer()

	http.HandleFunc("/sso/login", func(w http.ResponseWriter, r *http.Request) {
		if r.Method == http.MethodGet {
			sso.LoginPage(w, r)
		} else {
			sso.Login(w, r)
		}
	})
	http.HandleFunc("/sso/validate", sso.ValidateTicket)
	http.HandleFunc("/sso/logout", sso.Logout)

	http.ListenAndServe(":8080", nil)
}

应用端实现

app_client.go:

package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
)

const (
	SSOServerURL = "http://localhost:8080"
	AppCallbackURL = "http://localhost:8081/callback"
)

// AppServer 应用服务器
type AppServer struct {
	sessions map[string]string // sessionID -> username
}

func NewAppServer() *AppServer {
	return &AppServer{
		sessions: make(map[string]string),
	}
}

// ProtectedResource 受保护资源
func (a *AppServer) ProtectedResource(w http.ResponseWriter, r *http.Request) {
	// 检查本地Session
	cookie, err := r.Cookie("sessionid")
	if err != nil || a.sessions[cookie.Value] == "" {
		// 未登录,重定向到SSO
		loginURL := fmt.Sprintf("%s/sso/login?service=%s",
			SSOServerURL,
			url.QueryEscape(AppCallbackURL))
		http.Redirect(w, r, loginURL, http.StatusFound)
		return
	}

	// 已登录,返回受保护资源
	username := a.sessions[cookie.Value]
	fmt.Fprintf(w, "Welcome, %s! This is a protected resource.", username)
}

// Callback SSO回调
func (a *AppServer) Callback(w http.ResponseWriter, r *http.Request) {
	ticket := r.URL.Query().Get("ticket")
	if ticket == "" {
		http.Error(w, "Missing ticket", http.StatusBadRequest)
		return
	}

	// 验证票据
	validateURL := fmt.Sprintf("%s/sso/validate?ticket=%s&service=%s",
		SSOServerURL,
		ticket,
		url.QueryEscape(AppCallbackURL))

	resp, err := http.Get(validateURL)
	if err != nil {
		http.Error(w, "Failed to validate ticket", http.StatusInternalServerError)
		return
	}
	defer resp.Body.Close()

	var result struct {
		Success bool `json:"success"`
		User    struct {
			ID       int    `json:"id"`
			Username string `json:"username"`
		} `json:"user"`
	}

	json.NewDecoder(resp.Body).Decode(&result)

	if !result.Success {
		http.Error(w, "Invalid ticket", http.StatusUnauthorized)
		return
	}

	// 创建本地Session
	sessionID := generateSessionID()
	a.sessions[sessionID] = result.User.Username

	http.SetCookie(w, &http.Cookie{
		Name:     "sessionid",
		Value:    sessionID,
		Path:     "/",
		HttpOnly: true,
		MaxAge:   7200,
	})

	// 重定向到受保护资源
	http.Redirect(w, r, "/protected", http.StatusFound)
}

func generateSessionID() string {
	// 简化示例
	return fmt.Sprintf("sess_%d", time.Now().Unix())
}

func main() {
	app := NewAppServer()

	http.HandleFunc("/protected", app.ProtectedResource)
	http.HandleFunc("/callback", app.Callback)

	fmt.Println("App running on :8081")
	http.ListenAndServe(":8081", nil)
}

面试问答

什么是单点登录?SSO解决了什么问题?

答案:

定义:单点登录(SSO)允许用户只需登录一次,即可访问多个相互信任的应用系统。

解决的问题:

1. 多次登录:用户无需为每个系统重复登录
2. 密码管理:减少用户需要记住的密码数量
3. 用户体验:提升便捷性
4. 安全管理:统一身份管理,易于审计

Prev
第1章:认证与授权基础
Next
第3章:OAuth 2.0与OpenID Connect