diff --git a/doc/docs/files/10011_项目接入到SOP.md b/doc/docs/files/10011_项目接入到SOP.md index 7f4e5e18..5a84d025 100644 --- a/doc/docs/files/10011_项目接入到SOP.md +++ b/doc/docs/files/10011_项目接入到SOP.md @@ -6,7 +6,7 @@ ```xml -2.3.2.RELEASE +3.0.2 Hoxton.SR8 @@ -214,4 +214,4 @@ instance.getMetadata().put("sop.routes.path", "http://open.xxx.com/get_routes"); namingService.registerInstance(serviceId, instance); ``` -完成以上步骤后,php服务注册到nacos,网关会触发监听事件,获取新注册的服务,然后会向你的服务拉取路由配置。 \ No newline at end of file +完成以上步骤后,php服务注册到nacos,网关会触发监听事件,获取新注册的服务,然后会向你的服务拉取路由配置。 diff --git a/doc/pom.xml b/doc/pom.xml index bae1abe3..0b0f0832 100644 --- a/doc/pom.xml +++ b/doc/pom.xml @@ -3,7 +3,7 @@ 4.0.0 com.gitee.sop doc - 4.4.2-SNAPSHOT + 5.0.0-SNAPSHOT diff --git a/pom.xml b/pom.xml index 0c4069c7..ab53c48e 100644 --- a/pom.xml +++ b/pom.xml @@ -5,13 +5,13 @@ org.springframework.boot spring-boot-starter-parent - 2.3.2.RELEASE + 2.6.13 4.0.0 com.gitee.sop sop-parent - 4.4.2-SNAPSHOT + 5.0.0-SNAPSHOT pom 一个开放平台解决方案项目,基于Spring Cloud实现,目标是能够让用户快速得搭建起自己的开放平台 @@ -25,6 +25,7 @@ sop-test sop-sdk sop-website + sop-index @@ -35,21 +36,19 @@ 1.8 - 2.3.2.RELEASE + 2.6.13 - Hoxton.SR8 + 2021.0.5 - 2.2.5.RELEASE + 2021.0.5.0 + + 3.2.10 - 1.2.3 - - 4.11 - - 1.2.73 + 1.2.83 2.5 1.3.3 3.2.2 @@ -66,6 +65,7 @@ 1.16.9 6.2 5.2.0 + @@ -92,6 +92,11 @@ pom import + + org.apache.dubbo + dubbo-spring-boot-starter + ${dubbo.version} + com.google.guava diff --git a/sop-admin/pom.xml b/sop-admin/pom.xml index 79208df3..a2cb9082 100644 --- a/sop-admin/pom.xml +++ b/sop-admin/pom.xml @@ -6,7 +6,7 @@ com.gitee.sop sop-parent - 4.4.2-SNAPSHOT + 5.0.0-SNAPSHOT ../pom.xml @@ -17,4 +17,4 @@ sop-admin-server - \ No newline at end of file + diff --git a/sop-admin/sop-admin-server/pom.xml b/sop-admin/sop-admin-server/pom.xml index c9717c86..4a8a9b0f 100644 --- a/sop-admin/sop-admin-server/pom.xml +++ b/sop-admin/sop-admin-server/pom.xml @@ -5,13 +5,12 @@ com.gitee.sop sop-parent - 4.4.2-SNAPSHOT + 5.0.0-SNAPSHOT ../../pom.xml 4.0.0 sop-admin-server - 4.4.2-SNAPSHOT jar @@ -70,7 +69,7 @@ nacos-client true - + org.springframework.boot spring-boot-starter-test diff --git a/sop-auth/pom.xml b/sop-auth/pom.xml index 90c941c4..843589f6 100644 --- a/sop-auth/pom.xml +++ b/sop-auth/pom.xml @@ -5,7 +5,7 @@ com.gitee.sop sop-parent - 4.4.2-SNAPSHOT + 5.0.0-SNAPSHOT ../pom.xml @@ -38,7 +38,7 @@ com.gitee.sop sdk-java - 4.4.2-SNAPSHOT + 5.0.0-SNAPSHOT diff --git a/sop-common/pom.xml b/sop-common/pom.xml index 8542f928..ad3e2e88 100644 --- a/sop-common/pom.xml +++ b/sop-common/pom.xml @@ -6,7 +6,7 @@ com.gitee.sop sop-parent - 4.4.2-SNAPSHOT + 5.0.0-SNAPSHOT ../pom.xml @@ -26,4 +26,4 @@ - \ No newline at end of file + diff --git a/sop-common/sop-bridge-eureka/pom.xml b/sop-common/sop-bridge-eureka/pom.xml index 68d322dd..892eda01 100644 --- a/sop-common/sop-bridge-eureka/pom.xml +++ b/sop-common/sop-bridge-eureka/pom.xml @@ -5,7 +5,7 @@ com.gitee.sop sop-common - 4.4.2-SNAPSHOT + 5.0.0-SNAPSHOT ../pom.xml 4.0.0 @@ -29,4 +29,4 @@ - \ No newline at end of file + diff --git a/sop-common/sop-bridge-nacos/pom.xml b/sop-common/sop-bridge-nacos/pom.xml index e55f20bf..2a396825 100644 --- a/sop-common/sop-bridge-nacos/pom.xml +++ b/sop-common/sop-bridge-nacos/pom.xml @@ -5,13 +5,13 @@ 4.0.0 com.gitee.sop sop-bridge-nacos - 4.4.2-SNAPSHOT + 5.0.0-SNAPSHOT com.gitee.sop sop-gateway-common - 4.4.2-SNAPSHOT + 5.0.0-SNAPSHOT @@ -25,4 +25,4 @@ provided - \ No newline at end of file + diff --git a/sop-common/sop-gateway-common/pom.xml b/sop-common/sop-gateway-common/pom.xml index 659c49aa..ae8ef7f7 100644 --- a/sop-common/sop-gateway-common/pom.xml +++ b/sop-common/sop-gateway-common/pom.xml @@ -5,7 +5,7 @@ com.gitee.sop sop-common - 4.4.2-SNAPSHOT + 5.0.0-SNAPSHOT ../pom.xml diff --git a/sop-common/sop-service-common/pom.xml b/sop-common/sop-service-common/pom.xml index 3e279a84..38d39e10 100644 --- a/sop-common/sop-service-common/pom.xml +++ b/sop-common/sop-service-common/pom.xml @@ -5,14 +5,14 @@ 4.0.0 com.gitee.sop sop-service-common - 4.4.2-SNAPSHOT + 5.0.0-SNAPSHOT jar 1.8 - 2.3.2.RELEASE + 3.0.2 Hoxton.SR8 diff --git a/sop-example/pom.xml b/sop-example/pom.xml index bfa9f619..55501b45 100644 --- a/sop-example/pom.xml +++ b/sop-example/pom.xml @@ -5,7 +5,7 @@ com.gitee.sop sop-parent - 4.4.2-SNAPSHOT + 5.0.0-SNAPSHOT ../pom.xml @@ -17,4 +17,4 @@ sop-story sop-springmvc - \ No newline at end of file + diff --git a/sop-example/sop-springmvc/pom.xml b/sop-example/sop-springmvc/pom.xml index 055db4cc..cd0e23cb 100644 --- a/sop-example/sop-springmvc/pom.xml +++ b/sop-example/sop-springmvc/pom.xml @@ -5,7 +5,7 @@ com.gitee.sop sop-parent - 4.4.2-SNAPSHOT + 5.0.0-SNAPSHOT ../../pom.xml diff --git a/sop-example/sop-story/pom.xml b/sop-example/sop-story/pom.xml index 1d78e946..35664fe4 100644 --- a/sop-example/sop-story/pom.xml +++ b/sop-example/sop-story/pom.xml @@ -4,7 +4,7 @@ com.gitee.sop sop-parent - 4.4.2-SNAPSHOT + 5.0.0-SNAPSHOT ../../pom.xml diff --git a/sop-gateway/pom.xml b/sop-gateway/pom.xml index 493d0a4c..dae11134 100644 --- a/sop-gateway/pom.xml +++ b/sop-gateway/pom.xml @@ -4,7 +4,7 @@ com.gitee.sop sop-parent - 4.4.2-SNAPSHOT + 5.0.0-SNAPSHOT ../pom.xml diff --git a/sop-index/pom.xml b/sop-index/pom.xml new file mode 100644 index 00000000..47f8e36e --- /dev/null +++ b/sop-index/pom.xml @@ -0,0 +1,68 @@ + + + 4.0.0 + + com.gitee.sop + sop-parent + 5.0.0-SNAPSHOT + ../pom.xml + + sop-index + 5.0.0-SNAPSHOT + sop-index + sop-index + + + 1.8 + + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-web + + + + org.apache.dubbo + dubbo-spring-boot-starter + + + + commons-codec + commons-codec + + + + org.hibernate + hibernate-validator + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.projectlombok + lombok + true + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/sop-index/src/main/java/com/gitee/sop/index/SopIndexApplication.java b/sop-index/src/main/java/com/gitee/sop/index/SopIndexApplication.java new file mode 100644 index 00000000..07b6c2e3 --- /dev/null +++ b/sop-index/src/main/java/com/gitee/sop/index/SopIndexApplication.java @@ -0,0 +1,13 @@ +package com.gitee.sop.index; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SopIndexApplication { + + public static void main(String[] args) { + SpringApplication.run(SopIndexApplication.class, args); + } + +} diff --git a/sop-index/src/main/java/com/gitee/sop/index/common/ParamNames.java b/sop-index/src/main/java/com/gitee/sop/index/common/ParamNames.java new file mode 100644 index 00000000..a0d6e7f2 --- /dev/null +++ b/sop-index/src/main/java/com/gitee/sop/index/common/ParamNames.java @@ -0,0 +1,51 @@ +package com.gitee.sop.index.common; + +/** + * 请求参数名定义 + * + * 参数 类型 是否必填 最大长度 描述 示例值 + * app_id String 是 32 支付宝分配给开发者的应用ID 2014072300007148 + * method String 是 128 接口名称 alipay.trade.fastpay.refund.query + * format String 否 40 仅支持JSON JSON + * charset String 是 10 请求使用的编码格式,如utf-8,gbk,gb2312等 utf-8 + * sign_type String 是 10 商户生成签名字符串所使用的签名算法类型,目前支持RSA2和RSA,推荐使用RSA2 RSA2 + * sign String 是 344 商户请求参数的签名串,详见签名 详见示例 + * timestamp String 是 19 发送请求的时间,格式"yyyy-MM-dd HH:mm:ss" 2014-07-24 03:07:50 + * version String 是 3 调用的接口版本,固定为:1.0 1.0 + * notify_url String 否 256 支付宝服务器主动通知商户服务器里指定的页面http/https路径。 http://api.test.alipay.net/atinterface/receive_notify.htm + * app_auth_token String 否 40 详见应用授权概述 + * biz_content String 是 请求参数的集合,最大长度不限,除公共参数外所有请求参数都必须放在这个参数中传递,具体参照各产品快速接入文档 + * + * @author tanghc + */ +public class ParamNames { + /** 分配给开发者的应用ID */ + public static String APP_KEY_NAME = "app_id"; + /** 接口名称 */ + public static String API_NAME = "method"; + /** 仅支持JSON */ + public static String FORMAT_NAME = "format"; + /** 请求使用的编码格式 */ + public static String CHARSET_NAME = "charset"; + /** 商户生成签名字符串所使用的签名算法类型,目前支持RSA2和RSA,推荐使用RSA2 */ + public static String SIGN_TYPE_NAME = "sign_type"; + /** 商户请求参数的签名串 */ + public static String SIGN_NAME = "sign"; + /** 发送请求的时间 */ + public static String TIMESTAMP_NAME = "timestamp"; + /** 调用的接口版本 */ + public static String VERSION_NAME = "version"; + /** 开放平台主动通知商户服务器里指定的页面http/https路径 */ + public static String NOTIFY_URL_NAME = "notify_url"; + /** OAuth 2.0授权token */ + public static String APP_AUTH_TOKEN_NAME = "app_auth_token"; + /** 请求参数的集合,最大长度不限,除公共参数外所有请求参数都必须放在这个参数中传递,具体参照各产品快速接入文档 */ + public static String BIZ_CONTENT_NAME = "biz_content"; + + /** 时间戳格式 */ + public static String TIMESTAMP_PATTERN = "yyyy-MM-dd HH:mm:ss"; + + public static String HEADER_VERSION_NAME = "sop-version"; + + +} diff --git a/sop-index/src/main/java/com/gitee/sop/index/controller/IndexController.java b/sop-index/src/main/java/com/gitee/sop/index/controller/IndexController.java new file mode 100644 index 00000000..a8cd8dbd --- /dev/null +++ b/sop-index/src/main/java/com/gitee/sop/index/controller/IndexController.java @@ -0,0 +1,39 @@ +package com.gitee.sop.index.controller; + +import com.gitee.sop.index.controller.param.ApiRequest; +import com.gitee.sop.index.controller.param.ApiResponse; +import com.gitee.sop.index.service.RouteService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +/** + * 开放平台入口 + * + * @author 六如 + */ +@RestController +public class IndexController { + + @Autowired + private RouteService routeService; + + @GetMapping("/") + public String home() { + return "api"; + } + + /** + * 请求入口 + * + * @return 返回响应内容 + */ + @PostMapping("api") + public ApiResponse index(@Validated @RequestBody ApiRequest request) { + return routeService.route(request); + } + +} diff --git a/sop-index/src/main/java/com/gitee/sop/index/controller/param/ApiRequest.java b/sop-index/src/main/java/com/gitee/sop/index/controller/param/ApiRequest.java new file mode 100644 index 00000000..b145c7f2 --- /dev/null +++ b/sop-index/src/main/java/com/gitee/sop/index/controller/param/ApiRequest.java @@ -0,0 +1,123 @@ +package com.gitee.sop.index.controller.param; + +import lombok.Data; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.NotBlank; +import java.io.Serializable; + +/** + * 请求参数名定义 + *
+ * 参数	            类型	   是否必填	最大长度	    描述	                        示例值
+ * app_id	        String	是	    32	    支付宝分配给开发者的应用ID	        2014072300007148
+ * method	        String	是	    128	    接口名称	                        alipay.trade.fastpay.refund.query
+ * format	        String	否	    40	    仅支持JSON	                    JSON
+ * charset	        String	是	    10	    请求使用的编码格式,如utf-8,gbk,gb2312等	utf-8
+ * sign_type	    String	是	    10	    商户生成签名字符串所使用的签名算法类型,目前支持RSA2和RSA,推荐使用RSA2	RSA2
+ * sign	            String	是	    344	    商户请求参数的签名串,详见签名	详见示例
+ * timestamp	    String	是	    19	    发送请求的时间,格式"yyyy-MM-dd HH:mm:ss"	2014-07-24 03:07:50
+ * version	        String	是	    3	    调用的接口版本,固定为:1.0	1.0
+ * notify_url	    String	否	    256	    支付宝服务器主动通知商户服务器里指定的页面http/https路径。	http://api.test.alipay.net/atinterface/receive_notify.htm
+ * app_auth_token	String	否	    40	    详见应用授权概述
+ * biz_content	    String	是		        请求参数的集合,最大长度不限,除公共参数外所有请求参数都必须放在这个参数中传递,具体参照各产品快速接入文档
+ *
+ * @author tanghc + */ +@Data +public class ApiRequest implements Serializable { + private static final long serialVersionUID = 1815097687653555654L; + + /** + * 分配给开发者的应用ID + * + * @mock 2014072300007148 + */ + @NotBlank(message = "app_id不能为空") + @Length(max = 32) + private String app_id; + + /** + * 接口名称 + * + * @mock alipay.trade.fastpay.refund.query + */ + @NotBlank(message = "method不能为空") + @Length(max = 128) + private String method; + + /** + * 仅支持JSON + * + * @mock json + */ + @NotBlank(message = "format不能为空") + @Length(max = 40) + private String format; + + /** + * 请求使用的编码格式,如utf-8,gbk,gb2312等 + * + * @mock utf-8 + */ + @NotBlank(message = "charset不能为空") + @Length(max = 10) + private String charset; + + /** + * 商户生成签名字符串所使用的签名算法类型,目前支持RSA2和RSA,推荐使用RSA2 + * + * @mock RSA2 + */ + @NotBlank(message = "sign_type不能为空") + @Length(max = 10) + private String sign_type; + + /** + * 商户请求参数的签名串,详见签名 + */ + @NotBlank(message = "sign不能为空") + private String sign; + + /** + * 发送请求的时间,格式"yyyy-MM-dd HH:mm:ss" + * + * @mock 2014-07-24 03:07:50 + */ + @NotBlank(message = "timestamp不能为空") + @Length(max = 19) + private String timestamp; + + /** + * 调用的接口版本,固定为:1.0 + * + * @mock 1.0 + */ + @NotBlank(message = "version不能为空") + @Length(max = 10) + private String version; + + /** + * 支付宝服务器主动通知商户服务器里指定的页面http/https路径 + * + * @mock http://ww.xx.com/callback + */ + @Length(max = 256) + private String notify_url; + + /** + * 授权token,详见应用授权概述 + */ + @Length(max = 64) + private String app_auth_token; + + /** + * 请求参数的集合,最大长度不限,除公共参数外所有请求参数都必须放在这个参数中传递,具体参照各产品快速接入文档 + */ + @NotBlank(message = "biz_content不能为空") + private String biz_content; + + public String takeApiName() { + return method + version; + } +} diff --git a/sop-index/src/main/java/com/gitee/sop/index/controller/param/ApiResponse.java b/sop-index/src/main/java/com/gitee/sop/index/controller/param/ApiResponse.java new file mode 100644 index 00000000..9b031811 --- /dev/null +++ b/sop-index/src/main/java/com/gitee/sop/index/controller/param/ApiResponse.java @@ -0,0 +1,82 @@ +package com.gitee.sop.index.controller.param; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.Data; + +import java.io.Serializable; + +/** + * 默认的结果封装类. + *
+ *
+ * xml返回结果:
+ * 
+ *     50
+ *     Remote service error
+ *     isv.invalid-parameter
+ *     非法参数
+ * 
+ * 成功情况:
+ * 
+ *     0
+ *     成功消息
+ *     
+ *         ...返回内容
+ *     
+ * 
+ *
+ * json返回格式:
+ * {
+ *  "code":"50",
+ * 	"msg":"Remote service error",
+ * 	"sub_code":"isv.invalid-parameter",
+ * 	"sub_msg":"非法参数"
+ * }
+ * 成功情况:
+ * {
+ *  "code":"0",
+ * 	"msg":"成功消息内容。。。",
+ * 	"data":{
+ * 	    ...返回内容
+ *    }
+ * }
+ * 
+ *

+ * 字段说明: + * code:网关异常码
+ * msg:网关异常信息
+ * sub_code:业务异常码
+ * sub_msg:业务异常信息
+ * + * @author tanghc + */ +@Data +// 驼峰转下划线 +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class ApiResponse implements Serializable { + private static final long serialVersionUID = 6208496044702199437L; + + /** + * 网关异常码,范围0~100 成功返回"0" + */ + private String code = "0"; + + /** + * 网关异常信息 + */ + private String msg; + + /** + * 业务异常码 + */ + private String subCode; + + /** + * 业务异常信息 + */ + private String subMsg; + + + +} diff --git a/sop-index/src/main/java/com/gitee/sop/index/exception/ApiException.java b/sop-index/src/main/java/com/gitee/sop/index/exception/ApiException.java new file mode 100644 index 00000000..fd543e1d --- /dev/null +++ b/sop-index/src/main/java/com/gitee/sop/index/exception/ApiException.java @@ -0,0 +1,44 @@ +package com.gitee.sop.index.exception; + + + +import com.gitee.sop.index.message.ErrorFactory; +import com.gitee.sop.index.message.ErrorMeta; + +import java.util.Locale; + +/** + * @author tanghc + */ +public class ApiException extends RuntimeException { + + private transient Error error; + + private transient ErrorMeta errorMeta; + private transient Object[] params; + + public ApiException(ErrorMeta errorMeta, Object... params) { + this.errorMeta = errorMeta; + this.params = params; + } + + public ApiException(Throwable cause, ErrorMeta errorMeta, Object... params) { + super(cause); + this.errorMeta = errorMeta; + this.params = params; + } + + public Error getError(Locale locale) { + if (error == null) { + error = ErrorFactory.getError(this.errorMeta, locale, params); + } + return error; + } + + @Override + public String getMessage() { + String message = super.getMessage(); + return message == null ? errorMeta.toString() : message; + } + +} diff --git a/sop-index/src/main/java/com/gitee/sop/index/message/ErrorEnum.java b/sop-index/src/main/java/com/gitee/sop/index/message/ErrorEnum.java new file mode 100644 index 00000000..b9ab6791 --- /dev/null +++ b/sop-index/src/main/java/com/gitee/sop/index/message/ErrorEnum.java @@ -0,0 +1,130 @@ +package com.gitee.sop.index.message; + +/** + * 网关错误定义 + * @author tanghc + */ +public enum ErrorEnum { + /** 成功 */ + SUCCESS(Codes.CODE_SUCCESS, ""), + + /** 服务暂不可用 */ + ISP_UNKNOWN_ERROR(Codes.CODE_UNKNOWN, "isp.unknown-error"), + /** 微服务未知错误 */ + ISP_SERVICE_UNKNOWN_ERROR(Codes.CODE_UNKNOWN, "isp.service-unknown-error"), + /** 服务不可用,路由被禁用 */ + ISP_API_DISABLED(Codes.CODE_UNKNOWN, "isp.service-not-available"), + /** 网关响应超时 */ + ISP_GATEWAY_RESPONSE_TIMEOUT(Codes.CODE_UNKNOWN, "isp.gateway-response-timeout"), + /** 限流处理 */ + ISV_REQUEST_LIMIT(Codes.CODE_UNKNOWN, "isv.service-busy"), + + /** 无效的访问令牌 */ + AOP_INVALID_AUTH_TOKEN(Codes.CODE_AUTH, "aop.invalid-auth-token"), + /** 访问令牌已过期 */ + AOP_AUTH_TOKEN_TIME_OUT(Codes.CODE_AUTH, "aop.auth-token-time-out"), + /** 无效的应用授权令牌 */ + AOP_INVALID_APP_AUTH_TOKEN(Codes.CODE_AUTH, "aop.invalid-app-auth-token"), + /** 商户未授权当前接口 */ + AOP_INVALID_APP_AUTH_TOKEN_NO_API(Codes.CODE_AUTH, "aop.invalid-app-auth-token-no-api"), + /** 应用授权令牌已过期 */ + AOP_APP_AUTH_TOKEN_TIME_OUT(Codes.CODE_AUTH, "aop.app-auth-token-time-out"), + /** 商户未签约任何产品 */ + AOP_NO_PRODUCT_REG_BY_PARTNER(Codes.CODE_AUTH, "aop.no-product-reg-by-partner"), + + /** 缺少方法名参数 */ + ISV_MISSING_METHOD(Codes.CODE_MISSING, "isv.missing-method"), + /** 缺少签名参数 */ + ISV_MISSING_SIGNATURE(Codes.CODE_MISSING, "isv.missing-signature"), + /** 缺少签名类型参数 */ + ISV_MISSING_SIGNATURE_TYPE(Codes.CODE_MISSING, "isv.missing-signature-type"), + /** 缺少签名配置 */ + ISV_MISSING_SIGNATURE_KEY(Codes.CODE_MISSING, "isv.missing-signature-key"), + /** 缺少appId参数 */ + ISV_MISSING_APP_ID(Codes.CODE_MISSING, "isv.missing-app-id"), + /** 缺少时间戳参数 */ + ISV_MISSING_TIMESTAMP(Codes.CODE_MISSING, "isv.missing-timestamp"), + /** 缺少版本参数 */ + ISV_MISSING_VERSION(Codes.CODE_MISSING, "isv.missing-version"), + /** 解密出错, 未指定加密算法 */ + ISV_DECRYPTION_ERROR_MISSING_ENCRYPT_TYPE(Codes.CODE_MISSING, "isv.decryption-error-missing-encrypt-type"), + + /** 参数无效 */ + ISV_INVALID_PARAMETER(Codes.CODE_INVALID, "isv.invalid-parameter"), + /** 文件上传失败 */ + ISV_UPLOAD_FAIL(Codes.CODE_INVALID, "isv.upload-fail"), + /** 文件扩展名无效 */ + ISV_INVALID_FILE_EXTENSION(Codes.CODE_INVALID, "isv.invalid-file-extension"), + /** 文件大小无效 */ + ISV_INVALID_FILE_SIZE(Codes.CODE_INVALID, "isv.invalid-file-size"), + /** 不存在的方法名 */ + ISV_INVALID_METHOD(Codes.CODE_INVALID, "isv.invalid-method"), + /** 无效的数据格式 */ + ISV_INVALID_FORMAT(Codes.CODE_INVALID, "isv.invalid-format"), + /** 无效的签名类型 */ + ISV_INVALID_SIGNATURE_TYPE(Codes.CODE_INVALID, "isv.invalid-signature-type"), + /** 无效签名 */ + ISV_INVALID_SIGNATURE(Codes.CODE_INVALID, "isv.invalid-signature"), + /** 无效的加密类型 */ + ISV_INVALID_ENCRYPT_TYPE(Codes.CODE_INVALID, "isv.invalid-encrypt-type"), + /** 解密异常 */ + ISV_INVALID_ENCRYPT(Codes.CODE_INVALID, "isv.invalid-encrypt"), + /** 无效的appId参数 */ + ISV_INVALID_APP_ID(Codes.CODE_INVALID, "isv.invalid-app-id"), + /** 非法的时间戳参数 */ + ISV_INVALID_TIMESTAMP(Codes.CODE_INVALID, "isv.invalid-timestamp"), + /** 字符集错误 */ + ISV_INVALID_CHARSET(Codes.CODE_INVALID, "isv.invalid-charset"), + /** 摘要错误 */ + ISV_INVALID_DIGEST(Codes.CODE_INVALID, "isv.invalid-digest"), + /** 解密出错,不支持的加密算法 */ + ISV_DECRYPTION_ERROR_NOT_VALID_ENCRYPT_TYPE(Codes.CODE_INVALID, "isv.decryption-error-not-valid-encrypt-type"), + /** 解密出错, 未配置加密密钥或加密密钥格式错误 */ + ISV_DECRYPTION_ERROR_NOT_VALID_ENCRYPT_KEY(Codes.CODE_INVALID, "isv.decryption-error-not-valid-encrypt-key"), + /** 解密出错,未知异常 */ + ISV_DECRYPTION_ERROR_UNKNOWN(Codes.CODE_INVALID, "isv.decryption-error-unknown"), + /** 验签出错, 未配置对应签名算法的公钥或者证书 */ + ISV_MISSING_SIGNATURE_CONFIG(Codes.CODE_INVALID, "isv.missing-signature-config"), + /** 本接口不支持第三方代理调用 */ + ISV_NOT_SUPPORT_APP_AUTH(Codes.CODE_INVALID, "isv.not-support-app-auth"), + /** 可疑的攻击请求 */ + ISV_SUSPECTED_ATTACK(Codes.CODE_INVALID, "isv.suspected-attack"), + /** 无效的content-type */ + ISV_INVALID_CONTENT_TYPE(Codes.CODE_INVALID, "isv.invalid-content-type"), + + /** 业务处理失败 */ + BIZ_ERROR(Codes.CODE_BIZ, ""), + + /** 请检查配置的账户是否有当前接口权限 */ + ISV_INSUFFICIENT_ISV_PERMISSIONS(Codes.CODE_ISV_PERM, "isv.insufficient-isv-permissions"), + /** 代理的商户没有当前接口权限 */ + ISV_INSUFFICIENT_USER_PERMISSIONS(Codes.CODE_ISV_PERM, "isv.insufficient-user-permissions"), + /** 没有当前接口权限 */ + ISV_ROUTE_NO_PERMISSIONS(Codes.CODE_ISV_PERM, "isv.route-no-permissions"), + /** 禁止访问 */ + ISV_ACCESS_FORBIDDEN(Codes.CODE_ISV_PERM, "isv.access-forbidden"), + /** 禁止IP访问 */ + ISV_IP_FORBIDDEN(Codes.CODE_ISV_PERM, "isv.ip-forbidden"), + + ; + private ErrorMeta errorMeta; + + ErrorEnum(String code, String subCode) { + this.errorMeta = new ErrorMeta("open.error_", code, subCode); + } + + public ErrorMeta getErrorMeta() { + return errorMeta; + } + + private static class Codes { + public static final String CODE_SUCCESS = "10000"; + public static final String CODE_AUTH = "20001"; + public static final String CODE_MISSING = "40001"; + public static final String CODE_INVALID = "40002"; + public static final String CODE_BIZ = "40004"; + public static final String CODE_ISV_PERM = "40006"; + public static final String CODE_UNKNOWN = "20000"; + } + +} diff --git a/sop-index/src/main/java/com/gitee/sop/index/message/ErrorFactory.java b/sop-index/src/main/java/com/gitee/sop/index/message/ErrorFactory.java new file mode 100644 index 00000000..a80f4fd4 --- /dev/null +++ b/sop-index/src/main/java/com/gitee/sop/index/message/ErrorFactory.java @@ -0,0 +1,131 @@ +package com.gitee.sop.index.message; + + +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.support.MessageSourceAccessor; +import org.springframework.context.support.ResourceBundleMessageSource; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +/** + * 负责构建错误消息 + * + * @author tanghc + */ +@Slf4j +public class ErrorFactory { + + private ErrorFactory(){} + + public static final String SYS_ERR = "系统错误"; + + private static final String I18N_OPEN_ERROR = "i18n/open/error"; + public static final String UNDERLINE = "_"; + + private static Set noModuleCache = new HashSet<>(); + + private static Map errorCache = new HashMap<>(64); + + private static List localeList = Arrays.asList(Locale.ENGLISH, Locale.SIMPLIFIED_CHINESE); + + /** + * 错误信息的国际化信息 + */ + private static MessageSourceAccessor errorMessageSourceAccessor; + + /** + * 设置国际化资源信息 + */ + public static void initMessageSource(List isvModules) { + HashSet baseNamesSet = new HashSet<>(); + baseNamesSet.add(I18N_OPEN_ERROR); + + if (!isvModules.isEmpty()) { + baseNamesSet.addAll(isvModules); + } + + String[] totalBaseNames = baseNamesSet.toArray(new String[0]); + + log.info("加载错误码国际化资源:{}", StringUtils.arrayToCommaDelimitedString(totalBaseNames)); + ResourceBundleMessageSource bundleMessageSource = new ResourceBundleMessageSource(); + bundleMessageSource.setBasenames(totalBaseNames); + MessageSourceAccessor messageSourceAccessor = new MessageSourceAccessor(bundleMessageSource); + setErrorMessageSourceAccessor(messageSourceAccessor); + } + + /** + * 通过ErrorMeta,Locale,params构建国际化错误消息 + * + * @param errorMeta 错误信息 + * @param locale 本地化 + * @param params 参数 + * @return 如果没有配置国际化消息,则直接返回errorMeta中的信息 + */ + public static IError getError(ErrorMeta errorMeta, Locale locale, Object... params) { + if (locale == null) { + locale = Locale.SIMPLIFIED_CHINESE; + } + if (!localeList.contains(locale)) { + locale = Locale.ENGLISH; + } + String key = errorMeta.getModulePrefix() + errorMeta.getCode() + errorMeta.getSubCode() + locale.toString(); + IError error = errorCache.get(key); + if (error == null) { + Assert.notNull(locale, "未设置Locale"); + String modulePrefix = errorMeta.getModulePrefix(); + String code = errorMeta.getCode(); + // open.error_20000=Service is temporarily unavailable + String msg = getErrorMessage(modulePrefix + code, locale); + String subCode = errorMeta.getSubCode(); + // open.error_20000_isp.unknown-error=Service is temporarily unavailable + String subMsg = getErrorMessage(modulePrefix + code + UNDERLINE + subCode, locale, params); + if (StringUtils.isEmpty(msg)) { + msg = SYS_ERR; + } + if (StringUtils.isEmpty(subMsg)) { + subMsg = SYS_ERR; + } + // solution暂未实现,如果要实现,可以这样配置: + // open.error_20000_isp.unknown-error_solution=Service is temporarily unavailable + // String solution = getErrorMessage(modulePrefix + code + UNDERLINE + subCode + "_solution", locale, params); + error = new ErrorImpl(code, msg, subCode, subMsg, null); + errorCache.put(key, error); + } + return error; + } + + + public static void setErrorMessageSourceAccessor(MessageSourceAccessor errorMessageSourceAccessor) { + ErrorFactory.errorMessageSourceAccessor = errorMessageSourceAccessor; + } + + /** + * 返回本地化信息 + * + * @param module 错误模块 + * @param locale 本地化 + * @param params 参数 + * @return 返回信息 + */ + public static String getErrorMessage(String module, Locale locale, Object... params) { + if (noModuleCache.contains(module)) { + return null; + } + try { + return errorMessageSourceAccessor.getMessage(module, params, locale); + } catch (Exception e) { + noModuleCache.add(module); + return null; + } + } + + +} diff --git a/sop-index/src/main/java/com/gitee/sop/index/message/ErrorImpl.java b/sop-index/src/main/java/com/gitee/sop/index/message/ErrorImpl.java new file mode 100644 index 00000000..d1717598 --- /dev/null +++ b/sop-index/src/main/java/com/gitee/sop/index/message/ErrorImpl.java @@ -0,0 +1,24 @@ +package com.gitee.sop.index.message; + +import lombok.Data; + +/** + * @author tanghc + */ +@Data +public class ErrorImpl implements IError { + + private String code; + private String msg; + private String sub_code; + private String sub_msg; + private String solution; + + public ErrorImpl(String code, String msg, String sub_code, String sub_msg, String solution) { + this.code = code; + this.msg = msg; + this.sub_code = sub_code; + this.sub_msg = sub_msg; + this.solution = solution; + } +} diff --git a/sop-index/src/main/java/com/gitee/sop/index/message/ErrorMeta.java b/sop-index/src/main/java/com/gitee/sop/index/message/ErrorMeta.java new file mode 100644 index 00000000..d86dd938 --- /dev/null +++ b/sop-index/src/main/java/com/gitee/sop/index/message/ErrorMeta.java @@ -0,0 +1,50 @@ +package com.gitee.sop.index.message; + +import com.gitee.sop.index.exception.ApiException; +import lombok.Getter; + +import java.util.Locale; + +/** + * 错误对象 + * + * @author tanghc + */ +@Getter +public class ErrorMeta { + + private final String modulePrefix; + private final String code; + private final String subCode; + + public ErrorMeta(String modulePrefix, String code, String subCode) { + this.modulePrefix = modulePrefix; + this.code = code; + this.subCode = subCode; + } + + public IError getError(Locale locale) { + return ErrorFactory.getError(this, locale); + } + + /** + * 返回网关exception + * + * @param params 参数 + * @return 返回exception + */ + public ApiException getException(Object... params) { + if (params != null && params.length == 1) { + Object param = params[0]; + if (param instanceof Throwable) { + return new ApiException((Throwable) param, this); + } + } + return new ApiException(this, params); + } + + @Override + public String toString() { + return modulePrefix + code + "_" + subCode; + } +} diff --git a/sop-index/src/main/java/com/gitee/sop/index/message/IError.java b/sop-index/src/main/java/com/gitee/sop/index/message/IError.java new file mode 100644 index 00000000..3a332d2f --- /dev/null +++ b/sop-index/src/main/java/com/gitee/sop/index/message/IError.java @@ -0,0 +1,48 @@ +package com.gitee.sop.index.message; + +/** + * 定义错误返回 + * code(返回码) + * msg(返回码描述) + * sub_code(明细返回码) + * sub_msg(明细返回码描述) + * 解决方案 + * @author tanghc + */ +public interface IError { + /** + * 获取网关状态码 + * + * @return 返回状态码 + */ + String getCode(); + + /** + * 获取网关错误信息 + * + * @return 返回错误信息 + */ + String getMsg(); + + /** + * sub_code(明细返回码) + * @return sub_code(明细返回码) + */ + String getSub_code(); + + /** + * sub_msg(明细返回码描述) + * @return sub_msg(明细返回码描述) + */ + String getSub_msg(); + + /** + * 解决方案 + * @return 解决方案 + */ + String getSolution(); + + + + +} diff --git a/sop-index/src/main/java/com/gitee/sop/index/service/GenericServiceInvoker.java b/sop-index/src/main/java/com/gitee/sop/index/service/GenericServiceInvoker.java new file mode 100644 index 00000000..85984d9b --- /dev/null +++ b/sop-index/src/main/java/com/gitee/sop/index/service/GenericServiceInvoker.java @@ -0,0 +1,75 @@ +package com.gitee.sop.index.service; + +import org.apache.dubbo.config.ApplicationConfig; +import org.apache.dubbo.config.ReferenceConfig; +import org.apache.dubbo.config.RegistryConfig; +import org.apache.dubbo.rpc.service.GenericService; +import org.springframework.beans.factory.annotation.Value; + +import javax.annotation.PostConstruct; + +/** + * dubbo泛化调用 + * + * @author 六如 + */ +public class GenericServiceInvoker { + private ApplicationConfig applicationConfig; + + @Value("${nacos.host:localhost:8848}") + private String nacosHost; + + @Value("${nacos.group:DEFAULT}") + private String group; + + @Value("${spring.application.name}") + private String appName; + + @Value("${generic.timeout:5000}") + private int timeout; + + @PostConstruct + public void init() { + RegistryConfig registryConfig = buildRegistryConfig(nacosHost, group); + applicationConfig = new ApplicationConfig(); + applicationConfig.setName(appName + "-generic"); + applicationConfig.setRegistry(registryConfig); + } + + private RegistryConfig buildRegistryConfig(String nacosHost, String group) { + RegistryConfig config = new RegistryConfig(); + config.setGroup(group); + config.setAddress("nacos://" + nacosHost); + return config; + } + + public Object invoke(String interfaceName, String method, String[] parameterTypes, Object[] params) { + ReferenceConfig reference = new ReferenceConfig<>(); + reference.setGeneric("true"); + reference.setApplication(applicationConfig); + reference.setInterface(interfaceName); + reference.setGroup(group); + reference.setTimeout(timeout); + reference.setSticky(false); + reference.setCheck(false); + try { + removeGenericSymbol(parameterTypes); + GenericService genericService = reference.get(); + return genericService.$invoke(method, parameterTypes, params); + } finally { + reference.destroy(); + } + } + + /** + * remove generic from parameterTypes + */ + private void removeGenericSymbol(String[] parameterTypes) { + for (int i = 0; i < parameterTypes.length; i++) { + int index = parameterTypes[i].indexOf('<'); + if (index > -1) { + parameterTypes[i] = parameterTypes[i].substring(0, index); + } + } + } +} diff --git a/sop-index/src/main/java/com/gitee/sop/index/service/RouteService.java b/sop-index/src/main/java/com/gitee/sop/index/service/RouteService.java new file mode 100644 index 00000000..df4ca25b --- /dev/null +++ b/sop-index/src/main/java/com/gitee/sop/index/service/RouteService.java @@ -0,0 +1,28 @@ +package com.gitee.sop.index.service; + +import com.gitee.sop.index.controller.param.ApiRequest; +import com.gitee.sop.index.controller.param.ApiResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** + * 接口路由 + * + * @author 六如 + */ +@Service +public class RouteService { + + @Autowired + private ValidateService validateService; + + public ApiResponse route(ApiRequest apiRequest) { + // 签名校验 + validateService.validate(apiRequest); + + return null; + } + + + +} diff --git a/sop-index/src/main/java/com/gitee/sop/index/service/ValidateService.java b/sop-index/src/main/java/com/gitee/sop/index/service/ValidateService.java new file mode 100644 index 00000000..c5a9ae4a --- /dev/null +++ b/sop-index/src/main/java/com/gitee/sop/index/service/ValidateService.java @@ -0,0 +1,19 @@ +package com.gitee.sop.index.service; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.gitee.sop.index.controller.param.ApiRequest; +import org.springframework.stereotype.Service; + +/** + * @author 六如 + */ +@Service +public class ValidateService { + + public void validate(ApiRequest apiRequest) { + String jsonString = JSON.toJSONString(apiRequest); + JSONObject jsonObject = JSON.parseObject(jsonString); + + } +} diff --git a/sop-index/src/main/java/com/gitee/sop/index/service/validate/AbstractSigner.java b/sop-index/src/main/java/com/gitee/sop/index/service/validate/AbstractSigner.java new file mode 100644 index 00000000..193a6f22 --- /dev/null +++ b/sop-index/src/main/java/com/gitee/sop/index/service/validate/AbstractSigner.java @@ -0,0 +1,44 @@ +package com.gitee.sop.index.service.validate; + +import com.gitee.sop.gatewaycommon.message.ErrorEnum; +import com.gitee.sop.gatewaycommon.param.ApiParam; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +/** + * @author tanghc + */ +@Slf4j +public abstract class AbstractSigner implements Signer { + + /** + * 构建服务端签名串 + * + * @param params 接口参数 + * @param secret 秘钥 + * @return 返回服务端签名串 + */ + protected abstract String buildServerSign(ApiParam params, String secret); + + @Override + public boolean checkSign(ApiParam apiParam, String secret) { + String clientSign = apiParam.fetchSignAndRemove(); + if (StringUtils.isBlank(clientSign)) { + throw ErrorEnum.ISV_MISSING_SIGNATURE.getErrorMeta().getException(); + } + String serverSign = buildServerSign(apiParam, secret); + return clientSign.equals(serverSign); + } + + protected static String byte2hex(byte[] bytes) { + StringBuilder sign = new StringBuilder(); + for (int i = 0; i < bytes.length; i++) { + String hex = Integer.toHexString(bytes[i] & 0xFF); + if (hex.length() == 1) { + sign.append("0"); + } + sign.append(hex); + } + return sign.toString(); + } +} diff --git a/sop-index/src/main/java/com/gitee/sop/index/service/validate/ApiEncrypter.java b/sop-index/src/main/java/com/gitee/sop/index/service/validate/ApiEncrypter.java new file mode 100644 index 00000000..c590017f --- /dev/null +++ b/sop-index/src/main/java/com/gitee/sop/index/service/validate/ApiEncrypter.java @@ -0,0 +1,60 @@ +package com.gitee.sop.index.service.validate; + +import org.apache.commons.codec.digest.DigestUtils; +import com.gitee.sop.gatewaycommon.util.AESUtil; +import com.gitee.sop.gatewaycommon.util.RSANewUtil; +import com.gitee.sop.gatewaycommon.util.RSAUtil; + +/** + * 负责各类加解密 + * @author tanghc + * + */ +public class ApiEncrypter implements Encrypter { + + @Override + public String aesEncryptToHex(String content, String password) throws Exception { + return AESUtil.encryptToHex(content, password); + } + + @Override + public String aesDecryptFromHex(String hex, String password) throws Exception { + return AESUtil.decryptFromHex(hex, password); + } + + @Override + public String aesEncryptToBase64String(String content, String password) throws Exception { + return AESUtil.encryptToBase64String(content, password); + } + + @Override + public String aesDecryptFromBase64String(String base64String, String password) throws Exception { + return AESUtil.decryptFromBase64String(base64String, password); + } + + @Override + public String rsaDecryptByPrivateKey(String data, String privateKey) throws Exception { + return RSAUtil.decryptByPrivateKey(data, privateKey); + } + + @Override + public String rsaEncryptByPrivateKey(String data, String privateKey) throws Exception { + return RSAUtil.encryptByPrivateKey(data, privateKey); + } + + @Override + public String rsaDecryptByPrivateKeyNew(String data, String privateKey) throws Exception { + return RSANewUtil.decryptByPrivateKey(data, privateKey); + } + + @Override + public String rsaEncryptByPrivateKeyNew(String data, String privateKey) throws Exception { + return RSANewUtil.encryptByPrivateKey(data, privateKey); + } + + @Override + public String md5(String value) { + return DigestUtils.md5Hex(value); + } + +} diff --git a/sop-index/src/main/java/com/gitee/sop/index/service/validate/ApiSigner.java b/sop-index/src/main/java/com/gitee/sop/index/service/validate/ApiSigner.java new file mode 100644 index 00000000..5c771411 --- /dev/null +++ b/sop-index/src/main/java/com/gitee/sop/index/service/validate/ApiSigner.java @@ -0,0 +1,55 @@ +package com.gitee.sop.index.service.validate; + + +import com.gitee.sop.gatewaycommon.message.ErrorEnum; +import com.gitee.sop.gatewaycommon.param.ApiParam; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * 签名验证实现 + * + * @author tanghc + */ +public class ApiSigner extends AbstractSigner { + + private Map signEncipherMap = new HashMap<>(); + + public ApiSigner() { + signEncipherMap.put("md5", new SignEncipherMD5()); + signEncipherMap.put("hmac", new SignEncipherHMAC_MD5()); + } + + + @Override + public String buildServerSign(ApiParam param, String secret) { + String signMethod = param.fetchSignMethod(); + SignEncipher signEncipher = signEncipherMap.get(signMethod); + if (signEncipher == null) { + throw ErrorEnum.ISV_INVALID_SIGNATURE_TYPE.getErrorMeta().getException(signMethod); + } + + // 第一步:参数排序 + Set keySet = param.keySet(); + List paramNames = new ArrayList<>(keySet); + Collections.sort(paramNames); + + // 第二步:把所有参数名和参数值串在一起 + StringBuilder paramNameValue = new StringBuilder(); + for (String paramName : paramNames) { + paramNameValue.append(paramName).append(param.get(paramName)); + } + + // 第三步:使用MD5/HMAC加密 + String source = paramNameValue.toString(); + byte[] bytes = signEncipher.encrypt(source, secret); + + // 第四步:把二进制转化为大写的十六进制 + return byte2hex(bytes).toUpperCase(); + } +} diff --git a/sop-index/src/main/java/com/gitee/sop/index/service/validate/ApiValidator.java b/sop-index/src/main/java/com/gitee/sop/index/service/validate/ApiValidator.java new file mode 100644 index 00000000..c00d7cce --- /dev/null +++ b/sop-index/src/main/java/com/gitee/sop/index/service/validate/ApiValidator.java @@ -0,0 +1,282 @@ +package com.gitee.sop.index.service.validate; + +import com.gitee.sop.index.common.ParamNames; +import com.gitee.sop.index.controller.param.ApiRequest; +import com.gitee.sop.index.message.ErrorEnum; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.apache.tomcat.util.http.fileupload.UploadContext; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.util.StringUtils; +import org.springframework.util.unit.DataSize; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +/** + * 负责校验,校验工作都在这里 + * + * @author tanghc + */ +@Slf4j +@Getter +public class ApiValidator implements Validator { + + private static final int MILLISECOND_OF_ONE_SECOND = 1000; + private static final int STATUS_FORBIDDEN = 2; + + private static List FORMAT_LIST = Arrays.asList("json", "xml"); + +// @Autowired +// private IsvManager isvManager; +// +// @Autowired +// private IsvRoutePermissionManager isvRoutePermissionManager; +// +// @Autowired +// private IPBlacklistManager ipBlacklistManager; +// +// @Autowired +// private RouteConfigManager routeConfigManager; + + /** + * 单个文件大小 + */ + @Value("${upload.max-file-size:${spring.servlet.multipart.max-file-size:10MB}}") + private String maxFileSize; + + + @Override + public void validate(ApiRequest param) { +// checkIP(param); +// TargetRoute targetRoute = checkEnable(param); +// initFields(targetRoute, param); +// ApiConfig apiConfig = ApiContext.getApiConfig(); + checkAppKey(param); +// if (apiConfig.isIgnoreValidate() +// || BooleanUtils.toBoolean(targetRoute.getRouteDefinition().getIgnoreValidate())) { +// if (log.isDebugEnabled()) { +// log.debug("忽略签名校验, name:{}, version:{}", param.fetchName(), param.fetchVersion()); +// } +// } else { +// checkSign(param); +// } + checkSign(param); + checkTimeout(param); + checkFormat(param); + checkUploadFile(param); + checkPermission(param); + checkToken(param); + } + + /** + * 是否在IP黑名单中 + * + * @param param 接口参数 + */ +// protected void checkIP(ApiRequest param) { +// String ip = param.fetchIp(); +// if (ipBlacklistManager.contains(ip)) { +// throw ErrorEnum.ISV_IP_FORBIDDEN.getErrorMeta().getException(); +// } +// } + + /** + * 检测能否访问 + * + * @param param 接口参数 + */ +// protected TargetRoute checkEnable(ApiRequest param) { +// String name = param.getMethod(); +// if (name == null) { +// throw ErrorEnum.ISV_MISSING_METHOD.getErrorMeta().getException(); +// } +// String version = param.fetchVersion(); +// if (version == null) { +// throw ErrorEnum.ISV_MISSING_VERSION.getErrorMeta().getException(); +// } +// String routeId = param.fetchNameVersion(); +// // 检查路由是否存在 +// TargetRoute targetRoute = RouteRepositoryContext.getTargetRoute(routeId); +// if (targetRoute == null) { +// throw ErrorEnum.ISV_INVALID_METHOD.getErrorMeta().getException(); +// } +// // 检查路由是否启用 +// RouteConfig routeConfig = routeConfigManager.get(routeId); +// if (!routeConfig.enable()) { +// throw ErrorEnum.ISP_API_DISABLED.getErrorMeta().getException(); +// } +// return targetRoute; +// } + + private void initFields(TargetRoute targetRoute, ApiRequest apiParam) { + apiParam.setServiceId(targetRoute.getServiceDefinition().getServiceId()); + boolean mergeResult; + Boolean defaultSetting = ApiContext.getApiConfig().getMergeResult(); + if (defaultSetting != null) { + mergeResult = defaultSetting; + } else { + RouteDefinition routeDefinition = targetRoute.getRouteDefinition(); + mergeResult = routeDefinition == null || BooleanUtils.toBoolean(routeDefinition.getMergeResult()); + } + apiParam.setMergeResult(mergeResult); + } + + /** + * 校验上传文件内容 + * + * @param param + */ + protected void checkUploadFile(ApiRequest param) { + UploadContext uploadContext = param.fetchUploadContext(); + if (uploadContext != null) { + try { + List files = uploadContext.getAllFile(); + for (MultipartFile file : files) { + checkSingleFileSize(file); + checkFileMd5(param, file); + } + } catch (IOException e) { + log.error("验证上传文件MD5错误", e); + throw ErrorEnum.ISV_UPLOAD_FAIL.getErrorMeta().getException(); + } + } + } + + private void checkFileMd5(ApiRequest param, MultipartFile file) throws IOException { + // 客户端传来的文件md5 + String clientMd5 = param.getString(file.getName()); + if (clientMd5 != null) { + String fileMd5 = DigestUtils.md5Hex(file.getBytes()); + if (!clientMd5.equals(fileMd5)) { + throw ErrorEnum.ISV_UPLOAD_FAIL.getErrorMeta().getException(); + } + } + } + + /** + * 校验单个文件大小 + * + * @param file 文件 + */ + private void checkSingleFileSize(MultipartFile file) { + long fileSize = file.getSize(); + if (fileSize > DataSize.parse(maxFileSize).toBytes()) { + throw ErrorEnum.ISV_INVALID_FILE_SIZE.getErrorMeta().getException(file.getName(), maxFileSize); + } + } + + protected void checkTimeout(ApiRequest param) { + int timeoutSeconds = ApiContext.getApiConfig().getTimeoutSeconds(); + // 如果设置为0,表示不校验 + if (timeoutSeconds == 0) { + return; + } + if (timeoutSeconds < 0) { + throw new IllegalArgumentException("服务端timeoutSeconds设置错误"); + } + String requestTime = param.fetchTimestamp(); + try { + Date requestDate = new SimpleDateFormat(ParamNames.TIMESTAMP_PATTERN).parse(requestTime); + long requestMilliseconds = requestDate.getTime(); + if (System.currentTimeMillis() - requestMilliseconds > timeoutSeconds * MILLISECOND_OF_ONE_SECOND) { + throw ErrorEnum.ISV_INVALID_TIMESTAMP.getErrorMeta().getException(); + } + } catch (ParseException e) { + throw ErrorEnum.ISV_INVALID_TIMESTAMP.getErrorMeta().getException(param.fetchNameVersion()); + } + } + + protected void checkAppKey(ApiRequest param) { + if (StringUtils.isEmpty(param.fetchAppKey())) { + throw ErrorEnum.ISV_MISSING_APP_ID.getErrorMeta().getException(); + } + Isv isv = isvManager.getIsv(param.fetchAppKey()); + // 没有用户 + if (isv == null) { + throw ErrorEnum.ISV_INVALID_APP_ID.getErrorMeta().getException(); + } + // 禁止访问 + if (isv.getStatus() == null || isv.getStatus() == STATUS_FORBIDDEN) { + throw ErrorEnum.ISV_ACCESS_FORBIDDEN.getErrorMeta().getException(); + } + } + + protected void checkSign(ApiRequest param) { + String clientSign = param.getSign(); + try { + if (StringUtils.isEmpty(clientSign)) { + throw ErrorEnum.ISV_MISSING_SIGNATURE.getErrorMeta().getException(param.takeApiName(), ParamNames.SIGN_NAME); + } + ApiConfig apiConfig = ApiContext.getApiConfig(); + // 根据appId获取秘钥 + Isv isvInfo = isvManager.getIsv(param.fetchAppKey()); + String secret = isvInfo.getSecretInfo(); + if (StringUtils.isEmpty(secret)) { + throw ErrorEnum.ISV_MISSING_SIGNATURE_CONFIG.getErrorMeta().getException(); + } + Signer signer = apiConfig.getSigner(); + // 错误的sign + if (!signer.checkSign(param, secret)) { + throw ErrorEnum.ISV_INVALID_SIGNATURE.getErrorMeta().getException(param.fetchNameVersion()); + } + } finally { + // 校验过程中会移除sign,这里需要重新设置进去 + param.setSign(clientSign); + } + } + + + protected void checkFormat(ApiRequest param) { + String format = param.fetchFormat(); + boolean contains = FORMAT_LIST.contains(format.toLowerCase()); + + if (!contains) { + throw ErrorEnum.ISV_INVALID_FORMAT.getErrorMeta().getException(param.fetchNameVersion(), format); + } + } + + /** + * 校验访问权限 + * + * @param apiParam 参数 + */ + protected void checkPermission(ApiRequest apiParam) { + String routeId = apiParam.fetchNameVersion(); + TargetRoute targetRoute = RouteRepositoryContext.getRouteRepository().get(routeId); + RouteDefinition routeDefinition = targetRoute.getRouteDefinition(); + boolean needCheckPermission = BooleanUtils.toBoolean(routeDefinition.getPermission()); + if (needCheckPermission) { + String appKey = apiParam.fetchAppKey(); + boolean hasPermission = isvRoutePermissionManager.hasPermission(appKey, routeId); + if (!hasPermission) { + throw ErrorEnum.ISV_ROUTE_NO_PERMISSIONS.getErrorMeta().getException(); + } + } + } + + /** + * 校验token + * + * @param apiParam 参数 + */ + protected void checkToken(ApiRequest apiParam) { + String routeId = apiParam.fetchNameVersion(); + TargetRoute targetRoute = RouteRepositoryContext.getRouteRepository().get(routeId); + RouteDefinition routeDefinition = targetRoute.getRouteDefinition(); + boolean needToken = BooleanUtils.toBoolean(routeDefinition.getNeedToken()); + if (needToken) { + TokenValidator tokenValidator = ApiConfig.getInstance().getTokenValidator(); + boolean rightToken = tokenValidator.validateToken(apiParam); + if (!rightToken) { + throw ErrorEnum.AOP_INVALID_APP_AUTH_TOKEN.getErrorMeta().getException(); + } + } + } + +} diff --git a/sop-index/src/main/java/com/gitee/sop/index/service/validate/Encrypter.java b/sop-index/src/main/java/com/gitee/sop/index/service/validate/Encrypter.java new file mode 100644 index 00000000..3e5a8297 --- /dev/null +++ b/sop-index/src/main/java/com/gitee/sop/index/service/validate/Encrypter.java @@ -0,0 +1,95 @@ +package com.gitee.sop.index.service.validate; + +/** + * 负责加解密 + * + * @author tanghc + */ +public interface Encrypter { + + /** + * AES文本加密 + * + * @param content 明文 + * @param password 密码 + * @return 返回16进制内容 + * @throws Exception + */ + String aesEncryptToHex(String content, String password) throws Exception; + + /** + * AES文本解密 + * + * @param hex 待解密文本,16进制内容 + * @param password 密码 + * @return 返回明文 + * @throws Exception + */ + String aesDecryptFromHex(String hex, String password) throws Exception; + + /** + * AES文本加密 + * + * @param content 明文 + * @param password 密码 + * @return 返回base64内容 + * @throws Exception + */ + String aesEncryptToBase64String(String content, String password) throws Exception; + + /** + * AES文本解密 + * + * @param base64String 待解密文本,16进制内容 + * @param password 密码 + * @return 返回明文 + * @throws Exception + */ + String aesDecryptFromBase64String(String base64String, String password) throws Exception; + + /** + * RSA私钥解密 + * + * @param data 解密内容 + * @param privateKey 私钥 + * @return 返回明文 + * @throws Exception + */ + String rsaDecryptByPrivateKey(String data, String privateKey) throws Exception; + + /** + * 新版rsa私钥解密 + * @param data 解密内容 + * @param privateKey 私钥 + * @return 返回明文 + * @throws Exception + */ + String rsaDecryptByPrivateKeyNew(String data, String privateKey) throws Exception; + + /** + * RSA私钥加密 + * + * @param data 明文 + * @param privateKey 私钥 + * @return 返回密文 + * @throws Exception + */ + String rsaEncryptByPrivateKey(String data, String privateKey) throws Exception; + + /** + * 新版rsa私钥加密 + * @param data 明文 + * @param privateKey 私钥 + * @return 返回密文 + * @throws Exception + */ + String rsaEncryptByPrivateKeyNew(String data, String privateKey) throws Exception; + + /** + * md5加密,全部小写 + * + * @param value + * @return 返回md5内容 + */ + String md5(String value); +} diff --git a/sop-index/src/main/java/com/gitee/sop/index/service/validate/SignConfig.java b/sop-index/src/main/java/com/gitee/sop/index/service/validate/SignConfig.java new file mode 100644 index 00000000..ab8703e2 --- /dev/null +++ b/sop-index/src/main/java/com/gitee/sop/index/service/validate/SignConfig.java @@ -0,0 +1,38 @@ +package com.gitee.sop.index.service.validate; + +import com.gitee.sop.gatewaycommon.bean.SopConstants; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; + +/** + * @author tanghc + */ +public class SignConfig { + private static volatile Wrapper wrapper = new Wrapper() {}; + + public static void enableUrlencodeMode() { + wrapper = new Wrapper() { + @Override + public String wrapVal(Object val) { + String valStr = String.valueOf(val); + try { + return URLEncoder.encode(valStr, SopConstants.UTF8); + } catch (UnsupportedEncodingException e) { + return valStr; + } + } + }; + } + + public static String wrapVal(Object val) { + return wrapper.wrapVal(val); + } + + interface Wrapper { + default String wrapVal(Object val) { + return String.valueOf(val); + } + } + +} diff --git a/sop-index/src/main/java/com/gitee/sop/index/service/validate/SignEncipher.java b/sop-index/src/main/java/com/gitee/sop/index/service/validate/SignEncipher.java new file mode 100644 index 00000000..acc1119f --- /dev/null +++ b/sop-index/src/main/java/com/gitee/sop/index/service/validate/SignEncipher.java @@ -0,0 +1,14 @@ +package com.gitee.sop.index.service.validate; + +/** + * @author tanghc + */ +public interface SignEncipher { + /** + * 签名的摘要算法 + * @param input 待签名数据 + * @param secret 秘钥 + * @return 返回加密后的数据 + */ + byte[] encrypt(String input, String secret); +} diff --git a/sop-index/src/main/java/com/gitee/sop/index/service/validate/SignEncipherHMAC_MD5.java b/sop-index/src/main/java/com/gitee/sop/index/service/validate/SignEncipherHMAC_MD5.java new file mode 100644 index 00000000..7c5a027f --- /dev/null +++ b/sop-index/src/main/java/com/gitee/sop/index/service/validate/SignEncipherHMAC_MD5.java @@ -0,0 +1,37 @@ +package com.gitee.sop.index.service.validate; + +import com.gitee.sop.gatewaycommon.bean.SopConstants; +import com.gitee.sop.gatewaycommon.message.ErrorEnum; +import lombok.extern.slf4j.Slf4j; + +import javax.crypto.Mac; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +/** + * HMAC_MD5加密 + * @author tanghc + */ +@Slf4j +public class SignEncipherHMAC_MD5 implements SignEncipher { + + public static final String HMAC_MD5 = "HmacMD5"; + + @Override + public byte[] encrypt(String input, String secret) { + try { + SecretKey secretKey = new SecretKeySpec(secret.getBytes(SopConstants.CHARSET_UTF8), HMAC_MD5); + Mac mac = Mac.getInstance(secretKey.getAlgorithm()); + mac.init(secretKey); + return mac.doFinal(input.getBytes(SopConstants.CHARSET_UTF8)); + } catch (NoSuchAlgorithmException e) { + log.error("HMAC_MD5加密加密失败NoSuchAlgorithmException", e); + throw ErrorEnum.ISV_INVALID_SIGNATURE.getErrorMeta().getException(); + } catch (InvalidKeyException e) { + log.error("HMAC_MD5加密加密失败InvalidKeyException", e); + throw ErrorEnum.ISV_INVALID_SIGNATURE.getErrorMeta().getException(); + } + } +} diff --git a/sop-index/src/main/java/com/gitee/sop/index/service/validate/SignEncipherMD5.java b/sop-index/src/main/java/com/gitee/sop/index/service/validate/SignEncipherMD5.java new file mode 100644 index 00000000..e7a6cd9d --- /dev/null +++ b/sop-index/src/main/java/com/gitee/sop/index/service/validate/SignEncipherMD5.java @@ -0,0 +1,16 @@ +package com.gitee.sop.index.service.validate; + +import org.apache.commons.codec.digest.DigestUtils; + +import java.nio.charset.StandardCharsets; + +/** + * @author tanghc + */ +public class SignEncipherMD5 implements SignEncipher { + @Override + public byte[] encrypt(String input, String secret) { + String source = secret + input + secret; + return DigestUtils.md5(source.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/sop-index/src/main/java/com/gitee/sop/index/service/validate/Signer.java b/sop-index/src/main/java/com/gitee/sop/index/service/validate/Signer.java new file mode 100644 index 00000000..0f926da0 --- /dev/null +++ b/sop-index/src/main/java/com/gitee/sop/index/service/validate/Signer.java @@ -0,0 +1,22 @@ +package com.gitee.sop.index.service.validate; + +import com.gitee.sop.index.controller.param.ApiRequest; + +import javax.servlet.http.HttpServletRequest; + +/** + * 负责签名校验 + * @author tanghc + * + */ +public interface Signer { + + /** + * 签名校验 + * @param apiParam 参数 + * @param secret 秘钥 + * @return true签名正确 + */ + boolean checkSign(ApiRequest apiParam, String secret); + +} diff --git a/sop-index/src/main/java/com/gitee/sop/index/service/validate/TokenValidator.java b/sop-index/src/main/java/com/gitee/sop/index/service/validate/TokenValidator.java new file mode 100644 index 00000000..14186443 --- /dev/null +++ b/sop-index/src/main/java/com/gitee/sop/index/service/validate/TokenValidator.java @@ -0,0 +1,11 @@ +package com.gitee.sop.index.service.validate; + +import com.gitee.sop.gatewaycommon.param.ApiParam; + +/** + * @author tanghc + */ +@FunctionalInterface +public interface TokenValidator { + boolean validateToken(ApiParam apiParam); +} diff --git a/sop-index/src/main/java/com/gitee/sop/index/service/validate/Validator.java b/sop-index/src/main/java/com/gitee/sop/index/service/validate/Validator.java new file mode 100644 index 00000000..47aac94c --- /dev/null +++ b/sop-index/src/main/java/com/gitee/sop/index/service/validate/Validator.java @@ -0,0 +1,18 @@ +package com.gitee.sop.index.service.validate; + +import com.gitee.sop.index.controller.param.ApiRequest; + +/** + * 校验接口 + * + * @author tanghc + * + */ +public interface Validator { + /** + * 接口验证 + * @param apiRequest 接口参数 + */ + void validate(ApiRequest apiRequest); + +} diff --git a/sop-index/src/main/java/com/gitee/sop/index/service/validate/alipay/AlipayConstants.java b/sop-index/src/main/java/com/gitee/sop/index/service/validate/alipay/AlipayConstants.java new file mode 100644 index 00000000..37bfc6c2 --- /dev/null +++ b/sop-index/src/main/java/com/gitee/sop/index/service/validate/alipay/AlipayConstants.java @@ -0,0 +1,97 @@ +/** + * Alipay.com Inc. + * Copyright (c) 2004-2012 All Rights Reserved. + */ +package com.gitee.sop.index.service.validate.alipay; + +/** + * + * @author runzhi + */ +public class AlipayConstants { + + public static final String SIGN_TYPE = "sign_type"; + + public static final String SIGN_TYPE_RSA = "RSA"; + + /** + * sha256WithRsa 算法请求类型 + */ + public static final String SIGN_TYPE_RSA2 = "RSA2"; + + public static final String SIGN_ALGORITHMS = "SHA1WithRSA"; + + public static final String SIGN_SHA256RSA_ALGORITHMS = "SHA256WithRSA"; + + public static final String ENCRYPT_TYPE_AES = "AES"; + + public static final String APP_ID = "app_id"; + + public static final String FORMAT = "format"; + + public static final String METHOD = "method"; + + public static final String TIMESTAMP = "timestamp"; + + public static final String VERSION = "version"; + + public static final String SIGN = "sign"; + + public static final String ALIPAY_SDK = "alipay_sdk"; + + public static final String ACCESS_TOKEN = "auth_token"; + + public static final String APP_AUTH_TOKEN = "app_auth_token"; + + public static final String TERMINAL_TYPE = "terminal_type"; + + public static final String TERMINAL_INFO = "terminal_info"; + + public static final String CHARSET = "charset"; + + public static final String NOTIFY_URL = "notify_url"; + + public static final String RETURN_URL = "return_url"; + + public static final String ENCRYPT_TYPE = "encrypt_type"; + + //-----===-------/// + + public static final String BIZ_CONTENT_KEY = "biz_content"; + + /** 默认时间格式 **/ + public static final String DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss"; + + /** Date默认时区 **/ + public static final String DATE_TIMEZONE = "GMT+8"; + + /** UTF-8字符集 **/ + public static final String CHARSET_UTF8 = "UTF-8"; + + /** GBK字符集 **/ + public static final String CHARSET_GBK = "GBK"; + + /** JSON 应格式 */ + public static final String FORMAT_JSON = "json"; + + /** XML 应格式 */ + public static final String FORMAT_XML = "xml"; + + /** SDK版本号 */ + public static final String SDK_VERSION = "alipay-sdk-java-3.6.0.ALL"; + + public static final String PROD_CODE = "prod_code"; + + /** 老版本失败节点 */ + public static final String ERROR_RESPONSE = "error_response"; + + /** 新版本节点后缀 */ + public static final String RESPONSE_SUFFIX = "_response"; + + /** 加密后XML返回报文的节点名字 */ + public static final String RESPONSE_XML_ENCRYPT_NODE_NAME = "response_encrypted"; + + /** 批量请求id **/ + public static final String BATCH_REQUEST_ID = "batch_request_id"; + +} diff --git a/sop-index/src/main/java/com/gitee/sop/index/service/validate/alipay/AlipaySignature.java b/sop-index/src/main/java/com/gitee/sop/index/service/validate/alipay/AlipaySignature.java new file mode 100644 index 00000000..97aeba55 --- /dev/null +++ b/sop-index/src/main/java/com/gitee/sop/index/service/validate/alipay/AlipaySignature.java @@ -0,0 +1,618 @@ +/** + * Alipay.com Inc. + * Copyright (c) 2004-2012 All Rights Reserved. + */ +package com.gitee.sop.index.service.validate.alipay; + + +import com.gitee.sop.index.message.ErrorEnum; +import com.gitee.sop.index.service.validate.SignConfig; +import org.apache.commons.codec.binary.Base64; + +import javax.crypto.Cipher; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.StringWriter; +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * @author runzhi + */ +public class AlipaySignature { + + /** + * RSA最大加密明文大小 + */ + private static final int MAX_ENCRYPT_BLOCK = 117; + + /** + * RSA最大解密密文大小 + */ + private static final int MAX_DECRYPT_BLOCK = 128; + + + /** + * @param sortedParams + * @return + */ + public static String getSignContent(Map sortedParams) { + StringBuffer content = new StringBuffer(); + List keys = new ArrayList(sortedParams.keySet()); + Collections.sort(keys); + int index = 0; + for (int i = 0; i < keys.size(); i++) { + String key = keys.get(i); + String value = String.valueOf(sortedParams.get(key)); + if (StringUtils.areNotEmpty(key, value)) { + content.append((index == 0 ? "" : "&") + key + "=" + value); + index++; + } + } + return content.toString(); + } + + /** + * rsa内容签名 + * + * @param content + * @param publicKey + * @param charset + * @return + */ + public static String rsaSign(String content, String publicKey, String charset, + String signType) { + + if (AlipayConstants.SIGN_TYPE_RSA.equals(signType)) { + + return rsaSign(content, publicKey, charset); + } else if (AlipayConstants.SIGN_TYPE_RSA2.equals(signType)) { + + return rsa256Sign(content, publicKey, charset); + } else { + throw ErrorEnum.ISV_INVALID_SIGNATURE_TYPE.getErrorMeta().getException(); +// throw new AlipayApiException("Sign Type is Not Support : signType=" + signType); + } + + } + + /** + * sha256WithRsa 加签 + * + * @param content + * @param privateKey + * @param charset + * @return + */ + public static String rsa256Sign(String content, String privateKey, + String charset) { + + try { + PrivateKey priKey = getPrivateKeyFromPKCS8(AlipayConstants.SIGN_TYPE_RSA, + new ByteArrayInputStream(privateKey.getBytes())); + + java.security.Signature signature = java.security.Signature + .getInstance(AlipayConstants.SIGN_SHA256RSA_ALGORITHMS); + + signature.initSign(priKey); + + if (StringUtils.isEmpty(charset)) { + signature.update(content.getBytes()); + } else { + signature.update(content.getBytes(charset)); + } + + byte[] signed = signature.sign(); + + return new String(Base64.encodeBase64(signed)); + } catch (Exception e) { + throw ErrorEnum.ISV_INVALID_SIGNATURE.getErrorMeta().getException(e); +// throw new AlipayApiException("RSAcontent = " + content + "; charset = " + charset, e); + } + + } + + /** + * sha1WithRsa 加签 + * + * @param content + * @param publicKey + * @param charset + * @return + */ + public static String rsaSign(String content, String publicKey, + String charset) { + try { + PrivateKey priKey = getPrivateKeyFromPKCS8(AlipayConstants.SIGN_TYPE_RSA, + new ByteArrayInputStream(publicKey.getBytes())); + + java.security.Signature signature = java.security.Signature + .getInstance(AlipayConstants.SIGN_ALGORITHMS); + + signature.initSign(priKey); + + if (StringUtils.isEmpty(charset)) { + signature.update(content.getBytes()); + } else { + signature.update(content.getBytes(charset)); + } + + byte[] signed = signature.sign(); + + return new String(Base64.encodeBase64(signed)); + } catch (InvalidKeySpecException ie) { + throw ErrorEnum.ISV_INVALID_SIGNATURE_TYPE.getErrorMeta().getException(ie); +// throw new AlipayApiException("RSA私钥格式不正确,请检查是否正确配置了PKCS8格式的私钥", ie); + } catch (Exception e) { + throw ErrorEnum.ISV_INVALID_SIGNATURE.getErrorMeta().getException(e); +// throw new AlipayApiException("RSAcontent = " + content + "; charset = " + charset, e); + } + } + + public static String rsaSign(Map params, String publicKey, + String charset, String signType) { + String signContent = getSignContent(params); + + return rsaSign(signContent, publicKey, charset, signType); + + } + + public static PrivateKey getPrivateKeyFromPKCS8(String algorithm, + InputStream ins) throws Exception { + if (ins == null || StringUtils.isEmpty(algorithm)) { + return null; + } + + KeyFactory keyFactory = KeyFactory.getInstance(algorithm); + + byte[] encodedKey = StreamUtil.readText(ins, "UTF-8").getBytes(); + + encodedKey = Base64.decodeBase64(encodedKey); + + return keyFactory.generatePrivate(new PKCS8EncodedKeySpec(encodedKey)); + } + + public static String getSignCheckContentV1(Map params) { + if (params == null) { + return null; + } + + params.remove("sign"); + params.remove("sign_type"); + + StringBuilder content = new StringBuilder(); + List keys = new ArrayList(params.keySet()); + Collections.sort(keys); + + for (int i = 0; i < keys.size(); i++) { + String key = keys.get(i); + String value = SignConfig.wrapVal(params.get(key)); + content.append((i == 0 ? "" : "&") + key + "=" + value); + } + + return content.toString(); + } + + public static String getSignCheckContentV2(Map params) { + if (params == null) { + return null; + } + + params.remove("sign"); + + StringBuilder content = new StringBuilder(); + List keys = new ArrayList(params.keySet()); + Collections.sort(keys); + + for (int i = 0; i < keys.size(); i++) { + String key = keys.get(i); + String value = SignConfig.wrapVal(params.get(key)); + content.append((i == 0 ? "" : "&") + key + "=" + value); + } + + return content.toString(); + } + + public static boolean rsaCheckV1(Map params, String publicKey, + String charset) { + String sign = params.get("sign"); + String content = getSignCheckContentV1(params); + + return rsaCheckContent(content, sign, publicKey, charset); + } + + public static boolean rsaCheckV1(Map params, String publicKey, + String charset, String signType) { + String sign = params.get("sign"); + String content = getSignCheckContentV1(params); + + return rsaCheck(content, sign, publicKey, charset, signType); + } + + public static boolean rsaCheckV2(Map params, String publicKey, + String charset) { + String sign = params.get("sign"); + String content = getSignCheckContentV2(params); + + return rsaCheckContent(content, sign, publicKey, charset); + } + + public static boolean rsaCheckV2(Map params, String publicKey, + String charset, String signType) { + String sign = String.valueOf(params.get("sign")); + String content = getSignCheckContentV2(params); + + return rsaCheck(content, sign, publicKey, charset, signType); + } + + public static boolean rsaCheck(String content, String sign, String publicKey, String charset, + String signType) { + + if (AlipayConstants.SIGN_TYPE_RSA.equals(signType)) { + + return rsaCheckContent(content, sign, publicKey, charset); + + } else if (AlipayConstants.SIGN_TYPE_RSA2.equals(signType)) { + + return rsa256CheckContent(content, sign, publicKey, charset); + + } else { + throw ErrorEnum.ISV_INVALID_SIGNATURE_TYPE.getErrorMeta().getException(); +// throw new AlipayApiException("Sign Type is Not Support : signType=" + signType); + } + + } + + public static boolean rsa256CheckContent(String content, String sign, String publicKey, + String charset) { + try { + PublicKey pubKey = getPublicKeyFromX509("RSA", + new ByteArrayInputStream(publicKey.getBytes())); + + java.security.Signature signature = java.security.Signature + .getInstance(AlipayConstants.SIGN_SHA256RSA_ALGORITHMS); + + signature.initVerify(pubKey); + + if (StringUtils.isEmpty(charset)) { + signature.update(content.getBytes()); + } else { + signature.update(content.getBytes(charset)); + } + + return signature.verify(Base64.decodeBase64(sign.getBytes())); + } catch (Exception e) { + throw ErrorEnum.ISV_INVALID_SIGNATURE.getErrorMeta().getException(e); +// throw new AlipayApiException( +// "RSAcontent = " + content + ",sign=" + sign + ",charset = " + charset, e); + } + } + + public static boolean rsaCheckContent(String content, String sign, String publicKey, + String charset) { + try { + PublicKey pubKey = getPublicKeyFromX509("RSA", + new ByteArrayInputStream(publicKey.getBytes())); + + java.security.Signature signature = java.security.Signature + .getInstance(AlipayConstants.SIGN_ALGORITHMS); + + signature.initVerify(pubKey); + + if (StringUtils.isEmpty(charset)) { + signature.update(content.getBytes()); + } else { + signature.update(content.getBytes(charset)); + } + + return signature.verify(Base64.decodeBase64(sign.getBytes())); + } catch (Exception e) { + throw ErrorEnum.ISV_INVALID_SIGNATURE.getErrorMeta().getException(e); +// throw new AlipayApiException( +// "RSAcontent = " + content + ",sign=" + sign + ",charset = " + charset, e); + } + } + + public static PublicKey getPublicKeyFromX509(String algorithm, + InputStream ins) throws Exception { + KeyFactory keyFactory = KeyFactory.getInstance(algorithm); + + StringWriter writer = new StringWriter(); + StreamUtil.io(new InputStreamReader(ins), writer); + + byte[] encodedKey = writer.toString().getBytes(); + + encodedKey = Base64.decodeBase64(encodedKey); + + return keyFactory.generatePublic(new X509EncodedKeySpec(encodedKey)); + } + + /** + * 验签并解密 + *

+ * 目前适用于公众号
+ * params参数示例: + *
{ + *
biz_content=M0qGiGz+8kIpxe8aF4geWJdBn0aBTuJRQItLHo9R7o5JGhpic/MIUjvXo2BLB++BbkSq2OsJCEQFDZ0zK5AJYwvBgeRX30gvEj6eXqXRt16/IkB9HzAccEqKmRHrZJ7PjQWE0KfvDAHsJqFIeMvEYk1Zei2QkwSQPlso7K0oheo/iT+HYE8aTATnkqD/ByD9iNDtGg38pCa2xnnns63abKsKoV8h0DfHWgPH62urGY7Pye3r9FCOXA2Ykm8X4/Bl1bWFN/PFCEJHWe/HXj8KJKjWMO6ttsoV0xRGfeyUO8agu6t587Dl5ux5zD/s8Lbg5QXygaOwo3Fz1G8EqmGhi4+soEIQb8DBYanQOS3X+m46tVqBGMw8Oe+hsyIMpsjwF4HaPKMr37zpW3fe7xOMuimbZ0wq53YP/jhQv6XWodjT3mL0H5ACqcsSn727B5ztquzCPiwrqyjUHjJQQefFTzOse8snaWNQTUsQS7aLsHq0FveGpSBYORyA90qPdiTjXIkVP7mAiYiAIWW9pCEC7F3XtViKTZ8FRMM9ySicfuAlf3jtap6v2KPMtQv70X+hlmzO/IXB6W0Ep8DovkF5rB4r/BJYJLw/6AS0LZM9w5JfnAZhfGM2rKzpfNsgpOgEZS1WleG4I2hoQC0nxg9IcP0Hs+nWIPkEUcYNaiXqeBc=, + *
sign=rlqgA8O+RzHBVYLyHmrbODVSANWPXf3pSrr82OCO/bm3upZiXSYrX5fZr6UBmG6BZRAydEyTIguEW6VRuAKjnaO/sOiR9BsSrOdXbD5Rhos/Xt7/mGUWbTOt/F+3W0/XLuDNmuYg1yIC/6hzkg44kgtdSTsQbOC9gWM7ayB4J4c=, + * sign_type=RSA, + *
charset=UTF-8 + *
} + *

