目录

用-Python-实现一个简化但可运行的-单点登录SSO系统

用 Python 实现一个简化但可运行的 单点登录(SSO)系统

好的!下面我将用 Python 实现一个简化但可运行的 单点登录(SSO)系统,其中:

  • A 系统 作为 身份提供商(IdP)
  • B 系统 作为 服务提供商(SP)
  • 用户在 A 登录后,点击链接跳转到 B,B 自动登录,无需输入密码

我们将使用:

  • Flask 作为 Web 框架
  • JWT(JSON Web Token) 作为 SSO 令牌
  • 加密签名 保证令牌不可伪造
  • HTTPS(本地用 HTTP 演示,生产务必用 HTTPS)

🧩 系统架构

用户浏览器
   │
   ├── 访问 A 系统 → 登录 → 获得 session
   │
   └── 点击 "打开 B 系统" → 跳转到 A 的 /sso?target=B
        → A 生成 JWT → 重定向到 B 的 /sso?token=xxx
        → B 验证 JWT → 创建本地会话 → 展示页面

第一步:A 系统(IdP) — idp.py

# idp.py - A 系统(身份提供商)
from flask import Flask, request, redirect, url_for, session, render_template_string
import jwt
import time
import urllib.parse

app = Flask(__name__)
app.secret_key = 'super-secret-key-for-session'  # 用于 Flask session

# JWT 签名密钥(A 和 B 必须共享!生产环境应使用安全存储)
JWT_SECRET = 'sso-shared-secret-2025'

# 模拟用户数据库
USERS = {
    'alice': 'password123',
    'bob': 'securepass'
}

# A 系统首页(登录页)
@app.route('/')
def home():
    if 'user' in session:
        return render_template_string('''
            <h2>A 系统 - 已登录:{{ user }}</h2>
            <a href="/sso?target={{ b_url }}">👉 打开 B 系统(SSO)</a>
            <form method="post" action="/logout"><button>退出登录</button></form>
        ''', user=session['user'], b_url='http://localhost:5001')
    return render_template_string('''
        <h2>A 系统 - 登录</h2>
        <form method="post" action="/login">
            用户名: <input name="username"><br>
            密码: <input name="password" type="password"><br>
            <button type="submit">登录</button>
        </form>
    ''')

@app.route('/login', methods=['POST'])
def login():
    username = request.form['username']
    password = request.form['password']
    if USERS.get(username) == password:
        session['user'] = username
        return redirect('/')
    return '登录失败!', 401

@app.route('/logout', methods=['POST'])
def logout():
    session.pop('user', None)
    return redirect('/')

# SSO 接口:生成 JWT 并重定向到目标系统
@app.route('/sso')
def sso():
    if 'user' not in session:
        return redirect('/')  # 未登录则跳回首页

    target = request.args.get('target')
    if not target:
        return '缺少 target 参数', 400

    # 构造 JWT payload
    payload = {
        'sub': session['user'],          # 用户名
        'iss': 'A-System',               # 签发者
        'exp': int(time.time()) + 60,    # 60秒过期(生产可设为 30~120 秒)
        'iat': int(time.time()),
        'target': target                 # 可选:限制目标
    }

    # 生成 JWT
    token = jwt.encode(payload, JWT_SECRET, algorithm='HS256')

    # 重定向到 B 系统的 SSO 入口
    redirect_url = f"{target}/sso?token={urllib.parse.quote(token)}"
    return redirect(redirect_url)

if __name__ == '__main__':
    # A 系统运行在 5000 端口
    app.run(port=5000, debug=True)

第二步:B 系统(SP) — sp.py

# sp.py - B 系统(服务提供商)
from flask import Flask, request, session, redirect, render_template_string
import jwt
import time
import urllib.parse

app = Flask(__name__)
app.secret_key = 'b-system-secret-session-key'

# 必须与 A 系统的 JWT_SECRET 一致!
JWT_SECRET = 'sso-shared-secret-2025'

@app.route('/')
def home():
    if 'user' in session:
        return render_template_string('''
            <h2>B 系统 - 欢迎 {{ user }}!</h2>
            <p>✅ 你已通过 A 系统单点登录成功!</p>
            <form method="post" action="/logout"><button>退出 B 系统</button></form>
        ''', user=session['user'])
    else:
        return '''
            <h2>B 系统 - 未登录</h2>
            <p>请通过 A 系统访问本系统。</p>
            <a href="http://localhost:5000">前往 A 系统登录</a>
        '''

# SSO 入口:接收并验证 JWT
@app.route('/sso')
def sso():
    token = request.args.get('token')
    if not token:
        return '缺少 token', 400

    try:
        # 解码并验证 JWT
        payload = jwt.decode(token, JWT_SECRET, algorithms=['HS256'])

        # 检查是否过期(jwt.decode 自动验证 exp)
        # 可选:检查 iss 是否为 'A-System'
        if payload.get('iss') != 'A-System':
            return '无效签发者', 403

        # 可选:检查 target 是否匹配当前系统
        # target = payload.get('target')
        # if not target or 'localhost:5001' not in target:
        #     return '目标不匹配', 403

        # 创建 B 系统本地会话
        session['user'] = payload['sub']
        return redirect('/')

    except jwt.ExpiredSignatureError:
        return 'SSO 令牌已过期', 401
    except jwt.InvalidTokenError:
        return '无效的 SSO 令牌', 403

@app.route('/logout', methods=['POST'])
def logout():
    session.pop('user', None)
    return redirect('/')

if __name__ == '__main__':
    # B 系统运行在 5001 端口
    app.run(port=5001, debug=True)

第三步:运行与测试

1. 安装依赖

pip install flask PyJWT

2. 启动两个系统

# 终端 1
python idp.py      # A 系统,http://localhost:5000

# 终端 2
python sp.py       # B 系统,http://localhost:5001

3. 测试流程

  1. 打开浏览器访问 http://localhost:5000
  2. 用用户名 alice、密码 password123 登录 A 系统
  3. 点击 “打开 B 系统(SSO)” 链接
  4. 自动跳转到 B 系统,并显示:

    B 系统 - 欢迎 alice!
    ✅ 你已通过 A 系统单点登录成功!

全程无需在 B 系统输入用户名密码!


🔒 安全增强建议(生产环境)

  1. 使用 HTTPS:防止 token 被窃听(本地开发可用 mkcert 生成证书)
  2. JWT 密钥安全存储:不要硬编码,使用环境变量或密钥管理服务
  3. 缩短 token 有效期:建议 30~60 秒
  4. 绑定用户上下文(可选):
    • 在 JWT 中加入 jti(唯一 ID)防止重放
    • 绑定 IP 或 User-Agent(但可能影响移动设备体验)
  5. 验证 target 参数:防止 token 被用于其他系统
  6. 日志审计:记录 SSO 登录事件

🔄 扩展方向

  • 支持多个 SP(C、D 系统):只需共享 JWT_SECRET 并实现 /sso 接口
  • 添加单点登出(SLO):A 登出时通知所有 SP
  • 使用 OpenID Connect 替代自定义 JWT(更标准)

这个实现虽然简化,但完整展示了 A 作为 IdP 实现 SSO 的核心逻辑。你可以在此基础上扩展为生产级系统。需要我帮你加上 HTTPS、数据库用户管理或 OIDC 支持吗?