一个内部自用的轻量 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 和可选管理员
npm install复制示例配置:
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"如果不设置管理员密码,也可以通过第一个注册用户自动成为管理员。
确保 PostgreSQL 已启动并且 DATABASE_URL 可连接,然后执行:
npm run db:migrate
npm run db:seednpm run db:migrate 使用 prisma migrate deploy,适合服务器部署环境,不需要数据库账号拥有 CREATE DATABASE 权限。
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 登录/注册页。
- 业务应用跳转到 SSO
/authorize。 - 如果没有中心登录态,SSO Web 展示登录/注册页。
- 用户登录或注册。
- SSO Server 设置 HttpOnly SSO Cookie。
- SSO Server 签发 authorization code 并跳回业务应用。
- 业务应用用 code + PKCE verifier 调
/token换取 token。 - 业务应用用 access token 调
/userinfo获取用户信息。
注册规则:
- 注册时必须填写用户名、显示名称、邮箱和密码。
- 邮箱唯一,会随
/userinfo返回给业务系统。 - 第一个注册用户自动成为
ADMIN。 - 后续注册用户为
USER。 - 管理员可在管理页禁用用户。
- 生产环境可通过
REGISTRATION_ENABLED=false关闭注册。
无感登录不是保存业务系统密码,而是标准 SSO 跳转流程:
- 用户已经在 SSO 登录过,浏览器持有 SSO Server 的 HttpOnly Cookie。
- 新业务应用发现自己没有本地登录态,跳转到
/authorize。 - SSO Server 检测到中心 Cookie 有效,不展示登录页。
- SSO Server 直接签发 authorization code 并跳回业务应用。
- 用户看到的是短暂跳转,通常无需再次输入账号密码。
本地开发默认:
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 行为更稳定。
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
示例:
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/");业务系统需要完成:
- 生成
code_verifier。 - 计算
code_challenge = base64url(sha256(code_verifier))。 - 生成并保存
state。 - 跳转到:
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
- 回调时校验
state。 - 用
code + code_verifier调/token。 - 用 access token 调
/userinfo。
管理员登录 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 使用
HttpOnly、SameSite=Lax,生产环境应开启Secure。
生产环境建议:
SSO_COOKIE_SECURE=true- 使用 HTTPS。
- 关闭开放注册或加邀请码。
- JWT 签名密钥会持久化到
storage/,服务重启后旧 token 仍可验签。 - 配置反向代理、日志和备份。
npm run typecheck
npm run build
npm testPrisma:
npm run db:generate
npm run db:migrate
npm run db:seed开发服务:
npm run dev
npm run dev:demo-client- 打开
http://0.0.0.0:5174。 - 点击通过 SSO 登录。
- 在 SSO 页面注册账号。
- 注册成功后自动回到 Demo Client。
- 清理 Demo Client 本地登录态。
- 再次点击登录,应短暂跳转后无感回来。
- 点击退出 SSO。
- 再次登录应要求输入账号密码。
- 当前 refresh token 未实现;内部 Web 应用第一版可先依赖短期 access token + 业务应用本地 session。
- 当前 Client Secret 未用于 SPA demo;浏览器应用推荐使用 PKCE,无需 secret。