+ * + * @param params + * @param alipayPublicKey 支付宝公钥 + * @param cusPrivateKey 商户私钥 + * @param isCheckSign 是否验签 + * @param isDecrypt 是否解密 + * @return 解密后明文,验签失败则异常抛出 + */ + public static String checkSignAndDecrypt(Map params, String alipayPublicKey, + String cusPrivateKey, boolean isCheckSign, + boolean isDecrypt) { + String charset = params.get("charset"); + String bizContent = params.get("biz_content"); + if (isCheckSign) { + if (!rsaCheckV2(params, alipayPublicKey, charset)) { + throw ErrorEnum.ISV_INVALID_SIGNATURE.getErrorMeta().getException(); +// throw new AlipayApiException("rsaCheck failure:rsaParams=" + params); + } + } + + if (isDecrypt) { + return rsaDecrypt(bizContent, cusPrivateKey, charset); + } + + return bizContent; + } + + /** + * 验签并解密 + *

+ * 目前适用于公众号
+ * params参数示例: + *
{ + *
biz_content=M0qGiGz+8kIpxe8aF4geWJdBn0aBTuJRQItLHo9R7o5JGhpic/MIUjvXo2BLB++BbkSq2OsJCEQFDZ0zK5AJYwvBgeRX30gvEj6eXqXRt16/IkB9HzAccEqKmRHrZJ7PjQWE0KfvDAHsJqFIeMvEYk1Zei2QkwSQPlso7K0oheo/iT+HYE8aTATnkqD/ByD9iNDtGg38pCa2xnnns63abKsKoV8h0DfHWgPH62urGY7Pye3r9FCOXA2Ykm8X4/Bl1bWFN/PFCEJHWe/HXj8KJKjWMO6ttsoV0xRGfeyUO8agu6t587Dl5ux5zD/s8Lbg5QXygaOwo3Fz1G8EqmGhi4+soEIQb8DBYanQOS3X+m46tVqBGMw8Oe+hsyIMpsjwF4HaPKMr37zpW3fe7xOMuimbZ0wq53YP/jhQv6XWodjT3mL0H5ACqcsSn727B5ztquzCPiwrqyjUHjJQQefFTzOse8snaWNQTUsQS7aLsHq0FveGpSBYORyA90qPdiTjXIkVP7mAiYiAIWW9pCEC7F3XtViKTZ8FRMM9ySicfuAlf3jtap6v2KPMtQv70X+hlmzO/IXB6W0Ep8DovkF5rB4r/BJYJLw/6AS0LZM9w5JfnAZhfGM2rKzpfNsgpOgEZS1WleG4I2hoQC0nxg9IcP0Hs+nWIPkEUcYNaiXqeBc=, + *
sign=rlqgA8O+RzHBVYLyHmrbODVSANWPXf3pSrr82OCO/bm3upZiXSYrX5fZr6UBmG6BZRAydEyTIguEW6VRuAKjnaO/sOiR9BsSrOdXbD5Rhos/Xt7/mGUWbTOt/F+3W0/XLuDNmuYg1yIC/6hzkg44kgtdSTsQbOC9gWM7ayB4J4c=, + * sign_type=RSA, + *
charset=UTF-8 + *
} + *

+ * + * @param params + * @param alipayPublicKey 支付宝公钥 + * @param cusPrivateKey 商户私钥 + * @param isCheckSign 是否验签 + * @param isDecrypt 是否解密 + * @return 解密后明文,验签失败则异常抛出 + */ + public static String checkSignAndDecrypt(Map params, String alipayPublicKey, + String cusPrivateKey, boolean isCheckSign, + boolean isDecrypt, String signType) { + String charset = params.get("charset"); + String bizContent = params.get("biz_content"); + if (isCheckSign) { + if (!rsaCheckV2(params, alipayPublicKey, charset, signType)) { + throw ErrorEnum.ISV_INVALID_SIGNATURE.getErrorMeta().getException(); +// throw new AlipayApiException("rsaCheck failure:rsaParams=" + params); + } + } + + if (isDecrypt) { + return rsaDecrypt(bizContent, cusPrivateKey, charset); + } + + return bizContent; + } + + /** + * 加密并签名
+ * 目前适用于公众号 + * + * @param bizContent 待加密、签名内容 + * @param alipayPublicKey 支付宝公钥 + * @param cusPrivateKey 商户私钥 + * @param charset 字符集,如UTF-8, GBK, GB2312 + * @param isEncrypt 是否加密,true-加密 false-不加密 + * @param isSign 是否签名,true-签名 false-不签名 + * @return 加密、签名后xml内容字符串 + *

+ * 返回示例: + * + * 密文 + * RSA + * sign + * RSA + * + *

+ */ + public static String encryptAndSign(String bizContent, String alipayPublicKey, + String cusPrivateKey, String charset, boolean isEncrypt, + boolean isSign) { + StringBuilder sb = new StringBuilder(); + if (StringUtils.isEmpty(charset)) { + charset = AlipayConstants.CHARSET_GBK; + } + sb.append(""); + if (isEncrypt) {// 加密 + sb.append(""); + String encrypted = rsaEncrypt(bizContent, alipayPublicKey, charset); + sb.append("" + encrypted + ""); + sb.append("RSA"); + if (isSign) { + String sign = rsaSign(encrypted, cusPrivateKey, charset); + sb.append("" + sign + ""); + sb.append("RSA"); + } + sb.append(""); + } else if (isSign) {// 不加密,但需要签名 + sb.append(""); + sb.append("" + bizContent + ""); + String sign = rsaSign(bizContent, cusPrivateKey, charset); + sb.append("" + sign + ""); + sb.append("RSA"); + sb.append(""); + } else {// 不加密,不加签 + sb.append(bizContent); + } + return sb.toString(); + } + + /** + * 加密并签名
+ * 目前适用于公众号 + * + * @param bizContent 待加密、签名内容 + * @param alipayPublicKey 支付宝公钥 + * @param cusPrivateKey 商户私钥 + * @param charset 字符集,如UTF-8, GBK, GB2312 + * @param isEncrypt 是否加密,true-加密 false-不加密 + * @param isSign 是否签名,true-签名 false-不签名 + * @return 加密、签名后xml内容字符串 + *

+ * 返回示例: + * + * 密文 + * RSA + * sign + * RSA + * + *

+ */ + public static String encryptAndSign(String bizContent, String alipayPublicKey, + String cusPrivateKey, String charset, boolean isEncrypt, + boolean isSign, String signType) { + StringBuilder sb = new StringBuilder(); + if (StringUtils.isEmpty(charset)) { + charset = AlipayConstants.CHARSET_GBK; + } + sb.append(""); + if (isEncrypt) {// 加密 + sb.append(""); + String encrypted = rsaEncrypt(bizContent, alipayPublicKey, charset); + sb.append("" + encrypted + ""); + sb.append("RSA"); + if (isSign) { + String sign = rsaSign(encrypted, cusPrivateKey, charset, signType); + sb.append("" + sign + ""); + sb.append(""); + sb.append(signType); + sb.append(""); + } + sb.append(""); + } else if (isSign) {// 不加密,但需要签名 + sb.append(""); + sb.append("" + bizContent + ""); + String sign = rsaSign(bizContent, cusPrivateKey, charset, signType); + sb.append("" + sign + ""); + sb.append(""); + sb.append(signType); + sb.append(""); + sb.append(""); + } else {// 不加密,不加签 + sb.append(bizContent); + } + return sb.toString(); + } + + /** + * 公钥加密 + * + * @param content 待加密内容 + * @param publicKey 公钥 + * @param charset 字符集,如UTF-8, GBK, GB2312 + * @return 密文内容 + */ + public static String rsaEncrypt(String content, String publicKey, + String charset) { + try { + PublicKey pubKey = getPublicKeyFromX509(AlipayConstants.SIGN_TYPE_RSA, + new ByteArrayInputStream(publicKey.getBytes())); + Cipher cipher = Cipher.getInstance(AlipayConstants.SIGN_TYPE_RSA); + cipher.init(Cipher.ENCRYPT_MODE, pubKey); + byte[] data = StringUtils.isEmpty(charset) ? content.getBytes() + : content.getBytes(charset); + int inputLen = data.length; + ByteArrayOutputStream out = new ByteArrayOutputStream(); + int offSet = 0; + byte[] cache; + int i = 0; + // 对数据分段加密 + while (inputLen - offSet > 0) { + if (inputLen - offSet > MAX_ENCRYPT_BLOCK) { + cache = cipher.doFinal(data, offSet, MAX_ENCRYPT_BLOCK); + } else { + cache = cipher.doFinal(data, offSet, inputLen - offSet); + } + out.write(cache, 0, cache.length); + i++; + offSet = i * MAX_ENCRYPT_BLOCK; + } + byte[] encryptedData = Base64.encodeBase64(out.toByteArray()); + out.close(); + + return StringUtils.isEmpty(charset) ? new String(encryptedData) + : new String(encryptedData, charset); + } catch (Exception e) { + throw ErrorEnum.ISV_INVALID_SIGNATURE.getErrorMeta().getException(e); +// throw new AlipayApiException("EncryptContent = " + content + ",charset = " + charset, +// e); + } + } + + /** + * 私钥解密 + * + * @param content 待解密内容 + * @param publicKey 私钥 + * @param charset 字符集,如UTF-8, GBK, GB2312 + * @return 明文内容 + */ + public static String rsaDecrypt(String content, String publicKey, + String charset) { + try { + PrivateKey priKey = getPrivateKeyFromPKCS8(AlipayConstants.SIGN_TYPE_RSA, + new ByteArrayInputStream(publicKey.getBytes())); + Cipher cipher = Cipher.getInstance(AlipayConstants.SIGN_TYPE_RSA); + cipher.init(Cipher.DECRYPT_MODE, priKey); + byte[] encryptedData = StringUtils.isEmpty(charset) + ? Base64.decodeBase64(content.getBytes()) + : Base64.decodeBase64(content.getBytes(charset)); + int inputLen = encryptedData.length; + ByteArrayOutputStream out = new ByteArrayOutputStream(); + int offSet = 0; + byte[] cache; + int i = 0; + // 对数据分段解密 + while (inputLen - offSet > 0) { + if (inputLen - offSet > MAX_DECRYPT_BLOCK) { + cache = cipher.doFinal(encryptedData, offSet, MAX_DECRYPT_BLOCK); + } else { + cache = cipher.doFinal(encryptedData, offSet, inputLen - offSet); + } + out.write(cache, 0, cache.length); + i++; + offSet = i * MAX_DECRYPT_BLOCK; + } + byte[] decryptedData = out.toByteArray(); + out.close(); + + return StringUtils.isEmpty(charset) ? new String(decryptedData) + : new String(decryptedData, charset); + } catch (Exception e) { + throw ErrorEnum.ISV_INVALID_SIGNATURE.getErrorMeta().getException(e); +// throw new AlipayApiException("EncodeContent = " + content + ",charset = " + charset, e); + } + } + +} diff --git a/sop-index/src/main/java/com/gitee/sop/index/service/validate/alipay/AlipaySigner.java b/sop-index/src/main/java/com/gitee/sop/index/service/validate/alipay/AlipaySigner.java new file mode 100644 index 00000000..64fe7ccc --- /dev/null +++ b/sop-index/src/main/java/com/gitee/sop/index/service/validate/alipay/AlipaySigner.java @@ -0,0 +1,35 @@ +package com.gitee.sop.index.service.validate.alipay; + + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.gitee.sop.index.controller.param.ApiRequest; +import com.gitee.sop.index.message.ErrorEnum; +import com.gitee.sop.index.service.validate.Signer; + +/** + * 支付宝签名验证实现。 + * + * @author tanghc + * @see 支付宝签名 + */ +public class AlipaySigner implements Signer { + + @Override + public boolean checkSign(ApiRequest apiParam, String secret) { + // 服务端存的是公钥 + String publicKey = secret; + String charset = apiParam.getCharset(); + String signType = apiParam.getSign_type(); + if (signType == null) { + throw ErrorEnum.ISV_DECRYPTION_ERROR_MISSING_ENCRYPT_TYPE.getErrorMeta().getException(); + } + if (charset == null) { + throw ErrorEnum.ISV_INVALID_CHARSET.getErrorMeta().getException(); + } + String json = JSON.toJSONString(apiParam); + JSONObject params = JSON.parseObject(json); + return AlipaySignature.rsaCheckV2(params, publicKey, charset, signType); + } + +} diff --git a/sop-index/src/main/java/com/gitee/sop/index/service/validate/alipay/StreamUtil.java b/sop-index/src/main/java/com/gitee/sop/index/service/validate/alipay/StreamUtil.java new file mode 100644 index 00000000..ab0de39d --- /dev/null +++ b/sop-index/src/main/java/com/gitee/sop/index/service/validate/alipay/StreamUtil.java @@ -0,0 +1,141 @@ +/** + * Alipay.com Inc. + * Copyright (c) 2004-2012 All Rights Reserved. + */ +package com.gitee.sop.index.service.validate.alipay; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.Reader; +import java.io.StringWriter; +import java.io.Writer; + +/** + * + * @author runzhi + */ +public class StreamUtil { + private StreamUtil(){} + + private static final int DEFAULT_BUFFER_SIZE = 8192; + + public static void io(InputStream in, OutputStream out) throws IOException { + io(in, out, -1); + } + + public static void io(InputStream in, OutputStream out, int bufferSize) throws IOException { + if (bufferSize == -1) { + bufferSize = DEFAULT_BUFFER_SIZE; + } + + byte[] buffer = new byte[bufferSize]; + int amount; + + while ((amount = in.read(buffer)) >= 0) { + out.write(buffer, 0, amount); + } + } + + public static void io(Reader in, Writer out) throws IOException { + io(in, out, -1); + } + + public static void io(Reader in, Writer out, int bufferSize) throws IOException { + if (bufferSize == -1) { + bufferSize = DEFAULT_BUFFER_SIZE >> 1; + } + + char[] buffer = new char[bufferSize]; + int amount; + + while ((amount = in.read(buffer)) >= 0) { + out.write(buffer, 0, amount); + } + } + + public static OutputStream synchronizedOutputStream(OutputStream out) { + return new SynchronizedOutputStream(out); + } + + public static OutputStream synchronizedOutputStream(OutputStream out, Object lock) { + return new SynchronizedOutputStream(out, lock); + } + + public static String readText(InputStream in) throws IOException { + return readText(in, null, -1); + } + + public static String readText(InputStream in, String encoding) throws IOException { + return readText(in, encoding, -1); + } + + public static String readText(InputStream in, String encoding, int bufferSize) + throws IOException { + Reader reader = (encoding == null) ? new InputStreamReader(in) : new InputStreamReader(in, + encoding); + + return readText(reader, bufferSize); + } + + public static String readText(Reader reader) throws IOException { + return readText(reader, -1); + } + + public static String readText(Reader reader, int bufferSize) throws IOException { + StringWriter writer = new StringWriter(); + + io(reader, writer, bufferSize); + return writer.toString(); + } + + private static class SynchronizedOutputStream extends OutputStream { + private OutputStream out; + private Object lock; + + SynchronizedOutputStream(OutputStream out) { + this(out, out); + } + + SynchronizedOutputStream(OutputStream out, Object lock) { + this.out = out; + this.lock = lock; + } + + @Override + public void write(int datum) throws IOException { + synchronized (lock) { + out.write(datum); + } + } + + @Override + public void write(byte[] data) throws IOException { + synchronized (lock) { + out.write(data); + } + } + + @Override + public void write(byte[] data, int offset, int length) throws IOException { + synchronized (lock) { + out.write(data, offset, length); + } + } + + @Override + public void flush() throws IOException { + synchronized (lock) { + out.flush(); + } + } + + @Override + public void close() throws IOException { + synchronized (lock) { + out.close(); + } + } + } +} diff --git a/sop-index/src/main/java/com/gitee/sop/index/service/validate/alipay/StringUtils.java b/sop-index/src/main/java/com/gitee/sop/index/service/validate/alipay/StringUtils.java new file mode 100644 index 00000000..e1be5b0b --- /dev/null +++ b/sop-index/src/main/java/com/gitee/sop/index/service/validate/alipay/StringUtils.java @@ -0,0 +1,171 @@ +package com.gitee.sop.index.service.validate.alipay; + +/** + * 字符串工具类。 + * + * @author carver.gu + * @since 1.0, Sep 12, 2009 + */ +public class StringUtils { + + private StringUtils() {} + + /** + * 检查指定的字符串是否为空。 + *
    + *
  • SysUtils.isEmpty(null) = true
  • + *
  • SysUtils.isEmpty("") = true
  • + *
  • SysUtils.isEmpty(" ") = true
  • + *
  • SysUtils.isEmpty("abc") = false
  • + *
+ * + * @param value 待检查的字符串 + * @return true/false + */ + public static boolean isEmpty(String value) { + int strLen; + if (value == null || (strLen = value.length()) == 0) { + return true; + } + for (int i = 0; i < strLen; i++) { + if ((Character.isWhitespace(value.charAt(i)) == false)) { + return false; + } + } + return true; + } + + /** + * 检查对象是否为数字型字符串,包含负数开头的。 + */ + public static boolean isNumeric(Object obj) { + if (obj == null) { + return false; + } + char[] chars = obj.toString().toCharArray(); + int length = chars.length; + if(length < 1) { + return false; + } + + int i = 0; + if(length > 1 && chars[0] == '-') { + i = 1; + } + + for (; i < length; i++) { + if (!Character.isDigit(chars[i])) { + return false; + } + } + return true; + } + + /** + * 检查指定的字符串列表是否不为空。 + */ + public static boolean areNotEmpty(String... values) { + boolean result = true; + if (values == null || values.length == 0) { + result = false; + } else { + for (String value : values) { + result &= !isEmpty(value); + } + } + return result; + } + + /** + * 把通用字符编码的字符串转化为汉字编码。 + */ + public static String unicodeToChinese(String unicode) { + StringBuilder out = new StringBuilder(); + if (!isEmpty(unicode)) { + for (int i = 0; i < unicode.length(); i++) { + out.append(unicode.charAt(i)); + } + } + return out.toString(); + } + + /** + * 过滤不可见字符 + */ + public static String stripNonValidXMLCharacters(String input) { + if (input == null || ("".equals(input))) { + return ""; + } + StringBuilder out = new StringBuilder(); + char current; + for (int i = 0; i < input.length(); i++) { + current = input.charAt(i); + if ((current == 0x9) || (current == 0xA) || (current == 0xD) + || ((current >= 0x20) && (current <= 0xD7FF)) + || ((current >= 0xE000) && (current <= 0xFFFD)) + || ((current >= 0x10000) && (current <= 0x10FFFF))) { + out.append(current); + } + } + return out.toString(); + } + + public static String leftPad(String str, int size, char padChar) { + if (str == null) { + return null; + } else { + int pads = size - str.length(); + if (pads <= 0) { + return str; + } else { + return pads > 8192 ? leftPad(str, size, String.valueOf(padChar)) : padding(pads, padChar).concat(str); + } + } + } + + public static String leftPad(String str, int size, String padStr) { + if (str == null) { + return null; + } else { + if (isEmpty(padStr)) { + padStr = " "; + } + + int padLen = padStr.length(); + int strLen = str.length(); + int pads = size - strLen; + if (pads <= 0) { + return str; + } else if (padLen == 1 && pads <= 8192) { + return leftPad(str, size, padStr.charAt(0)); + } else if (pads == padLen) { + return padStr.concat(str); + } else if (pads < padLen) { + return padStr.substring(0, pads).concat(str); + } else { + char[] padding = new char[pads]; + char[] padChars = padStr.toCharArray(); + + for(int i = 0; i < pads; ++i) { + padding[i] = padChars[i % padLen]; + } + + return (new String(padding)).concat(str); + } + } + } + + private static String padding(int repeat, char padChar) { + if (repeat < 0) { + throw new IndexOutOfBoundsException("Cannot pad a negative amount: " + repeat); + } else { + char[] buf = new char[repeat]; + + for(int i = 0; i < buf.length; ++i) { + buf[i] = padChar; + } + + return new String(buf); + } + } +} diff --git a/sop-index/src/main/java/com/gitee/sop/index/service/validate/taobao/TaobaoSigner.java b/sop-index/src/main/java/com/gitee/sop/index/service/validate/taobao/TaobaoSigner.java new file mode 100644 index 00000000..2de3536e --- /dev/null +++ b/sop-index/src/main/java/com/gitee/sop/index/service/validate/taobao/TaobaoSigner.java @@ -0,0 +1,66 @@ +package com.gitee.sop.index.service.validate.taobao; + + +import com.gitee.sop.gatewaycommon.bean.SopConstants; +import com.gitee.sop.gatewaycommon.message.ErrorEnum; +import com.gitee.sop.gatewaycommon.param.ApiParam; +import com.gitee.sop.gatewaycommon.validate.AbstractSigner; +import com.gitee.sop.gatewaycommon.validate.SignConfig; +import com.gitee.sop.gatewaycommon.validate.SignEncipher; +import com.gitee.sop.gatewaycommon.validate.SignEncipherHMAC_MD5; +import com.gitee.sop.gatewaycommon.validate.SignEncipherMD5; +import org.apache.commons.lang3.StringUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * 淘宝开放平台签名验证实现,http://open.taobao.com/doc.htm?docId=101617&docType=1 + * + * @author tanghc + */ +public class TaobaoSigner extends AbstractSigner { + + private Map signEncipherMap = new HashMap<>(); + + public TaobaoSigner() { + signEncipherMap.put("md5", new SignEncipherMD5()); + signEncipherMap.put("hmac", new SignEncipherHMAC_MD5()); + } + + + @Override + public String buildServerSign(ApiParam param, String secret) { + String signMethod = param.fetchSignMethod(); + if (signMethod == null) { + signMethod = SopConstants.DEFAULT_SIGN_METHOD; + } + SignEncipher signEncipher = signEncipherMap.get(signMethod); + if (signEncipher == null) { + throw ErrorEnum.ISV_INVALID_SIGNATURE_TYPE.getErrorMeta().getException(signMethod); + } + + // 第一步:参数排序 + Set keySet = param.keySet(); + List paramNames = new ArrayList<>(keySet); + Collections.sort(paramNames); + + // 第二步:把所有参数名和参数值串在一起 + StringBuilder paramNameValue = new StringBuilder(); + for (String paramName : paramNames) { + String val = SignConfig.wrapVal(param.get(paramName)); + paramNameValue.append(paramName).append(val); + } + + // 第三步:使用MD5/HMAC加密 + String source = paramNameValue.toString(); + byte[] bytes = signEncipher.encrypt(source, secret); + + // 第四步:把二进制转化为大写的十六进制 + return byte2hex(bytes).toUpperCase(); + } +} diff --git a/sop-index/src/main/resources/application.properties b/sop-index/src/main/resources/application.properties new file mode 100644 index 00000000..8284e9f4 --- /dev/null +++ b/sop-index/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.application.name=sop-index diff --git a/sop-sdk/pom.xml b/sop-sdk/pom.xml index 1861a415..9e16d90c 100644 --- a/sop-sdk/pom.xml +++ b/sop-sdk/pom.xml @@ -6,7 +6,7 @@ com.gitee.sop sop-parent - 4.4.2-SNAPSHOT + 5.0.0-SNAPSHOT ../pom.xml @@ -25,4 +25,4 @@ sdk-java - \ No newline at end of file + diff --git a/sop-sdk/sdk-java/pom.xml b/sop-sdk/sdk-java/pom.xml index 61aa2527..7d5e41ec 100644 --- a/sop-sdk/sdk-java/pom.xml +++ b/sop-sdk/sdk-java/pom.xml @@ -4,13 +4,13 @@ com.gitee.sop sop-parent - 4.4.2-SNAPSHOT + 5.0.0-SNAPSHOT ../../pom.xml 4.0.0 sdk-java - 4.4.2-SNAPSHOT + 5.0.0-SNAPSHOT diff --git a/sop-test/pom.xml b/sop-test/pom.xml index 1a9d3b11..4acacd17 100644 --- a/sop-test/pom.xml +++ b/sop-test/pom.xml @@ -5,7 +5,7 @@ com.gitee.sop sop-parent - 4.4.2-SNAPSHOT + 5.0.0-SNAPSHOT ../pom.xml diff --git a/sop-website/pom.xml b/sop-website/pom.xml index 58532e05..8c357f15 100644 --- a/sop-website/pom.xml +++ b/sop-website/pom.xml @@ -6,7 +6,7 @@ com.gitee.sop sop-parent - 4.4.2-SNAPSHOT + 5.0.0-SNAPSHOT ../pom.xml @@ -17,4 +17,4 @@ sop-website-server - \ No newline at end of file + diff --git a/sop-website/sop-website-server/pom.xml b/sop-website/sop-website-server/pom.xml index 170e9031..ede437ab 100644 --- a/sop-website/sop-website-server/pom.xml +++ b/sop-website/sop-website-server/pom.xml @@ -5,7 +5,7 @@ com.gitee.sop sop-website - 4.4.2-SNAPSHOT + 5.0.0-SNAPSHOT ../pom.xml