--- name: kdshrsso description: >- Design and implement Kingdee EAS/SHR single sign-on. Patterns: (A) override platform CAS/OAuth filters; (B) Servlet + LtpaToken (OAToSHR / IDM); (B-qy) Weaver E10 or DingTalk OAuth in qy.war then POST index2sso. Covers eassso/login_withForm.js, OAToSHR, CallbackSuffixCompatFilter, Apusic *.do callback 404, LTPA Location multi-encode fix, index2sso vs OTP2sso, logoutHR.js federated Weaver logout + service whitelist, AttackFilter CSRF/Referer, adSsoConfig, OASSOConfig, KDPortalAuthenticationFilter, WeaverAuthController vs UnifiedAuth. BEFORE coding: ask intake questions in this skill (IdP, pattern A/B/B-qy, env, mapping, logout). When user says 上线/切生产/go-live: follow Go-live section; list files to change; confirm prod URLs before editing. --- # Kingdee SSO — Implementation Patterns Pick **one primary pattern** per SHR SSO integration. Do not mix servlet Ltpa login with filter `_const_cas_assertion_` on the same entry without explicit design. --- ## Agent intake — ask before coding (mandatory) When the user asks to implement or port SSO for a **new project / new IdP**, do **not** jump straight into code. First confirm goals with short questions (batch them). Mark unknowns; block only on items that change architecture. ### Must confirm (block if unanswered) 1. **IdP type**: 泛微 E10 / 钉钉 / 飞书 / 企业 IDM / Azure AD / other?有无接口或对接文档? 2. **Pattern**: A(全站 `/shr` CAS 过滤)/ B(`/shr/api/oAToSHR`)/ **B-qy**(`qy.war` `/qy/auth/{platform}`)?不确定则按 IdP 用本 skill Quick decision 推荐后让用户确认。 3. **SHR / EAS 环境**: `shrServerName`(协议+主机+端口)、是否 Apusic、是否已有或可部署 **qy.war**? 4. **人员映射**: IdP 用户字段 ↔ sHR 哪一列(`PERSON_NUMBER` / `cfidmusercode` / 手机号 / …)? 5. **callbackUrl**: 拟注册的完整回调(**通常无 `.do`**);与现网 host 是否一致? 6. **门户入口**: 是否改 `eassso` + `login_withForm.js`?`/shr/home.do` 是否保留本地登录? 7. **联邦退出**: 退出后是否必须回 IdP 登录页再进 sHR?IdP 是否有 **service/redirect 白名单**? ### Should confirm (default OK if user says「按 BaoYPro」) 8. 桌面进 sHR:默认 **HTML POST `index2sso`**(禁止 LTPA 进 Location)。 9. PC 深链:默认 **显式** `/qy/auth/{platform}/app`,不用 UA 版 Unified `/auth/app`。 10. 配置:`INTEGRATION_CONFIG_PATH` + `integration-config-*.json`;密钥不进 git。 11. 与待办是否同期做?若是,一并加载 **kdshrpmsg** 并做其 intake(深链入口与 SSO 一致)。 ### Output before coding Reply with a short **目标确认表**(模式、入口 URL、callback、映射、退出、是否动门户 JS),等用户点头或补答案后再改代码。若用户已贴齐文档+明确「按 B-qy / 按报业」,可只复述假设请一次确认。 --- ## Go-live / 切生产 (mandatory when user says 上线) For **BaoYPro-style B-qy**, do not silently rewrite files. First ask for production SHR URL, ecology URL, appId/secret, dataCenter. Then list and edit: | # | File / config | Change | |---|---------------|--------| | 1 | `set-server-env.sh` (or equiv.) | `ENV=prod` → `INTEGRATION_CONFIG_PATH=.../integration-config-prod.json`;重启 | | 2 | `integration-config-prod.json` | `shrServerName`、`dataCenter`、`allowedPrefixes`、`appId`/`appSecret`、`callbackUrl`、`weaver.ecologyUrl` | | 3 | `login_withForm.js` | `ENV_CONFIG.prod.*` + `getCurrentEnv()` 识别生产 Host | | 4 | `logoutHR.js` | `prod.shrBaseUrl` / `weaverAppId`;`currentEnv='prod'`(或按 host) | | 5 | 泛微后台 | 生产 callback 注册;**退出 service 白名单** = 生产 SHR 根地址 | Also remind **kdshrpmsg** go-live for `weaverTodoConfig.properties`. Full checklist: project `docs/integration/go-live-prod.html`. 验收:生产门户登录、退出后再进 SHR、地址栏无测试 IP。 --- ## Quick decision ``` Need SSO? │ ├─ IdP is Azure AD / standard OIDC (POST token + userinfo)? │ └─ Option A (Filter overlay) — see azure-oauth-reference.md │ ├─ IdP is enterprise IDM, portal/OA deep-links with todo URLs? │ ├─ Already on YHPro-style OAToSHR + index2sso? → stay Option B │ └─ Want platform-native SHR session, all /shr URLs auto-OAuth? │ └─ Option A with IDM TicketValidator — see idm-filter-reference.md │ ├─ IdP is 泛微 E10 (or DingTalk) OAuth code flow into sHR? │ └─ Option B-qy: qy.war /qy/auth/{platform}/app + POST index2sso │ — prefer explicit /weaver or /dingtalk; avoid UA-only /auth/app for PC deep links │ — see weaver-oauth-qywar-reference.md │ └─ Entry from EAS portal eassso/login?service=... ? ├─ Option A: portal links to SHR URL directly, or JS forwards service URL ├─ Option B: JS/servlet bridge to /shr/api/oAToSHR └─ Option B-qy: JS → /qy/auth/weaver/app.do?appId&redirect= (BaoYPro) ``` | | **Option A: Filter overlay** | **Option B: Servlet + LtpaToken** | **Option B-qy: qy.war + index2sso** | |--|------------------------------|-----------------------------------|-------------------------------------| | Entry | `/shr?adsso` or any SHR URL via CAS | `/shr/api/oAToSHR` (or portal JS) | `/qy/auth/weaver/app` (or `/dingtalk/...`) | | Callback | `{host}/shr?code=` | `{host}/shr/api/oAToSHR?code=` | `{host}/qy/auth/callback/{platform}/{appType}` | | Session | `_const_cas_assertion_` | `index2sso.jsp` + Ltpa | Same as B: `index2sso.jsp` + Ltpa | | Config | `adSsoConfig.properties` | `OASSOConfig.properties` | `integration-config*.json` + `INTEGRATION_CONFIG_PATH` | | Deploy | Overlay platform filters | Servlet in shr war | `lib/sp` jar + `qy.war` | | Best for | Seamless `/shr` browse; Azure/OIDC | IDM + OAToSHR | 泛微 E10 / 钉钉 via custom integration | **Not covered:** JSP-only redirect, `ActiveDirAuthHandler` (Kerberos), product default CAS ticket-only login. --- ## End-to-end flows (compare) ### Option A ``` Browser → /shr/... (or /shr?adsso) → KDPortalAuthenticationFilter → 302 IdP authorize → callback /shr?code= → KDPortalTicketValidationFilter → TicketValidator.validate() → _const_cas_assertion_ → onSuccessfulValidation → target URL ``` ### Option B (YHPro / IDM) ``` Portal → eassso/login?service=SHR_URL [optional JS bridge] → /shr/api/oAToSHR?redirect=|service=SHR_URL → IDM authorize (state = encoded target) → callback /shr/api/oAToSHR?code=&state= → index2sso.jsp?username&password(Ltpa)&redirectTo → SHR session → target URL ``` ### Option B-qy (BaoYPro / Weaver E10) ``` Portal → eassso/login?service=SHR_URL → login_withForm.js → /qy/auth/weaver/app.do?appId&redirect= → 泛微 authorize → /qy/auth/callback/weaver/pa → Filter forward → pa.do → token/profile → sHR user → WeaverShrLanding HTML POST → /shr/index2sso.jsp → SHR session → redirectTo ``` Logout (federated): `logoutHR.js` → `/qy/auth/weaver/logout.do?appId&redirect=` → E10 logout `service` = SSO entry URL; **Weaver 退出白名单 must include SHR host**. Full detail: [weaver-oauth-qywar-reference.md](weaver-oauth-qywar-reference.md) --- # Option A — Platform Class Overlay ## When to use | Scenario | Fit | |----------|-----| | Azure AD / Office 365 OAuth2 | Yes — use `OAuth20CodeValidata` | | Enterprise IDM on all `/shr` URLs | Yes — custom `IdmCodeValidata` ([idm-filter-reference.md](idm-filter-reference.md)) | | EAS CAS username/password only | No | | Kerberos/AD domain | No — `ActiveDirAuthHandler` | ## Architecture Platform: `SHRTicketValidationFilter extends KDPortalTicketValidationFilter`. Override hooks: - `retrieveTicketFromRequest()` — treat OAuth `code` as ticket - `getTicketValidator()` / `getTicketValidatorToSso()` — return custom `TicketValidator` - `onSuccessfulValidation()` — user check, deep-link redirect via `urlToMap` ## Checklist ``` - [ ] IdP URLs; client_id; redirect_uri = https://{host}/shr (not servlet path) - [ ] adSsoConfig.properties + ConfigAddressUtil - [ ] KDPortalAuthenticationFilter — authorize redirect; whitelist local login paths - [ ] KDPortalTicketValidationFilter — code branch - [ ] TicketValidator (OAuth20CodeValidata or IdmCodeValidata) - [ ] onSuccessfulValidation — T_PM_USER check; urlToMap / state redirect - [ ] Portal: direct SHR links or login button → /shr?adsso ([portal-login-reference.md](portal-login-reference.md)) - [ ] web.xml: Authentication Filter before Validation Filter - [ ] Overlay deploy; restart; verify _const_cas_assertion_ in session ``` ## Config template (Azure/OIDC) Path: `{eas.properties.dir}/adSsoConfig.properties` ```properties authUrl=https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize tokenUrl=https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token client_id= client_secret= shrUrl=https://{host}/shr userUrl=https://graph.microsoft.com/v1.0/me dataCenter= locale=L2 mobileEid= ``` ## Troubleshooting | Symptom | Check | |---------|--------| | No redirect to IdP | Filter deployed; `flag` whitelist; existing assertion | | Callback fails | Filter order; `code` in `retrieveTicketFromRequest` | | User not found | Mapping in TicketValidator; `T_PM_USER.FNUMBER` | | Portal still shows login form | eassso bridge not forwarding to SHR — see portal doc | | Deep link lost | `urlToMap` / `state` set before authorize redirect | Reference: [azure-oauth-reference.md](azure-oauth-reference.md), [idm-filter-reference.md](idm-filter-reference.md) --- # Option B — Servlet + LtpaToken (Portal / IDM) ## When to use | Scenario | Fit | |----------|-----| | Portal/OA todo deep links (`?redirect=`) | Yes | | IDM OAuth with servlet callback | Yes | | Federated logout JSON (`idmLogoutUrl`) | Yes | | All `/shr` browse should auto-OAuth without servlet | No — use Option A | | Azure AD on main `/shr` URL | No — use Option A | ## Checklist ``` - [ ] OASSOConfig.properties (authorizePath, getAccessTokenPath, ssoUrl, idmBaseUrl, serverName) - [ ] OAToSHR: authorize() + callBack() + generateShrLoginUrl() - [ ] SsoUtil: config, getAccessToken (GET IDM), checkUserExist (cfidmusercode) - [ ] SHRToIDMLogout + logoutHR.js (if IDM federated logout required) - [ ] web.xml servlets; cas.client.UrlPattern excludes servlet + index2sso.jsp - [ ] IDM redirect_uri = ssoUrl (/shr/api/oAToSHR) - [ ] Portal link or login_withForm.js bridge - [ ] person.cfidmusercode populated for all SSO users ``` ## Config template (IDM) Path: `{EAS_HOME}/server/properties/{customer}/OASSOConfig.properties` ```properties authorizePath=https://idm.example.com/oauth2/authorize getAccessTokenPath=https://idm.example.com/token/codeInfo client_id=shr client_secret= response_type=code ssoUrl=https://shr.example.com/shr/api/oAToSHR serverName=https://shr.example.com idmBaseUrl=https://idm.example.com ``` ## Dynamic redirect (`state`) `state` is **dynamic per request**, not fixed: 1. `redirect` param (preferred) → URL-encoded into OAuth `state` 2. fallback `service` param (CAS-style, used by eassso bridge) 3. default homepage if both absent Callback decodes `state` → `index2sso.jsp` `redirectTo`. ## Troubleshooting | Symptom | Check | |---------|--------| | Servlet 500 on init | OASSOConfig path; `ssoUrl` key present (not only redirect_uri1/2) | | Token exchange fails | GET vs POST API; redirect_uri match | | User not found | person.cfidmusercode vs IDM user_info.id | | Redirect loop | UrlPattern excludes /api/oAToSHR, /index2sso.jsp | | Logout incomplete | Frontend follows idmLogoutUrl | Reference: [idm-servlet-reference.md](idm-servlet-reference.md) --- # Option B-qy — Weaver E10 / DingTalk via qy.war (summary) Session = Option B (`index2sso` + LTPA). OAuth runs in **qy.war** under `/qy/auth/*`. BaoYPro prefers **explicit** `/qy/auth/weaver/app(.do)` (not UA-based Unified `/auth/app`). ## Checklist ``` - [ ] integration jar in lib/sp; qy.war in easweb.ear + Filter + Spring config - [ ] integration-config: callbackUrl == IdP register (no .do); ecologyUrl / appId / secret - [ ] CallbackSuffixCompatFilter + Controller *.do mappings + suffix-pattern=false - [ ] Landing: WeaverShrLanding / writeShrSsoLoginForm POST to index2sso (never LTPA in Location) - [ ] Portal login_withForm.js → /qy/auth/weaver/app.do?appId&redirect= - [ ] logoutHR.js → /qy/auth/weaver/logout.do?appId&redirect= + Weaver service whitelist - [ ] AttackFilter: allow /qy/ (or accept CSRF risk) if external Referer blocks /qy - [ ] Prefer index2sso.jsp over OTP2sso.jsp for OAuth LTPA ``` ## What actually fixed “must copy-paste URL to login” | Problem | Root cause | Fix | |---------|------------|-----| | Callback `/pa` 404 | Apusic needs `*.do` for external GET | Filter forward to `.do`; map both paths | | Paste address bar then login works | LTPA in Location multi-encoded (`%2525252B`) | Callback HTML **POST** form to index2sso — not GET Location | | Logout then stay in Weaver | `service` not on Weaver whitelist / wrong service | Whitelist SHR host; service = SSO entry URL | | External Referer → `/qy` 404 | Apusic AttackFilter CSRF | Whitelist `/qy/` + Weaver host | Reference: [weaver-oauth-qywar-reference.md](weaver-oauth-qywar-reference.md) --- # Portal bridge (eassso/login) EAS portal sends users to `eassso/login?service={target}`. Patterns differ: | Pattern | Portal JS behavior | Who does OAuth | |---------|-------------------|----------------| | **A — direct** | `location.replace(serviceParam)` → SHR URL | SHR CAS filters | | **A — adsso button** | Link to `/shr?adsso` | SHR CAS filters | | **B — servlet bridge** | `location.replace(oaSsoUrl + '?redirect=' + encodeURIComponent(target))` | OAToSHR servlet | | **B-qy** | `/qy/auth/weaver/app.do?appId=&redirect=` | qy WeaverAuthController | Common whitelist: keep `/shr/home.do` on native eassso login for emergency local accounts. **Anti-patterns:** - Hardcoding env URLs in JS — inject from server config/JSP when possible - Editing only `runtime/.../sso_web.war` without src overlay — lost on redeploy - `needRedirect` when `service` empty — may force SSO on bare eassso/login - Mixing Option A filters with Option B OAToSHR on same entry - Putting LTPA `password` in 302 Location on Apusic (multi-encode) Full detail: [portal-login-reference.md](portal-login-reference.md), [weaver-oauth-qywar-reference.md](weaver-oauth-qywar-reference.md) --- # Logout | Pattern | Mechanism | |---------|-----------| | Option A | Platform `/eassso/logout` chain; extend for IdP if needed | | Option B (IDM) | `SHRToIDMLogout` → JSON `{ idmLogoutUrl }`; `logoutHR.js` redirects browser | | Option B-qy (Weaver / BaoYPro) | `logoutHR.js` → `/qy/auth/weaver/logout.do?appId&redirect=` → E10 `/sp/integration/oauth2.0/logout?service=` where **service = SHR SSO entry**; Weaver **退出白名单** must include SHR host | | IDM spec | `{idmBaseUrl}/token/code-logout?redirect_url={encoded authorize URL}` | Keep logout URL config in **one place** where possible. BaoYPro: JS holds test/prod `weaverAppId` + `shrBaseUrl`; server builds SSO entry as `service` and HTML-encodes once (`WeaverShrLanding.writeLogoutThenService`). If address bar after logout shows `service=http://{ecology}/`, whitelist failed or old package — user will stay in Weaver after login. --- # Option A ↔ Option B migration | From → To | Key changes | |-----------|-------------| | **B → A** | IDM redirect_uri: `/api/oAToSHR` → `/shr`; implement IdmCodeValidata; remove OAToSHR from login path; portal JS forwards to SHR URL not servlet; replace index2sso with assertion session | | **A → B** | Register OAToSHR; exclude paths in UrlPattern; IDM redirect_uri → servlet; portal JS → OAToSHR; add SHRToIDMLogout | | **B ↔ B-qy** | Different OAuth host (IDM servlet vs qy.war); same index2sso landing; portal JS target and IdP redirect_uri must match chosen pattern | Can run **dual entry** during migration if IdP registers two redirect_uri values; avoid long-term dual login paths. Detail: [migration-guide.md](migration-guide.md) --- # Config field mapping (IDM) | OASSOConfig (Option B) | adSsoConfig (Option A) | Notes | |------------------------|------------------------|-------| | authorizePath | authUrl | IDM authorize endpoint | | getAccessTokenPath | tokenUrl | B: GET query; A: may need IdmCodeValidata | | ssoUrl | shrUrl + redirect_uri | B uses servlet path; A uses `/shr` | | serverName | shrUrl host | SHR base URL | | idmBaseUrl | (logout only) | code-logout endpoint | | client_id / client_secret | same | | | — | userUrl | Not needed if token response includes user_info | B-qy uses `integration-config*.json` (`callbackUrl`, `ecologyUrl`, `appId`) instead of OASSOConfig — see weaver reference. --- # Search terms ``` # Option A KDPortalAuthenticationFilter, KDPortalTicketValidationFilter, OAuth20CodeValidata ConfigAddressUtil, adSsoConfig, adsso, SHRTicketValidationFilter # Option B OAToSHR, SsoUtil, SHRToIDMLogout, OASSOConfig, index2sso.jsp LtpaTokenManager, cfidmusercode # Option B-qy WeaverAuthController, WeaverShrLanding, CallbackSuffixCompatFilter /qy/auth/weaver/app, /qy/auth/weaver/logout, /qy/auth/callback/weaver integration-config, ecologyUrl, AttackFilter, serverNameWhiteList logoutHR.js, writeLogoutThenService, index2sso.jsp OTP2sso.jsp (do not use for Weaver OAuth LTPA) # Portal login_withForm.js, login_withForm.jsp, eassso/login, service, logoutHR.js ssoParamemter.properties, cas.client.UrlPattern ``` --- # References | Doc | Content | |-----|---------| | [azure-oauth-reference.md](azure-oauth-reference.md) | Option A — Azure AD / GYPro | | [idm-filter-reference.md](idm-filter-reference.md) | Option A — IDM TicketValidator | | [idm-servlet-reference.md](idm-servlet-reference.md) | Option B — OAToSHR / YHPro | | [weaver-oauth-qywar-reference.md](weaver-oauth-qywar-reference.md) | Option B-qy — Weaver E10 / qy.war + POST index2sso | | [portal-login-reference.md](portal-login-reference.md) | eassso/login_withForm.js bridge | | [migration-guide.md](migration-guide.md) | A ↔ B migration steps |