Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .env.release.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ SKILLHUB_WEB_IMAGE=ghcr.io/iflytek/skillhub-web
SKILLHUB_SCANNER_IMAGE=ghcr.io/iflytek/skillhub-scanner
POSTGRES_IMAGE=postgres:16-alpine
REDIS_IMAGE=redis:7-alpine
SPRING_PROFILES_ACTIVE=docker

# Public entrypoint seen by browsers/CLI, no trailing slash.
# Default to localhost so `runtime.sh up` works as a zero-config quickstart.
Expand Down Expand Up @@ -92,6 +93,14 @@ OAUTH2_GITLAB_CLIENT_SECRET=
OAUTH2_GITLAB_BASE_URI=https://gitlab.com
OAUTH2_GITLAB_DISPLAY_NAME=GitLab

# Optional: configure DingTalk (钉钉) OAuth2 login.
# Add dingtalk to SPRING_PROFILES_ACTIVE (for example: docker,dingtalk) to enable it.
# Register your app at https://open-dev.dingtalk.com and request the Contact.User.Read scope.
# SkillHub uses the official minimal authorization scope "openid".
OAUTH2_DINGTALK_CLIENT_ID=
OAUTH2_DINGTALK_CLIENT_SECRET=
OAUTH2_DINGTALK_DISPLAY_NAME=钉钉

# Optional: OIDC login (e.g. Keycloak, Okta, Azure AD).
# Replace "OIDC" in variable names with your registration id (uppercase).
# The registration id becomes identity_binding.provider_code — keep it stable.
Expand Down
5 changes: 4 additions & 1 deletion compose.release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ services:
ports:
- "${API_PORT:-8080}:8080"
environment:
SPRING_PROFILES_ACTIVE: docker
SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE:-docker}
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/${POSTGRES_DB:-skillhub}
SPRING_DATASOURCE_USERNAME: ${POSTGRES_USER:-skillhub}
SPRING_DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD:-skillhub_demo}
Expand Down Expand Up @@ -96,6 +96,9 @@ services:
BOOTSTRAP_ADMIN_EMAIL: ${BOOTSTRAP_ADMIN_EMAIL:-admin@skillhub.local}
OAUTH2_GITHUB_CLIENT_ID: ${OAUTH2_GITHUB_CLIENT_ID:-local-placeholder}
OAUTH2_GITHUB_CLIENT_SECRET: ${OAUTH2_GITHUB_CLIENT_SECRET:-local-placeholder}
OAUTH2_DINGTALK_CLIENT_ID: ${OAUTH2_DINGTALK_CLIENT_ID:-}
OAUTH2_DINGTALK_CLIENT_SECRET: ${OAUTH2_DINGTALK_CLIENT_SECRET:-}
OAUTH2_DINGTALK_DISPLAY_NAME: ${OAUTH2_DINGTALK_DISPLAY_NAME:-钉钉}
SPRING_MAIL_HOST: ${SPRING_MAIL_HOST:-}
SPRING_MAIL_PORT: ${SPRING_MAIL_PORT:-25}
SPRING_MAIL_USERNAME: ${SPRING_MAIL_USERNAME:-}
Expand Down
19 changes: 19 additions & 0 deletions deploy/k8s/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ cp secret.yaml.example secret.yaml
| bootstrap-admin-password | 管理员密码 | 是 |
| oauth2-github-client-id | GitHub OAuth ID | 否 |
| oauth2-github-client-secret | GitHub OAuth 密钥 | 否 |
| oauth2-dingtalk-client-id | 钉钉 OAuth AppKey | 否 |
| oauth2-dingtalk-client-secret | 钉钉 OAuth AppSecret | 否 |
| skill-scanner-llm-api-key | LLM API 密钥 | 否 |
| skill-scanner-llm-base-url | 本地/自定义 LLM 服务地址 | 否 |
| skill-scanner-llm-model | Scanner 使用的 LLM 模型名 | 否 |
Expand Down Expand Up @@ -213,6 +215,7 @@ kubectl apply -k overlays/with-infra/ # 或 overlays/external/
| redis-connect-timeout | 未设置 | Redis 建连超时 |
| redis-timeout | 未设置 | Redis 命令超时 |
| redis-client-name | 未设置 | Redis 客户端名称 |
| spring-profiles-active | docker | Spring profile;启用钉钉时改为 `docker,dingtalk` |
| storage-base-path | /var/lib/skillhub/storage | 技能存储路径 |
| skillhub-storage-provider | local | 存储类型(local/s3) |
| skill-scanner-enabled | true | 是否启用扫描器 |
Expand All @@ -224,6 +227,7 @@ kubectl apply -k overlays/with-infra/ # 或 overlays/external/
| bootstrap-admin-display-name | Platform Admin | 管理员显示名称 |
| bootstrap-admin-email | admin@example.com | 管理员邮箱 |
| session-cookie-secure | false | HTTPS 环境设为 true |
| oauth2-dingtalk-display-name | 钉钉 | 钉钉登录入口显示名称 |

