# begin_sso **Repository Path**: yangsj/begin_sso ## Basic Information - **Project Name**: begin_sso - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-04-23 - **Last Updated**: 2026-10-15 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Begin SSO 登录接口的请求/响应样例、错误码及小程序到 H5 的交接流程见 [统一登录接口文档](docs/login-api.md)。 统一登录页支持 `classic`、`console`、`minimal`、`marketing` 四套样式(`marketing` 沿用 begin_marketing 普通用户登录页风格),由登录中心按 `clientId` 同时匹配项目的 `login-ui` 与 `login-copy`,业务系统无需传 `ui`;主题和文案现支持数据库实时更新(第 8.9 节),无需重启;支持通过标准 `sso-login__*` 类名、`--sso-*` 变量和项目 CSS 文件扩展,并按项目配置 `login-copy` 标题、说明、按钮和页脚文案;具体见接口文档第 8.5–8.7 节。 begin_marketing 已接入统一组件,默认使用 marketing 样式;只提供可信 SSO userId,业务账号与数据归属由营销侧后续适配。接入配置见接口文档第 8.8 节。 统一登录服务,沿用 `begin_web` 的 Java 17 / Spring Boot 3.3.7 / MyBatis 3.0.4 / Druid 1.2.23 / Flyway / MySQL 技术选型。服务端分为 `begin-domain`、`begin-infrastructure`、`begin-application`、`begin-site-web`;新增独立 `begin-sso-client` 模块供业务系统接入统一登录。小程序工程为同级 `../begin_wx`。统一网页登录、应用配置和客户端接入见 [接口文档第 8 节](docs/login-api.md#8-统一网页登录与业务系统接入)。 ## 数据模型 | 表 | 用途 | 主键及约束 | | --- | --- | --- | | `sso_user` | 主账号 | 自增 `id` 主键,另有不可变、唯一 `BIGINT user_id`;`user_status VARCHAR(16)`(`ACTIVE`;被 unionid 合并的旧主账号标记为 `MERGED`,不再可登录) | | `sso_identity` | 微信/手机号/支付宝/用户名密码子账号 | 自增 `id`,`user_id` 外键指向 `sso_user.user_id`,`(provider, issuer_scope, subject)` 唯一;密码只存 `credential_hash`。微信身份有两行:小程序行 `issuer_scope=AppID`、`subject=openid`;开放平台 unionid 行 `issuer_scope=UNIONID`、`subject=unionid`(jscode2session 未返回 unionid 时不落此行) | | `sso_id_clock` | 跨实例账号 ID 分配 | 自增 `id`,固定第一行由事务锁定 | | `sso_session` | 一次登录对应一条会话,小程序与H5共用有效期 | 自增 `id`,`user_id` 外键指向 `sso_user.user_id`;分别存 MINI 令牌和 H5 Cookie 凭证的 SHA-256 摘要 | | `sso_password_login_bucket` | 跨实例密码失败限流(V9) | `bucket_key` 为用户名/IP 维度的 SHA-256 摘要主键;保存窗口内失败及校验中请求数,不存明文用户名/IP | | `sso_login_handoff` | 统一网页登录的 2 分钟交接凭证(V10 建表、V11 严格一次性) | 唯一凭证摘要,绑定源会话、应用、精确回调地址及浏览器请求证明;数据库时间签发后 120 秒到期,成功兑换后立即作废,任何重放都拒绝 | `user_id` 为 `100` + 13 位 Unix 毫秒时间戳 + 3 位序号(000–999),共 19 位。分配时锁定数据库行并读取数据库时间;同一毫秒最多生成 1000 个,发生时钟回拨或号段耗尽时失败而不是重复发号。`id` 是表内自增主键,不对外作为用户 ID。由于 JavaScript 数字无法精确表示 19 位整数,登录响应中的 `user.id` 是 `user_id` 的**十进制字符串**。Flyway 的内部元数据表不属于业务表。 六张业务表均有 `create_time` 和自动维护的 `update_time`;`sso_session.expire_time` 表示登录会话过期时间。主账号状态使用字符串 `user_status`(当前有效值 `ACTIVE`);登录身份的状态仍为字符串 `status`。本地库已应用的历史迁移不可回改,时间列统一命名由 `V5__explicit_time_names.sql` 增量完成。V9 新增独立的限流表,不修改已有会话结构;原四张账号表仍使用自增 `id`。 `V6__document_business_tables.sql` 给当时五张业务表的 35 个字段添加中文数据库备注;`provider`、`user_status`、身份 `status` 和会话 `kind` 的备注说明当前取值。它们是应用约定的字符串值,不是 MySQL `ENUM` 或数据库 `CHECK` 约束。V7 又增加带备注的 `web_token_hash`;V8 删除不再使用的票据表后,最终是四张业务表、28 个字段。历史 V1–V7 保持原样,新库也会依次执行它们后再执行 V8。 `V7__share_mini_session_with_h5.sql` 给同一会话增加 `web_token_hash`,供可选的受保护 H5 工具箱使用。若其他客户端通过该接口进入 H5,小程序 Bearer 和 H5 HttpOnly Cookie 会分别使用独立随机凭证,共用一条 `sso_session` 的过期时间及主账号状态;再次进入会轮换 H5 凭证。当前 `begin_wx` 小程序登录成功后只打开公开官网首页,**不在官网 URL 传 token/userId,也不调用受保护工具箱交接接口**;旧版带参入口已通过第三方统计被证实有凭证外传风险。V8 删除空的 `sso_ticket` 表并停用旧票据接口;仍调用 `/sso/ticket` 的旧客户端需要更新。 ### 子账号怎样区分 每条 `sso_identity` 记录对应一种可绑定到主账号的身份;`sso_identity.user_id` 与 `sso_session.user_id` 均存 `sso_user.user_id` 的 19 位业务用户号,而不是表内自增 `id`。一个主账号可以有多条身份记录。`provider` 是**字符串**,与 `issuer_scope`、`subject` 一起唯一标识一个子账号: 四张账号表的 `id` 仍为自增主键(例如 `1`);所有**关联主账号**的列统一命名 `user_id` 并外键引用 `sso_user.user_id`(例如 `1001760000000000000`)。已执行的 V1–V3 保持不变,V4 先将旧关联值回填为业务用户号,再替换外键和列。 | provider | issuer_scope | subject | credential_hash | 目前是否可登录 | | --- | --- | --- | --- | --- | | `WECHAT` | 小程序 AppID | 服务端换取的 openid | `NULL` | 可以,`wx.login` code 服务端验证 | | `ALIPAY` | 支付宝应用 ID | 服务端验证的支付宝用户标识 | `NULL` | 尚未接入支付宝授权 | | `PHONE` | `E164` | 规范化手机号,例如 `+8613800138000` | `NULL` | 尚未接入短信验证码 | | `PASSWORD` | `LOCAL` | 规范化用户名(4–32 位小写字母、数字、`._-`) | bcrypt 哈希,不存明文 | 可以,先登录主账号再绑定 | 绑定新身份不会自动合并两个主账号;已经属于其他主账号的身份会拒绝绑定。密码凭证由 Flyway `V3__password_identity.sql` 增量添加。 ## 本地运行 准备 MySQL 数据库(数据库必须存在且账号拥有建表/索引权限;Flyway 自动创建业务表,建议 MySQL 8+): ```sql CREATE DATABASE begin_sso CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ``` 准备 Java 17+、Maven 3.6.3+,然后在**当前项目目录**执行: ```bash export DB_USER='your-local-user' export DB_PWD='your-local-password' export WECHAT_APP_ID='your-mini-program-app-id' export WECHAT_APP_SECRET='your-mini-program-app-secret' export SSO_COOKIE_SECURE=false # 仅限本机 HTTP 调试;正式环境保持默认 true mvn clean package java -jar begin-site-web/target/begin_sso.jar ``` 应用默认监听 `8090`,数据库连接为 `jdbc:mysql://127.0.0.1:3306/begin_sso`。`DB_USER`、`DB_PWD` 没有仓库默认值;不要把生产凭据放进配置文件。工具箱 HTML 位于本工程 `begin-site-web/src/main/resources/internal/toolbox.html`,由 Maven 直接打入 JAR,不依赖 `begin_wx` 的目录结构或 Node 进程。`mvn test` 的单元测试无需数据库;当前本地 MySQL 已完成 Flyway 建表,但微信登录真机联调仍需要真实 AppID/Secret。当前本地 MySQL 为 5.7,Flyway 会提示其不在社区版支持范围内,正式环境建议升级 MySQL。 ## API - `POST /auth/wechat/login`,请求 `{"code":"wx.login 返回的 code"}`。服务端调用微信 `jscode2session`,按 `(WECHAT, AppID, openid)` 找到或创建主账号,返回 `{token,expireTime,userId,openid,user:{id,name}}`;`userId` 与 `user.id` 是同一个 19 位十进制字符串,`openid` 是服务端验证所得,`expireTime` 是 Unix 毫秒时间戳。首次登录会创建账号和会话;jscode2session 若返回 `unionid`(小程序已绑定开放平台),会为同一主账号追加一条 `(WECHAT, UNIONID, unionid)` 身份。若该 unionid 已属于另一主账号(历史数据或未来多应用场景),当前主账号会在同一事务内**自动并入** unionid 属主:身份与会话全部迁移、旧账号标记 `MERGED`,登录返回属主主账号——不会产生第三个账号,也不会拒绝登录。`/auth/wechat/bind` 同样适用。密码登录不返回 openid。 - `POST /auth/wechat/bind`,携带 `Authorization: Bearer ` 和新的微信 `code`,将服务端验证过的微信身份绑定至当前主账号。身份已属于其他主账号则返回 409,不自动合并。 - `POST /auth/password/bind`,携带有效 Bearer token 和 `{"username":"alice.name","password":"至少12字符的密码"}`,为当前主账号绑定本地用户名密码(密码 UTF-8 最多 72 字节);用户名已存在返回 409。 - `POST /auth/password/login`,请求 `{"username":"alice.name","password":"..."}`,密码校验成功后返回主账号及会话信息(不含 openid);用户名/密码错误为 401,凭证正确但账号或身份禁用为 403。 - `POST /auth/logout`,携带有效 Bearer token,删除该会话,同时撤销其 MINI token 与关联 H5 Cookie,返回 `{"loggedOut":true}`;不影响同账号其他会话。 - `POST /auth/logout-all`,携带有效 Bearer token,删除当前账号全部会话(含发起请求的会话),其他账号不受影响。账号仍可重新登录。以上接口不接收目标 userId;无效/过期凭证返回 401(重复退出也返回 401),缺少必填 Authorization 由框架返回 400。 - `POST /h5/logout`、`POST /h5/logout-all`,对应 Cookie 鉴权的退出接口,必须携带与请求协议、主机、端口一致的 `Origin`,缺失或跨源返回 403;成功后清除 `/` 与旧 `/tools` 路径 Cookie。工具箱页面提供两个退出按钮。 - `GET /me/identities`,Bearer 鉴权后查看当前账号已绑定的身份类型与状态(不返回平台 subject)。 - `GET /me/profile`,Bearer 鉴权返回 `{"userId":"100..."}`——当前会话归属的可信业务用户号(19 位十进制字符串)。会话缺失、过期或主账号被禁用/已合并返回 401。该接口只回用户号,不返回昵称、状态或身份列表;资料看 `/me/identities`。 - `GET /tools?token=&userId=<19位业务用户号>`:保留的受保护工具箱交接入口,**当前小程序只在微信登录成功后打开公开的 `https://www.begincode.net/` 首页,不调用此接口**。若其他客户端使用该入口,服务端仍会校验 MINI 会话及 userId,并签发独立的 H5 Cookie。 - `GET /tools`:无有效 Cookie 时返回 401,有效时提供 H5 工具页面;同源所有工具复用该 Cookie。 - `GET /h5/me`:H5 Cookie 鉴权,返回服务器从会话中读取的业务 `{userId:"..."}`,用于核对其他同源 H5 请求确实延续同一登录状态。 - `POST /sso/ticket` 已移除;旧客户端请求将返回 404。`GET /tools?ticket=...` 不再兑换票据,会转到不含敏感参数的错误页。 - `GET /health` 健康检查(只反映进程存活,不代表数据库或微信接口已联通)。 - 登录审计日志:登录/绑定/合并/退出事件写入 `logs/login-audit.log`(logback 滚动:按天+10MB 切割,保留 30 天,上限 200MB)。登录/绑定包含 provider、subject、IP,退出包含可信 userId 和 CURRENT/ALL 范围;限流拒绝记录 `reason=RATE_LIMITED`。**不记录** code、token、密码。IP 使用 Servlet 容器解析的远端地址,不直接信任客户端 `X-Forwarded-For`。 - `GET /docs.html` 静态使用文档(接口列表、请求/响应样例、错误码、小程序→H5 交接流程);自包含单文件,无外部依赖。 手机号和支付宝是已经支持的**子账号数据类型**,但不接受客户端传来的手机号或支付宝 ID 作为身份凭证;必须先接入短信验证码或支付宝服务端授权校验,再调用内部 `bindVerified` / 建立对应登录流程。这两个平台的真实验证接口及账号合并不在当前版本中。 ## 部署边界 密码登录默认使用 MySQL 共享限流:15 分钟固定窗口内,同一规范化用户名最多 5 次失败,同一 IP 最多 30 次失败;第 6/31 次请求返回 HTTP 429、`code=RATE_LIMITED` 与秒数 `Retry-After`。窗口从该维度首次预占开始;校验中的请求占用额度,成功只释放本次预占,不清空其他失败,未知用户名、超长密码及禁用账号的拒绝也计数。窗口结束后恢复,数据每 10 分钟批量清理超过一天的过期窗口。限流使用独立事务,业务回滚不撤销失败计数;存储故障时不绕过限流。 可用环境变量:`SSO_PASSWORD_ACCOUNT_LIMIT`(默认 5)、`SSO_PASSWORD_IP_LIMIT`(默认 30)、`SSO_PASSWORD_WINDOW_SECONDS`(默认 900,允许 1–86400 秒)。所有实例应使用相同配置。反向代理部署应在容器层配置可信代理对客户端 IP、原始协议和端口的解析,例如 Tomcat 的 `server.forward-headers-strategy=native` 和受限的 `server.tomcat.remoteip.internal-proxies`,且代理必须覆盖外部传入的转发头、后端只允许可信代理访问;否则 IP 限流会按代理地址计数,H5 Origin 校验也可能失败。不得直接信任公网提供的转发头。 新增数据库集成测试需显式设置 `SSO_SECURITY_DB_TESTS=true`,并将 `spring.datasource.url` 指向隔离 MySQL;普通 `mvn test` 不启用这些测试。使用 Java 17。测试覆盖跨实例并发额度、窗口恢复、回滚后计数保留、账号隔离及 MINI/H5 同时撤销。 当前小程序的 `../begin_wx/miniprogram/config.js` 使用线上 `/sso` API 和 BeginCode 公开官网首页。前者需配置微信 request 合法域名,后者需配置 `web-view` 业务域名;小程序不再把长期 token/userId 放在官网 URL。官网没有 SSO 接入,打开首页**不等于**官网会员已登录。此前旧版带参 URL 会被官网第三方统计发送出去,已发布的旧客户端须尽快更新并评估撤销受影响会话。受保护的 `/sso/tools?token=...` 入口仍保留给其他客户端,若使用它也必须使用 HTTPS、Secure Cookie,并对敏感 query 脱敏。新增 Cookie 写接口同样应具备 CSRF/Origin 保护;清理过期会话、微信域名备案和真实微信登录验收仍需完成。小程序接入退出接口后,还需清除其本地 session 缓存;本次变更只修改本 SSO 工程。 登录样式与文案按 clientId 使用 Guava 内存缓存,默认写入后 30 秒过期、最多 1000 项;数据库修改在各实例缓存过期后的下一次访问生效,无需重启。`SSO_LOGIN_PRESENTATION_CACHE_TTL=0s` 可禁用缓存,容量由 `SSO_LOGIN_PRESENTATION_CACHE_MAXIMUM_SIZE` 配置。只缓存展示配置,不影响 token 实时校验及短期凭证兑换。详见 [接口文档第 8.9 节](docs/login-api.md#89-数据库展示配置与-guava-内存缓存)。