Skip to content

kuankqaq/SSO

Repository files navigation

Internal SSO

一个内部自用的轻量 SSO/OIDC 服务,使用本地账号密码、PostgreSQL、授权码 + PKCE,支持注册、无感登录、用户管理、Client 管理和业务系统快速接入。

服务端口

默认本地端口:

  • SSO 服务:http://0.0.0.0:6001,同时提供 API 和 Web UI
  • Demo Client:http://0.0.0.0:5174

技术栈

  • Node.js + TypeScript
  • Fastify
  • React + Vite
  • PostgreSQL
  • Prisma
  • argon2 密码哈希
  • jose JWT 签发与验证

项目结构

apps/
  sso-server/     SSO/OIDC API 服务
  sso-web/        登录、注册、账户中心、管理页
  demo-client/    业务系统接入示例
packages/
  sso-client/     前端接入 SDK,封装 PKCE 和授权跳转
  shared/         共享类型与常量
prisma/
  schema.prisma   数据模型
  seed.ts         初始化 demo client 和可选管理员

快速启动

1. 安装依赖

npm install

2. 配置环境变量

复制示例配置:

cp .env.example .env

至少修改 DATABASE_URL

DATABASE_URL="postgresql://postgres:postgres@0.0.0.0:5432/internal_sso?schema=public"
SSO_ISSUER="http://0.0.0.0:6001"
SSO_WEB_ORIGIN="http://0.0.0.0:6001"
SSO_COOKIE_DOMAIN="0.0.0.0"
SSO_COOKIE_SECURE="false"
SSO_SESSION_DAYS="7"
SSO_JWT_PRIVATE_KEY_PATH="./storage/jwt-private-key.pem"
SSO_JWT_PUBLIC_KEY_PATH="./storage/jwt-public-key.pem"
REGISTRATION_ENABLED="true"

如果希望 seed 阶段创建管理员账号,设置:

ADMIN_USERNAME="admin"
ADMIN_EMAIL="admin@internal.local"
ADMIN_PASSWORD="your-strong-password"
ADMIN_DISPLAY_NAME="Administrator"

如果不设置管理员密码,也可以通过第一个注册用户自动成为管理员。

3. 初始化数据库

确保 PostgreSQL 已启动并且 DATABASE_URL 可连接,然后执行:

npm run db:migrate
npm run db:seed

npm run db:migrate 使用 prisma migrate deploy,适合服务器部署环境,不需要数据库账号拥有 CREATE DATABASE 权限。

4. 启动服务

SSO Server 会直接托管 SSO Web 构建产物,所以登录、注册、管理页和 API 都在 6001 一个服务里。

先构建并启动 SSO 服务:

npm run dev

再打开另一个终端启动 Demo Client:

npm run dev:demo-client

访问:

SSO:        http://0.0.0.0:6001
Demo Client: http://0.0.0.0:5174

首次点击“通过 SSO 登录”会跳转到 SSO 登录/注册页。

登录与注册流程

  1. 业务应用跳转到 SSO /authorize
  2. 如果没有中心登录态,SSO Web 展示登录/注册页。
  3. 用户登录或注册。
  4. SSO Server 设置 HttpOnly SSO Cookie。
  5. SSO Server 签发 authorization code 并跳回业务应用。
  6. 业务应用用 code + PKCE verifier 调 /token 换取 token。
  7. 业务应用用 access token 调 /userinfo 获取用户信息。

注册规则:

  • 注册时必须填写用户名、显示名称、邮箱和密码。
  • 邮箱唯一,会随 /userinfo 返回给业务系统。
  • 第一个注册用户自动成为 ADMIN
  • 后续注册用户为 USER
  • 管理员可在管理页禁用用户。
  • 生产环境可通过 REGISTRATION_ENABLED=false 关闭注册。

无感登录说明

无感登录不是保存业务系统密码,而是标准 SSO 跳转流程:

  1. 用户已经在 SSO 登录过,浏览器持有 SSO Server 的 HttpOnly Cookie。
  2. 新业务应用发现自己没有本地登录态,跳转到 /authorize
  3. SSO Server 检测到中心 Cookie 有效,不展示登录页。
  4. SSO Server 直接签发 authorization code 并跳回业务应用。
  5. 用户看到的是短暂跳转,通常无需再次输入账号密码。

本地开发默认:

SSO:      http://0.0.0.0:6001
Demo App: http://0.0.0.0:5174

内网部署建议使用同一父域名:

sso.internal.example.com
app1.internal.example.com
app2.internal.example.com

这样 Cookie、跳转和 SameSite 行为更稳定。

OIDC/OAuth2 端点

SSO Server 默认 issuer:

http://0.0.0.0:6001

端点:

GET  /.well-known/openid-configuration
GET  /authorize
POST /token
GET  /userinfo
GET  /jwks.json
GET  /logout
POST /logout
POST /login
POST /register
GET  /session

管理端点:

GET   /api/admin/summary
POST  /api/admin/clients
PATCH /api/admin/users/:id

业务系统接入

方式一:使用 @internal-sso/client

示例:

import { createSsoClient } from "@internal-sso/client";

const sso = createSsoClient({
  issuer: "http://0.0.0.0:6001",
  clientId: "demo-client",
  redirectUri: "http://0.0.0.0:5174/callback",
  scope: "openid profile email"
});

await sso.login();

回调页处理:

const callback = sso.handleCallback();

const response = await fetch("http://0.0.0.0:6001/token", {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({
    grant_type: "authorization_code",
    code: callback.code,
    client_id: callback.clientId,
    redirect_uri: callback.redirectUri,
    code_verifier: callback.codeVerifier
  })
});

const tokens = await response.json();

获取用户信息:

const userInfo = await fetch("http://0.0.0.0:6001/userinfo", {
  headers: {
    authorization: `Bearer ${tokens.access_token}`
  }
}).then((res) => res.json());

退出:

sso.logout("http://0.0.0.0:5174/");

方式二:自己实现授权码 + PKCE

业务系统需要完成:

  1. 生成 code_verifier
  2. 计算 code_challenge = base64url(sha256(code_verifier))
  3. 生成并保存 state
  4. 跳转到:
http://0.0.0.0:6001/authorize?client_id=...&redirect_uri=...&response_type=code&scope=openid%20profile%20email&state=...&code_challenge=...&code_challenge_method=S256
  1. 回调时校验 state
  2. code + code_verifier/token
  3. 用 access token 调 /userinfo

创建 Client

管理员登录 SSO Web 后,在管理页创建 Client,填写:

  • 应用名称
  • Redirect URI,例如 http://0.0.0.0:5174/callback
  • Allowed Origin,例如 http://0.0.0.0:5174

创建后会得到 client_id,业务系统接入时使用它。

默认 seed 会创建:

client_id: demo-client
redirect_uri: http://0.0.0.0:5174/callback
post_logout_redirect_uri: http://0.0.0.0:5174/

安全设计

已实现:

  • 密码使用 argon2 哈希。
  • Session token、authorization code 入库前做 SHA-256 哈希。
  • Authorization code 单次使用。
  • Authorization code 默认 5 分钟过期。
  • Access token / ID token 默认 15 分钟过期。
  • redirect_uri 必须和 Client 配置精确匹配。
  • 必须使用 PKCE S256
  • 登录和注册接口有简单 IP 级限流。
  • SSO Cookie 使用 HttpOnlySameSite=Lax,生产环境应开启 Secure

生产环境建议:

  • SSO_COOKIE_SECURE=true
  • 使用 HTTPS。
  • 关闭开放注册或加邀请码。
  • JWT 签名密钥会持久化到 storage/,服务重启后旧 token 仍可验签。
  • 配置反向代理、日志和备份。

常用命令

npm run typecheck
npm run build
npm test

Prisma:

npm run db:generate
npm run db:migrate
npm run db:seed

开发服务:

npm run dev
npm run dev:demo-client

验证清单

  1. 打开 http://0.0.0.0:5174
  2. 点击通过 SSO 登录。
  3. 在 SSO 页面注册账号。
  4. 注册成功后自动回到 Demo Client。
  5. 清理 Demo Client 本地登录态。
  6. 再次点击登录,应短暂跳转后无感回来。
  7. 点击退出 SSO。
  8. 再次登录应要求输入账号密码。

当前限制

  • 当前 refresh token 未实现;内部 Web 应用第一版可先依赖短期 access token + 业务应用本地 session。
  • 当前 Client Secret 未用于 SPA demo;浏览器应用推荐使用 PKCE,无需 secret。

About

单点登录,提供sdk,适合学习使用

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors