skillhub/docs/09-deployment.md
XiaoSeS 934cfa6ded
Some checks are pending
Deploy Docs / build (push) Waiting to run
Deploy Docs / Deploy (push) Blocked by required conditions
Security / Dependency Review (push) Waiting to run
Security / CodeQL (java-kotlin) (push) Waiting to run
Security / CodeQL (javascript-typescript) (push) Waiting to run
Security / CodeQL (python) (push) Waiting to run
feat(auth): add Feishu as a public login provider (R1-A2) (#877)
* feat(auth): let providers override OAuth userinfo loading

Some providers do not return a flat, standard userinfo payload, so
DefaultOAuth2UserService cannot read them. Add ProviderOAuth2UserService
so a provider can claim its own registration id and supply the loading
step, while everything after it stays shared.

The override runs inside the RemoteIdentityIoExecutor boundary added in
R1-A, so a provider's HTTP call does not hold the surrounding
transaction open. Registrations without an override keep using the
default user service unchanged.

Part of R1-A2 (public Provider adapters) per
openspec/changes/enterprise-identity-platform/rollout-plan.md.

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>

* feat(auth): add Feishu as a public login provider

Adds Feishu (Lark) as a public sign-in option: it authenticates a
SkillHub platform account and nothing more. No Organization membership,
no directory sync, no Namespace grants.

Feishu deviates from standard OAuth in two ways this handles:
its userinfo response is wrapped in a {code, msg, data} envelope, and it
reports errors with HTTP 200. FeishuOAuth2UserService unwraps that
envelope into flat attributes; FeishuClaimsExtractor maps them to the
shared OAuthClaims, so account decisions still run through the unified
identity core added in R1-A.

Subject and email semantics, which decide whether a login can reach an
existing account:

- open_id is the only subject. union_id stays in extra rather than
  acting as a fallback: a subject that can change between logins would
  split one person across two platform accounts. Promoting union_id
  later needs an explicit alias migration.
- A blank or missing open_id fails the login instead of binding the
  literal string "null".
- emailVerified is always false. Feishu emails are imported by an
  organization admin and never confirmed with the user, so they carry no
  verification signal and cannot be used to join an existing account.

Operational bounds: the userinfo call has connect and read timeouts so an
unresponsive Feishu endpoint cannot hold a login thread, and the
OAuth2Error description carries only the provider error code, because an
upstream message can quote the request URI and with it the access token.
Like the GitHub and GitLab extractors, the claims extractor logs nothing.

The login button follows the existing config-driven catalog: with no
client id configured, /api/v1/auth/methods does not list Feishu and no
button renders. No frontend code change is needed; the icon resolves by
provider name.

Adapted from the implementation in #696 by @yhd4711499, re-extracted onto
current main with the subject, logging and timeout changes above.

Part of R1-A2 (public Provider adapters) per
openspec/changes/enterprise-identity-platform/rollout-plan.md.

Co-authored-by: yhd4711499 <yhd4711499@users.noreply.github.com>
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>

* fix(auth): bound Feishu userinfo response and stop subject leaking into displayName

Three defects found reviewing this batch against the R1-A2 spec.

Response size limit. The spec's scope line asks for "远程 I/O 超时与响应大小
限制"; only the timeouts were implemented, so a misconfigured or hostile
OAUTH2_FEISHU_BASE_URI could stream an unbounded body into the parser.
Reads at most 64 KB before parsing, mirroring the 10 MB cap the shared
WebClientConfig already applies. Uses InputStream.readNBytes rather than
adding commons-io or guava, neither of which skillhub-auth declares.

Synthesized displayName. Falling back to "feishu-<open_id>" wrote the
external subject into UserAccount.displayName and into
UserActivatedEvent, carrying it somewhere event consumers may log it --
against the R1-A gate that logs must not contain the subject. Now stops
at name -> en_name like the GitHub and GitLab extractors.

Unused mobile attribute. A phone number was extracted into the principal
attributes and read by nothing. It is PII the spec did not ask for and it
widened the redaction surface for free.

Also drops a constructor overload that only passed List.of() through, and
a test that duplicated the blank-subject path.

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>

* docs(auth): document the provider adapter contract and Feishu operator setup

AGENTS.md and CONTRIBUTING.md both require docs updates when auth flows or
deployment config change; this batch changed both and touched no docs.

03-authentication-design.md described adding a provider as "branch on
registrationId inside CustomOAuth2UserService", which the
ProviderOAuth2UserService strategy supersedes. Rewrites that recipe:
register an OAuthClaimsExtractor bean per provider, add a
ProviderOAuth2UserService only when the userinfo response is non-standard,
and note that the login page needs no code change. Also records the
provider-side obligations that are easy to get wrong -- stable subject with
no fallback, emailVerified only on proven ownership, bounded remote calls,
no subject in logs -- and un-comments the config example, which still
listed GitLab as a future possibility.

faq.md told operators to delete "the github and gitlab blocks" to hide SSO
buttons. That advice was already incomplete and gets worse per provider, so
it now explains the config-driven mechanism: an empty client id keeps the
entry off the login page, no file edit needed.

09-deployment.md listed only the GitHub credentials. Adds GitLab and Feishu,
and flags a deployment trap: Feishu emails are admin-imported so
emailVerified is always false, and skillhub.access-policy.mode=EMAIL_DOMAIN
denies every unverified email, which would reject all Feishu logins.

Squares the Feishu logo viewBox. It was 407.87x324.19 while login-button
renders it in a square w-5 h-5 box, so the mark was distorted.

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>

* feat(deploy): wire Feishu credentials into the release surfaces

.env.release.example advertised OAUTH2_FEISHU_* knobs that no deployment
path could actually deliver. compose.release.yml has no env_file, so every
variable must be listed explicitly, and the Helm chart and k8s base only
mapped the GitHub secret keys. Setting the documented variables therefore
did nothing.

Adds Feishu to compose.release.yml, the Helm secret template and values,
the k8s deployment and its secret example. GitLab had the identical gap, so
it is wired at the same time rather than leaving the example file half true.

validate-release-config.sh only checked that GitHub's id and secret appear
together. A half-configured provider renders a login button whose exchange
then fails, so the check now loops over all three providers. Its test gained
both-directions cases per provider plus a fully configured pass; reverting
the loop to GitHub-only makes them fail.

Also adds the provider's only failure log. Nothing downstream records a
Feishu userinfo failure -- OAuth2LoginFailureHandler does not log either --
so the previous code was silent on error. Logs the exception class and
Feishu's own error code, never the upstream msg, which can quote the access
token; a test asserts the code is present and the token is not.

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>

* fix(auth): use JSON token exchange for Feishu OAuth

Made-with: Proma
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>

* fix(auth): preserve Feishu OAuth browser redirect

Add safe phase-level OAuth diagnostics and redact callback credentials from request logs.

Made-with: Proma
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>

* docs(deploy): clarify Feishu OAuth configuration and validation

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>

* fix(deploy): pass Feishu redirect URI through releases

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>

* fix(deploy): pass S3 chunked encoding setting

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>

* fix(deploy): preserve default Feishu callback derivation

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>

---------

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
Co-authored-by: yhd4711499 <yhd4711499@users.noreply.github.com>
2026-09-21 14:39:22 +08:00

24 KiB
Raw Blame History

skillhub 部署架构与运维

1 运行模型

当前仓库只保留两种运行方式:

  • 开发环境:make dev-all
    • 前端和后端运行在宿主机
    • docker-compose.yml 只负责 PostgreSQL、Redis、MinIO
  • 单机交付环境:docker compose --env-file .env.release -f compose.release.yml up -d
    • 前端和后端都运行在容器内
  • 使用 GitHub Actions 发布到 GHCR 的镜像
  • 默认发布多架构镜像:server / web 覆盖 linux/amd64linux/arm64linux/riscv64scanner 暂保持 linux/amd64linux/arm64
    • PostgreSQL、Redis 与应用容器一起通过 Compose 启动

不再维护本地构建整套 demo 容器的中间模式,也不再保留 docker-compose.prod.yml

2 单机交付拓扑

┌──────────────┐
│ Browser / CLI│
└──────┬───────┘
       │
       ▼
┌──────────────┐
│   Web/Nginx  │  published image
└──────┬───────┘
       │ /api/*
       ▼
┌──────────────┐
│ Spring Boot  │  published image
└───┬────┬─────┘
    │    │
    ▼    ▼
 PostgreSQL  Redis

说明:

  • Web 容器提供静态资源,并将 /api/*/oauth2/*/.well-known/* 反代到后端
  • 后端默认运行 docker profile不再启用本地 mock 登录
  • PostgreSQL / Redis 默认只绑定 127.0.0.1
  • 对象存储推荐使用外部 S3 / OSS通过环境变量注入

3 Profile 约定

Profile 用途 说明
local 本地源码开发能力 启用 mock 登录、开发种子账号、调试日志
docker 容器运行时能力 启用容器运行时相关能力,不会自动打开首登管理员

单机交付环境使用 SPRING_PROFILES_ACTIVE=docker,原因如下:

  • 生产环境不应开启 X-Mock-User-Id 这一类本地开发旁路能力
  • 容器环境仍然保留 docker profile 的运行时能力,首个管理员账户初始化不依赖该 profile通过环境变量控制
  • 数据库、Redis、OSS、站点公网地址全部改为环境变量优先

如需启用首登管理员,来源于以下环境变量:

  • BOOTSTRAP_ADMIN_ENABLED=true(发布模板默认已开启)
  • BOOTSTRAP_ADMIN_USERNAME(默认 admin
  • BOOTSTRAP_ADMIN_PASSWORD(默认 ChangeMe!2026

建议:

  • 生产环境务必修改 BOOTSTRAP_ADMIN_PASSWORDvalidate-release-config.sh 会拒绝默认值)
  • 完成首次登录后立即修改管理员密码
  • 如果已有外部身份源,通常不需要启用 bootstrap admin
  • SKILLHUB_PUBLIC_BASE_URL 应配置为最终 HTTPS 域名,避免 OAuth / Cookie / 设备码链接异常

4 开发环境

开发入口保持不变:

make dev-all

行为:

  • docker-compose.yml 启动 PostgreSQL、Redis、MinIO
  • server 在宿主机通过 Maven Wrapper 启动
  • web 在宿主机通过 Vite 启动

常用命令:

make dev
make dev-all
make dev-down
make dev-all-down
make dev-all-reset

5 单机交付环境

5.1 启动

cp .env.release.example .env.release
make validate-release-config
docker compose --env-file .env.release -f compose.release.yml up -d

默认访问地址:

  • Web UI: SKILLHUB_PUBLIC_BASE_URL
  • Backend API: http://localhost:8080

5.2 连接外部 Redis Cluster

发布 Compose 默认仍使用内置单机 Redis。连接外部 Redis Cluster 时,在 .env.release 中设置标准 Spring Boot 配置,不需要额外的模式开关:

SPRING_DATA_REDIS_CLUSTER_NODES=redis-0.example.com:6379,redis-1.example.com:6379,redis-2.example.com:6379
SPRING_DATA_REDIS_CLUSTER_MAX_REDIRECTS=5
SPRING_DATA_REDIS_USERNAME=skillhub
SPRING_DATA_REDIS_PASSWORD=replace-with-secret
SPRING_DATA_REDIS_SSL_ENABLED=true
SPRING_DATA_REDIS_CONNECT_TIMEOUT=5s
SPRING_DATA_REDIS_TIMEOUT=3s

Cluster 节点返回给客户端的所有地址必须能从 server 容器访问。Redis Cluster 只支持数据库 0;不要为 Cluster 设置非零的 SPRING_DATA_REDIS_DATABASE。配置 Cluster 节点后Spring Boot 自动忽略单机 host/portCompose 中的内置 Redis 容器仍会启动,但不会被 Server 使用。

对真实 Cluster 运行功能检查:

REDIS_CLUSTER_TEST_NODES=redis-0.example.com:6379,redis-1.example.com:6379,redis-2.example.com:6379 \
REDIS_CLUSTER_TEST_USERNAME=skillhub \
REDIS_CLUSTER_TEST_PASSWORD=replace-with-secret \
make test-redis-cluster

该检查覆盖 Spring Data 读写、Spring Session 保存/读取/删除和 Redisson Stream。

5.3 连接外部 Redis Sentinel

Sentinel 使用标准 Spring Boot 配置。数据节点和 Sentinel 可以使用不同 ACL

SPRING_DATA_REDIS_SENTINEL_MASTER=mymaster
SPRING_DATA_REDIS_SENTINEL_NODES=sentinel-0.example.com:26379,sentinel-1.example.com:26379,sentinel-2.example.com:26379
SPRING_DATA_REDIS_USERNAME=skillhub
SPRING_DATA_REDIS_PASSWORD=replace-with-data-node-secret
SPRING_DATA_REDIS_SENTINEL_USERNAME=sentinel-user
SPRING_DATA_REDIS_SENTINEL_PASSWORD=replace-with-sentinel-secret
SKILLHUB_REDIS_SENTINEL_CHECK_SENTINELS_LIST=true

Sentinel 配置优先于 Cluster 和单机 host/port。在 Kubernetes 等 Sentinel 返回地址与客户端入口不一致的环境中,可以将 SKILLHUB_REDIS_SENTINEL_CHECK_SENTINELS_LIST 设为 false

5.4 关键文件

  • compose.release.yml
    • 使用发布镜像,不在用户机器上执行本地构建
    • 负责拉起 PostgreSQL、Redis、server、web
    • PostgreSQL、Redis 默认只绑定到 127.0.0.1
    • Web 和后端都支持运行时环境变量注入不需要为每个环境重建镜像S3/OSS 的 SKILLHUB_STORAGE_S3_* 变量会透传到 server
  • .env.release.example
    • 运行时变量模板
    • 包含镜像名、镜像版本、端口、数据库凭证、外部 OSS、站点公网地址和首登管理员参数
  • scripts/validate-release-config.sh
    • 在启动前校验 .env.release
    • 可提前拦截占位值、URL 格式错误、缺失的 OSS 凭据、危险的明文默认值

阿里云 OSS 等不支持 AWS chunked encoding 的对象存储,需要在 .env.release 中设置:

SKILLHUB_STORAGE_S3_DISABLE_CHUNKED_ENCODING=true

该变量由 compose.release.yml 透传到 server修改后需要重新创建 server 容器:

docker compose --env-file .env.release -f compose.release.yml up -d --force-recreate server

5.5 镜像标签约定

  • edge
    • main 分支最新构建
    • 用于内部持续验证
  • vX.Y.Z
    • 对应 Git tag
    • 用于稳定版本交付
  • latest
    • 仅在语义化版本 tag 发布时更新

推荐:

  • 默认快速启动:SKILLHUB_VERSION=latest
  • 团队内部试用:SKILLHUB_VERSION=edge
  • 对外演示或严格可复现环境:固定为某个 vX.Y.Z

6 GitHub Actions 发布流程

发布工作流文件:.github/workflows/publish-images.yml

触发条件:

  • release.published
  • 手动 workflow_dispatch

流程:

  1. 检出代码
  2. 登录 GHCR
  3. 分别构建 server/Dockerfileweb/Dockerfile
  4. 推送镜像:
    • ghcr.io/iflytek/skillhub-server
    • ghcr.io/iflytek/skillhub-web
  5. 写入 edge / vX.Y.Z / latest / sha-* 标签
  6. 同时发布多架构 manifestserver / web 覆盖 linux/amd64linux/arm64linux/riscv64scanner 暂保持 linux/amd64linux/arm64

7 配置管理

7.1 请求限流配置

限流默认开启。未配置分类覆盖时,各接口使用代码中 @RateLimit 声明的默认值,现有部署无需调整。

可通过环境变量关闭全部限流,或按分类覆盖额度和时间窗口:

SKILLHUB_RATELIMIT_ENABLED=false
SKILLHUB_RATELIMIT_CATEGORIES_SEARCH_ANONYMOUS=100
SKILLHUB_RATELIMIT_CATEGORIES_SEARCH_AUTHENTICATED=300
SKILLHUB_RATELIMIT_CATEGORIES_SEARCH_WINDOW_SECONDS=60

支持的配置字段为 authenticatedanonymouswindow-seconds。分类名称来自接口的 @RateLimit(category = "..."),例如 searchdownloadpublishresolve。只设置其中一个字段时, 其他字段仍回退到接口默认值。

Docker Compose 用户需要显式传入变量,宿主机环境变量不会自动注入容器:

services:
  server:
    environment:
      SKILLHUB_RATELIMIT_CATEGORIES_SEARCH_ANONYMOUS: "100"
      SKILLHUB_RATELIMIT_CATEGORIES_SEARCH_AUTHENTICATED: "300"
      SKILLHUB_RATELIMIT_CATEGORIES_SEARCH_WINDOW_SECONDS: "60"

修改后重启 server 容器生效。超过额度时接口返回 HTTP 429;该配置只调整阈值,不改变 Redis 限流算法或响应格式。

前端运行时配置通过 web/runtime-config.js.template 注入。与认证兼容层相关的新变量如下:

  • SKILLHUB_WEB_AUTH_DIRECT_ENABLED
    • 是否在前端打开账号密码兼容接入层
    • 默认应为 false
  • SKILLHUB_WEB_AUTH_DIRECT_PROVIDER
    • 前端调用 /api/v1/auth/direct/login 时使用的 provider例如 private-sso
  • SKILLHUB_WEB_AUTH_SESSION_BOOTSTRAP_ENABLED
    • 是否在前端打开企业 SSO 被动会话兼容入口
    • 默认应为 false
  • SKILLHUB_WEB_AUTH_SESSION_BOOTSTRAP_PROVIDER
    • 前端调用 /api/v1/auth/session/bootstrap 时使用的 provider例如 private-sso
  • SKILLHUB_WEB_AUTH_SESSION_BOOTSTRAP_AUTO
    • 是否在登录页加载后自动尝试一次 bootstrap
    • 建议私有版初期保持 false

注意:

  • 前端密码兼容层打开之前,后端仍必须同步打开 skillhub.auth.direct.enabled=true
  • 前端开关打开之前,后端仍必须同步打开 skillhub.auth.session-bootstrap.enabled=true
  • 前后端任一侧未开启,都不会破坏原有登录方式;只会使该兼容入口不可用或不显示

开发环境:

  • 本地命令与 docker-compose.yml
  • 非敏感默认值可直接落库或写入本地配置

单机交付环境:

  • 使用 .env.release 管理 Compose 变量

  • 如果 GHCR 包保持私有,用户需要先 docker login ghcr.io

  • 推荐将敏感变量放入 CI/CD Secret 或主机上的受控 .env.release

  • 外部对象存储通过 SKILLHUB_STORAGE_S3_* 注入

  • 前端反代和运行时 API 地址通过 SKILLHUB_API_UPSTREAM / SKILLHUB_WEB_API_BASE_URL 注入

  • SKILLHUB_TRUST_FORWARDED_PROTO 默认保持 false。只有 Web 容器仅能经由可信 TLS 终止代理访问,且该代理会覆盖客户端传入的 X-Forwarded-Proto 时才设为 true;否则客户端可伪造协议并影响 OAuth 回调、重定向和安全 Cookie 判断

  • 如果通过网关部署在 /skillhub/ 等子路径,需同时配置:

    • SKILLHUB_WEB_BASE_PATH=/skillhub/
    • SKILLHUB_WEB_API_BASE_URL=/skillhub
    • SKILLHUB_PUBLIC_BASE_URL=https://example.com/skillhub 网关可以在转发到 Web 容器前将该前缀重写掉,但公网 URL 仍必须保留前缀,确保 OAuth、CLI 和 registry 链接正确。
  • 如果要开放真实登录,再补充对应 Provider 的 client id/secret

    • GitHubOAUTH2_GITHUB_CLIENT_ID / OAUTH2_GITHUB_CLIENT_SECRET

    • GitLabOAUTH2_GITLAB_CLIENT_ID / OAUTH2_GITLAB_CLIENT_SECRET(自建实例再设 OAUTH2_GITLAB_BASE_URI

    • 飞书:OAUTH2_FEISHU_CLIENT_ID / OAUTH2_FEISHU_CLIENT_SECRET。 Endpoint 默认配置为:

      • OAUTH2_FEISHU_AUTHORIZATION_URI=https://accounts.feishu.cn/open-apis/authen/v1/authorize
      • OAUTH2_FEISHU_PROTOCOL_VERSION=v3
      • OAUTH2_FEISHU_TOKEN_URI=https://accounts.feishu.cn/oauth/v3/token
      • OAUTH2_FEISHU_USER_INFO_URI=https://open.feishu.cn/open-apis/authen/v1/user_info
      • OAUTH2_FEISHU_REDIRECT_URI=可选Compose 默认根据 SKILLHUB_PUBLIC_BASE_URL 生成 /login/oauth2/code/feishuHelm/K8s 未设置时由 Spring 使用 {baseUrl};经过特殊反向代理或本地动态端口时应显式设置完整回调 URL

      Lark 国际版、私有化部署或企业网关可分别覆盖这三个完整 endpoint历史的 OAUTH2_FEISHU_AUTHORIZE_URI / OAUTH2_FEISHU_BASE_URI 仍可作为 base-URI 兼容回退。OAUTH2_FEISHU_TOKEN_URI 必须指向支持 JSON authorization-code exchange 的 endpoint。OAUTH2_FEISHU_PROTOCOL_VERSION 只允许 v2v3 默认 v3,不会自动 fallback。

    留空即不展示该入口,无需改配置文件。注意:飞书邮箱由企业管理员导入、未经用户 确认,因此 emailVerified 恒为 false若在 application.yml 中把 skillhub.access-policy.mode 设为 EMAIL_DOMAIN,该策略会拒绝所有未验证邮箱, 飞书登录将一律失败。启用飞书时请保留默认的 OPEN 或改用其他准入模式。

    启用飞书前,使用一个测试租户完成一次真实回调验收。不要把真实 client secret 写入仓库、报告或聊天记录;只在受控的 .env.release、CI Secret 或 Kubernetes Secret 中注入:

    1. 在飞书自建应用中登记 https://<公网域名>/login/oauth2/code/feishu,并开启用户信息所需权限;如果使用 本地预览,则把 OAUTH2_FEISHU_REDIRECT_URI 设置为预览 Web 地址对应的完整回调 URL。

    2. 在受控环境设置 OAUTH2_FEISHU_CLIENT_IDOAUTH2_FEISHU_CLIENT_SECRET,确认 OAUTH2_FEISHU_PROTOCOL_VERSION 与 token endpoint 匹配,然后运行:

      make validate-release-config
      docker compose --env-file .env.release -f compose.release.yml up -d
      curl -fsS http://127.0.0.1:8080/actuator/health
      curl -fsS http://127.0.0.1:8080/api/v1/auth/methods
      
    3. 在登录页选择“飞书”,确认浏览器跳转到配置的授权域名;完成授权后应回到 /login/oauth2/code/feishu,最终进入 / 或原始的 root-relative returnTo

    4. 用同一个飞书账号再次登录,确认仍绑定同一个 SkillHub 账号;再用已禁用的 SkillHub 账号登录,预期跳转 /access-denied,且不创建新 Session。

    5. 检查日志中只有 provider、HTTP 状态、错误码和阶段信息,不应出现 client secret、 authorization code、access token、open_id 或上游错误文本:

      docker compose -f compose.release.yml logs --tail=200 server \
        | rg -i 'client_secret|authorization code|access[_-]?token|open_id|secret|token'
      

    本地 mock 回调只能证明 SkillHub 与协议形状的集成,不能替代上述真实租户验收。 没有可用飞书租户时,应将该项记录为“未验证”,不要宣称 Feishu 登录已通过。

  • 如果要启用密码重置验证码邮件,参见:docs/19-smtp-password-reset-email-setup.md

8 OIDC 登录配置

SkillHub 复用 Spring Security OAuth2 Client 的 OIDC 支持。前端不需要单独 配置回调页;登录页会从 /api/v1/auth/methods 读取后端暴露的 OAUTH_REDIRECT 方法并跳转到 /oauth2/authorization/{registrationId}

生产环境接入 OIDC 时,为后端增加一组 OAuth2 client registration 配置即可。 下面以 oidc 作为 registration id

SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OIDC_CLIENT_ID=replace-me
SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OIDC_CLIENT_SECRET=replace-me
SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OIDC_PROVIDER=oidc
SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OIDC_AUTHORIZATION_GRANT_TYPE=authorization_code
SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OIDC_REDIRECT_URI={baseUrl}/login/oauth2/code/{registrationId}
SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OIDC_SCOPE=openid,profile,email
SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OIDC_CLIENT_NAME=OIDC
SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_OIDC_ISSUER_URI=https://idp.example.com/realms/skillhub

要接入多个 OIDC IdP使用不同 registration id例如 oktakeycloak 并把上面的环境变量中的 OIDC 替换为对应大写 id。registration id 会作为 identity_binding.provider_code,请保持稳定。

警告Registration ID 冲突

每个 OIDC 提供商必须使用唯一的 registration ID。Registration ID 作为 identity_binding.provider_code 存储在数据库中,用于将外部身份映射到平台 用户。如果两个不同的 IdP 使用了相同的 registration ID例如都使用 oidc 会导致不同 IdP 的用户 sub 值空间混用,可能出现身份绑定错误或账户冲突。

建议使用有意义的 registration ID例如 oktakeycloakazure-ad 而不是通用的 oidc。一旦投入使用,不要更改 registration ID否则现有用户 将无法登录。

Docker Compose 发布模板默认只透传常用变量。若使用 OIDC请通过 compose override 或部署平台环境变量把上述 SPRING_SECURITY_* 变量注入 server 容器。Kubernetes 部署同理,将这些变量放入 backend-deployment.yamlserver 容器环境变量或统一的配置管理系统中。

9 裸金属上线清单

推荐顺序:

  1. 准备服务器基础环境
    • 安装 Docker Engine 与 Docker Compose Plugin
    • 配置公网 HTTPS 入口,确保最终访问域名已经确定
    • 打开 80 / 443,避免直接暴露 5432 / 6379
  2. 填写 .env.release
    • SKILLHUB_PUBLIC_BASE_URL 填最终 HTTPS 域名,且不要带尾部 /;子路径部署时必须包含外部路径前缀
    • SKILLHUB_STORAGE_PROVIDER=s3
    • 按云厂商 OSS / S3 兼容参数填写 SKILLHUB_STORAGE_S3_*
    • 设置非默认的 POSTGRES_PASSWORD
    • 模板默认已开启首登管理员,务必将 BOOTSTRAP_ADMIN_PASSWORD 改为强密码
  3. 启动前校验
    • 运行 make validate-release-config
    • 确认没有 replace-mechange-this-*ChangeMe!2026 之类的占位值
  4. 首次启动
    • 运行 docker compose --env-file .env.release -f compose.release.yml up -d
    • 检查 docker compose --env-file .env.release -f compose.release.yml ps
    • 检查 curl -i http://127.0.0.1:8080/actuator/health
  5. 首登收尾
    • 仅在启用了 BOOTSTRAP_ADMIN_ENABLED=true 时,使用 BOOTSTRAP_ADMIN_USERNAME / BOOTSTRAP_ADMIN_PASSWORD 登录
    • 立即修改管理员密码
    • 如果后续完全走 OAuth可将 BOOTSTRAP_ADMIN_ENABLED=false

10 可观测性

维度 方案
健康检查 web/nginx-healthserver/actuator/health
请求关联 响应头和日志中的 X-Request-Id / request.id
日志 文本或 ECS 风格 JSON均输出到容器 stdout / stderr
Trace none、Micrometer + OTel SDK、或外部 Java Agent 三选一
指标 Spring Boot ActuatorPrometheus 是可选后端,不是 Trace 前置条件

10.1 通用配置

默认配置不要求 Collector、SkyWalking 或 Elasticsearch

SKILLHUB_TRACING_MODE=none
SKILLHUB_LOG_FORMAT=json
SKILLHUB_SERVICE_VERSION=v0.2.15
SKILLHUB_SERVICE_ENVIRONMENT=production

发布 Compose 默认使用 ECS 风格 JSON由 Filebeat、Fluent Bit 或容器平台采集 stdout。 本地源码开发仍可使用 SKILLHUB_LOG_FORMAT=text。SkillHub 不直接连接 Elasticsearch。 JSON 日志使用以下稳定字段:

  • request.idSkillHub 请求、响应和审计关联 ID。
  • trace.idspan.id:当前存在有效 Trace 时输出。
  • service.nameservice.versionservice.environment

SKILLHUB_LOG_ASYNC_QUEUE_SIZE 默认是 1024。JSON 日志队列是有界且非阻塞的;采集端 阻塞时允许丢弃日志以保护业务线程,数据库中的 audit_log 仍是审计事实来源。

10.2 三种 Tracing 模式

三种模式只能选择一种,切换后需要重启:

模式 适用场景 必需配置
none 不部署链路追踪 SKILLHUB_TRACING_MODE=none
otel-sdk 厂商中立 OTLP/Collector 模式、采样率;需要导出时再配置 endpoint
external-agent 使用 SkyWalking Agent 原生能力 模式、唯一的外部 Agent不得配置 OTLP endpoint

OTel SDK 模式的最小配置:

SKILLHUB_TRACING_MODE=otel-sdk
SKILLHUB_LOG_FORMAT=json
SKILLHUB_TRACING_SAMPLING_PROBABILITY=0.1
MANAGEMENT_OTLP_TRACING_ENDPOINT=http://otel-collector:4318/v1/traces
SKILLHUB_OTLP_TIMEOUT=5s
SKILLHUB_OTLP_COMPRESSION=gzip

未设置 MANAGEMENT_OTLP_TRACING_ENDPOINT 时,otel-sdk 仍可建立进程内 Trace但不会 创建 OTLP Exporter也不会尝试连接默认地址。noneexternal-agent 模式配置 endpoint 会启动失败。

External Agent 模式的应用侧配置:

SKILLHUB_TRACING_MODE=external-agent
SKILLHUB_LOG_FORMAT=json

部署平台还必须通过 JVM 启动参数挂载且只挂载一个 Agent。SkillHub 无法可靠识别任意 Java Agent因此上线前应检查实际 JAVA_TOOL_OPTIONS 或容器启动命令,确认没有同时启用 OTel Agent、SkyWalking Agent 和应用内 otel-sdk。SkyWalking Agent 模式可以通过官方 Logback Toolkit 输出 trace.idspan.id 是否可用取决于 Agent 版本。

10.3 OTel Collector 接入 SkyWalking

下面是只转发 Trace 的最小 Collector 配置:

receivers:
  otlp:
    protocols:
      http:
        endpoint: 0.0.0.0:4318

processors:
  batch: {}

exporters:
  otlp/skywalking:
    endpoint: skywalking-oap:11800
    tls:
      insecure: true

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [otlp/skywalking]

SkyWalking OAP 10.3 还需要启用 OTLP Trace handler、Zipkin receiver 和 Zipkin query

SW_OTEL_RECEIVER_ENABLED_HANDLERS=otlp-traces
SW_RECEIVER_ZIPKIN=default
SW_QUERY_ZIPKIN=default

应用使用 Collector 的 OTLP/HTTP 4318 端口Collector 使用 OAP 的 OTLP/gRPC 11800 端口。生产环境应按网络边界配置 TLS上例中的 insecure: true 只适用于受控的 容器内部网络。

SkyWalking 10.3 会把 OTLP Trace 转换为 Zipkin Trace并通过 Zipkin Query/Lens 查询。 这条路径不提供 SkyWalking Java Agent 的完整原生拓扑、慢 SQL 和 Profiling 能力。需要 这些能力时使用 external-agent,不要同时启用 otel-sdk

10.4 日志与 Trace 联查

JSON 日志由采集器写入 Elasticsearch 后,在 Kibana 通过 trace.id 查询;同一个 trace.id 可在 SkyWalking 的 Zipkin Query/Lens 或 Agent 原生查询界面中定位调用链。 request.id 始终可以用于 SkillHub 内部日志和审计关联。

当采样率小于 1.0 时,日志仍是全量输出,因此部分日志虽有请求关联信息,但在 SkyWalking 中没有被保留的 Trace。这是头部采样的预期行为。

10.5 回滚

遇到观测后端异常时:

  1. SKILLHUB_TRACING_MODE 改为 none
  2. 删除 MANAGEMENT_OTLP_TRACING_ENDPOINT
  3. 需要进一步降低日志开销时,将 SKILLHUB_LOG_FORMAT 改为 text
  4. 滚动重启 Server。

关闭 Trace 和 JSON 日志不会改变请求、数据库或异步任务的业务语义。

开发者接入统一标准的最小步骤、内部/外部 HTTP Client 传播边界和扩展点见: 可观测性开发者接入指南

11 安全扫描服务

如果要启用 skill-scanner 后端链路,当前仓库建议按下面的方式部署:

  • 本地共享目录场景可以使用 local 模式
  • Kubernetes 或分离部署场景应使用 upload 模式

当前 deploy/k8s 已按分离部署建模,因此推荐:

  • SKILLHUB_SECURITY_SCANNER_ENABLED=true
  • SKILLHUB_SECURITY_SCANNER_URL=http://skillhub-scanner:8000
  • SKILLHUB_SECURITY_SCANNER_MODE=upload

相关文件:

  • deploy/k8s/scanner-deployment.yaml
  • deploy/k8s/services.yaml
  • deploy/k8s/backend-deployment.yaml
  • scripts/verify-scanner.sh
  • docs/security-scanning.md

12 数据迁移

Flyway 仍是唯一 schema 变更入口:

  • 路径:server/skillhub-app/src/main/resources/db/migration/
  • 命名:V{version}__{description}.sql
  • 启动策略:应用容器启动时自动执行迁移