From a07eedcd684345aa5ed364ab97e2c25b59c4c72e Mon Sep 17 00:00:00 2001 From: "382696293@qq.com" <382696293@qq.com> Date: Thu, 7 Mar 2024 17:23:32 +0800 Subject: [PATCH] =?UTF-8?q?=E6=95=B0=E4=BA=91accessToken=E8=8E=B7=E5=8F=96?= =?UTF-8?q?=E4=BB=A5=E5=8F=8A=E7=BC=93=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/core/constant/CacheConstants.java | 14 + .../core/constant/SecurityConstants.java | 5 + .../core/domain/shuyun/AccessToken.java | 85 ++++ .../core/utils/http/HttpClientUtils.java | 394 ++++++++++++++++++ flossom-modules/flossom-system/pom.xml | 13 + .../system/utils/shuyun/ActionMethod.java | 14 + .../system/utils/shuyun/ShuYunApiUtils.java | 55 +++ .../system/utils/shuyun/ShuYunConfig.java | 66 +++ .../main/resources/lib/open-platform-sdk.jar | Bin 0 -> 15663 bytes .../src/main/resources/shuyun.properties | 3 + 10 files changed, 649 insertions(+) create mode 100644 flossom-common/flossom-common-core/src/main/java/com/flossom/common/core/domain/shuyun/AccessToken.java create mode 100644 flossom-common/flossom-common-core/src/main/java/com/flossom/common/core/utils/http/HttpClientUtils.java create mode 100644 flossom-modules/flossom-system/src/main/java/com/flossom/system/utils/shuyun/ActionMethod.java create mode 100644 flossom-modules/flossom-system/src/main/java/com/flossom/system/utils/shuyun/ShuYunApiUtils.java create mode 100644 flossom-modules/flossom-system/src/main/java/com/flossom/system/utils/shuyun/ShuYunConfig.java create mode 100644 flossom-modules/flossom-system/src/main/resources/lib/open-platform-sdk.jar create mode 100644 flossom-modules/flossom-system/src/main/resources/shuyun.properties diff --git a/flossom-common/flossom-common-core/src/main/java/com/flossom/common/core/constant/CacheConstants.java b/flossom-common/flossom-common-core/src/main/java/com/flossom/common/core/constant/CacheConstants.java index 612ff1d..da4c1a5 100644 --- a/flossom-common/flossom-common-core/src/main/java/com/flossom/common/core/constant/CacheConstants.java +++ b/flossom-common/flossom-common-core/src/main/java/com/flossom/common/core/constant/CacheConstants.java @@ -76,4 +76,18 @@ public class CacheConstants * 微信小程序 access_token */ public static final long WX_ACCESS_TOKEN_EXPIRATION = 100; + + /** + * 数云 access_token Key 缓存 + */ + public static final String SHUYUN_ACCESS_TOKEN_CACHE = "shuyun_access_token_cache"; + + /** + * 数云 access_token 值 HKey缓存 + */ + public static final String SHUYUN_ACCESS_TOKEN_CACHE_VALUE = "value"; + /** + * 数云 access_token 有效期 HKey缓存 + */ + public static final String SHUYUN_ACCESS_TOKEN_CACHE_EXPIRY = "expiry"; } diff --git a/flossom-common/flossom-common-core/src/main/java/com/flossom/common/core/constant/SecurityConstants.java b/flossom-common/flossom-common-core/src/main/java/com/flossom/common/core/constant/SecurityConstants.java index a9d58c2..c75b740 100644 --- a/flossom-common/flossom-common-core/src/main/java/com/flossom/common/core/constant/SecurityConstants.java +++ b/flossom-common/flossom-common-core/src/main/java/com/flossom/common/core/constant/SecurityConstants.java @@ -61,4 +61,9 @@ public class SecurityConstants * 角色权限 */ public static final String ROLE_PERMISSION = "role_permission"; + + /** + * + */ + public static final String SHUYUN_REQUEST_TIME = "Gateway-Request-Time"; } diff --git a/flossom-common/flossom-common-core/src/main/java/com/flossom/common/core/domain/shuyun/AccessToken.java b/flossom-common/flossom-common-core/src/main/java/com/flossom/common/core/domain/shuyun/AccessToken.java new file mode 100644 index 0000000..6feda3d --- /dev/null +++ b/flossom-common/flossom-common-core/src/main/java/com/flossom/common/core/domain/shuyun/AccessToken.java @@ -0,0 +1,85 @@ +package com.flossom.common.core.domain.shuyun; + +/** + * 数云api的 accessToken 有效期为 1个月 + * 数云建议一天更新一次 + */ +public class AccessToken { + + /** + * 代表租户的授权值,可能过期的,是否过期看isOverDue + */ + private String accessToken; + + /** + * 应用ID + */ + private String appId; + + /** + * 0:代表租户级别,1:店铺授权 此字段是为了扩展业务 + */ + private String authType; + + /** + * 如果是租户级别的授权,则此值代表是哪个租户的授权 + * 如果是店铺授权,则此值代表是哪个店铺的授权 + */ + private String authValue; + + /** + * 0:代表已更新 1:代表已过期,如果是授权过期,则需要租户在页面进行手动授权,产生新accessToken + */ + private String isOverDue; + + public String getAccessToken() { + return accessToken; + } + + public void setAccessToken(String accessToken) { + this.accessToken = accessToken; + } + + public String getAppId() { + return appId; + } + + public void setAppId(String appId) { + this.appId = appId; + } + + public String getAuthType() { + return authType; + } + + public void setAuthType(String authType) { + this.authType = authType; + } + + public String getAuthValue() { + return authValue; + } + + public void setAuthValue(String authValue) { + this.authValue = authValue; + } + + public String getIsOverDue() { + return isOverDue; + } + + public void setIsOverDue(String isOverDue) { + this.isOverDue = isOverDue; + } + + @Override + public String toString() { + return "AccessToken{" + + "accessToken='" + accessToken + '\'' + + ", appId='" + appId + '\'' + + ", authType='" + authType + '\'' + + ", authValue='" + authValue + '\'' + + ", isOverDue='" + isOverDue + '\'' + + '}'; + } +} diff --git a/flossom-common/flossom-common-core/src/main/java/com/flossom/common/core/utils/http/HttpClientUtils.java b/flossom-common/flossom-common-core/src/main/java/com/flossom/common/core/utils/http/HttpClientUtils.java new file mode 100644 index 0000000..97e1de8 --- /dev/null +++ b/flossom-common/flossom-common-core/src/main/java/com/flossom/common/core/utils/http/HttpClientUtils.java @@ -0,0 +1,394 @@ +package com.flossom.common.core.utils.http; + + +import com.flossom.common.core.utils.StringUtils; +import org.apache.http.Consts; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +import org.apache.http.client.HttpClient; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.config.RequestConfig.Builder; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.utils.URIBuilder; +import org.apache.http.conn.ConnectTimeoutException; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.conn.ssl.SSLContextBuilder; +import org.apache.http.conn.ssl.TrustStrategy; +import org.apache.http.conn.ssl.X509HostnameVerifier; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.util.EntityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocket; +import java.io.IOException; +import java.net.SocketTimeoutException; +import java.net.URI; +import java.security.GeneralSecurityException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +public class HttpClientUtils { + + private static final Logger log = LoggerFactory.getLogger(HttpClientUtils.class); + + public static final int connTimeout = 10000; + public static final int readTimeout = 10000; + public static final String charset = "UTF-8"; + private static HttpClient client = null; + + static { + PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(); + cm.setMaxTotal(128); + cm.setDefaultMaxPerRoute(128); + client = HttpClients.custom().setConnectionManager(cm).build(); + } + + public static String postJsonParameters(String url, String parameterStr) throws Exception { + return postJson(url, parameterStr, charset, connTimeout, readTimeout); + } + + public static String postParameters(String url, String parameterStr) throws Exception { + return post(url, parameterStr, "application/x-www-form-urlencoded", charset, connTimeout, readTimeout); + } + + public static String postParameters(String url, String parameterStr, String charset, Integer connTimeout, Integer readTimeout) throws ConnectTimeoutException, SocketTimeoutException, Exception { + return post(url, parameterStr, "application/x-www-form-urlencoded", charset, connTimeout, readTimeout); + } + + public static String postParameters(String url, Map params) throws ConnectTimeoutException, + SocketTimeoutException, Exception { + return postForm(url, params, null, connTimeout, readTimeout); + } + + public static String postParameters(String url, Map params, Integer connTimeout, Integer readTimeout) throws ConnectTimeoutException, + SocketTimeoutException, Exception { + return postForm(url, params, null, connTimeout, readTimeout); + } + + public static String get(String url) throws Exception { + return get(url, charset, null, null); + } + + public static String get(String url, String charset) throws Exception { + return get(url, charset, connTimeout, readTimeout); + } + + public static String getParameters(String url, Map params) throws Exception { + URIBuilder builder = new URIBuilder(url); + if (params != null) { + for (String key : params.keySet()) { + builder.addParameter(key, params.get(key)); + } + } + URI uri = builder.build(); + return get(uri.toString(), charset, null, null); + } + + /** + * 发送一个 Post 请求, 使用指定的字符集编码. + * + * @param url + * @param body RequestBody + * @param charset 编码 + * @param connTimeout 建立链接超时时间,毫秒. + * @param readTimeout 响应超时时间,毫秒. + * @return ResponseBody, 使用指定的字符集编码. + * @throws ConnectTimeoutException 建立链接超时异常 + * @throws SocketTimeoutException 响应超时 + * @throws Exception + */ + public static String postJson(String url, String body, String charset, Integer connTimeout, Integer readTimeout) throws Exception { + HttpClient client = null; + HttpPost post = new HttpPost(url); + String result = ""; + try { + if (StringUtils.isNotBlank(body)) { + HttpEntity entity = new StringEntity(body, ContentType.APPLICATION_JSON); + post.setEntity(entity); + } + + // 设置参数 + Builder customReqConf = RequestConfig.custom(); + if (connTimeout != null) { + customReqConf.setConnectTimeout(connTimeout); + } + if (readTimeout != null) { + customReqConf.setSocketTimeout(readTimeout); + } + post.setConfig(customReqConf.build()); + + HttpResponse res; + if (url.startsWith("https")) { + // 执行 Https 请求. + client = createSSLInsecureClient(); + res = client.execute(post); + } else { + // 执行 Http 请求. + client = HttpClientUtils.client; + res = client.execute(post); + } + result = EntityUtils.toString(res.getEntity(), charset); + } catch (Exception ex) { + log.error("HttpClient request error!", ex); + throw ex; + } finally { + post.releaseConnection(); + if (url.startsWith("https") && client != null && client instanceof CloseableHttpClient) { + ((CloseableHttpClient) client).close(); + } + } + return result; + } + + /** + * 发送一个 Post 请求, 使用指定的字符集编码. + * + * @param url + * @param body RequestBody + * @param mimeType 例如 application/xml "application/x-www-form-urlencoded" a=1&b=2&c=3 + * @param charset 编码 + * @param connTimeout 建立链接超时时间,毫秒. + * @param readTimeout 响应超时时间,毫秒. + * @return ResponseBody, 使用指定的字符集编码. + * @throws ConnectTimeoutException 建立链接超时异常 + * @throws SocketTimeoutException 响应超时 + * @throws Exception + */ + public static String post(String url, String body, String mimeType, String charset, Integer connTimeout, Integer readTimeout) + throws ConnectTimeoutException, SocketTimeoutException, Exception { + HttpClient client = null; + HttpPost post = new HttpPost(url); + String result = ""; + try { + if (StringUtils.isNotBlank(body)) { + HttpEntity entity = new StringEntity(body, ContentType.create(mimeType, charset)); + post.setEntity(entity); + } + // 设置参数 + Builder customReqConf = RequestConfig.custom(); + if (connTimeout != null) { + customReqConf.setConnectTimeout(connTimeout); + } + if (readTimeout != null) { + customReqConf.setSocketTimeout(readTimeout); + } + post.setConfig(customReqConf.build()); + + HttpResponse res; + if (url.startsWith("https")) { + // 执行 Https 请求. + client = createSSLInsecureClient(); + res = client.execute(post); + } else { + // 执行 Http 请求. + client = HttpClientUtils.client; + res = client.execute(post); + } + result = EntityUtils.toString(res.getEntity(), charset); + } catch (Exception ex) { + log.error("HttpClient request error!", ex); + throw ex; + } finally { + post.releaseConnection(); + if (url.startsWith("https") && client != null && client instanceof CloseableHttpClient) { + ((CloseableHttpClient) client).close(); + } + } + return result; + } + + /** + * 提交form表单 + * + * @param url + * @param params + * @param connTimeout + * @param readTimeout + * @return + * @throws ConnectTimeoutException + * @throws SocketTimeoutException + * @throws Exception + */ + public static String postForm(String url, Map params, Map headers, Integer connTimeout, Integer readTimeout) throws ConnectTimeoutException, + SocketTimeoutException, Exception { + + HttpClient client = null; + HttpPost post = new HttpPost(url); + try { + if (params != null && !params.isEmpty()) { + List formParams = new ArrayList(); + Set> entrySet = params.entrySet(); + for (Entry entry : entrySet) { + formParams.add(new BasicNameValuePair(entry.getKey(), entry.getValue())); + } + UrlEncodedFormEntity entity = new UrlEncodedFormEntity(formParams, Consts.UTF_8); + post.setEntity(entity); + } + + if (headers != null && !headers.isEmpty()) { + for (Entry entry : headers.entrySet()) { + post.addHeader(entry.getKey(), entry.getValue()); + } + } + // 设置参数 + Builder customReqConf = RequestConfig.custom(); + if (connTimeout != null) { + customReqConf.setConnectTimeout(connTimeout); + } + if (readTimeout != null) { + customReqConf.setSocketTimeout(readTimeout); + } + post.setConfig(customReqConf.build()); + HttpResponse res = null; + if (url.startsWith("https")) { + // 执行 Https 请求. + client = createSSLInsecureClient(); + res = client.execute(post); + } else { + // 执行 Http 请求. + client = HttpClientUtils.client; + res = client.execute(post); + } + return EntityUtils.toString(res.getEntity(), charset); + } catch (Exception ex) { + log.error("HttpClient request error!", ex); + throw ex; + } finally { + post.releaseConnection(); + if (url.startsWith("https") && client != null + && client instanceof CloseableHttpClient) { + ((CloseableHttpClient) client).close(); + } + } + } + + /** + * 发送一个 GET 请求 + * + * @param url + * @param charset + * @param connTimeout 建立链接超时时间,毫秒. + * @param readTimeout 响应超时时间,毫秒. + * @return + * @throws ConnectTimeoutException 建立链接超时 + * @throws SocketTimeoutException 响应超时 + * @throws Exception + */ + public static String get(String url, String charset, Integer connTimeout, Integer readTimeout) + throws ConnectTimeoutException, SocketTimeoutException, Exception { + + HttpClient client = null; + HttpGet get = new HttpGet(url); + String result = ""; + try { + // 设置参数 + Builder customReqConf = RequestConfig.custom(); + if (connTimeout != null) { + customReqConf.setConnectTimeout(connTimeout); + } + if (readTimeout != null) { + customReqConf.setSocketTimeout(readTimeout); + } + get.setConfig(customReqConf.build()); + + HttpResponse res = null; + + if (url.startsWith("https")) { + // 执行 Https 请求. + client = createSSLInsecureClient(); + res = client.execute(get); + } else { + // 执行 Http 请求. + client = HttpClientUtils.client; + res = client.execute(get); + } + result = EntityUtils.toString(res.getEntity(), charset); + } catch (Exception ex) { + log.error("HttpClient request error!", ex); + throw ex; + } finally { + get.releaseConnection(); + if (url.startsWith("https") && client != null && client instanceof CloseableHttpClient) { + ((CloseableHttpClient) client).close(); + } + } + return result; + } + + /** + * 从 response 里获取 charset + * + * @param ressponse + * @return + */ + @SuppressWarnings("unused") + private static String getCharsetFromResponse(HttpResponse ressponse) { + // Content-Type:text/html; charset=GBK + if (ressponse.getEntity() != null && ressponse.getEntity().getContentType() != null && ressponse.getEntity().getContentType().getValue() != null) { + String contentType = ressponse.getEntity().getContentType().getValue(); + if (contentType.contains("charset=")) { + return contentType.substring(contentType.indexOf("charset=") + 8); + } + } + return null; + } + + /** + * 创建 SSL连接 + * + * @return + * @throws GeneralSecurityException + */ + private static CloseableHttpClient createSSLInsecureClient() throws GeneralSecurityException { + try { + SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustStrategy() { + public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException { + return true; + } + }).build(); + SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext, new X509HostnameVerifier() { + + @Override + public boolean verify(String arg0, SSLSession arg1) { + return true; + } + + @Override + public void verify(String host, SSLSocket ssl) + throws IOException { + } + + @Override + public void verify(String host, X509Certificate cert) + throws SSLException { + } + + @Override + public void verify(String host, String[] cns, + String[] subjectAlts) throws SSLException { + } + }); + return HttpClients.custom().setSSLSocketFactory(sslsf).build(); + } catch (GeneralSecurityException e) { + throw e; + } + } + +} diff --git a/flossom-modules/flossom-system/pom.xml b/flossom-modules/flossom-system/pom.xml index 0d15844..d4e1ce0 100644 --- a/flossom-modules/flossom-system/pom.xml +++ b/flossom-modules/flossom-system/pom.xml @@ -99,6 +99,15 @@ javase 3.5.1 + + + + com.shuyun.open + open-platform-sdk + 1.0.2 + system + ${project.basedir}/src/main/resources/lib/open-platform-sdk.jar + @@ -107,6 +116,10 @@ org.springframework.boot spring-boot-maven-plugin + + + true + diff --git a/flossom-modules/flossom-system/src/main/java/com/flossom/system/utils/shuyun/ActionMethod.java b/flossom-modules/flossom-system/src/main/java/com/flossom/system/utils/shuyun/ActionMethod.java new file mode 100644 index 0000000..7d2d4d5 --- /dev/null +++ b/flossom-modules/flossom-system/src/main/java/com/flossom/system/utils/shuyun/ActionMethod.java @@ -0,0 +1,14 @@ +package com.flossom.system.utils.shuyun; + +public class ActionMethod { + + private String accessToken; + + public String getAccessToken() { + return accessToken; + } + + public void setAccessToken(String accessToken) { + this.accessToken = accessToken; + } +} diff --git a/flossom-modules/flossom-system/src/main/java/com/flossom/system/utils/shuyun/ShuYunApiUtils.java b/flossom-modules/flossom-system/src/main/java/com/flossom/system/utils/shuyun/ShuYunApiUtils.java new file mode 100644 index 0000000..d46ec76 --- /dev/null +++ b/flossom-modules/flossom-system/src/main/java/com/flossom/system/utils/shuyun/ShuYunApiUtils.java @@ -0,0 +1,55 @@ +package com.flossom.system.utils.shuyun; + +import com.flossom.common.core.constant.CacheConstants; +import com.flossom.common.redis.service.RedisService; +import com.flossom.common.core.utils.http.HttpClientUtils; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.time.ZoneId; + + +/** + * 数云工具类 + */ +@Component +public class ShuYunApiUtils { + + protected final static Logger logger = LoggerFactory.getLogger(ShuYunApiUtils.class); + + private static ShuYunConfig shuYunConfig; + + private static RedisService redisService; + + @Autowired + public void setShuYunConfig(ShuYunConfig shuYunConfig) { + ShuYunApiUtils.shuYunConfig = shuYunConfig; + } + + @Autowired + public void setRedisService(RedisService redisService) { + ShuYunApiUtils.redisService = redisService; + } + + /** + * 获取 accessToken,缓存没有就请求数云回调接口,将accessToken传递过来 + * + * @return + * @throws Exception + */ + public static String getAccessToken() throws Exception { + Integer expiryTime = redisService.getCacheMapValue(CacheConstants.SHUYUN_ACCESS_TOKEN_CACHE, CacheConstants.SHUYUN_ACCESS_TOKEN_CACHE_VALUE); + if (expiryTime == null || LocalDateTime.now().atZone(ZoneId.systemDefault()).toEpochSecond() > expiryTime) { + String accessTokenUrl = StringUtils.replace(shuYunConfig.getActionMethod().getAccessToken(), "{appid}", shuYunConfig.getAppid()); + logger.info("刷新accessToken地址:{}", accessTokenUrl); + String result = HttpClientUtils.get(accessTokenUrl); + logger.info("请求刷新accessToken结果:{}", result); + } + return redisService.getCacheMapValue(CacheConstants.SHUYUN_ACCESS_TOKEN_CACHE, CacheConstants.SHUYUN_ACCESS_TOKEN_CACHE_VALUE); + } + +} diff --git a/flossom-modules/flossom-system/src/main/java/com/flossom/system/utils/shuyun/ShuYunConfig.java b/flossom-modules/flossom-system/src/main/java/com/flossom/system/utils/shuyun/ShuYunConfig.java new file mode 100644 index 0000000..81da0a0 --- /dev/null +++ b/flossom-modules/flossom-system/src/main/java/com/flossom/system/utils/shuyun/ShuYunConfig.java @@ -0,0 +1,66 @@ +package com.flossom.system.utils.shuyun; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.context.annotation.Configuration; + +/** + * 数云接口配置 + * + * @author flossom + */ +@Configuration +@RefreshScope +@ConfigurationProperties(prefix = "shuyun") +public class ShuYunConfig { + + /** + * 接口地址 + */ + private String url; + + /** + * 数云 appid + */ + private String appid; + + /** + * 数云 security + */ + private String security; + + private ActionMethod actionMethod; + + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getAppid() { + return appid; + } + + public void setAppid(String appid) { + this.appid = appid; + } + + public String getSecurity() { + return security; + } + + public void setSecurity(String security) { + this.security = security; + } + + public ActionMethod getActionMethod() { + return actionMethod; + } + + public void setActionMethod(ActionMethod actionMethod) { + this.actionMethod = actionMethod; + } +} diff --git a/flossom-modules/flossom-system/src/main/resources/lib/open-platform-sdk.jar b/flossom-modules/flossom-system/src/main/resources/lib/open-platform-sdk.jar new file mode 100644 index 0000000000000000000000000000000000000000..6230e1a524479be083f09325696b58eca47aa562 GIT binary patch literal 15663 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