浙政钉

This commit is contained in:
zjf committed 2026-05-07 23:38:26 +08:00
1 parent 1666184356
commit a0be0ebc0c
8 files changed
+255 -74

No files matched your search

+14
View File
@@ -155,6 +155,19 @@
<version>5.7.0</version>
<scope>compile</scope>
</dependency>
<!-- 政务钉钉开放平台 SDK(本地 jar:src/lib/zwdd-sdk-java-1.2.0.jar) -->
<dependency>
<groupId>com.alibaba.xxpt</groupId>
<artifactId>zwdd-sdk-java</artifactId>
<version>1.2.0</version>
<scope>system</scope>
<systemPath>${project.basedir}/src/lib/zwdd-sdk-java-1.2.0.jar</systemPath>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.10</version>
</dependency>
</dependencies>
<profiles>
<profile>
@@ -190,6 +203,7 @@
</goals>
<configuration>
<loaderImplementation>CLASSIC</loaderImplementation>
<includeSystemScope>true</includeSystemScope>
</configuration>
</execution>
</executions>
@@ -0,0 +1,41 @@
package com.bsd.pjcloud.thirdparty.config;
import com.alibaba.xxpt.gateway.shared.client.http.ExecutableClient;
import com.bsd.pjcloud.thirdparty.exception.ThirdPartyException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
/**
* 政务钉钉开放平台 {@link ExecutableClient} 单例初始化(与官方 SDK 文档一致,全局只 init 一次)。
*/
@Component
@RequiredArgsConstructor
public class ZzdOpenPlatformExecutableClientHolder {
private final ZzdOpenPlatformProperties zzdOpenPlatform;
private final Object initLock = new Object();
private volatile boolean initialized;
public ExecutableClient client() {
if (!zzdOpenPlatform.isEnabled() || !zzdOpenPlatform.isAppCredentialConfigured()) {
throw new ThirdPartyException("浙政钉开放平台未启用或未配置 appKey/appSecret", "ZZD_OPEN_SDK_CFG");
}
if (!initialized) {
synchronized (initLock) {
if (!initialized) {
ExecutableClient c = ExecutableClient.getInstance();
c.setDomainName(zzdOpenPlatform.getDomain());
c.setProtocal(zzdOpenPlatform.getProtocol());
c.setAccessKey(zzdOpenPlatform.getAppKey());
c.setSecretKey(zzdOpenPlatform.getAppSecret());
c.setValidateAfterInactivity(60_000);
c.init();
initialized = true;
}
}
}
return ExecutableClient.getInstance();
}
}
@@ -32,6 +32,11 @@ public class ZzdOpenPlatformProperties {
private String appSecret = "";
/**
* H5 微应用 JSAPI 鉴权用 AgentId(管理后台应用详情,非 appKey)
*/
private String jsapiAgentId = "";
public boolean isAppCredentialConfigured() {
return StringUtils.hasText(appKey) && StringUtils.hasText(appSecret);
}
@@ -17,6 +17,8 @@ import com.bsd.pjcloud.common.log.annotation.SysLog;
import com.bsd.pjcloud.common.security.annotation.Inner;
import com.bsd.pjcloud.thirdparty.dto.*;
import com.bsd.pjcloud.thirdparty.service.SmsService;
import com.bsd.pjcloud.thirdparty.exception.ThirdPartyException;
import com.bsd.pjcloud.thirdparty.vo.ZzdJsapiConfigVO;
import com.bsd.pjcloud.thirdparty.service.ThirdPartyOAuth2Service;
import com.bsd.pjcloud.thirdparty.service.ThirdPartyUserService;
import com.bsd.pjcloud.thirdparty.utils.SmsCodeUtil;
@@ -87,6 +89,17 @@ public class ThirdPartyOAth2Cotroller {
* @param token 浙政钉前端获取的token
* @return
*/
@GetMapping("/zzdJsapiConfig")
@Operation(summary = "政务钉钉H5 JSAPI鉴权(dd.authConfig 所需 ticket)")
@Inner(value = false)
public R<ZzdJsapiConfigVO> zzdJsapiConfig() {
try {
return R.ok(thirdPartyOAuth2Service.buildZzdJsapiConfigForH5());
} catch (ThirdPartyException e) {
return R.failed(e.getMessage());
}
}
@PostMapping("/zzdsso")
@Operation(summary = "浙政钉单点登录")
@SysLog("浙政钉单点登录")
@@ -4,7 +4,19 @@ 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.alibaba.xxpt.gateway.shared.api.request.OapiGetJsapiTokenJsonRequest;
import com.alibaba.xxpt.gateway.shared.api.request.OapiGettokenJsonRequest;
import com.alibaba.xxpt.gateway.shared.api.request.OapiRpcOauth2DingtalkAppUserJsonRequest;
import com.alibaba.xxpt.gateway.shared.api.response.OapiGetJsapiTokenJsonResponse;
import com.alibaba.xxpt.gateway.shared.api.response.OapiGettokenJsonResponse;
import com.alibaba.xxpt.gateway.shared.api.response.OapiRpcOauth2DingtalkAppUserJsonResponse;
import com.alibaba.xxpt.gateway.shared.client.http.ExecutableClient;
import com.alibaba.xxpt.gateway.shared.client.http.IntelligentGetClient;
import com.alibaba.xxpt.gateway.shared.client.http.IntelligentPostClient;
import com.alibaba.xxpt.gateway.shared.client.http.api.OapiResponse;
import com.alibaba.xxpt.gateway.shared.client.http.api.OapiSpResultContent;
import com.bsd.pjcloud.thirdparty.config.OAuth2ClientConfig;
import com.bsd.pjcloud.thirdparty.config.ZzdOpenPlatformExecutableClientHolder;
import com.bsd.pjcloud.thirdparty.config.ZzdOpenPlatformProperties;
import com.bsd.pjcloud.thirdparty.constant.UserTypeEnum;
import com.bsd.pjcloud.thirdparty.dto.*;
@@ -12,6 +24,7 @@ import com.bsd.pjcloud.thirdparty.entity.ThirdPartyDeptEntity;
import com.bsd.pjcloud.thirdparty.entity.ThirdPartyLogEntity;
import com.bsd.pjcloud.thirdparty.entity.ThirdPartyUserEntity;
import com.bsd.pjcloud.thirdparty.exception.ThirdPartyException;
import com.bsd.pjcloud.thirdparty.vo.ZzdJsapiConfigVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
@@ -48,6 +61,12 @@ public class ThirdPartyOAuth2Service {
private volatile String zzdOpenCachedAppAccessToken;
private volatile long zzdOpenCachedAppAccessTokenExpireMs;
private final Object zzdJsapiTicketLock = new Object();
private volatile String zzdOpenCachedJsapiTicket;
private volatile long zzdOpenCachedJsapiTicketExpireMs;
@Autowired
private OAuth2ClientConfig config;
private final String baseUrl = "http://192.168.93.121:8880";
@@ -65,6 +84,8 @@ public class ThirdPartyOAuth2Service {
private final ZzdOpenPlatformProperties zzdOpenPlatform;
private final ZzdOpenPlatformExecutableClientHolder zzdExecutableClientHolder;
public TokenDTO getAccessToken() {
TokenDTO cachedToken = tokenCache.get("access_token");
@@ -390,12 +411,8 @@ public class ThirdPartyOAuth2Service {
return getZZDUserInfo(authCode, UserTypeEnum.ZZD.getType(), true);
}
private String zzdOpenBaseUrl() {
return zzdOpenPlatform.getProtocol() + "://" + zzdOpenPlatform.getDomain();
}
/**
* 开放平台文档:POST /gettoken.json,表单 appkey、appsecret;返回 content.data.accessToken
* 开放平台:官方 SDK {@code IntelligentGetClient /gettoken.json},解析 content.data 中 accessToken
*/
private String getZzdOpenAppAccessToken() {
long now = System.currentTimeMillis();
@@ -407,97 +424,111 @@ public class ThirdPartyOAuth2Service {
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());
ExecutableClient ec = zzdExecutableClientHolder.client();
IntelligentGetClient getClient = ec.newIntelligentGetClient("/gettoken.json");
OapiGettokenJsonRequest req = new OapiGettokenJsonRequest();
req.setAppkey(zzdOpenPlatform.getAppKey());
req.setAppsecret(zzdOpenPlatform.getAppSecret());
OapiGettokenJsonResponse resp = getClient.get(req);
zzdAssertSdkTopSuccess(resp, "gettoken");
JSONObject data = zzdSdkSpResultData(resp.getContent(), "gettoken");
String accessToken = data.getString("accessToken");
if (!StringUtils.hasText(accessToken)) {
throw new ThirdPartyException("浙政钉 gettoken 未返回 accessToken", "ZZD_OPEN_TOKEN_BAD");
}
} catch (NumberFormatException ignored) {
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;
} catch (ThirdPartyException e) {
throw e;
} catch (Exception e) {
log.error("浙政钉 gettoken SDK 调用异常", e);
throw new ThirdPartyException("浙政钉获取应用 access_token 失败", "ZZD_OPEN_TOKEN_SDK");
}
zzdOpenCachedAppAccessToken = accessToken;
zzdOpenCachedAppAccessTokenExpireMs = System.currentTimeMillis() + expiresIn * 1000L;
return accessToken;
}
}
/**
* POST /rpc/oauth2/dingtalk_app_user.json,表单 access_token、auth_code
* 官方 SDK {@code IntelligentPostClient /rpc/oauth2/dingtalk_app_user.json}
*/
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");
String appToken = getZzdOpenAppAccessToken();
ExecutableClient ec = zzdExecutableClientHolder.client();
IntelligentPostClient postClient = ec.newIntelligentPostClient("/rpc/oauth2/dingtalk_app_user.json");
OapiRpcOauth2DingtalkAppUserJsonRequest req = new OapiRpcOauth2DingtalkAppUserJsonRequest();
req.setAccess_token(appToken);
req.setAuth_code(authCode);
OapiRpcOauth2DingtalkAppUserJsonResponse resp = postClient.post(req);
zzdAssertSdkTopSuccess(resp, "dingtalk_app_user");
JSONObject data = zzdSdkDingtalkAppUserContentData(resp.getContent());
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;
} catch (ThirdPartyException e) {
throw e;
} catch (Exception e) {
log.error("浙政钉 dingtalk_app_user SDK 调用异常", e);
throw new ThirdPartyException("浙政钉获取用户失败", "ZZD_OPEN_USER_SDK");
}
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");
private static void zzdAssertSdkTopSuccess(OapiResponse resp, String apiLabel) {
if (resp == null || !Boolean.TRUE.equals(resp.getSuccess())) {
String msg = resp != null && StringUtils.hasText(resp.getMessage()) ? resp.getMessage() : "success!=true";
throw new ThirdPartyException("浙政钉 " + apiLabel + " 网关失败: " + msg, "ZZD_OPEN_FAIL");
}
JSONObject root = JSON.parseObject(body);
if (root == null || !root.getBooleanValue("success")) {
throw new ThirdPartyException("浙政钉接口失败: " + body, "ZZD_OPEN_FAIL");
}
JSONObject content = root.getJSONObject("content");
}
private static JSONObject zzdSdkSpResultData(OapiSpResultContent content, String apiLabel) {
if (content == null) {
throw new ThirdPartyException("浙政钉响应缺少 content: " + body, "ZZD_OPEN_BAD");
throw new ThirdPartyException("浙政钉 " + apiLabel + " 响应无 content", "ZZD_OPEN_BAD");
}
if (!content.getBooleanValue("success")) {
throw new ThirdPartyException("浙政钉 content 失败: " + body, "ZZD_OPEN_FAIL");
if (content.getSuccess() != null && !content.getSuccess()) {
throw new ThirdPartyException("浙政钉 " + apiLabel + " 业务失败: " + content.getResponseCode() + " " + content.getResponseMessage(), "ZZD_OPEN_FAIL");
}
JSONObject data = content.getJSONObject("data");
String dataStr = content.getData();
if (!StringUtils.hasText(dataStr)) {
throw new ThirdPartyException("浙政钉 " + apiLabel + " 响应无 data", "ZZD_OPEN_BAD");
}
JSONObject data = JSON.parseObject(dataStr);
if (data == null) {
throw new ThirdPartyException("浙政钉响应缺少 data: " + body, "ZZD_OPEN_BAD");
throw new ThirdPartyException("浙政钉 " + apiLabel + " data 非 JSON", "ZZD_OPEN_BAD");
}
return data;
}
private static JSONObject zzdSdkDingtalkAppUserContentData(String contentStr) {
if (!StringUtils.hasText(contentStr)) {
throw new ThirdPartyException("浙政钉 dingtalk_app_user 响应无 content body", "ZZD_OPEN_BAD");
}
JSONObject content = JSON.parseObject(contentStr);
if (content == null) {
throw new ThirdPartyException("浙政钉 dingtalk_app_user content 非 JSON", "ZZD_OPEN_BAD");
}
if (content.containsKey("success") && !content.getBooleanValue("success")) {
throw new ThirdPartyException("浙政钉 dingtalk_app_user 业务失败: " + content.getString("responseCode") + " " + content.getString("responseMessage"), "ZZD_OPEN_FAIL");
}
JSONObject data = content.getJSONObject("data");
if (data != null) {
return data;
}
return content;
}
private static ZZDUserDTO mapOpenPlatformUserToZzdDto(JSONObject data) {
ZZDUserDTO dto = new ZZDUserDTO();
String lastName = data.getString("lastName");
@@ -728,6 +759,64 @@ public class ThirdPartyOAuth2Service {
}
/**
* 政务钉钉 H5 JSAPI 鉴权:前端 {@code dd.authConfig({ ticket, jsApiList })}。
* 官方 SDK:{@code IntelligentGetClient /get_jsapi_token.json},请求体带应用 {@code accessToken},解析 {@code content.data.accessToken} 为 ticket。
*/
public ZzdJsapiConfigVO buildZzdJsapiConfigForH5() {
if (!zzdOpenPlatform.isEnabled() || !zzdOpenPlatform.isAppCredentialConfigured()) {
throw new ThirdPartyException("浙政钉开放平台未启用或未配置 appKey/appSecret", "ZZD_JSAPI_CFG");
}
String ticket = getZzdOpenJsapiTokenTicket();
ZzdJsapiConfigVO vo = new ZzdJsapiConfigVO();
vo.setTicket(ticket);
vo.setJsApiList(Arrays.asList("getAuthCode", "runtime.permission.requestAuthCode"));
return vo;
}
private String getZzdOpenJsapiTokenTicket() {
long now = System.currentTimeMillis();
if (StringUtils.hasText(zzdOpenCachedJsapiTicket) && now < zzdOpenCachedJsapiTicketExpireMs - 120_000L) {
return zzdOpenCachedJsapiTicket;
}
synchronized (zzdJsapiTicketLock) {
now = System.currentTimeMillis();
if (StringUtils.hasText(zzdOpenCachedJsapiTicket) && now < zzdOpenCachedJsapiTicketExpireMs - 120_000L) {
return zzdOpenCachedJsapiTicket;
}
String appToken = getZzdOpenAppAccessToken();
try {
ExecutableClient ec = zzdExecutableClientHolder.client();
IntelligentGetClient getClient = ec.newIntelligentGetClient("/get_jsapi_token.json");
OapiGetJsapiTokenJsonRequest req = new OapiGetJsapiTokenJsonRequest();
req.setAccessToken(appToken);
OapiGetJsapiTokenJsonResponse resp = getClient.get(req);
zzdAssertSdkTopSuccess(resp, "get_jsapi_token");
JSONObject data = zzdSdkSpResultData(resp.getContent(), "get_jsapi_token");
String ticket = data.getString("accessToken");
if (!StringUtils.hasText(ticket)) {
throw new ThirdPartyException("浙政钉 get_jsapi_token 响应无 data.accessToken", "ZZD_JSAPI_TK_BAD");
}
int ttl = 7100;
try {
Object ex = data.get("expiresIn");
if (ex != null) {
ttl = Integer.parseInt(ex.toString());
}
} catch (NumberFormatException ignored) {
}
zzdOpenCachedJsapiTicket = ticket;
zzdOpenCachedJsapiTicketExpireMs = System.currentTimeMillis() + ttl * 1000L;
return ticket;
} catch (ThirdPartyException e) {
throw e;
} catch (Exception e) {
log.error("浙政钉 get_jsapi_token SDK 调用异常", e);
throw new ThirdPartyException("浙政钉获取 JSAPI ticket 失败", "ZZD_JSAPI_TK_SDK");
}
}
}
/**
* 诗画
* @param accountId
@@ -0,0 +1,17 @@
package com.bsd.pjcloud.thirdparty.vo;
import lombok.Data;
import java.util.List;
/**
* 政务钉钉 / 专有钉钉 H5:{@code dd.authConfig} 参数。
* ticket 来自开放平台 {@code POST /get_jsapi_token.json} 返回的 {@code content.data.accessToken}(文档称鉴权 ticket)。
*/
@Data
public class ZzdJsapiConfigVO {
private String ticket;
private List<String> jsApiList;
}
@@ -52,6 +52,8 @@ zzd:
tenant-id: "196729"
app-key: ${ZZD_OPEN_APP_KEY:pjx_zhhqyth-wt2VrVa0olcqEBBJJb}
app-secret: ${ZZD_OPEN_APP_SECRET:rT5zbpHO0L7raRHJtvlIs26N8hn53JDv1K1SNa7X}
# H5 使用 dd.authConfig 时仅需 ak/sk;jsapi-agent-id 为旧版 dd.config 方案保留,可不配
# jsapi-agent-id:
# 正元智慧
# 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