feat(auth): add DingTalk OAuth2 login support#467
Conversation
|
|
DingTalk (钉钉) uses a non-standard OAuth2 flow that requires: - JSON body for token exchange (instead of form-urlencoded) - Custom header (x-acs-dingtalk-access-token) for user info requests This PR integrates DingTalk by leveraging the existing OAuthClaimsExtractor strategy pattern, adding three provider-specific components: - DingTalkClaimsExtractor: maps DingTalk user fields to normalized OAuthClaims - DingTalkTokenResponseClient: handles DingTalk's JSON token exchange - DingTalkOAuth2UserService: fetches user info via DingTalk's custom header SecurityConfig uses delegating wrappers to route DingTalk requests to these custom components while preserving standard behavior for all other providers (GitHub, GitLab, OIDC). No changes needed to OAuthLoginFlowService, IdentityBindingService, AuthMethodCatalog, or frontend LoginButton — all are provider-agnostic.
…ess policy DingTalkOAuth2UserService was directly calling IdentityBindingService.bindOrCreate(), bypassing access policy evaluation. Refactored to delegate to OAuthLoginFlowService.authenticate() for consistent policy + binding, matching the pattern used by CustomOAuth2UserService. Also aligned the returned DefaultOAuth2User structure (providerLogin attribute key, authorities from platformRoles) with CustomOAuth2UserService so OAuth2LoginSuccessHandler works uniformly across all providers.
a1dd34c to
f7370c8
Compare
…ring Data JPA proxy Package-private JPA repository interfaces cannot be proxied by Spring Data's JpaRepositoryFactory when using Spring Boot devtools or certain class loader configurations, causing UnsatisfiedDependencyException at startup.
|
I have completed the CLA signing. Please re-run the CLA check, thanks. |
|
感谢 @konglong87 的贡献!钉钉登录集成对企业用户很有价值。整体实现思路清晰,正确处理了钉钉的非标准 OAuth2 流程(JSON token 交换 + 自定义 header)。 我们进行了多视角代码审查,发现了一些需要修复的问题。其中有 2 个阻断性问题必须在合并前解决: 🚨 必须修复1. 身份标识选择错误(Critical)问题: 修复:使用 // DingTalkClaimsExtractor.java
String unionId = (String) attrs.get("unionId");
String openId = (String) attrs.get("openId");
if (unionId == null || unionId.isEmpty()) {
throw new OAuth2AuthenticationException(
new OAuth2Error("missing_union_id",
"DingTalk response missing required unionId field", null));
}
return new OAuthClaims(
"dingtalk",
unionId, // 使用 unionId 而非 openId
syntheticEmail,
true,
nick,
attrs
);2. Scope 配置错误(Critical)问题: 修复: # application.yml
dingtalk:
scope:
- openid # 或 "openid corpid" 如果需要企业信息同步修改
|
1. 身份标识: openId → unionId (跨应用唯一) - DingTalkClaimsExtractor: subject使用unionId, 新增null校验 - DingTalkOAuth2UserService: nameAttribute改为unionId - application.yml: user-name-attribute改为unionId 2. Scope配置: dingtalk → openid (钉钉OAuth2正确scope) - application.yml: scope改为openid - .env.release.example: 新增scope说明注释 3. RestTemplate超时配置 (5s connect + 10s read) - DingTalkTokenResponseClient: 防止无限阻塞 - DingTalkOAuth2UserService: 同步配置 4. NPE风险修复 - DingTalkTokenResponseClient: JsonNode null检查 5. 敏感信息泄露修复 - DingTalkTokenResponseClient: 移除raw_response, 只保留expireIn 6. 硬编码URL修复 - DingTalkOAuth2UserService: userInfoUri从ClientRegistration配置读取 7. 单元测试覆盖 (12个测试全部通过) - DingTalkClaimsExtractorTest: 4个 - DingTalkTokenResponseClientTest: 6个 - DingTalkOAuth2UserServiceTest: 2个
|
already fix,please check it.
|
- Change SecurityConfig to @configuration(proxyBeanMethods=false) to avoid CGLIB proxy issues with constructor-injected beans - Add @Autowired to DingTalkOAuth2UserService public constructor so Spring resolves the correct constructor when multiple constructors exist - Change DingTalk scope from openid to corpid: DingTalk does not return id_token in its token response, so openid scope causes Spring Security to fail with invalid_id_token error. corpid scope works correctly with DingTalk's authorization endpoint - Add error logging to OAuth2LoginFailureHandler for easier debugging - Add DingTalk client-id/client-secret env vars to application-local.yml
|
感谢 review!: 阻断性问题:
强烈建议项: 其他建议: |
Add DingTalk (钉钉) OAuth2 login support to SkillHub.
DingTalk uses a non-standard OAuth2 flow:
This PR leverages the existing OAuthClaimsExtractor strategy pattern, adding three provider-specific components:
┌─────────────────────────────┬────────────────────────────────────────────────────────────────────────┐
│ New file │ Purpose │
├─────────────────────────────┼────────────────────────────────────────────────────────────────────────┤
│ DingTalkClaimsExtractor │ Maps DingTalk fields (openId, nick, unionId) to normalized OAuthClaims │
├─────────────────────────────┼────────────────────────────────────────────────────────────────────────┤
│ DingTalkTokenResponseClient │ Handles DingTalk JSON token exchange │
├─────────────────────────────┼────────────────────────────────────────────────────────────────────────┤
│ DingTalkOAuth2UserService │ Fetches user info via DingTalk custom header │
└─────────────────────────────┴────────────────────────────────────────────────────────────────────────┘
SecurityConfig uses delegating wrappers to route registrationId=dingtalk to these custom components, preserving standard behavior for GitHub, GitLab, and OIDC.
Files changed (7 total)
New (4):
Modified (3):
Zero-change components
No changes needed to:
Configuration
Set these environment variables to enable DingTalk login:
OAUTH2_DINGTALK_CLIENT_ID=
OAUTH2_DINGTALK_CLIENT_SECRET=
Register your app at https://open-dev.dingtalk.com/ and request the Contact.User.Read scope.
Test plan