### Secret 配置项

Expand All @@ -237,10 +241,25 @@ kubectl apply -k overlays/with-infra/ # 或 overlays/external/
| bootstrap-admin-password | 管理员密码 | 是 |
| oauth2-github-client-id | GitHub OAuth ID | 否 |
| oauth2-github-client-secret | GitHub OAuth 密钥 | 否 |
| oauth2-dingtalk-client-id | 钉钉 OAuth AppKey | 否 |
| oauth2-dingtalk-client-secret | 钉钉 OAuth AppSecret | 否 |
| skill-scanner-llm-api-key | LLM API 密钥 | 否 |
| skill-scanner-llm-base-url | 本地/自定义 LLM 服务地址 | 否 |
| skill-scanner-llm-model | LLM 模型名称 | 否 |

### 钉钉 OAuth2

钉钉登录默认关闭。启用时:

1. 将 `base/configmap.yaml` 的 `spring-profiles-active` 改为 `docker,dingtalk`
2. 在 `base/secret.yaml` 填写 `oauth2-dingtalk-client-id` 和
`oauth2-dingtalk-client-secret`
3. 在钉钉开放平台将回调地址配置为
`{站点公网地址}/login/oauth2/code/dingtalk`

授权 scope 固定为 `openid`。详细契约参见
[钉钉官方教程](https://developers.dingtalk.com/document/orgapp/tutorial-obtaining-user-personal-information)。

### 存储配置

**本地存储(默认)**
Expand Down
24 changes: 23 additions & 1 deletion deploy/k8s/base/backend-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ spec:
name: http
env:
- name: SPRING_PROFILES_ACTIVE
value: docker
valueFrom:
configMapKeyRef:
name: skillhub-config
key: spring-profiles-active

# Database
- name: SPRING_DATASOURCE_URL
Expand Down Expand Up @@ -217,6 +220,25 @@ spec:
key: oauth2-github-client-secret
optional: true

# DingTalk OAuth2 (optional; requires the dingtalk Spring profile)
- name: OAUTH2_DINGTALK_CLIENT_ID
valueFrom:
secretKeyRef:
name: skillhub-secret
key: oauth2-dingtalk-client-id
optional: true
- name: OAUTH2_DINGTALK_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: skillhub-secret
key: oauth2-dingtalk-client-secret
optional: true
- name: OAUTH2_DINGTALK_DISPLAY_NAME
valueFrom:
configMapKeyRef:
name: skillhub-config
key: oauth2-dingtalk-display-name

volumeMounts:
- name: skillhub-storage
mountPath: /var/lib/skillhub/storage
Expand Down
6 changes: 6 additions & 0 deletions deploy/k8s/base/configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ kind: ConfigMap
metadata:
name: skillhub-config
data:
# Add dingtalk to enable DingTalk OAuth2, for example: docker,dingtalk
spring-profiles-active: docker

# Redis 配置
# 使用外部 Redis:修改为外部主机地址
# 使用内置 Redis(overlays/with-infra):保持 redis
Expand Down Expand Up @@ -46,6 +49,9 @@ data:
# Session 配置
# HTTP 环境设为 false,HTTPS 环境设为 true
session-cookie-secure: "false"

# DingTalk OAuth2 display name (credentials are stored in Secret)
oauth2-dingtalk-display-name: 钉钉
---
apiVersion: v1
kind: PersistentVolumeClaim
Expand Down
4 changes: 4 additions & 0 deletions deploy/k8s/base/secret.yaml.example
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ stringData:
oauth2-github-client-id: ""
oauth2-github-client-secret: ""

# DingTalk OAuth(可选;同时在 ConfigMap 中启用 dingtalk profile)
oauth2-dingtalk-client-id: ""
oauth2-dingtalk-client-secret: ""

# LLM 配置(可选,用于技能扫描)
skill-scanner-llm-api-key: ""
skill-scanner-llm-base-url: ""
Expand Down
20 changes: 20 additions & 0 deletions docs/03-authentication-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,26 @@ Spring Security OAuth2 Client 原生支持多 Provider 并存,新增 Provider
2. `CustomOAuth2UserService` 中按 `registrationId` 分支处理用户属性映射
3. 前端登录页增加对应按钮(通过 `/api/v1/auth/providers` 自动发现)

### 3.7 钉钉 OAuth2 契约

钉钉接入遵循[获取用户个人信息教程](https://developers.dingtalk.com/document/orgapp/tutorial-obtaining-user-personal-information)中的新版 OAuth2 契约:授权地址使用
`https://login.dingtalk.com/oauth2/auth`,授权 scope 固定为最小可用值
`openid`,token 与用户信息端点分别使用 `/v1.0/oauth2/userAccessToken` 和
`/v1.0/contact/users/me`。`corpid` 不能单独作为授权 scope。

钉钉的 `openid` 是 OAuth2 授权参数,不表示其 token 响应是 OIDC。适配器在外发
授权 URL 中保留 `scope=openid`,但在 Spring Security 内部将该 registration 按
普通 OAuth2 处理,避免框架转入要求 `id_token` 的 OIDC 分支。其他真正的 OIDC
registration 仍保留 `openid` 和 nonce。

身份映射遵循以下约束:

- 稳定 subject 按 `unionId -> openId -> userId` 回退
- identity binding 始终使用 `provider=dingtalk` 与稳定 subject,不依赖邮箱
- 用户信息端点没有返回真实邮箱时传 `null`,且 `emailVerified=false`
- 即使端点返回邮箱,也不能视为钉钉已验证邮箱,`emailVerified` 仍为 `false`
- provider 默认关闭,仅在显式启用 `dingtalk` Spring profile 并配置凭证时注册

## 4. 核心接口设计

```java
Expand Down
29 changes: 28 additions & 1 deletion docs/09-deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
|---------|------|------|
| `local` | 本地源码开发能力 | 启用 mock 登录、开发种子账号、调试日志 |
| `docker` | 容器运行时能力 | 启用容器运行时相关能力,不会自动打开首登管理员 |
| `dingtalk` | 钉钉 OAuth2 登录 | 默认关闭;必须与运行 profile 组合并配置 AppKey/AppSecret |

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

Expand Down Expand Up @@ -249,7 +250,33 @@ Sentinel 配置优先于 Cluster 和单机 `host`/`port`。在 Kubernetes 等 Se
- 如果要开放真实登录,再补充 `OAUTH2_GITHUB_CLIENT_ID` / `OAUTH2_GITHUB_CLIENT_SECRET`
- 如果要启用密码重置验证码邮件,参见:`docs/19-smtp-password-reset-email-setup.md`

## 8 OIDC 登录配置
## 8 外部身份源配置

### 8.1 钉钉 OAuth2

钉钉 Provider 默认不注册。启用时在 `.env.release` 中设置:

```bash
SPRING_PROFILES_ACTIVE=docker,dingtalk
OAUTH2_DINGTALK_CLIENT_ID=your-app-key
OAUTH2_DINGTALK_CLIENT_SECRET=your-app-secret
OAUTH2_DINGTALK_DISPLAY_NAME=钉钉
```

在钉钉开放平台将回调地址配置为
`{SKILLHUB_PUBLIC_BASE_URL}/login/oauth2/code/dingtalk`,开通读取个人信息所需权限并
发布应用。授权 scope 固定为官方新版 OAuth2 契约的 `openid`;不要改为单独的
`corpid`。契约参见[钉钉官方教程](https://developers.dingtalk.com/document/orgapp/tutorial-obtaining-user-personal-information)。

Compose 会将 profile 与三个 `OAUTH2_DINGTALK_*` 变量传给 Server 容器。
`make validate-release-config` 会拒绝“启用 profile 但缺少凭证”和“配置凭证但未启用
profile”两类不完整配置。

Kubernetes 部署需要将 ConfigMap 的 `spring-profiles-active` 改为
`docker,dingtalk`,并在 Secret 中填写 `oauth2-dingtalk-client-id` 与
`oauth2-dingtalk-client-secret`。Deployment 已将这些配置映射到相同的运行时环境变量。

### 8.2 OIDC 登录

SkillHub 复用 Spring Security OAuth2 Client 的 OIDC 支持。前端不需要单独
配置回调页;登录页会从 `/api/v1/auth/methods` 读取后端暴露的
Expand Down
15 changes: 15 additions & 0 deletions docs/skillhub/en/guide/kubernetes.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ cp secret.yaml.example secret.yaml
| bootstrap-admin-password | Admin password | Yes |
| oauth2-github-client-id | GitHub OAuth ID | No |
| oauth2-github-client-secret | GitHub OAuth secret | No |
| oauth2-dingtalk-client-id | DingTalk OAuth AppKey | No |
| oauth2-dingtalk-client-secret | DingTalk OAuth AppSecret | No |
| skill-scanner-llm-api-key | LLM API key | No |
| skill-scanner-llm-base-url | Local/custom LLM service base URL | No |
| skill-scanner-llm-model | LLM model name used by the scanner | No |
Expand Down Expand Up @@ -171,6 +173,7 @@ kubectl apply -k overlays/with-infra/ # or overlays/external/
|---|---|---|
| redis-host | redis | Redis host address |
| redis-port | 6379 | Redis port |
| spring-profiles-active | docker | Set to `docker,dingtalk` to enable DingTalk login |
| storage-base-path | /var/lib/skillhub/storage | Skill storage path |
| skillhub-storage-provider | local | Storage type (local/s3) |
| skill-scanner-enabled | true | Enable scanner |
Expand All @@ -182,6 +185,18 @@ kubectl apply -k overlays/with-infra/ # or overlays/external/
| bootstrap-admin-display-name | Platform Admin | Admin display name |
| bootstrap-admin-email | admin@example.com | Admin email |
| session-cookie-secure | false | Set to true for HTTPS |
| oauth2-dingtalk-display-name | 钉钉 | DingTalk login display name |

### DingTalk OAuth2

DingTalk login is disabled by default. Set `spring-profiles-active` in the
ConfigMap to `docker,dingtalk`, then provide `oauth2-dingtalk-client-id` and
`oauth2-dingtalk-client-secret` in the Secret. Configure the callback URL in
DingTalk Open Platform as `{public-site-url}/login/oauth2/code/dingtalk`. The
authorization scope is fixed to `openid`.

See the [official DingTalk tutorial](https://developers.dingtalk.com/document/orgapp/tutorial-obtaining-user-personal-information)
for the authorization contract.

### Storage Configuration

Expand Down
13 changes: 13 additions & 0 deletions docs/skillhub/guide/kubernetes.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ cp secret.yaml.example secret.yaml
| bootstrap-admin-password | 管理员密码 | 是 |
| oauth2-github-client-id | GitHub OAuth ID | 否 |
| oauth2-github-client-secret | GitHub OAuth 密钥 | 否 |
| oauth2-dingtalk-client-id | 钉钉 OAuth AppKey | 否 |
| oauth2-dingtalk-client-secret | 钉钉 OAuth AppSecret | 否 |
| skill-scanner-llm-api-key | LLM API 密钥 | 否 |
| skill-scanner-llm-base-url | 本地/自定义 LLM 服务地址 | 否 |
| skill-scanner-llm-model | Scanner 使用的 LLM 模型名 | 否 |
Expand Down Expand Up @@ -171,6 +173,7 @@ kubectl apply -k overlays/with-infra/ # 或 overlays/external/
|---|---|---|
| redis-host | redis | Redis 主机地址 |
| redis-port | 6379 | Redis 端口 |
| spring-profiles-active | docker | 启用钉钉登录时改为 `docker,dingtalk` |
| storage-base-path | /var/lib/skillhub/storage | 技能存储路径 |
| skillhub-storage-provider | local | 存储类型(local/s3) |
| skill-scanner-enabled | true | 是否启用扫描器 |
Expand All @@ -182,6 +185,16 @@ kubectl apply -k overlays/with-infra/ # 或 overlays/external/
| bootstrap-admin-display-name | Platform Admin | 管理员显示名称 |
| bootstrap-admin-email | admin@example.com | 管理员邮箱 |
| session-cookie-secure | false | HTTPS 环境设为 true |
| oauth2-dingtalk-display-name | 钉钉 | 钉钉登录入口显示名称 |

### 钉钉 OAuth2

钉钉登录默认关闭。将 ConfigMap 的 `spring-profiles-active` 改为
`docker,dingtalk`,并在 Secret 中填写 `oauth2-dingtalk-client-id` 与
`oauth2-dingtalk-client-secret` 后才会注册登录入口。钉钉开放平台的回调地址应为
`{站点公网地址}/login/oauth2/code/dingtalk`,授权 scope 固定为 `openid`。

完整授权契约参见[钉钉官方教程](https://developers.dingtalk.com/document/orgapp/tutorial-obtaining-user-personal-information)。

### 存储配置

Expand Down
32 changes: 32 additions & 0 deletions scripts/tests/validate-release-config-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,31 @@ write_env "$invalid_redis_sentinel_check_env" "release-download-secret-32-bytes-
printf '%s\n' "SKILLHUB_REDIS_SENTINEL_CHECK_SENTINELS_LIST=yes" >>"$invalid_redis_sentinel_check_env"
expect_fail "$invalid_redis_sentinel_check_env" "SKILLHUB_REDIS_SENTINEL_CHECK_SENTINELS_LIST must be true or false"

dingtalk_env="$tmp/dingtalk.env"
write_env "$dingtalk_env" "release-download-secret-32-bytes-minimum"
cat >>"$dingtalk_env" <<EOF
SPRING_PROFILES_ACTIVE=docker,dingtalk
OAUTH2_DINGTALK_CLIENT_ID=ding-client-id
OAUTH2_DINGTALK_CLIENT_SECRET=ding-client-secret
EOF
"$SCRIPT" "$dingtalk_env" >/dev/null

dingtalk_missing_secret_env="$tmp/dingtalk-missing-secret.env"
write_env "$dingtalk_missing_secret_env" "release-download-secret-32-bytes-minimum"
cat >>"$dingtalk_missing_secret_env" <<EOF
SPRING_PROFILES_ACTIVE=docker,dingtalk
OAUTH2_DINGTALK_CLIENT_ID=ding-client-id
EOF
expect_fail "$dingtalk_missing_secret_env" "OAUTH2_DINGTALK_CLIENT_SECRET is required"

dingtalk_disabled_env="$tmp/dingtalk-disabled.env"
write_env "$dingtalk_disabled_env" "release-download-secret-32-bytes-minimum"
cat >>"$dingtalk_disabled_env" <<EOF
OAUTH2_DINGTALK_CLIENT_ID=ding-client-id
OAUTH2_DINGTALK_CLIENT_SECRET=ding-client-secret
EOF
expect_fail "$dingtalk_disabled_env" "SPRING_PROFILES_ACTIVE must include dingtalk"

draft_env="$tmp/draft.env"
while IFS= read -r line || [[ -n "$line" ]]; do
case "$line" in
Expand All @@ -163,4 +188,11 @@ while IFS= read -r line || [[ -n "$line" ]]; do
done <"$REPO_ROOT/.env.release.draft" >"$draft_env"
expect_fail "$draft_env" "POSTGRES_PASSWORD"

grep -Fq 'OAUTH2_DINGTALK_CLIENT_ID: ${OAUTH2_DINGTALK_CLIENT_ID:-}' "$REPO_ROOT/compose.release.yml" \
|| fail "compose.release.yml does not pass OAUTH2_DINGTALK_CLIENT_ID"
grep -Fq 'key: oauth2-dingtalk-client-secret' "$REPO_ROOT/deploy/k8s/base/backend-deployment.yaml" \
|| fail "Kubernetes deployment does not pass the DingTalk client secret"
grep -Fq 'spring-profiles-active: docker' "$REPO_ROOT/deploy/k8s/base/configmap.yaml" \
|| fail "Kubernetes config does not expose Spring profile activation"

echo "validate-release-config-test passed"
17 changes: 17 additions & 0 deletions scripts/validate-release-config.sh
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,23 @@ if [ -n "$oauth_secret" ] && [ -z "$oauth_id" ]; then
error "OAUTH2_GITHUB_CLIENT_ID is required when OAUTH2_GITHUB_CLIENT_SECRET is set"
fi

dingtalk_profiles=",${SPRING_PROFILES_ACTIVE:-docker},"
dingtalk_id="${OAUTH2_DINGTALK_CLIENT_ID:-}"
dingtalk_secret="${OAUTH2_DINGTALK_CLIENT_SECRET:-}"
case "$dingtalk_profiles" in
*,dingtalk,*)
require_non_empty OAUTH2_DINGTALK_CLIENT_ID
require_non_empty OAUTH2_DINGTALK_CLIENT_SECRET
reject_values OAUTH2_DINGTALK_CLIENT_ID "placeholder" "local-placeholder"
reject_values OAUTH2_DINGTALK_CLIENT_SECRET "placeholder" "local-placeholder"
;;
*)
if [ -n "$dingtalk_id" ] || [ -n "$dingtalk_secret" ]; then
error "SPRING_PROFILES_ACTIVE must include dingtalk when DingTalk OAuth2 credentials are set"
fi
;;
esac

if [ "$errors" -gt 0 ]; then
echo "Release config validation failed: $errors error(s), $warnings warning(s)." >&2
exit 1
Expand Down
22 changes: 22 additions & 0 deletions server/skillhub-app/src/main/resources/application-dingtalk.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
spring:
config:
activate:
on-profile: dingtalk
security:
oauth2:
client:
registration:
dingtalk:
client-id: ${OAUTH2_DINGTALK_CLIENT_ID}
client-secret: ${OAUTH2_DINGTALK_CLIENT_SECRET}
scope:
- openid
authorization-grant-type: authorization_code
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
client-name: ${OAUTH2_DINGTALK_DISPLAY_NAME:钉钉}
provider:
dingtalk:
authorization-uri: https://login.dingtalk.com/oauth2/auth
token-uri: https://api.dingtalk.com/v1.0/oauth2/userAccessToken
user-info-uri: https://api.dingtalk.com/v1.0/contact/users/me
user-name-attribute: unionId
Loading