docs: Casdoor SSO 部署运维手册 + 泛化 E2E 检查脚本(沉淀 31.23 踩坑)
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
# DeepData × Casdoor SSO 部署运维手册(31.23)
|
||||
|
||||
> 适用范围:DeepData(`com.deepdata`)网关单点登录接入自建 Casdoor v2.153.0。
|
||||
> 生产实例:31.23(Casdoor `:1013`,DeepData 网关 `:8080`,前端/入口 `deepdata.yhdd.apescale.com:1014`)。
|
||||
> 代码版本:≥ `4e2ec55`(M3:移除 accountOnly,本地登录与 SSO 共存;登录时自动建号,无定时同步)。
|
||||
|
||||
## 1. 架构与登录流
|
||||
|
||||
```
|
||||
浏览器 → DeepData 网关 /api/user/oauth/login?next=…
|
||||
→ 302 Casdoor /login/oauth/authorize(state+next 存 HttpOnly cookie,TTL 300s)
|
||||
→ Casdoor 登录/同意
|
||||
→ 回调 /api/user/oauth/callback?code&state
|
||||
→ 网关换 token → JWKS 验 id_token → upsert 本地用户(自动建号/刷组刷角色)
|
||||
→ 302 next?sso_token=<本地HS256 JWT>(含 role/groups claims)
|
||||
→ 前端 callback.html 种 token 进入应用
|
||||
```
|
||||
|
||||
失败时回跳 `next?sso_error=<code>`(`state_mismatch` / `code_exchange` / `id_token_verify` / `internal`)。
|
||||
|
||||
角色映射:Casdoor 组 `deepdata-admin|dev|annotator|viewer`(组路径 `datamate/<group>`)→ 本地 `role`,由 `RoleResolver` 按「映射 > 主角色 > defaultRole」解析;组与角色随每次 SSO 登录刷新。
|
||||
|
||||
## 2. Casdoor 侧一次性配置(生产现状)
|
||||
|
||||
| 对象 | 值 | 说明 |
|
||||
|------|-----|------|
|
||||
| 组织 | `admin/built-in`、`admin/datamate` | `password_type=bcrypt` |
|
||||
| 组(owner=datamate) | `deepdata-admin` `deepdata-dev` `deepdata-annotator` `deepdata-viewer` | 与 casbin 规则对齐 |
|
||||
| 用户(owner=datamate) | `dmin`(4 组)、`viewer1`(viewer) | **`tag='DeepData'` 必须与应用 tags 一致**;建议给真实 email |
|
||||
| 证书 | `admin/cert-built-in` | 应用引用用**裸名** `cert-built-in` |
|
||||
| 应用 | `admin/DataMate` | clientId `144f4d00553aa7db0aab`;tags `["DeepData"]`;tokenFields `["Groups"]`;`redirectUri=https://deepdata.yhdd.apescale.com:1014/api/user/oauth/callback`;**`expire_in_hours ≥ 1`** |
|
||||
| Casbin | dmin 4 条 / viewer1 1 条 | 角色→组权限 |
|
||||
|
||||
### 踩坑清单(均已踩过,排查先对这里)
|
||||
|
||||
1. **`expire_in_hours` 必须 ≥ 1**:应用列名是 `expire_in_hours`(double),不是 token_expiration。为 0 时 `expires_in=0`,id_token `exp=iat`,网关验证报 `JWT expired N milliseconds ago`。
|
||||
`UPDATE application SET expire_in_hours=1 WHERE owner='admin' AND name='DataMate';`
|
||||
2. **用户 tag 必须在应用 tags 里**:否则 casdoor 登录报 `User's tag: is not listed in the application's tags`。
|
||||
3. **`/api/login` 收 JSON body**(不是表单):`{"type":"login","application":"DataMate","username":"dmin","password":"…","organization":"datamate","autoSignin":true}`。
|
||||
4. **`/api/grant-consent` 的 `application` 必须全名** `admin/DataMate`(裸名报 `Invalid application`);session cookie 与登录**同 host**(cookie 域陷阱)。
|
||||
5. **authorize 无服务端 302**:带 session 的 GET 只返回 SPA 页面,重定向由前端 `grant-consent` 后完成——脚本化验证直接调 `POST /api/grant-consent`。
|
||||
|
||||
## 3. DeepData 侧配置
|
||||
|
||||
`deployment/docker/deepdata/.env`(compose 注入,变量名映射见 `docker-compose.yml`):
|
||||
|
||||
```ini
|
||||
DEEPDATA_CASDOOR_ENABLED=true
|
||||
DEEPDATA_CASDOOR_ISSUER=https://sso.yhdd.apescale.com:1014
|
||||
DEEPDATA_CASDOOR_AUTHORIZE_URI=http://192.168.31.23:1013/login/oauth/authorize
|
||||
DEEPDATA_CASDOOR_TOKEN_URI=http://192.168.31.23:1013/api/login/oauth/access_token
|
||||
DEEPDATA_CASDOOR_JWKS_URI=http://192.168.31.23:1013/.well-known/jwks.json
|
||||
DEEPDATA_CASDOOR_CLIENT_ID=144f4d00553aa7db0aab
|
||||
DEEPDATA_CASDOOR_CLIENT_SECRET=***
|
||||
DEEPDATA_CASDOOR_REDIRECT_URI=https://deepdata.yhdd.apescale.com:1014/api/user/oauth/callback
|
||||
JWT_SECRET=<不变,本地 JWT 签名密钥>
|
||||
```
|
||||
|
||||
> ISSUER 用公网域名(id_token issuer 断言用);三个 URI 用内网直连(网关→casdoor 同机)。
|
||||
|
||||
## 4. 构建与部署
|
||||
|
||||
```bash
|
||||
cd /opt/deepdata/deepdata
|
||||
make gateway-docker-build frontend-docker-build # 产物 deepdata-{gateway,frontend}:latest
|
||||
cd deployment/docker/deepdata
|
||||
docker compose up -d --force-recreate deepdata-gateway deepdata-frontend
|
||||
```
|
||||
|
||||
注意:跳板机链路构建长任务易断连,长构建用 `nohup … > /tmp/gw_build.log 2>&1 &` 后台跑再轮询日志。
|
||||
同步代码到 31.23 **只按文件同步、禁用 `rsync --delete`**,同步后 `git hash-object` 逐文件核对。
|
||||
|
||||
## 5. 验证
|
||||
|
||||
```bash
|
||||
GW_BASE=https://deepdata.yhdd.apescale.com:1014 \
|
||||
CASDOOR_BASE=http://192.168.31.23:1013 \
|
||||
SSO_CLIENT_ID=144f4d00553aa7db0aab \
|
||||
LOCAL_ADMIN_PASSWORD=<本地admin密码> \
|
||||
SSO_TEST_USERS="dmin:<dmin密码>:admin viewer1:<viewer1密码>:viewer" \
|
||||
bash scripts/casdoor-sso/e2e-check.sh
|
||||
```
|
||||
|
||||
覆盖:配置公开且无 secret 泄漏 → 本地登录(与 SSO 共存)→ SSO 全链路(authorize→登录→consent→callback→sso_token→/me 角色断言)。
|
||||
|
||||
网络注意:公网域名经 222.128.20.101 转发到 31.23:1014,**不要用 `--resolve` 把域名指到 192.168.31.23**(该地址本地未监听);E2E 直接访问公网域名即可。
|
||||
|
||||
## 6. 排障
|
||||
|
||||
| 现象 | 原因 | 处理 |
|
||||
|------|------|------|
|
||||
| `sso_error=id_token_verify` + 网关日志 `JWT expired` | 应用 `expire_in_hours=0` | 见 §2 坑 1 |
|
||||
| `sso_error=internal` + `provision race … reusing existing user` 后 NPE | 本地 email 列 NOT NULL UNIQUE,空 email 互撞(代码已修为空 email 合成 `username@casdoor.local`) | 升级网关镜像;存量行 `UPDATE users SET email=… WHERE email='';` |
|
||||
| casdoor 登录 `User's tag: is not listed…` | 用户 tag 不在应用 tags | `UPDATE user SET tag='DeepData' WHERE owner='datamate';` |
|
||||
| grant-consent `Invalid application` | application 用了裸名 | 用 `admin/DataMate` |
|
||||
| authorize 返回 200 不跳转 | 正常(SPA 行为);若脚本验证用 grant-consent | — |
|
||||
| `/sso/casdoor/config` `enabled:false` | `DEEPDATA_CASDOOR_ENABLED` 未注入 | 检查 .env + compose 变量名映射 |
|
||||
Executable
+86
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env bash
|
||||
# DeepData × Casdoor SSO 全链路 E2E 检查(在能同时访问网关与 Casdoor 的机器上运行)
|
||||
#
|
||||
# 用法:
|
||||
# GW_BASE=https://deepdata.yhdd.apescale.com:1014 \
|
||||
# CASDOOR_BASE=http://192.168.31.23:1013 \
|
||||
# SSO_CLIENT_ID=<clientId> SSO_CLIENT_SECRET=<secret> \
|
||||
# LOCAL_ADMIN_PASSWORD=*** \
|
||||
# bash e2e-check.sh
|
||||
#
|
||||
# 可选环境变量(有默认值,按实际部署修改):
|
||||
# SSO_APP_NAME=DataMate Casdoor 应用名
|
||||
# SSO_APP_OWNER=admin Casdoor 应用 owner(grant-consent 需 owner/name 全名)
|
||||
# SSO_ORG=datamate Casdoor 组织名(登录用)
|
||||
# SSO_REDIRECT_URI=$GW_BASE/api/user/oauth/callback
|
||||
# SSO_TEST_USERS="dmin:Dmin@2026:admin viewer1:Viewer1@2026:viewer" 用户名:密码:期望角色
|
||||
# LOCAL_ADMIN=admin 本地管理员(验证本地登录共存)
|
||||
set -u
|
||||
|
||||
GW="${GW_BASE:?GW_BASE required, e.g. https://deepdata.yhdd.apescale.com:1014}"
|
||||
CD="${CASDOOR_BASE:?CASDOOR_BASE required, e.g. http://192.168.31.23:1013}"
|
||||
APP="${SSO_APP_NAME:-DataMate}"
|
||||
APP_FULL="${SSO_APP_OWNER:-admin}/$APP"
|
||||
ORG="${SSO_ORG:-datamate}"
|
||||
REDIR="${SSO_REDIRECT_URI:-$GW/api/user/oauth/callback}"
|
||||
CLIENT_ID="${SSO_CLIENT_ID:?SSO_CLIENT_ID required}"
|
||||
ADMIN_U="${LOCAL_ADMIN:-admin}"
|
||||
ADMIN_P="${LOCAL_ADMIN_PASSWORD:*** LOCAL_ADMIN_PASSWORD required}"
|
||||
|
||||
TMPD=$(mktemp -d); trap 'rm -rf "$TMPD"' EXIT
|
||||
fail=0
|
||||
ck() { if [ "$1" = "0" ]; then echo "PASS: $2"; else echo "FAIL: $2"; fail=1; fi; }
|
||||
|
||||
echo "=== [1] /sso/casdoor/config ==="
|
||||
CFG=$(curl -sk "$GW/sso/casdoor/config")
|
||||
echo "$CFG"
|
||||
echo "$CFG" | grep -q '"enabled":true' && ck 0 "enabled=true" || ck 1 "enabled"
|
||||
# clientSecret 不得出现在公开配置里
|
||||
if [ -n "${SSO_CLIENT_SECRET:-}" ] && echo "$CFG" | grep -q "$SSO_CLIENT_SECRET"; then
|
||||
ck 1 "clientSecret 泄漏"; else ck 0 "无 secret 泄漏"; fi
|
||||
|
||||
echo "=== [2] 本地登录 $ADMIN_U(accountOnly 已移除,本地登录与 SSO 共存)==="
|
||||
LOGIN=$(curl -sk -X POST "$GW/api/user/login" -H 'Content-Type: application/json' \
|
||||
-d "{\"username\":\"$ADMIN_U\",\"password\":\"$ADMIN_P\"}")
|
||||
TOK=$(echo "$LOGIN" | sed -n 's/.*"token":"\([^"]*\)".*/\1/p')
|
||||
[ -n "$TOK" ] && ck 0 "本地登录拿 token" || { echo "$LOGIN"; ck 1 "本地登录"; }
|
||||
ME=$(curl -sk "$GW/api/user/me" -H "Authorization: Bearer $TOK")
|
||||
echo "$ME"
|
||||
|
||||
# SSO 单用户链路: 网关登录页 → casdoor JSON 登录 → grant-consent 取 code → 网关 callback → /me
|
||||
sso_test() {
|
||||
local U="$1" P="$2" EXPECT="$3"
|
||||
local GJ="$TMPD/gw_$U" CJ="$TMPD/cd_$U" HDRS AUTHURL STATE LR CODE CB FINAL SSTOK ME2
|
||||
HDRS=$(curl -sk -c "$GJ" -D - -o /dev/null "$GW/api/user/oauth/login?next=/sso/callback.html")
|
||||
AUTHURL=$(echo "$HDRS" | grep -i '^location:' | tr -d '\r' | awk '{print $2}')
|
||||
STATE=$(echo "$AUTHURL" | sed -n 's/.*state=\([^&]*\).*/\1/p')
|
||||
[ -n "$AUTHURL" ] && [ -n "$STATE" ] && ck 0 "$U 拿到 authorize URL" || { echo "authurl=$AUTHURL"; ck 1 "$U authorize"; return; }
|
||||
LR=$(curl -s -c "$CJ" -X POST -H 'Content-Type: application/json' \
|
||||
-d "{\"type\":\"login\",\"application\":\"$APP\",\"username\":\"$U\",\"password\":\"$P\",\"organization\":\"$ORG\",\"autoSignin\":true}" \
|
||||
"$CD/api/login")
|
||||
echo "$LR" | grep -q '"status": *"ok"' && ck 0 "$U casdoor 登录" || { echo "$LR"; ck 1 "$U casdoor 登录"; return; }
|
||||
CODE=$(curl -s -b "$CJ" -X POST -H 'Content-Type: application/json' \
|
||||
-d "{\"application\":\"$APP_FULL\",\"clientId\":\"$CLIENT_ID\",\"provider\":\"\",\"signinMethod\":\"\",\"responseType\":\"code\",\"redirectUri\":\"$REDIR\",\"scope\":\"openid profile email\",\"state\":\"$STATE\",\"nonce\":\"\",\"challenge\":\"\",\"resource\":\"\"}" \
|
||||
"$CD/api/grant-consent" | sed -n 's/.*"data": *"\([^"]*\)".*/\1/p')
|
||||
[ -n "$CODE" ] && ck 0 "$U 拿到 authorization code" || { ck 1 "$U grant-consent"; return; }
|
||||
CB=$(curl -sk -b "$GJ" -D - -o /dev/null "$GW/api/user/oauth/callback?code=$CODE&state=$STATE")
|
||||
FINAL=$(echo "$CB" | grep -i '^location:' | tr -d '\r' | awk '{print $2}')
|
||||
echo "final: $FINAL"
|
||||
SSTOK=$(echo "$FINAL" | sed -n 's/.*sso_token=//p')
|
||||
[ -n "$SSTOK" ] && ck 0 "$U 拿到 sso_token" || { echo "$CB" | head -5; ck 1 "$U sso_token"; return; }
|
||||
ME2=$(curl -sk "$GW/api/user/me" -H "Authorization: Bearer $SSTOK")
|
||||
echo "$ME2"
|
||||
echo "$ME2" | grep -q "\"role\":\"$EXPECT\"" && ck 0 "$U 角色=$EXPECT" || ck 1 "$U 角色(期望 $EXPECT)"
|
||||
}
|
||||
|
||||
i=3
|
||||
for entry in ${SSO_TEST_USERS:-}; do
|
||||
U="${entry%%:*}"; rest="${entry#*:}"; P="${rest%%:*}"; EXPECT="${rest#*:}"
|
||||
echo "=== [$i] SSO $U → $EXPECT ==="
|
||||
sso_test "$U" "$P" "$EXPECT"
|
||||
i=$((i+1))
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "=== 汇总 fail=$fail ==="
|
||||
exit "$fail"
|
||||
Reference in New Issue
Block a user