本文目录导读:

单点登录(SSO)的核心原理是在一个独立的认证中心进行一次登录,即可访问所有相互信任的应用系统。
要理解其代码体现,我们先拆解其核心思想:Cookie 共享 + Token 验证。
最常见的 SSO 实现是基于中央认证服务(CAS,Central Authentication Service) 的模式,下面用最简化的伪代码和逻辑流程,来展示其核心原理。
核心原理流程图解(文字版)
- 首次访问 App1:用户未登录 -> 重定向到 SSO 登录页。
- 在 SSO 登录:输入账号密码 -> SSO 服务器验证成功。
- SSO 发放票据:
- 生成一个全局会话(Global Session),并设置一个 SSO Cookie(如
sso_token)到sso.com域。 - 生成一个 ST(Service Ticket,服务票据),并附加在 URL 上跳转回 App1。
- 生成一个全局会话(Global Session),并设置一个 SSO Cookie(如
- App1 验证 ST:App1 拿着 ST 去 SSO 服务器验证,验证通过,建立 App1 的本地会话(Local Session),设置 App1 域的 Cookie。
- 访问 App2:用户点击 App2,发现没有 App2 的本地会话。
- App2 重定向到 SSO:SSO 服务器检查到用户浏览器有
sso_token(来自第一步),验证全局会话有效。 - SSO 直接发放 Ticket:无需再次输入密码,SSO 直接生成一个新的 ST 跳转回 App2。
- App2 验证 ST:流程同第4步,建立 App2 本地会话。
代码实现核心环节(伪代码 + 关键逻辑)
为了方便理解,我们假设有三个服务:
sso-server.com:认证中心app1.com:应用1app2.com:应用2user:浏览器用户
环节1:SSO 服务器(认证中心)
SSO 服务器是整个系统的核心,负责管理全局会话和发放票据。
# -*- coding: utf-8 -*-
# 假设这是一个 Python Flask 服务,运行在 sso-server.com
import uuid
import time
# 存储:全局 Session 和 Ticket
# 生产环境会用 Redis
global_sessions = {} # key: sso_token, value: {user_id, login_time}
ticket_storage = {} # key: service_ticket, value: {user_id, service_url, timestamp}
@app.route('/login', methods=['GET', 'POST'])
def login():
# 1. 判断是否已经登录(检查 SSO Cookie)
sso_token = request.cookies.get('sso_token')
if sso_token and sso_token in global_sessions:
# 已经登录,直接颁发 ST(Service Ticket)并跳转回原应用
service_url = request.args.get('service') # app1.com/callback
st = str(uuid.uuid4())
ticket_storage[st] = {'user_id': global_sessions[sso_token]['user_id'], 'service_url': service_url, time.time()}
# 重定向到: app1.com/callback?ticket=xxxx
return redirect(f'{service_url}?ticket={st}')
# 2. 未登录,显示登录表单(POST 请求处理)
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
# 验证密码(省略具体验证逻辑)
if authenticate(username, password):
# 创建全局 Session
new_sso_token = str(uuid.uuid4())
global_sessions[new_sso_token] = {'user_id': username, 'login_time': time.time()}
# 创建 ST(Service Ticket)
service_url = request.args.get('service')
st = str(uuid.uuid4())
ticket_storage[st] = {'user_id': username, 'service_url': service_url, 'timestamp': time.time()}
# 设置 SSO Cookie (Domain=sso-server.com)
resp = redirect(f'{service_url}?ticket={st}')
resp.set_cookie('sso_token', new_sso_token, domain='sso-server.com', httponly=True)
return resp
# 3. 显示登录页
return render_template('login.html')
@app.route('/validate') # 提供给应用服务器验证 ST 的接口
def validate_ticket():
# app1 带着 ticket 和 自己的 URL 来验证
ticket = request.args.get('ticket')
service = request.args.get('service')
stored_ticket = ticket_storage.pop(ticket, None) # 一次性使用
if stored_ticket and stored_ticket['service_url'] == service:
# 验证成功,返回用户信息(如 user_id)
return jsonify({'success': True, 'user_id': stored_ticket['user_id']})
else:
return jsonify({'success': False}), 401
@app.route('/logout')
def logout():
sso_token = request.cookies.get('sso_token')
if sso_token in global_sessions:
del global_sessions[sso_token]
# 清除 SSO Cookie
resp = redirect('http://sso-server.com/login')
resp.set_cookie('sso_token', '', expires=0, domain='sso-server.com')
return resp
关键点解释:
- 全局 Session(
global_sessions):用户登录后,服务器记得此人。 - SSO Cookie(
sso_token):浏览器记住“我在 SSO 登录了”。注意domain必须一致(如都设为.sso-server.com)。 - ST(Service Ticket):一次性、有时效、短字符串,防止重放攻击。
环节2:应用服务器(App1 / App2)
应用服务器负责拦截未登录请求,与 SSO 服务器交互。
# -*- coding: utf-8 -*-
# 假设这是一个 Flask 服务,运行在 app1.com
import requests
# 存储本地的用户会话
local_sessions = {} # key: app_local_token, value: {user_id}
@app.route('/callback') # 接收 SSO 跳转回来的 Ticket
def callback():
ticket = request.args.get('ticket')
# 1. 拿着 ticket 去 SSO 服务器验证
validate_url = f'http://sso-server.com/validate?ticket={ticket}&service={request.url}'
response = requests.get(validate_url)
if response.json().get('success'):
user_id = response.json()['user_id']
# 2. 验证成功,创建本地会话
app_local_token = str(uuid.uuid4())
local_sessions[app_local_token] = {'user_id': user_id}
# 3. 设置本地 Cookie (Domain=app1.com)
resp = redirect('http://app1.com/dashboard')
resp.set_cookie('app_session', app_local_token, domain='app1.com', httponly=True)
return resp
else:
return "验证失败", 401
@app.route('/dashboard')
def dashboard():
# 受保护的页面,检查本地会话
app_session = request.cookies.get('app_session')
if app_session in local_sessions:
return f"欢迎 {local_sessions[app_session]['user_id']} 来到 App1"
else:
# 没有本地会话,重定向到 SSO 登录
# Redirect to: http://sso-server.com/login?service=http://app1.com/callback
return redirect(f'http://sso-server.com/login?service={request.url}')
关键点解释:
- 重定向引导:未登录 -> 跳到 SSO -> 带 Ticket 回
callback-> 建立本地会话。 - 本地 Cookie vs SSO Cookie:
app_session用于 App1 业务;sso_token用于全局认证,不同域名。 - 验证委托:App1 不完全信任浏览器给的 Ticket,一定要去 SSO 服务器后台验证(防止伪造)。
最终用户视角的代码体现(浏览器)
浏览器在这个过程中,Cookie 的 Domain 和 HttpOnly 属性至关重要。
--- // 步骤1:访问 app1.com // 浏览器有 app1.com 的 Cookie 吗? 没有。 // 步骤2:重定向到 sso-server.com/login?service=app1.com/callback // 浏览器有 sso-server.com 的 Cookie 吗? 没有。 // 步骤3:用户输入密码,提交 POST 到 sso-server.com // SSO 服务器返回: // Set-Cookie: sso_token=abc123; Domain=sso-server.com; HttpOnly; Path=/ // 并重定向到 app1.com/callback?ticket=st_xxx // 步骤4:浏览器访问 app1.com/callback?ticket=st_xxx // 浏览器自动带上 app1.com 的 Cookie (此时还没有) // app1 服务器验证 ticket,返回: // Set-Cookie: app_session=xyz789; Domain=app1.com; HttpOnly; Path=/ // 并重定向到 app1.com/dashboard // 步骤5:用户首次访问 app2.com // 浏览器检查 app2.com 的 Cookie? 没有。 // app2 重定向到 sso-server.com/login?service=app2.com/callback // 浏览器访问 sso-server.com,自动带上 Cookie: sso_token=abc123 // SSO 服务器验证 sso_token 有效,直接返回 Ticket,免登录!
代码体现的核心原则
- 使用 Cookie 传递 Token:
sso_token(全局)和app_session(本地)的存在是 SSO 工作的基础。 - Token 验证发生在服务器端:
validate_ticket()是安全核心,防止客户端伪造。 - 重定向是 SSO 的“传输层”:
302 Redirect是连接不同应用的桥梁。 - 本地会话隔离:每个应用有自己的
local_sessions,即使 SSO 服务器挂了,已登录的应用短期内仍可正常工作。 - 一次性票据:
ticket用完即销毁,防止重放攻击。
实际生产中的挑战(超出基础代码范围但必须考虑):
- 跨域问题:不同域名如何共享
sso_token?解决方案有:子域名共用父域 Cookie、iframe+postMessage、OAuth2/OpenID Connect 的授权码模式。 - 安全问题:CSRF(跨站请求伪造)、XSS(跨站脚本攻击)防范;HTTPS 必须;Token 过期机制。
- Session 分布式:
global_sessions和ticket_storage必须使用 Redis 等分布式缓存,否则单点故障。
简单一句话总结:SSO 的代码体现就是“统一认证 + 票据传递 + 本地会话创建”的三层协作,核心的工具是 Cookie 和重定向,核心的规则是“服务器端验证,不信任客户端”。