浙政钉

This commit is contained in:
zjf committed 2026-05-07 22:05:18 +08:00
1 parent 4e5afdc272
commit 61460ce32f
6 files changed
+259 -8

No files matched your search

@@ -5,5 +5,12 @@ import lombok.Data;
@Data
public class UrlTokenDTO {
/**
* 旧数字政务/第三方网关下发的 uabaseToken(逐步淘汰)
*/
private String token;
/**
* 浙政钉 H5 官方 dd.getAuthCode 返回的临时码
*/
private String authCode;
}
@@ -38,5 +38,10 @@ public class OAuth2ClientConfig {
*/
private String zzdShUserInfoUrl;
/**
* 浙政钉官方 H5 authCode 换取用户信息(GET ?authCode=),响应体需与 {@code ApiResponse<ZZDUserDTO>} 一致
*/
private String zzdAuthCodeUserInfoUrl;
}
@@ -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);
}
}
@@ -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();
@@ -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<String, String> body = new LinkedMultiValueMap<>();
body.add("appkey", zzdOpenPlatform.getAppKey());
body.add("appsecret", zzdOpenPlatform.getAppSecret());
HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(body, headers);
ResponseEntity<String> 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<String, String> body = new LinkedMultiValueMap<>();
body.add("access_token", appToken);
body.add("auth_code", authCode);
HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(body, headers);
ResponseEntity<String> 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());
@@ -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