diff --git a/.env.release.example b/.env.release.example index 73800c73..add0f570 100644 --- a/.env.release.example +++ b/.env.release.example @@ -138,6 +138,16 @@ OAUTH2_FEISHU_USER_INFO_URI=https://open.feishu.cn/open-apis/authen/v1/user_info OAUTH2_FEISHU_REDIRECT_URI= OAUTH2_FEISHU_DISPLAY_NAME=飞书 +# Optional: DingTalk login as a public sign-in provider. Leaving the client id empty keeps the +# button off the login page. Use the app's AppKey as the client id and AppSecret as the secret. +# Like Feishu, DingTalk returns an organization-recorded email without attesting ownership, so +# emailVerified is always false and the EMAIL_DOMAIN access policy would reject every login. +OAUTH2_DINGTALK_CLIENT_ID= +OAUTH2_DINGTALK_CLIENT_SECRET= +OAUTH2_DINGTALK_AUTHORIZE_URI=https://login.dingtalk.com +OAUTH2_DINGTALK_BASE_URI=https://api.dingtalk.com +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. diff --git a/charts/skillhub/templates/secret.yaml b/charts/skillhub/templates/secret.yaml index d00c41a4..523bc386 100644 --- a/charts/skillhub/templates/secret.yaml +++ b/charts/skillhub/templates/secret.yaml @@ -67,6 +67,14 @@ stringData: oauth2-feishu-client-secret: {{ .Values.secrets.oauth2FeishuClientSecret | quote }} {{- end }} + # OAuth2 DingTalk (optional) + {{- if .Values.secrets.oauth2DingtalkClientId }} + oauth2-dingtalk-client-id: {{ .Values.secrets.oauth2DingtalkClientId | quote }} + {{- end }} + {{- if .Values.secrets.oauth2DingtalkClientSecret }} + oauth2-dingtalk-client-secret: {{ .Values.secrets.oauth2DingtalkClientSecret | quote }} + {{- end }} + # Scanner LLM 配置 (optional) {{- if .Values.secrets.scannerLlmApiKey }} skill-scanner-llm-api-key: {{ .Values.secrets.scannerLlmApiKey | quote }} diff --git a/charts/skillhub/templates/server-deployment.yaml b/charts/skillhub/templates/server-deployment.yaml index 7e635fe8..244bc4c9 100644 --- a/charts/skillhub/templates/server-deployment.yaml +++ b/charts/skillhub/templates/server-deployment.yaml @@ -381,6 +381,20 @@ spec: value: {{ . | quote }} {{- end }} + # OAuth2 DingTalk (optional) + - name: OAUTH2_DINGTALK_CLIENT_ID + valueFrom: + secretKeyRef: + name: {{ include "skillhub.secretName" . }} + key: oauth2-dingtalk-client-id + optional: true + - name: OAUTH2_DINGTALK_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: {{ include "skillhub.secretName" . }} + key: oauth2-dingtalk-client-secret + optional: true + {{- if .Values.server.javaOpts }} - name: JAVA_OPTS value: {{ .Values.server.javaOpts }} diff --git a/charts/skillhub/values.yaml b/charts/skillhub/values.yaml index f004f3f0..555ace85 100644 --- a/charts/skillhub/values.yaml +++ b/charts/skillhub/values.yaml @@ -103,6 +103,8 @@ secrets: oauth2GithubClientSecret: "" oauth2FeishuClientId: "" oauth2FeishuClientSecret: "" + oauth2DingtalkClientId: "" + oauth2DingtalkClientSecret: "" scannerLlmApiKey: "" scannerLlmBaseUrl: "" scannerLlmModel: "" diff --git a/compose.release.yml b/compose.release.yml index 6789ddeb..77cc571a 100644 --- a/compose.release.yml +++ b/compose.release.yml @@ -128,6 +128,11 @@ services: OAUTH2_FEISHU_USER_INFO_URI: ${OAUTH2_FEISHU_USER_INFO_URI:-${OAUTH2_FEISHU_BASE_URI:-https://open.feishu.cn}/open-apis/authen/v1/user_info} OAUTH2_FEISHU_REDIRECT_URI: ${OAUTH2_FEISHU_REDIRECT_URI:-${SKILLHUB_PUBLIC_BASE_URL:-http://localhost}/login/oauth2/code/feishu} OAUTH2_FEISHU_DISPLAY_NAME: ${OAUTH2_FEISHU_DISPLAY_NAME:-飞书} + OAUTH2_DINGTALK_CLIENT_ID: ${OAUTH2_DINGTALK_CLIENT_ID:-local-placeholder} + OAUTH2_DINGTALK_CLIENT_SECRET: ${OAUTH2_DINGTALK_CLIENT_SECRET:-local-placeholder} + OAUTH2_DINGTALK_AUTHORIZE_URI: ${OAUTH2_DINGTALK_AUTHORIZE_URI:-https://login.dingtalk.com} + OAUTH2_DINGTALK_BASE_URI: ${OAUTH2_DINGTALK_BASE_URI:-https://api.dingtalk.com} + 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:-} diff --git a/deploy/k8s/base/backend-deployment.yaml b/deploy/k8s/base/backend-deployment.yaml index 52e53b12..01319d16 100644 --- a/deploy/k8s/base/backend-deployment.yaml +++ b/deploy/k8s/base/backend-deployment.yaml @@ -248,6 +248,20 @@ spec: value: "https://accounts.feishu.cn/oauth/v3/token" - name: OAUTH2_FEISHU_USER_INFO_URI value: "https://open.feishu.cn/open-apis/authen/v1/user_info" + + # OAuth2 DingTalk (optional) + - 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 volumeMounts: - name: skillhub-storage mountPath: /var/lib/skillhub/storage diff --git a/deploy/k8s/base/secret.yaml.example b/deploy/k8s/base/secret.yaml.example index f9c119d6..f983fa19 100644 --- a/deploy/k8s/base/secret.yaml.example +++ b/deploy/k8s/base/secret.yaml.example @@ -31,6 +31,10 @@ stringData: oauth2-feishu-client-id: "" oauth2-feishu-client-secret: "" + # 钉钉 OAuth(可选,用于钉钉登录;留空则登录页不展示该入口) + oauth2-dingtalk-client-id: "" + oauth2-dingtalk-client-secret: "" + # LLM 配置(可选,用于技能扫描) skill-scanner-llm-api-key: "" skill-scanner-llm-base-url: "" diff --git a/docs/03-authentication-design.md b/docs/03-authentication-design.md index ef5aa98a..478e3420 100644 --- a/docs/03-authentication-design.md +++ b/docs/03-authentication-design.md @@ -282,6 +282,14 @@ spring: # 飞书的 scope 配在开放平台应用上,不在这里传 client-authentication-method: client_secret_post authorization-grant-type: authorization_code + dingtalk: + client-id: ${OAUTH2_DINGTALK_CLIENT_ID} + client-secret: ${OAUTH2_DINGTALK_CLIENT_SECRET} + # 故意不声明 scope:钉钉的授权端点要 scope=openid,但在这里声明会让 + # Spring 把该注册当成 OIDC 客户端并附加 nonce,而钉钉不接受 nonce。 + # scope 由 DingTalkAuthorizationRequestCustomizer 在请求阶段补上。 + client-authentication-method: none + authorization-grant-type: authorization_code provider: feishu: # Full endpoints are configurable for Lark, private deployments, and gateways. @@ -300,12 +308,24 @@ Spring Security OAuth2 Client 原生支持多 Provider 并存,新增 Provider 第 2 步是按 Provider 注册一个 Bean,而不是在某个类里按 `registrationId` 分支。 账号匹配、建号、资料权威和账号守卫都在 `OAuthClaims` 之后共享,Provider 自己不做这些决策。 -如果该 Provider 的 userinfo 响应不是标准的扁平结构(例如飞书用 -`{code, msg, data}` 信封,且以 HTTP 200 返回错误),再额外实现一个 -`ProviderOAuth2UserService`:它声明自己负责哪个 `registrationId`, -接管 userinfo 的加载步骤,其余流程不变。该覆盖运行在 +如果该 Provider 的协议有偏离标准之处,按偏离的环节实现对应的策略接口, +每个接口都声明自己负责哪个 `registrationId`,由框架分发,不需要在共享类里写分支: + +| 偏离环节 | 策略接口 | 现有实现 | +|---|---|---| +| 授权请求参数 | `ProviderAuthorizationRequestCustomizer` | 钉钉补 `openid` scope | +| token 交换 | `ProviderTokenResponseClient` | 钉钉用 JSON body 而非表单 | +| userinfo 加载 | `ProviderOAuth2UserService` | 飞书拆信封;钉钉用自定义 token header | + +以 userinfo 为例:飞书用 `{code, msg, data}` 信封且以 HTTP 200 返回错误, +钉钉则把 token 放在 `x-acs-dingtalk-access-token` 而不是 `Authorization: Bearer`。 +两者都只接管加载步骤,其余流程不变。该覆盖运行在 `RemoteIdentityIoExecutor` 边界内,因此 Provider 的 HTTP 调用不会持有数据库事务。 +Provider 的实现**不得**自己做账号决策 —— 不建号、不绑定、不建 session。 +这些一律交给统一身份核心,否则每个 Provider 都会长出一套账号逻辑, +正是统一身份认证要消除的问题。 + Provider 侧还需遵守:subject 必须稳定(不要用可能在两次登录间变化的字段做 fallback,否则同一个人会被拆成两个平台账号)、只有在 Provider 真正证明了邮箱 所有权时才置 `emailVerified=true`、远程调用要有超时与响应大小上限、 diff --git a/docs/09-deployment.md b/docs/09-deployment.md index 76117217..24ca4121 100644 --- a/docs/09-deployment.md +++ b/docs/09-deployment.md @@ -314,11 +314,13 @@ services: 兼容回退。`OAUTH2_FEISHU_TOKEN_URI` 必须指向支持 JSON authorization-code exchange 的 endpoint。`OAUTH2_FEISHU_PROTOCOL_VERSION` 只允许 `v2` 或 `v3`, 默认 `v3`,不会自动 fallback。 + - 钉钉:`OAUTH2_DINGTALK_CLIENT_ID` / `OAUTH2_DINGTALK_CLIENT_SECRET` + (分别填应用的 AppKey 与 AppSecret) - 留空即不展示该入口,无需改配置文件。注意:飞书邮箱由企业管理员导入、未经用户 - 确认,因此 `emailVerified` 恒为 false;若在 `application.yml` 中把 + 留空即不展示该入口,无需改配置文件。注意:飞书和钉钉的邮箱都由企业管理员导入、 + 未经用户确认,因此 `emailVerified` 恒为 false;若在 `application.yml` 中把 `skillhub.access-policy.mode` 设为 `EMAIL_DOMAIN`,该策略会拒绝所有未验证邮箱, - 飞书登录将一律失败。启用飞书时请保留默认的 `OPEN` 或改用其他准入模式。 + 这两个入口的登录将一律失败。启用它们时请保留默认的 `OPEN` 或改用其他准入模式。 启用飞书前,使用一个测试租户完成一次真实回调验收。不要把真实 client secret 写入仓库、报告或聊天记录;只在受控的 `.env.release`、CI Secret 或 Kubernetes diff --git a/docs/skillhub/en/faq.md b/docs/skillhub/en/faq.md index c00abdf8..4db5d8bb 100644 --- a/docs/skillhub/en/faq.md +++ b/docs/skillhub/en/faq.md @@ -197,7 +197,7 @@ A: Login entries are config-driven: `/api/v1/auth/methods` only returns registra So there are two ways to hide one: - Leave the matching environment variable unset (for example, omit `OAUTH2_FEISHU_CLIENT_ID`). No config file change needed. -- Or edit `application.yml` and comment out or delete the relevant registration block (`github`, `gitlab`, `feishu`) under `spring.security.oauth2.client.registration`, along with its `provider` section. Spring Boot then won't create that registration at startup. +- Or edit `application.yml` and comment out or delete the relevant registration block (`github`, `gitlab`, `feishu`, `dingtalk`) under `spring.security.oauth2.client.registration`, along with its `provider` section. Spring Boot then won't create that registration at startup. ## Q: Is SkillHub's security scanning (Skill Scanner) developed in-house by iFLYTEK? What license does it use? diff --git a/docs/skillhub/faq.md b/docs/skillhub/faq.md index 75054ca0..e8a094d9 100644 --- a/docs/skillhub/faq.md +++ b/docs/skillhub/faq.md @@ -199,7 +199,7 @@ A: 登录入口是配置驱动的:`/api/v1/auth/methods` 只返回配置了真 - 留空对应的环境变量即可(例如不设置 `OAUTH2_FEISHU_CLIENT_ID`),无需改动配置文件。 - 或修改 `application.yml`,注释/删除 `spring.security.oauth2.client.registration` - 下对应的注册块(`github`、`gitlab`、`feishu`)以及对应的 `provider` 段, + 下对应的注册块(`github`、`gitlab`、`feishu`、`dingtalk`)以及对应的 `provider` 段, Spring Boot 启动时便不会创建该注册。 ## Q: SkillHub 的安全扫描(Skill Scanner)是讯飞自研的吗?使用什么协议? diff --git a/scripts/tests/validate-release-config-test.sh b/scripts/tests/validate-release-config-test.sh index e18e2648..83e73a70 100755 --- a/scripts/tests/validate-release-config-test.sh +++ b/scripts/tests/validate-release-config-test.sh @@ -292,7 +292,7 @@ expect_fail "$invalid_redis_sentinel_check_env" "SKILLHUB_REDIS_SENTINEL_CHECK_S # An OAuth client id without its secret (or vice versa) leaves the provider half-configured: # the login button renders but the exchange fails. Checked for every supported provider. -for provider in GITHUB GITLAB FEISHU; do +for provider in GITHUB GITLAB FEISHU DINGTALK; do missing_oauth_secret_env="$tmp/missing-oauth-secret.env" write_env "$missing_oauth_secret_env" "release-download-secret-32-bytes-minimum" printf 'OAUTH2_%s_CLIENT_ID=real-client-id\n' "$provider" >>"$missing_oauth_secret_env" diff --git a/scripts/validate-release-config.sh b/scripts/validate-release-config.sh index e9a85899..c7189304 100755 --- a/scripts/validate-release-config.sh +++ b/scripts/validate-release-config.sh @@ -380,7 +380,7 @@ if [ "${REDIS_BIND_ADDRESS:-127.0.0.1}" != "127.0.0.1" ]; then warn "REDIS_BIND_ADDRESS is not 127.0.0.1; confirm Redis exposure is intended" fi -for provider in GITHUB GITLAB FEISHU; do +for provider in GITHUB GITLAB FEISHU DINGTALK; do eval "oauth_id=\"\${OAUTH2_${provider}_CLIENT_ID:-}\"" eval "oauth_secret=\"\${OAUTH2_${provider}_CLIENT_SECRET:-}\"" if [ -n "$oauth_id" ] && [ -z "$oauth_secret" ]; then