From 61460ce32f7e24c416f31c95340078609a6c7c46 Mon Sep 17 00:00:00 2001 From: zjf <1799650030@qq.com> Date: Thu, 7 May 2026 22:05:18 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B5=99=E6=94=BF=E9=92=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pjcloud/thirdparty/dto/UrlTokenDTO.java | 7 + .../thirdparty/config/OAuth2ClientConfig.java | 5 + .../config/ZzdOpenPlatformProperties.java | 38 ++++ .../controller/ThirdPartyOAth2Cotroller.java | 9 +- .../service/ThirdPartyOAuth2Service.java | 196 +++++++++++++++++- .../src/main/resources/application.yml | 12 ++ 6 files changed, 259 insertions(+), 8 deletions(-) create mode 100644 pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/config/ZzdOpenPlatformProperties.java diff --git a/pjcloud-thirdparty/pjcloud-thirdparty-api/src/main/java/com/bsd/pjcloud/thirdparty/dto/UrlTokenDTO.java b/pjcloud-thirdparty/pjcloud-thirdparty-api/src/main/java/com/bsd/pjcloud/thirdparty/dto/UrlTokenDTO.java index 7e2c48744..eae2c2cc4 100644 --- a/pjcloud-thirdparty/pjcloud-thirdparty-api/src/main/java/com/bsd/pjcloud/thirdparty/dto/UrlTokenDTO.java +++ b/pjcloud-thirdparty/pjcloud-thirdparty-api/src/main/java/com/bsd/pjcloud/thirdparty/dto/UrlTokenDTO.java @@ -5,5 +5,12 @@ import lombok.Data; @Data public class UrlTokenDTO { + /** + * 旧数字政务/第三方网关下发的 uabaseToken(逐步淘汰) + */ private String token; + /** + * 浙政钉 H5 官方 dd.getAuthCode 返回的临时码 + */ + private String authCode; } diff --git a/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/config/OAuth2ClientConfig.java b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/config/OAuth2ClientConfig.java index d2e381b8e..51dbd6290 100644 --- a/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/config/OAuth2ClientConfig.java +++ b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/config/OAuth2ClientConfig.java @@ -38,5 +38,10 @@ public class OAuth2ClientConfig { */ private String zzdShUserInfoUrl; + /** + * 浙政钉官方 H5 authCode 换取用户信息(GET ?authCode=),响应体需与 {@code ApiResponse} 一致 + */ + private String zzdAuthCodeUserInfoUrl; + } diff --git a/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/config/ZzdOpenPlatformProperties.java b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/config/ZzdOpenPlatformProperties.java new file mode 100644 index 000000000..459b0901f --- /dev/null +++ b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/config/ZzdOpenPlatformProperties.java @@ -0,0 +1,38 @@ +package com.bsd.pjcloud.thirdparty.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +/** + * 浙政钉开放平台(正式:openplatform-pro.ding.zj.gov.cn)企业内应用免登。 + * app-key / app-secret 请放在 Nacos 或环境变量,勿提交仓库。 + */ +@Data +@Component +@ConfigurationProperties(prefix = "zzd.open") +public class ZzdOpenPlatformProperties { + + /** + * 为 false 时不走开放平台直连,仍使用 oauth2.client.zzd-auth-code-user-info-url 等旧网关配置 + */ + private boolean enabled = true; + + private String protocol = "https"; + + private String domain = "openplatform-pro.ding.zj.gov.cn"; + + /** + * 租户 ID(与管理工作台源码中 realmId、前端 corpId 一致,用于核对日志) + */ + private String tenantId = "196729"; + + private String appKey = ""; + + private String appSecret = ""; + + public boolean isAppCredentialConfigured() { + return StringUtils.hasText(appKey) && StringUtils.hasText(appSecret); + } +} diff --git a/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/controller/ThirdPartyOAth2Cotroller.java b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/controller/ThirdPartyOAth2Cotroller.java index 193d6a0ba..7f3879d33 100644 --- a/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/controller/ThirdPartyOAth2Cotroller.java +++ b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/controller/ThirdPartyOAth2Cotroller.java @@ -91,9 +91,10 @@ public class ThirdPartyOAth2Cotroller { @GlobalTransactional @Transactional(rollbackFor = Exception.class) public R zzdsso(@RequestBody UrlTokenDTO urlTokenDTO) { + String authCode = urlTokenDTO.getAuthCode(); String token = urlTokenDTO.getToken(); - if (StringUtils.isBlank(token)){ - return R.failed("token不能为空"); + if (StringUtils.isBlank(authCode) && StringUtils.isBlank(token)) { + return R.failed("token与authCode不能均为空"); } //获取数字政务token通过浙政钉前端获取的token,调试阶段先写死 @@ -123,7 +124,9 @@ public class ThirdPartyOAth2Cotroller { // throw new RuntimeException(e); // } //暂时先注释,模拟一下就行 - ZZDUserDTO zzdUserDTO = thirdPartyOAuth2Service.zzdsso(token); + ZZDUserDTO zzdUserDTO = StringUtils.isNotBlank(authCode) + ? thirdPartyOAuth2Service.zzdssoByAuthCode(authCode) + : thirdPartyOAuth2Service.zzdsso(token); ZZDUserDTO.Ext ext = zzdUserDTO.getExt(); String employeeCode = ext.getEmployeeCode(); String mobile = zzdUserDTO.getMobile(); diff --git a/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/service/ThirdPartyOAuth2Service.java b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/service/ThirdPartyOAuth2Service.java index 2930afad8..7e40f6d55 100644 --- a/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/service/ThirdPartyOAuth2Service.java +++ b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/service/ThirdPartyOAuth2Service.java @@ -3,7 +3,9 @@ package com.bsd.pjcloud.thirdparty.service; import com.alibaba.fastjson.JSON; import com.bsd.pjcloud.admin.api.entity.SysUser; import com.bsd.pjcloud.common.core.util.R; +import com.alibaba.fastjson.JSONObject; import com.bsd.pjcloud.thirdparty.config.OAuth2ClientConfig; +import com.bsd.pjcloud.thirdparty.config.ZzdOpenPlatformProperties; import com.bsd.pjcloud.thirdparty.constant.UserTypeEnum; import com.bsd.pjcloud.thirdparty.dto.*; import com.bsd.pjcloud.thirdparty.entity.ThirdPartyDeptEntity; @@ -27,6 +29,7 @@ import org.springframework.web.client.HttpStatusCodeException; import org.springframework.web.client.ResourceAccessException; import org.springframework.web.client.RestTemplate; +import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.*; import java.util.concurrent.ConcurrentHashMap; @@ -40,6 +43,11 @@ public class ThirdPartyOAuth2Service { private static final RestTemplate restTemplate = new RestTemplate(); + private final Object zzdOpenTokenLock = new Object(); + + private volatile String zzdOpenCachedAppAccessToken; + private volatile long zzdOpenCachedAppAccessTokenExpireMs; + @Autowired private OAuth2ClientConfig config; private final String baseUrl = "http://192.168.93.121:8880"; @@ -55,6 +63,8 @@ public class ThirdPartyOAuth2Service { private final ThirdPartyLogService thirdPartyLogService; + private final ZzdOpenPlatformProperties zzdOpenPlatform; + public TokenDTO getAccessToken() { TokenDTO cachedToken = tokenCache.get("access_token"); @@ -368,11 +378,173 @@ public class ThirdPartyOAuth2Service { //使用token获取当前登录人的信息(浙政钉和诗画的在一起) // ZZDUserDTO zzdUser = getZZDUserInfo(zzdToken); - ZZDUserDTO zzdUser = getZZDUserInfo(token,UserTypeEnum.ZZD.getType()); + ZZDUserDTO zzdUser = getZZDUserInfo(token, UserTypeEnum.ZZD.getType(), false); return zzdUser; } + public ZZDUserDTO zzdssoByAuthCode(String authCode) { + if (zzdOpenPlatform.isEnabled() && zzdOpenPlatform.isAppCredentialConfigured()) { + return getZzdUserByOpenPlatformAuthCode(authCode); + } + return getZZDUserInfo(authCode, UserTypeEnum.ZZD.getType(), true); + } + + private String zzdOpenBaseUrl() { + return zzdOpenPlatform.getProtocol() + "://" + zzdOpenPlatform.getDomain(); + } + + /** + * 开放平台文档:POST /gettoken.json,表单 appkey、appsecret;返回 content.data.accessToken + */ + private String getZzdOpenAppAccessToken() { + long now = System.currentTimeMillis(); + if (zzdOpenCachedAppAccessToken != null && now < zzdOpenCachedAppAccessTokenExpireMs - 60_000L) { + return zzdOpenCachedAppAccessToken; + } + synchronized (zzdOpenTokenLock) { + now = System.currentTimeMillis(); + if (zzdOpenCachedAppAccessToken != null && now < zzdOpenCachedAppAccessTokenExpireMs - 60_000L) { + return zzdOpenCachedAppAccessToken; + } + String url = zzdOpenBaseUrl() + "/gettoken.json"; + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("appkey", zzdOpenPlatform.getAppKey()); + body.add("appsecret", zzdOpenPlatform.getAppSecret()); + HttpEntity> entity = new HttpEntity<>(body, headers); + ResponseEntity response; + try { + response = restTemplate.postForEntity(url, entity, String.class); + } catch (HttpStatusCodeException e) { + log.error("浙政钉 gettoken HTTP异常 状态:{} 响应:{}", e.getRawStatusCode(), e.getResponseBodyAsString()); + throw new ThirdPartyException("浙政钉获取应用 access_token 失败", "ZZD_OPEN_TOKEN_HTTP"); + } catch (ResourceAccessException e) { + log.error("浙政钉 gettoken 网络异常", e); + throw new ThirdPartyException("浙政钉开放平台暂不可用", "ZZD_OPEN_TOKEN_NET"); + } + String respBody = response.getBody(); + JSONObject data = parseZzdOpenContentData(respBody, url); + String accessToken = data.getString("accessToken"); + if (!StringUtils.hasText(accessToken)) { + throw new ThirdPartyException("浙政钉 gettoken 未返回 accessToken", "ZZD_OPEN_TOKEN_BAD"); + } + int expiresIn = 7100; + try { + Object ex = data.get("expiresIn"); + if (ex != null) { + expiresIn = Integer.parseInt(ex.toString()); + } + } catch (NumberFormatException ignored) { + } + zzdOpenCachedAppAccessToken = accessToken; + zzdOpenCachedAppAccessTokenExpireMs = System.currentTimeMillis() + expiresIn * 1000L; + return accessToken; + } + } + + /** + * POST /rpc/oauth2/dingtalk_app_user.json,表单 access_token、auth_code + */ + private ZZDUserDTO getZzdUserByOpenPlatformAuthCode(String authCode) { + String appToken = getZzdOpenAppAccessToken(); + String url = zzdOpenBaseUrl() + "/rpc/oauth2/dingtalk_app_user.json"; + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("access_token", appToken); + body.add("auth_code", authCode); + HttpEntity> entity = new HttpEntity<>(body, headers); + ResponseEntity response; + try { + response = restTemplate.postForEntity(url, entity, String.class); + } catch (HttpStatusCodeException e) { + log.error("浙政钉 dingtalk_app_user HTTP异常 状态:{} 响应:{}", e.getRawStatusCode(), e.getResponseBodyAsString()); + throw new ThirdPartyException("浙政钉获取用户失败", "ZZD_OPEN_USER_HTTP"); + } catch (ResourceAccessException e) { + log.error("浙政钉 dingtalk_app_user 网络异常", e); + throw new ThirdPartyException("浙政钉开放平台暂不可用", "ZZD_OPEN_USER_NET"); + } + JSONObject data = parseZzdOpenContentData(response.getBody(), url); + ZZDUserDTO dto = mapOpenPlatformUserToZzdDto(data); + if (StringUtils.hasText(zzdOpenPlatform.getTenantId()) && dto.getExt() != null + && StringUtils.hasText(dto.getExt().getRealmId()) + && !zzdOpenPlatform.getTenantId().equals(dto.getExt().getRealmId())) { + log.warn("浙政钉用户 realmId={} 与配置 zzd.open.tenant-id={} 不一致", dto.getExt().getRealmId(), zzdOpenPlatform.getTenantId()); + } + return dto; + } + + private static JSONObject parseZzdOpenContentData(String body, String requestUrl) { + if (!StringUtils.hasText(body)) { + throw new ThirdPartyException("浙政钉接口空响应: " + requestUrl, "ZZD_OPEN_EMPTY"); + } + JSONObject root = JSON.parseObject(body); + if (root == null || !root.getBooleanValue("success")) { + throw new ThirdPartyException("浙政钉接口失败: " + body, "ZZD_OPEN_FAIL"); + } + JSONObject content = root.getJSONObject("content"); + if (content == null) { + throw new ThirdPartyException("浙政钉响应缺少 content: " + body, "ZZD_OPEN_BAD"); + } + if (!content.getBooleanValue("success")) { + throw new ThirdPartyException("浙政钉 content 失败: " + body, "ZZD_OPEN_FAIL"); + } + JSONObject data = content.getJSONObject("data"); + if (data == null) { + throw new ThirdPartyException("浙政钉响应缺少 data: " + body, "ZZD_OPEN_BAD"); + } + return data; + } + + private static ZZDUserDTO mapOpenPlatformUserToZzdDto(JSONObject data) { + ZZDUserDTO dto = new ZZDUserDTO(); + String lastName = data.getString("lastName"); + String nickCn = data.getString("nickNameCn"); + dto.setUsername(lastName != null ? lastName : nickCn); + dto.setNickName(lastName != null ? lastName : nickCn); + dto.setMobile(data.getString("mobile")); + dto.setRegionCode(null); + Object orgId = data.get("organizationId"); + if (orgId instanceof Number) { + dto.setOrganizationId(((Number) orgId).intValue()); + } else { + dto.setOrganizationId(0); + } + Object accountId = data.get("accountId"); + int userId = 0; + if (accountId instanceof Number) { + long lv = ((Number) accountId).longValue(); + if (lv <= Integer.MAX_VALUE && lv >= Integer.MIN_VALUE) { + userId = (int) lv; + } + } else if (accountId != null) { + try { + long lv = Long.parseLong(accountId.toString()); + if (lv <= Integer.MAX_VALUE && lv >= Integer.MIN_VALUE) { + userId = (int) lv; + } + } catch (NumberFormatException ignored) { + } + } + dto.setUserId(userId); + ZZDUserDTO.Ext ext = dto.new Ext(); + String employeeCode = data.getString("employeeCode"); + if (!StringUtils.hasText(employeeCode)) { + employeeCode = data.getString("account"); + } + ext.setEmployeeCode(employeeCode); + ext.setOrganizationCode(data.getString("organizationCode")); + ext.setNickNameCn(nickCn); + ext.setOpenid(data.getString("openid")); + Object realm = data.get("realmId"); + ext.setRealmId(realm == null ? null : realm.toString()); + ext.setMobile(data.getString("mobile")); + dto.setExt(ext); + return dto; + } + public String getZZDToken(String token) { String url = baseUrl+config.getZzdTokenUrl()+token; @@ -392,10 +564,24 @@ public class ThirdPartyOAuth2Service { * @return */ public ZZDUserDTO getZZDUserInfo(String zzdToken,String type) { + return getZZDUserInfo(zzdToken, type, false); + } + + /** + * @param zzdAuthCodeFlow true 时走 oauth2.client.zzd-auth-code-user-info-url,query 为 authCode + */ + public ZZDUserDTO getZZDUserInfo(String credential, String type, boolean zzdAuthCodeFlow) { // 构建请求URL try { - String url = baseUrl + config.getZzdShUserInfoUrl()+"?token=" + zzdToken; + String requestPath = zzdAuthCodeFlow ? config.getZzdAuthCodeUserInfoUrl() : config.getZzdShUserInfoUrl(); + if (zzdAuthCodeFlow && !StringUtils.hasText(requestPath)) { + throw new ThirdPartyException("未配置浙政钉 authCode 换取用户:请设置 zzd.open.app-key/app-secret(开放平台)或 oauth2.client.zzd-auth-code-user-info-url(网关)", "ZZD_AUTH_CODE_CONFIG"); + } + String param = zzdAuthCodeFlow ? "authCode" : "token"; + String url = zzdAuthCodeFlow + ? baseUrl + requestPath + "?" + param + "=" + URLEncoder.encode(credential, StandardCharsets.UTF_8) + : baseUrl + requestPath + "?" + param + "=" + credential; // 获取 Bearer Token String accessToken = getAccessToken().getAccess_token(); @@ -414,7 +600,7 @@ public class ThirdPartyOAuth2Service { if (response.getBody().getCode().equals("-1")){ log.error("获取浙政钉/诗画用户信息失败"); - log.error("token",zzdToken); + log.error("token",credential); log.error("response",JSON.toJSONString(response.getBody())); ThirdPartyLogEntity thirdPartyLogEntity = new ThirdPartyLogEntity(); if (type.equals(UserTypeEnum.ZZD.getType())){ @@ -423,8 +609,8 @@ public class ThirdPartyOAuth2Service { thirdPartyLogEntity.setApiName(UserTypeEnum.SH.getDescription()); } // thirdPartyLogEntity.setApiName("获取浙政钉/诗画用户信息失败"); - thirdPartyLogEntity.setRequestUrl( baseUrl + config.getZzdShUserInfoUrl()); - thirdPartyLogEntity.setRequestParameter(zzdToken); + thirdPartyLogEntity.setRequestUrl( baseUrl + requestPath); + thirdPartyLogEntity.setRequestParameter(credential); thirdPartyLogEntity.setResponseBody(JSON.toJSONString(response.getBody())); thirdPartyLogEntity.setErrorCode(response.getBody().getCode()); thirdPartyLogEntity.setErrorMsg(response.getBody().getDescription()); diff --git a/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/resources/application.yml b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/resources/application.yml index a1250c208..3ac1e85ff 100644 --- a/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/resources/application.yml +++ b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/resources/application.yml @@ -40,6 +40,18 @@ oauth2: shjn_token_url: /prod-api/login/shpj # 浙证钉和诗画公用的获取用户信息接口 zzd_sh_user_info_url: /prod-api/oauth2/apis/sso/v1/fetch_user_by_uabase_token + # 浙政钉官方免登:前端 authCode 换用户信息(按实际网关/开放平台对接后填写,留空则 authCode 登录会报错) + # zzd_auth_code_user_info_url: /prod-api/... + +# 浙政钉开放平台直连(配置 app-key/app-secret 后 authCode 走 gettoken + dingtalk_app_user,无需 zzd_auth_code_user_info_url) +zzd: + open: + enabled: true + protocol: https + domain: openplatform-pro.ding.zj.gov.cn + tenant-id: "196729" + app-key: ${ZZD_OPEN_APP_KEY:pjx_zhhqyth-wt2VrVa0olcqEBBJJb} + app-secret: ${ZZD_OPEN_APP_SECRET:rT5zbpHO0L7raRHJtvlIs26N8hn53JDv1K1SNa7X} # 正元智慧 # https://gwyc.zj.gov.cn/API/%E7%9C%81%E5%B9%B3%E5%8F%B0API%E5%AF%B9%E6%8E%A5%E6%8E%A5%E5%8F%A3.html