From 46a546f1daa776ac031b035b86b027e82f712a12 Mon Sep 17 00:00:00 2001 From: tanghc Date: Wed, 27 Feb 2019 12:37:14 +0800 Subject: [PATCH] init --- .gitignore | 42 +- README.md | 62 +- mvnw | 286 ++++++++ mvnw.cmd | 161 +++++ pom.xml | 20 + sop-book/pom.xml | 15 + sop-book/sop-book-api/.gitignore | 25 + sop-book/sop-book-api/pom.xml | 46 ++ .../java/com/hhdd/book/api/domain/Book.java | 12 + .../hhdd/book/api/service/BookService.java | 14 + sop-book/sop-book-web/.gitignore | 25 + .../.mvn/wrapper/maven-wrapper.jar | Bin 0 -> 48337 bytes .../.mvn/wrapper/maven-wrapper.properties | 1 + sop-book/sop-book-web/mvnw | 286 ++++++++ sop-book/sop-book-web/mvnw.cmd | 161 +++++ sop-book/sop-book-web/pom.xml | 84 +++ .../gitee/sop/bookweb/SopBookApplication.java | 20 + .../consumer/StoryServiceConsumer.java | 14 + .../sop/bookweb/controller/ApiResult.java | 74 +++ .../bookweb/controller/BookController.java | 73 +++ .../gitee/sop/bookweb/param/BookParam.java | 11 + .../src/main/resources/application.properties | 6 + .../sop/bookweb/SopBookApplicationTests.java | 17 + sop-gateway-common/.gitignore | 25 + sop-gateway-common/pom.xml | 131 ++++ sop-gateway-common/readme.md | 3 + .../sop/gatewaycommon/bean/ApiConfig.java | 116 ++++ .../sop/gatewaycommon/bean/ApiContext.java | 186 ++++++ .../sop/gatewaycommon/bean/RequestMode.java | 17 + .../gatewaycommon/bean/ServiceApiInfo.java | 29 + .../sop/gatewaycommon/bean/SopConstants.java | 33 + .../AlipayZuulConfiguration.java | 16 + .../configuration/BaseZuulConfiguration.java | 112 ++++ .../configuration/BaseZuulController.java | 44 ++ .../TaobaoZuulConfiguration.java | 23 + .../gatewaycommon/easyopen/EasyopenRoute.java | 20 + .../easyopen/EasyopenZuulConfig.java | 74 +++ .../filter/PostEasyopenResultFilter.java | 46 ++ .../gatewaycommon/exception/ApiException.java | 26 + .../gatewaycommon/filter/BaseZuulFilter.java | 96 +++ .../sop/gatewaycommon/filter/ErrorFilter.java | 59 ++ .../filter/PostResultFilter.java | 49 ++ .../gatewaycommon/filter/PreTokenFilter.java | 41 ++ .../filter/PreValidateFilter.java | 48 ++ .../manager/ApiMetaChangeListener.java | 25 + .../gatewaycommon/manager/ApiMetaConfig.java | 20 + .../gatewaycommon/manager/ApiMetaContext.java | 13 + .../gatewaycommon/manager/ApiMetaManager.java | 22 + .../manager/DefaultApiMetaContext.java | 62 ++ .../manager/DefaultApiMetaManager.java | 51 ++ .../manager/SopRouteLocator.java | 45 ++ .../sop/gatewaycommon/message/Error.java | 48 ++ .../sop/gatewaycommon/message/ErrorEnum.java | 66 ++ .../gatewaycommon/message/ErrorFactory.java | 120 ++++ .../sop/gatewaycommon/message/ErrorImpl.java | 23 + .../sop/gatewaycommon/message/ErrorMeta.java | 47 ++ .../sop/gatewaycommon/param/ApiParam.java | 197 ++++++ .../gatewaycommon/param/ApiParamParser.java | 104 +++ .../gatewaycommon/param/ApiUploadContext.java | 42 ++ .../gitee/sop/gatewaycommon/param/Param.java | 70 ++ .../sop/gatewaycommon/param/ParamNames.java | 46 ++ .../sop/gatewaycommon/param/ParamParser.java | 19 + .../gatewaycommon/param/UploadContext.java | 36 + .../sop/gatewaycommon/result/ApiResult.java | 77 +++ .../result/ApiResultExecutor.java | 125 ++++ .../result/JsonResultSerializer.java | 16 + .../sop/gatewaycommon/result/Result.java | 12 + .../gatewaycommon/result/ResultBuilder.java | 27 + .../gatewaycommon/result/ResultExecutor.java | 10 + .../result/ResultSerializer.java | 16 + .../result/XmlResultSerializer.java | 16 + .../secret/AppSecretManager.java | 32 + .../secret/CacheAppSecretManager.java | 33 + .../secret/FileAppSecretManager.java | 56 ++ .../gatewaycommon/session/ApiHttpSession.java | 242 +++++++ .../session/ApiRedisTemplate.java | 30 + .../session/ApiSessionManager.java | 100 +++ .../session/RedisHttpSession.java | 264 ++++++++ .../session/RedisSessionManager.java | 100 +++ .../gatewaycommon/session/SessionManager.java | 20 + .../gitee/sop/gatewaycommon/util/AESUtil.java | 146 +++++ .../sop/gatewaycommon/util/KeyStore.java | 25 + .../sop/gatewaycommon/util/RSANewUtil.java | 208 ++++++ .../gitee/sop/gatewaycommon/util/RSAUtil.java | 312 +++++++++ .../sop/gatewaycommon/util/RedisLockUtil.java | 139 ++++ .../sop/gatewaycommon/util/RequestUtil.java | 137 ++++ .../sop/gatewaycommon/util/ResponseUtil.java | 119 ++++ .../gitee/sop/gatewaycommon/util/XmlUtil.java | 32 + .../validate/AbstractSigner.java | 36 + .../gatewaycommon/validate/ApiEncrypter.java | 60 ++ .../sop/gatewaycommon/validate/ApiSigner.java | 56 ++ .../gatewaycommon/validate/ApiValidator.java | 150 +++++ .../sop/gatewaycommon/validate/Encrypter.java | 95 +++ .../gatewaycommon/validate/SignEncipher.java | 13 + .../validate/SignEncipherHMAC_MD5.java | 37 ++ .../validate/SignEncipherMD5.java | 16 + .../sop/gatewaycommon/validate/Signer.java | 20 + .../sop/gatewaycommon/validate/Validator.java | 18 + .../validate/alipay/AlipayConstants.java | 97 +++ .../validate/alipay/AlipaySignature.java | 616 ++++++++++++++++++ .../validate/alipay/AlipaySigner.java | 34 + .../validate/alipay/StreamUtil.java | 134 ++++ .../validate/alipay/StringUtils.java | 167 +++++ .../validate/taobao/TaobaoSigner.java | 64 ++ .../resources/i18n/open/error_en.properties | 55 ++ .../i18n/open/error_zh_CN.properties | 107 +++ sop-gateway/.gitignore | 25 + sop-gateway/.mvn/wrapper/maven-wrapper.jar | Bin 0 -> 48337 bytes .../.mvn/wrapper/maven-wrapper.properties | 1 + sop-gateway/mvnw | 286 ++++++++ sop-gateway/mvnw.cmd | 161 +++++ sop-gateway/pom.xml | 74 +++ .../sop/gateway/SopGatewayApplication.java | 17 + .../gitee/sop/gateway/config/ZuulConfig.java | 35 + .../src/main/resources/application.properties | 28 + .../java/com/gitee/sop/gateway/PostTest.java | 127 ++++ .../gateway/SopGatewayApplicationTests.java | 17 + .../com/gitee/sop/gateway/SopPostTest.java | 121 ++++ .../java/com/gitee/sop/gateway/TestBase.java | 102 +++ sop-registry/.gitignore | 25 + sop-registry/.mvn/wrapper/maven-wrapper.jar | Bin 0 -> 48337 bytes .../.mvn/wrapper/maven-wrapper.properties | 1 + sop-registry/mvnw | 286 ++++++++ sop-registry/mvnw.cmd | 161 +++++ sop-registry/pom.xml | 64 ++ .../sop/registry/SopRegistryApplication.java | 16 + .../src/main/resources/application.properties | 12 + .../registry/SopRegistryApplicationTests.java | 17 + sop-server-common/.gitignore | 25 + sop-server-common/pom.xml | 92 +++ sop-server-common/readme.md | 3 + .../servercommon/annotation/ApiAbility.java | 23 + .../servercommon/annotation/ApiMapping.java | 64 ++ .../sop/servercommon/bean/ParamNames.java | 46 ++ .../sop/servercommon/bean/ServiceApiInfo.java | 39 ++ .../sop/servercommon/bean/ServiceConfig.java | 57 ++ .../sop/servercommon/bean/ServiceContext.java | 139 ++++ .../AlipayServerConfiguration.java | 24 + .../BaseServerConfiguration.java | 78 +++ .../DefaultGlobalExceptionHandler.java | 46 ++ .../configuration/GlobalExceptionHandler.java | 7 + .../TaobaoServerConfiguration.java | 9 + .../exception/ServiceException.java | 26 + .../ServiceContextInterceptor.java | 25 + .../servercommon/manager/ApiMetaManager.java | 12 + .../manager/DefaultRequestMappingEvent.java | 100 +++ .../manager/RedisApiMetaManager.java | 29 + .../manager/RequestMappingEvent.java | 10 + .../mapping/ApiMappingHandlerMapping.java | 76 +++ .../servercommon/mapping/ApiMappingInfo.java | 17 + .../mapping/ApiMappingRequestCondition.java | 54 ++ .../ApiMappingStringValueResolver.java | 19 + .../servercommon/message/ServiceError.java | 25 + .../message/ServiceErrorEnum.java | 20 + .../message/ServiceErrorFactory.java | 110 ++++ .../message/ServiceErrorImpl.java | 17 + .../message/ServiceErrorMeta.java | 38 ++ .../param/ApiArgumentResolver.java | 39 ++ .../servercommon/param/ParamValidator.java | 12 + .../param/ServiceParamValidator.java | 67 ++ .../result/DefaultServiceResultBuilder.java | 49 ++ .../result/ServiceResultBuilder.java | 29 + .../resources/i18n/isp/error_en.properties | 4 + .../resources/i18n/isp/error_zh_CN.properties | 5 + sop-story/pom.xml | 15 + sop-story/sop-story-api/.gitignore | 25 + sop-story/sop-story-api/pom.xml | 46 ++ .../com/gitee/sop/story/api/domain/Story.java | 18 + .../sop/story/api/service/StoryService.java | 15 + sop-story/sop-story-web/.gitignore | 25 + sop-story/sop-story-web/mvnw | 286 ++++++++ sop-story/sop-story-web/mvnw.cmd | 161 +++++ sop-story/sop-story-web/pom.xml | 78 +++ .../sop/bookweb/SopStoryApplication.java | 16 + .../sop/bookweb/config/OpenServerConfig.java | 23 + .../bookweb/controller/AlipayController.java | 28 + .../bookweb/controller/Story2Controller.java | 46 ++ .../bookweb/controller/StoryController.java | 85 +++ .../sop/bookweb/message/StoryErrorEnum.java | 21 + .../src/main/resources/application.properties | 15 + .../resources/i18n/isp/bizerror_en.properties | 3 + .../i18n/isp/bizerror_zh_CN.properties | 4 + .../sop/bookweb/SopStoryApplicationTests.java | 17 + sop-test/pom.xml | 102 +++ sop-test/readme.md | 3 + sop-test/src/main/java/com/gitee/sop/App.java | 13 + .../gitee/sop/alipay/AlipayApiException.java | 49 ++ .../com/gitee/sop/alipay/AlipayConstants.java | 97 +++ .../com/gitee/sop/alipay/AlipaySignature.java | 611 +++++++++++++++++ .../java/com/gitee/sop/alipay/StreamUtil.java | 134 ++++ .../com/gitee/sop/alipay/StringUtils.java | 167 +++++ .../java/com/gitee/sop/taobao/Constants.java | 10 + .../com/gitee/sop/taobao/TaobaoSignature.java | 95 +++ .../com/gitee/sop/AlipayClientPostTest.java | 87 +++ .../com/gitee/sop/TaobaoClientPostTest.java | 77 +++ .../src/test/java/com/gitee/sop/TestBase.java | 100 +++ 196 files changed, 13210 insertions(+), 47 deletions(-) create mode 100755 mvnw create mode 100644 mvnw.cmd create mode 100644 pom.xml create mode 100644 sop-book/pom.xml create mode 100644 sop-book/sop-book-api/.gitignore create mode 100644 sop-book/sop-book-api/pom.xml create mode 100644 sop-book/sop-book-api/src/main/java/com/hhdd/book/api/domain/Book.java create mode 100644 sop-book/sop-book-api/src/main/java/com/hhdd/book/api/service/BookService.java create mode 100644 sop-book/sop-book-web/.gitignore create mode 100644 sop-book/sop-book-web/.mvn/wrapper/maven-wrapper.jar create mode 100644 sop-book/sop-book-web/.mvn/wrapper/maven-wrapper.properties create mode 100755 sop-book/sop-book-web/mvnw create mode 100644 sop-book/sop-book-web/mvnw.cmd create mode 100644 sop-book/sop-book-web/pom.xml create mode 100644 sop-book/sop-book-web/src/main/java/com/gitee/sop/bookweb/SopBookApplication.java create mode 100644 sop-book/sop-book-web/src/main/java/com/gitee/sop/bookweb/consumer/StoryServiceConsumer.java create mode 100644 sop-book/sop-book-web/src/main/java/com/gitee/sop/bookweb/controller/ApiResult.java create mode 100644 sop-book/sop-book-web/src/main/java/com/gitee/sop/bookweb/controller/BookController.java create mode 100644 sop-book/sop-book-web/src/main/java/com/gitee/sop/bookweb/param/BookParam.java create mode 100644 sop-book/sop-book-web/src/main/resources/application.properties create mode 100644 sop-book/sop-book-web/src/test/java/com/gitee/sop/bookweb/SopBookApplicationTests.java create mode 100644 sop-gateway-common/.gitignore create mode 100644 sop-gateway-common/pom.xml create mode 100644 sop-gateway-common/readme.md create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/bean/ApiConfig.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/bean/ApiContext.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/bean/RequestMode.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/bean/ServiceApiInfo.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/bean/SopConstants.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/configuration/AlipayZuulConfiguration.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/configuration/BaseZuulConfiguration.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/configuration/BaseZuulController.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/configuration/TaobaoZuulConfiguration.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/easyopen/EasyopenRoute.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/easyopen/EasyopenZuulConfig.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/easyopen/filter/PostEasyopenResultFilter.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/exception/ApiException.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/filter/BaseZuulFilter.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/filter/ErrorFilter.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/filter/PostResultFilter.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/filter/PreTokenFilter.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/filter/PreValidateFilter.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/manager/ApiMetaChangeListener.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/manager/ApiMetaConfig.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/manager/ApiMetaContext.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/manager/ApiMetaManager.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/manager/DefaultApiMetaContext.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/manager/DefaultApiMetaManager.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/manager/SopRouteLocator.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/message/Error.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/message/ErrorEnum.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/message/ErrorFactory.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/message/ErrorImpl.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/message/ErrorMeta.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/param/ApiParam.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/param/ApiParamParser.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/param/ApiUploadContext.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/param/Param.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/param/ParamNames.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/param/ParamParser.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/param/UploadContext.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/result/ApiResult.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/result/ApiResultExecutor.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/result/JsonResultSerializer.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/result/Result.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/result/ResultBuilder.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/result/ResultExecutor.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/result/ResultSerializer.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/result/XmlResultSerializer.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/secret/AppSecretManager.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/secret/CacheAppSecretManager.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/secret/FileAppSecretManager.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/session/ApiHttpSession.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/session/ApiRedisTemplate.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/session/ApiSessionManager.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/session/RedisHttpSession.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/session/RedisSessionManager.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/session/SessionManager.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/util/AESUtil.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/util/KeyStore.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/util/RSANewUtil.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/util/RSAUtil.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/util/RedisLockUtil.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/util/RequestUtil.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/util/ResponseUtil.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/util/XmlUtil.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/AbstractSigner.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/ApiEncrypter.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/ApiSigner.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/ApiValidator.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/Encrypter.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/SignEncipher.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/SignEncipherHMAC_MD5.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/SignEncipherMD5.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/Signer.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/Validator.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/alipay/AlipayConstants.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/alipay/AlipaySignature.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/alipay/AlipaySigner.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/alipay/StreamUtil.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/alipay/StringUtils.java create mode 100644 sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/taobao/TaobaoSigner.java create mode 100644 sop-gateway-common/src/main/resources/i18n/open/error_en.properties create mode 100644 sop-gateway-common/src/main/resources/i18n/open/error_zh_CN.properties create mode 100644 sop-gateway/.gitignore create mode 100644 sop-gateway/.mvn/wrapper/maven-wrapper.jar create mode 100644 sop-gateway/.mvn/wrapper/maven-wrapper.properties create mode 100755 sop-gateway/mvnw create mode 100644 sop-gateway/mvnw.cmd create mode 100644 sop-gateway/pom.xml create mode 100644 sop-gateway/src/main/java/com/gitee/sop/gateway/SopGatewayApplication.java create mode 100644 sop-gateway/src/main/java/com/gitee/sop/gateway/config/ZuulConfig.java create mode 100644 sop-gateway/src/main/resources/application.properties create mode 100644 sop-gateway/src/test/java/com/gitee/sop/gateway/PostTest.java create mode 100644 sop-gateway/src/test/java/com/gitee/sop/gateway/SopGatewayApplicationTests.java create mode 100644 sop-gateway/src/test/java/com/gitee/sop/gateway/SopPostTest.java create mode 100644 sop-gateway/src/test/java/com/gitee/sop/gateway/TestBase.java create mode 100644 sop-registry/.gitignore create mode 100644 sop-registry/.mvn/wrapper/maven-wrapper.jar create mode 100644 sop-registry/.mvn/wrapper/maven-wrapper.properties create mode 100755 sop-registry/mvnw create mode 100644 sop-registry/mvnw.cmd create mode 100644 sop-registry/pom.xml create mode 100644 sop-registry/src/main/java/com/gitee/sop/registry/SopRegistryApplication.java create mode 100644 sop-registry/src/main/resources/application.properties create mode 100644 sop-registry/src/test/java/com/gitee/sop/registry/SopRegistryApplicationTests.java create mode 100644 sop-server-common/.gitignore create mode 100644 sop-server-common/pom.xml create mode 100644 sop-server-common/readme.md create mode 100644 sop-server-common/src/main/java/com/gitee/sop/servercommon/annotation/ApiAbility.java create mode 100644 sop-server-common/src/main/java/com/gitee/sop/servercommon/annotation/ApiMapping.java create mode 100644 sop-server-common/src/main/java/com/gitee/sop/servercommon/bean/ParamNames.java create mode 100644 sop-server-common/src/main/java/com/gitee/sop/servercommon/bean/ServiceApiInfo.java create mode 100644 sop-server-common/src/main/java/com/gitee/sop/servercommon/bean/ServiceConfig.java create mode 100644 sop-server-common/src/main/java/com/gitee/sop/servercommon/bean/ServiceContext.java create mode 100644 sop-server-common/src/main/java/com/gitee/sop/servercommon/configuration/AlipayServerConfiguration.java create mode 100644 sop-server-common/src/main/java/com/gitee/sop/servercommon/configuration/BaseServerConfiguration.java create mode 100644 sop-server-common/src/main/java/com/gitee/sop/servercommon/configuration/DefaultGlobalExceptionHandler.java create mode 100644 sop-server-common/src/main/java/com/gitee/sop/servercommon/configuration/GlobalExceptionHandler.java create mode 100644 sop-server-common/src/main/java/com/gitee/sop/servercommon/configuration/TaobaoServerConfiguration.java create mode 100644 sop-server-common/src/main/java/com/gitee/sop/servercommon/exception/ServiceException.java create mode 100644 sop-server-common/src/main/java/com/gitee/sop/servercommon/interceptor/ServiceContextInterceptor.java create mode 100644 sop-server-common/src/main/java/com/gitee/sop/servercommon/manager/ApiMetaManager.java create mode 100644 sop-server-common/src/main/java/com/gitee/sop/servercommon/manager/DefaultRequestMappingEvent.java create mode 100644 sop-server-common/src/main/java/com/gitee/sop/servercommon/manager/RedisApiMetaManager.java create mode 100644 sop-server-common/src/main/java/com/gitee/sop/servercommon/manager/RequestMappingEvent.java create mode 100644 sop-server-common/src/main/java/com/gitee/sop/servercommon/mapping/ApiMappingHandlerMapping.java create mode 100644 sop-server-common/src/main/java/com/gitee/sop/servercommon/mapping/ApiMappingInfo.java create mode 100644 sop-server-common/src/main/java/com/gitee/sop/servercommon/mapping/ApiMappingRequestCondition.java create mode 100644 sop-server-common/src/main/java/com/gitee/sop/servercommon/mapping/ApiMappingStringValueResolver.java create mode 100644 sop-server-common/src/main/java/com/gitee/sop/servercommon/message/ServiceError.java create mode 100644 sop-server-common/src/main/java/com/gitee/sop/servercommon/message/ServiceErrorEnum.java create mode 100644 sop-server-common/src/main/java/com/gitee/sop/servercommon/message/ServiceErrorFactory.java create mode 100644 sop-server-common/src/main/java/com/gitee/sop/servercommon/message/ServiceErrorImpl.java create mode 100644 sop-server-common/src/main/java/com/gitee/sop/servercommon/message/ServiceErrorMeta.java create mode 100644 sop-server-common/src/main/java/com/gitee/sop/servercommon/param/ApiArgumentResolver.java create mode 100644 sop-server-common/src/main/java/com/gitee/sop/servercommon/param/ParamValidator.java create mode 100644 sop-server-common/src/main/java/com/gitee/sop/servercommon/param/ServiceParamValidator.java create mode 100644 sop-server-common/src/main/java/com/gitee/sop/servercommon/result/DefaultServiceResultBuilder.java create mode 100644 sop-server-common/src/main/java/com/gitee/sop/servercommon/result/ServiceResultBuilder.java create mode 100644 sop-server-common/src/main/resources/i18n/isp/error_en.properties create mode 100644 sop-server-common/src/main/resources/i18n/isp/error_zh_CN.properties create mode 100644 sop-story/pom.xml create mode 100644 sop-story/sop-story-api/.gitignore create mode 100644 sop-story/sop-story-api/pom.xml create mode 100644 sop-story/sop-story-api/src/main/java/com/gitee/sop/story/api/domain/Story.java create mode 100644 sop-story/sop-story-api/src/main/java/com/gitee/sop/story/api/service/StoryService.java create mode 100644 sop-story/sop-story-web/.gitignore create mode 100755 sop-story/sop-story-web/mvnw create mode 100644 sop-story/sop-story-web/mvnw.cmd create mode 100644 sop-story/sop-story-web/pom.xml create mode 100644 sop-story/sop-story-web/src/main/java/com/gitee/sop/bookweb/SopStoryApplication.java create mode 100644 sop-story/sop-story-web/src/main/java/com/gitee/sop/bookweb/config/OpenServerConfig.java create mode 100644 sop-story/sop-story-web/src/main/java/com/gitee/sop/bookweb/controller/AlipayController.java create mode 100644 sop-story/sop-story-web/src/main/java/com/gitee/sop/bookweb/controller/Story2Controller.java create mode 100644 sop-story/sop-story-web/src/main/java/com/gitee/sop/bookweb/controller/StoryController.java create mode 100644 sop-story/sop-story-web/src/main/java/com/gitee/sop/bookweb/message/StoryErrorEnum.java create mode 100644 sop-story/sop-story-web/src/main/resources/application.properties create mode 100644 sop-story/sop-story-web/src/main/resources/i18n/isp/bizerror_en.properties create mode 100644 sop-story/sop-story-web/src/main/resources/i18n/isp/bizerror_zh_CN.properties create mode 100644 sop-story/sop-story-web/src/test/java/com/gitee/sop/bookweb/SopStoryApplicationTests.java create mode 100644 sop-test/pom.xml create mode 100644 sop-test/readme.md create mode 100644 sop-test/src/main/java/com/gitee/sop/App.java create mode 100644 sop-test/src/main/java/com/gitee/sop/alipay/AlipayApiException.java create mode 100644 sop-test/src/main/java/com/gitee/sop/alipay/AlipayConstants.java create mode 100644 sop-test/src/main/java/com/gitee/sop/alipay/AlipaySignature.java create mode 100644 sop-test/src/main/java/com/gitee/sop/alipay/StreamUtil.java create mode 100644 sop-test/src/main/java/com/gitee/sop/alipay/StringUtils.java create mode 100644 sop-test/src/main/java/com/gitee/sop/taobao/Constants.java create mode 100644 sop-test/src/main/java/com/gitee/sop/taobao/TaobaoSignature.java create mode 100644 sop-test/src/test/java/com/gitee/sop/AlipayClientPostTest.java create mode 100644 sop-test/src/test/java/com/gitee/sop/TaobaoClientPostTest.java create mode 100644 sop-test/src/test/java/com/gitee/sop/TestBase.java diff --git a/.gitignore b/.gitignore index a1c2a238..c456c4a3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,23 +1,25 @@ -# Compiled class file -*.class +/target/ +!.mvn/wrapper/maven-wrapper.jar -# Log file -*.log +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache -# BlueJ files -*.ctxt +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr -# Mobile Tools for Java (J2ME) -.mtj.tmp/ - -# Package Files # -*.jar -*.war -*.nar -*.ear -*.zip -*.tar.gz -*.rar - -# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml -hs_err_pid* +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +/build/ diff --git a/README.md b/README.md index f122b38a..3d04a52c 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,45 @@ -# SOP +# SOP(Simple Open Platform) -#### 介绍 -一个简单易用的接口开放平台(Simple Open Platform),可实现类似于淘宝开放平台的调用方式。 +一个开放平台解决方案项目,基于Spring Cloud实现,目标是能够让用户快速得搭建起自己的开放平台。 -#### 软件架构 -软件架构说明 +SOP提供了两种接口调用方式,分别是:[支付宝开放平台](https://docs.open.alipay.com/api)的调用方式和[淘宝开放平台](http://open.taobao.com/api.htm?docId=285&docType=2)的调用方式。 +通过简单的配置后,你的项目就具备了和支付宝开放平台的一样的接口提供能力。 + +SOP封装了开放平台大部分功能包括:签名验证、统一异常处理、统一返回内容 、业务参数验证(JSR-303)、秘钥管理等,未来还会实现更多功能。 + +## 项目特点 + +- 接入方式简单,与老项目不冲突,老项目注册到注册中心,然后的方法上加上注解即可。 +- 架构松耦合,业务代码实现在各自微服务上,SOP不参与业务实现,这也是Spring Cloud微服务体系带来的好处。 +- 扩展简单,开放平台对应的功能各自独立,可以自定义实现自己的需求,如:更改参数,更改签名规则等。 + +## 谁能使用这个项目 + +- 有现成的项目,想改造成开放平台供他人调用 +- 有现成的项目,想暴露其中几个接口并通过开放平台供他人调用 +- 想搭一个开放平台新项目,并结合微服务的方式去维护 +- 对开放平台感兴趣的朋友 + +以上情况都可以考虑使用SOP + +## 架构图 -#### 安装教程 -1. xxxx -2. xxxx -3. xxxx +## 工程说明 -#### 使用说明 +- sop-registry:注册中心,eureka实现 +- sop-gateway:网关,zuul实现,统一访问入口 +- sop-gateway-common:网关公共模块,封装常用功能,包含签名校验等功能。 +- sop-server-common:微服务端公共模块,封装配套功能 +- sop-story:示例,story服务,同时作为Provider提供服务 +- sop-book:示例,book服务,也是Consumer,调用story提供的服务 +- sop-test:测试用例 -1. xxxx -2. xxxx -3. xxxx - -#### 参与贡献 - -1. Fork 本仓库 -2. 新建 Feat_xxx 分支 -3. 提交代码 -4. 新建 Pull Request +## 相关文档 -#### 码云特技 -1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md -2. 码云官方博客 [blog.gitee.com](https://blog.gitee.com) -3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解码云上的优秀开源项目 -4. [GVP](https://gitee.com/gvp) 全称是码云最有价值开源项目,是码云综合评定出的优秀开源项目 -5. 码云官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help) -6. 码云封面人物是一档用来展示码云会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/) \ No newline at end of file +## 沟通交流 + +Q群:328419269 diff --git a/mvnw b/mvnw new file mode 100755 index 00000000..5551fde8 --- /dev/null +++ b/mvnw @@ -0,0 +1,286 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven2 Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" + # TODO classpath? +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + wget "$jarUrl" -O "$wrapperJarPath" + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + curl -o "$wrapperJarPath" "$jarUrl" + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 00000000..e5cfb0ae --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,161 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven2 Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" +FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + echo Found %WRAPPER_JAR% +) else ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" + echo Finished downloading %WRAPPER_JAR% +) +@REM End of extension + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/pom.xml b/pom.xml new file mode 100644 index 00000000..7804453f --- /dev/null +++ b/pom.xml @@ -0,0 +1,20 @@ + + + 4.0.0 + com.gitee.sop + sop-parent + 1.0.0-SNAPSHOT + pom + + + sop-registry + sop-gateway + sop-gateway-common + sop-server-common + sop-story + sop-book + sop-test + + \ No newline at end of file diff --git a/sop-book/pom.xml b/sop-book/pom.xml new file mode 100644 index 00000000..6a343033 --- /dev/null +++ b/sop-book/pom.xml @@ -0,0 +1,15 @@ + + + 4.0.0 + com.gitee.sop + sop-book-parent + 1.0.0-SNAPSHOT + pom + + + sop-book-api + sop-book-web + + \ No newline at end of file diff --git a/sop-book/sop-book-api/.gitignore b/sop-book/sop-book-api/.gitignore new file mode 100644 index 00000000..c456c4a3 --- /dev/null +++ b/sop-book/sop-book-api/.gitignore @@ -0,0 +1,25 @@ +/target/ +!.mvn/wrapper/maven-wrapper.jar + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +/build/ diff --git a/sop-book/sop-book-api/pom.xml b/sop-book/sop-book-api/pom.xml new file mode 100644 index 00000000..c47d23c4 --- /dev/null +++ b/sop-book/sop-book-api/pom.xml @@ -0,0 +1,46 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.1.2.RELEASE + + + com.gitee.sop + sop-book-api + 1.0-SNAPSHOT + + + 1.8 + Greenwich.RELEASE + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.projectlombok + lombok + 1.18.4 + provided + + + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + + \ No newline at end of file diff --git a/sop-book/sop-book-api/src/main/java/com/hhdd/book/api/domain/Book.java b/sop-book/sop-book-api/src/main/java/com/hhdd/book/api/domain/Book.java new file mode 100644 index 00000000..a286924a --- /dev/null +++ b/sop-book/sop-book-api/src/main/java/com/hhdd/book/api/domain/Book.java @@ -0,0 +1,12 @@ +package com.gitee.book.api.domain; + +import lombok.Data; + +/** + * @author tanghc + */ +@Data +public class Book { + private int id; + private String name; +} diff --git a/sop-book/sop-book-api/src/main/java/com/hhdd/book/api/service/BookService.java b/sop-book/sop-book-api/src/main/java/com/hhdd/book/api/service/BookService.java new file mode 100644 index 00000000..1076be9d --- /dev/null +++ b/sop-book/sop-book-api/src/main/java/com/hhdd/book/api/service/BookService.java @@ -0,0 +1,14 @@ +package com.hhdd.book.api.service; + +import com.gitee.book.api.domain.Book; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; + +/** + * @author tanghc + */ +@RequestMapping("/book") +public interface BookService { + @RequestMapping("/getBook") + Book getBook(@RequestParam("id") int id); +} diff --git a/sop-book/sop-book-web/.gitignore b/sop-book/sop-book-web/.gitignore new file mode 100644 index 00000000..c456c4a3 --- /dev/null +++ b/sop-book/sop-book-web/.gitignore @@ -0,0 +1,25 @@ +/target/ +!.mvn/wrapper/maven-wrapper.jar + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +/build/ diff --git a/sop-book/sop-book-web/.mvn/wrapper/maven-wrapper.jar b/sop-book/sop-book-web/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..01e67997377a393fd672c7dcde9dccbedf0cb1e9 GIT binary patch literal 48337 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 \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" + # TODO classpath? +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + wget "$jarUrl" -O "$wrapperJarPath" + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + curl -o "$wrapperJarPath" "$jarUrl" + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/sop-book/sop-book-web/mvnw.cmd b/sop-book/sop-book-web/mvnw.cmd new file mode 100644 index 00000000..e5cfb0ae --- /dev/null +++ b/sop-book/sop-book-web/mvnw.cmd @@ -0,0 +1,161 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven2 Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" +FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + echo Found %WRAPPER_JAR% +) else ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" + echo Finished downloading %WRAPPER_JAR% +) +@REM End of extension + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/sop-book/sop-book-web/pom.xml b/sop-book/sop-book-web/pom.xml new file mode 100644 index 00000000..793e2484 --- /dev/null +++ b/sop-book/sop-book-web/pom.xml @@ -0,0 +1,84 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.1.2.RELEASE + + + com.gitee.sop + sop-book-web + 0.0.1-SNAPSHOT + sop-story + Demo project for Spring Boot + + + 1.8 + Greenwich.RELEASE + + + + + com.gitee.sop + sop-book-api + 1.0-SNAPSHOT + + + com.gitee.sop + sop-story-api + 1.0-SNAPSHOT + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.projectlombok + lombok + 1.18.4 + compile + + + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + + + diff --git a/sop-book/sop-book-web/src/main/java/com/gitee/sop/bookweb/SopBookApplication.java b/sop-book/sop-book-web/src/main/java/com/gitee/sop/bookweb/SopBookApplication.java new file mode 100644 index 00000000..cfb16be7 --- /dev/null +++ b/sop-book/sop-book-web/src/main/java/com/gitee/sop/bookweb/SopBookApplication.java @@ -0,0 +1,20 @@ +package com.gitee.sop.bookweb; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; +import org.springframework.cloud.openfeign.EnableFeignClients; + +// 允许调用其他服务 +@EnableFeignClients +// 服务注册 +@EnableDiscoveryClient +@SpringBootApplication +public class SopBookApplication { + + public static void main(String[] args) { + SpringApplication.run(SopBookApplication.class, args); + } + +} + diff --git a/sop-book/sop-book-web/src/main/java/com/gitee/sop/bookweb/consumer/StoryServiceConsumer.java b/sop-book/sop-book-web/src/main/java/com/gitee/sop/bookweb/consumer/StoryServiceConsumer.java new file mode 100644 index 00000000..cce5945b --- /dev/null +++ b/sop-book/sop-book-web/src/main/java/com/gitee/sop/bookweb/consumer/StoryServiceConsumer.java @@ -0,0 +1,14 @@ +package com.gitee.sop.bookweb.consumer; + +import com.gitee.sop.story.api.service.StoryService; +import org.springframework.cloud.openfeign.FeignClient; + +/** + * 调用story服务 + * + * @author tanghc + */ +// value对应的spring.application.name +@FeignClient("story-service") +public interface StoryServiceConsumer extends StoryService { +} diff --git a/sop-book/sop-book-web/src/main/java/com/gitee/sop/bookweb/controller/ApiResult.java b/sop-book/sop-book-web/src/main/java/com/gitee/sop/bookweb/controller/ApiResult.java new file mode 100644 index 00000000..a39434b8 --- /dev/null +++ b/sop-book/sop-book-web/src/main/java/com/gitee/sop/bookweb/controller/ApiResult.java @@ -0,0 +1,74 @@ +package com.gitee.sop.bookweb.controller; + +import com.thoughtworks.xstream.annotations.XStreamAlias; +import lombok.Data; + +/** + * 默认的结果封装类. + *
+ *
+ * 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 + */ +@XStreamAlias("response") +@Data +public class ApiResult { + + /** + * 网关异常信息 + */ + private String msg; + + /** + * 业务异常码 + */ + private String sub_msg; + + /** + * 业务异常信息 + */ + private String sub_code; + + /** + * 返回结果 + */ + private Object data; +} diff --git a/sop-book/sop-book-web/src/main/java/com/gitee/sop/bookweb/controller/BookController.java b/sop-book/sop-book-web/src/main/java/com/gitee/sop/bookweb/controller/BookController.java new file mode 100644 index 00000000..bde5634e --- /dev/null +++ b/sop-book/sop-book-web/src/main/java/com/gitee/sop/bookweb/controller/BookController.java @@ -0,0 +1,73 @@ +package com.gitee.sop.bookweb.controller; + +import com.gitee.book.api.domain.Book; +import com.gitee.sop.bookweb.consumer.StoryServiceConsumer; +import com.gitee.sop.bookweb.param.BookParam; +import com.gitee.sop.story.api.domain.Story; +import com.hhdd.book.api.service.BookService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Arrays; + +/** + * book服务 + * + * @author tanghc + */ +@RestController +public class BookController implements BookService { + + @Autowired + StoryServiceConsumer storyServiceConsumer; + + @Override + public Book getBook(int id) { + Book book = new Book(); + book.setId(id); + book.setName("汪汪队"); + return book; + } + + @RequestMapping("listBookAndStory") + public Object listBookAndStory(int id) { + Book book = new Book(); + book.setId(id); + book.setName("汪汪队"); + + // 调用story服务 + Story story = storyServiceConsumer.getStory(id); + + return Arrays.asList(book, story); + } + + @RequestMapping("getBook2") + public Object getBookError(int id) { + if (id == 0) { + throw new RuntimeException("id不能为空"); + } + Book book = new Book(); + book.setId(id); + book.setName("汪汪队"); + return Arrays.asList(book); + } + + + + @RequestMapping("getBook3") + public Object getBook3(@RequestBody BookParam param) { + if (param.getId() == 0) { + throw new RuntimeException("id不能为空"); + } + Book book = new Book(); + book.setId(param.getId()); + book.setName("小马宝莉"); + + ApiResult apiResult = new ApiResult(); + apiResult.setData(book); + return apiResult; + } + +} diff --git a/sop-book/sop-book-web/src/main/java/com/gitee/sop/bookweb/param/BookParam.java b/sop-book/sop-book-web/src/main/java/com/gitee/sop/bookweb/param/BookParam.java new file mode 100644 index 00000000..47703b09 --- /dev/null +++ b/sop-book/sop-book-web/src/main/java/com/gitee/sop/bookweb/param/BookParam.java @@ -0,0 +1,11 @@ +package com.gitee.sop.bookweb.param; + +import lombok.Data; + +/** + * @author tanghc + */ +@Data +public class BookParam { + private int id; +} diff --git a/sop-book/sop-book-web/src/main/resources/application.properties b/sop-book/sop-book-web/src/main/resources/application.properties new file mode 100644 index 00000000..c163532c --- /dev/null +++ b/sop-book/sop-book-web/src/main/resources/application.properties @@ -0,0 +1,6 @@ +server.port=3333 +spring.application.name=book-service +eureka.host=localhost +eureka.port=1111 +eureka.client.serviceUrl.defaultZone=http://${eureka.host}:${eureka.port}/eureka/ + diff --git a/sop-book/sop-book-web/src/test/java/com/gitee/sop/bookweb/SopBookApplicationTests.java b/sop-book/sop-book-web/src/test/java/com/gitee/sop/bookweb/SopBookApplicationTests.java new file mode 100644 index 00000000..6fa12ac2 --- /dev/null +++ b/sop-book/sop-book-web/src/test/java/com/gitee/sop/bookweb/SopBookApplicationTests.java @@ -0,0 +1,17 @@ +package com.gitee.sop.bookweb; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +@SpringBootTest +public class SopBookApplicationTests { + + @Test + public void contextLoads() { + } + +} + diff --git a/sop-gateway-common/.gitignore b/sop-gateway-common/.gitignore new file mode 100644 index 00000000..c456c4a3 --- /dev/null +++ b/sop-gateway-common/.gitignore @@ -0,0 +1,25 @@ +/target/ +!.mvn/wrapper/maven-wrapper.jar + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +/build/ diff --git a/sop-gateway-common/pom.xml b/sop-gateway-common/pom.xml new file mode 100644 index 00000000..9c1c6bf9 --- /dev/null +++ b/sop-gateway-common/pom.xml @@ -0,0 +1,131 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.1.2.RELEASE + + + com.gitee.sop + sop-gateway-common + 1.0.0-SNAPSHOT + sop-gateway-common + sop-gateway-common + + + 1.8 + Greenwich.RELEASE + + 1.0.13 + 1.7.5 + + + 4.11 + + 1.2.15 + 2.5 + 1.3.3 + 3.2.2 + 1.4.7 + 1.7 + 6.0.10.Final + 0.31 + 3.2.0 + 18.0 + 1.8.8.RELEASE + 4.1.25.Final + 2.0.5.Final + 5.5.12 + 5.5.8 + 5.2.0 + 6.2 + 1.11 + + + + + org.springframework.cloud + spring-cloud-starter-netflix-zuul + + + org.springframework.boot + spring-boot-starter-test + test + + + com.thoughtworks.xstream + xstream + ${xstream.version} + compile + + + com.alibaba + fastjson + ${fastjson.version} + + + org.springframework.boot + spring-boot-starter-data-redis + + + + commons-collections + commons-collections + ${commons-collection.version} + + + commons-io + commons-io + ${commons-io.version} + + + commons-fileupload + commons-fileupload + ${commons-fileupload.version} + + + commons-codec + commons-codec + ${commons-codec.version} + + + org.projectlombok + lombok + 1.18.4 + provided + + + + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + + + diff --git a/sop-gateway-common/readme.md b/sop-gateway-common/readme.md new file mode 100644 index 00000000..05c58128 --- /dev/null +++ b/sop-gateway-common/readme.md @@ -0,0 +1,3 @@ +# sop-gateway-common + +zuul通用组件,主要封装网关的一些功能。 diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/bean/ApiConfig.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/bean/ApiConfig.java new file mode 100644 index 00000000..47168791 --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/bean/ApiConfig.java @@ -0,0 +1,116 @@ +package com.gitee.sop.gatewaycommon.bean; + +import com.gitee.sop.gatewaycommon.configuration.BaseZuulController; +import com.gitee.sop.gatewaycommon.param.ApiParamParser; +import com.gitee.sop.gatewaycommon.param.ParamParser; +import com.gitee.sop.gatewaycommon.result.ApiResultExecutor; +import com.gitee.sop.gatewaycommon.result.JsonResultSerializer; +import com.gitee.sop.gatewaycommon.result.ResultExecutor; +import com.gitee.sop.gatewaycommon.result.ResultSerializer; +import com.gitee.sop.gatewaycommon.result.XmlResultSerializer; +import com.gitee.sop.gatewaycommon.secret.AppSecretManager; +import com.gitee.sop.gatewaycommon.secret.CacheAppSecretManager; +import com.gitee.sop.gatewaycommon.session.ApiSessionManager; +import com.gitee.sop.gatewaycommon.session.SessionManager; +import com.gitee.sop.gatewaycommon.validate.ApiEncrypter; +import com.gitee.sop.gatewaycommon.validate.ApiSigner; +import com.gitee.sop.gatewaycommon.validate.ApiValidator; +import com.gitee.sop.gatewaycommon.validate.Encrypter; +import com.gitee.sop.gatewaycommon.validate.Signer; +import com.gitee.sop.gatewaycommon.validate.Validator; +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * @author tanghc + */ +@Data +public class ApiConfig { + + private static ApiConfig instance = new ApiConfig(); + + private ApiConfig() { + } + + /** + * 合并结果处理 + */ + private ResultExecutor resultExecutor = new ApiResultExecutor(); + + /** + * json序列化 + */ + private ResultSerializer jsonResultSerializer = new JsonResultSerializer(); + /** + * xml序列化 + */ + private ResultSerializer xmlResultSerializer = new XmlResultSerializer(); + + /** + * app秘钥管理 + */ + private AppSecretManager appSecretManager = new CacheAppSecretManager(); + + /** + * 加密工具 + */ + private Encrypter encrypter = new ApiEncrypter(); + + /** + * 签名工具 + */ + private Signer signer = new ApiSigner(); + + /** + * 参数解析 + */ + private ParamParser paramParser = new ApiParamParser(); + + /** + * 验证 + */ + private Validator validator = new ApiValidator(); + + /** + * session管理 + */ + private SessionManager sessionManager = new ApiSessionManager(); + + /** + * 错误模块 + */ + private List i18nModules = new ArrayList(); + + /** + * 基础Controller + */ + private BaseZuulController baseZuulController = new BaseZuulController(); + + + // -------- fields --------- + /** + * 忽略验证 + */ + private boolean ignoreValidate; + + /** + * 超时时间 + */ + private int timeoutSeconds = 60 * 5; + + public void addAppSecret(Map appSecretPair) { + this.appSecretManager.addAppSecret(appSecretPair); + } + + public static ApiConfig getInstance() { + return instance; + } + + public static void setInstance(ApiConfig apiConfig) { + instance = apiConfig; + } + +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/bean/ApiContext.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/bean/ApiContext.java new file mode 100644 index 00000000..72ff62d7 --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/bean/ApiContext.java @@ -0,0 +1,186 @@ +package com.gitee.sop.gatewaycommon.bean; + +import com.gitee.sop.gatewaycommon.param.UploadContext; +import com.netflix.zuul.context.RequestContext; +import com.gitee.sop.gatewaycommon.param.ApiParam; +import com.gitee.sop.gatewaycommon.session.SessionManager; + +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.util.Locale; + +/** + * 应用上下文,方便获取信息 + * + * @author tanghc + * + */ +public class ApiContext { + private static final String ATTR_PARAM = "zuul.common.api.param"; + private static final String ATTR_UPLOAD_CONTEXT = "zuul.common.api.upload_context"; + + private ApiContext(){} + + private static void setAttr(String name, Object val) { + HttpServletRequest request = getRequest(); + if (request != null) { + request.setAttribute(name, val); + } + } + + private static Object getAttr(String name) { + HttpServletRequest request = getRequest(); + if (request == null) { + return null; + } + return request.getAttribute(name); + } + + /** + * 获取随机码 + * @return 返回随机码 + */ + public static String getRandomKey() { + HttpSession session = getSession(); + if (session == null) { + return null; + } + return (String) session.getAttribute(SopConstants.RANDOM_KEY_NAME); + } + + + + /** + * 获取HttpServletRequest + * + * @return HttpServletRequest + */ + public static HttpServletRequest getRequest() { + return RequestContext.getCurrentContext().getRequest(); + } + + /** + * 返回默认的HttpServletRequest.getSession(); + * + * @return 没有返回null + */ + public static HttpSession getSession() { + HttpServletRequest req = getRequest(); + if (req == null) { + return null; + } else { + return req.getSession(); + } + } + + /** + * 获取session管理器 + * @return 返回SessionManager + */ + public static SessionManager getSessionManager() { + return getApiConfig().getSessionManager(); + } + + /** + * 返回自定义的session,被SessionManager管理 + * + * @return 如果sessionId为null,则返回null + */ + public static HttpSession getManagedSession() { + String sessionId = getSessionId(); + if (sessionId != null) { + return getSessionManager().getSession(sessionId); + } else { + return null; + } + } + + /** + * 同getSessionId() + * @return 返回accessToken,没有返回null + */ + public static String getAccessToken() { + return getSessionId(); + } + + + /** + * 获取登陆的token + * + * @return 没有返回null + */ + public static String getSessionId() { + ApiParam apiParam = getApiParam(); + if (apiParam == null) { + return null; + } + return apiParam.fetchAccessToken(); + } + + /** + * 获取本地化,从HttpServletRequest中获取,没有则返回Locale.SIMPLIFIED_CHINESE + * + * @return Locale + */ + public static Locale getLocale() { + HttpServletRequest req = getRequest(); + if (req == null) { + return Locale.SIMPLIFIED_CHINESE; + } + return req.getLocale(); + } + + public static void setApiParam(ApiParam apiParam) { + setAttr(ATTR_PARAM, apiParam); + } + + /** + * 获取系统参数 + * + * @return 返回ApiParam + */ + public static ApiParam getApiParam() { + return (ApiParam) getAttr(ATTR_PARAM); + } + + public static ApiConfig getApiConfig() { + return ApiConfig.getInstance(); + } + + public static void setApiConfig(ApiConfig apiConfig) { + ApiConfig.setInstance(apiConfig); + } + + + public static ServletContext getServletContext() { + ServletContext ctx = null; + HttpSession session = getSession(); + if (session != null) { + ctx = session.getServletContext(); + } + return ctx; + } + + /** + * 获取上传文件,如果客户端有文件上传,从这里取。 + * @return 如果没有文件上传,返回null + */ + public static UploadContext getUploadContext() { + return (UploadContext) getAttr(ATTR_UPLOAD_CONTEXT); + } + + public static void setUploadContext(UploadContext uploadCtx) { + setAttr(ATTR_UPLOAD_CONTEXT, uploadCtx); + } + + /** + * 获取response + * @return 返回response + */ + public static HttpServletResponse getResponse() { + return RequestContext.getCurrentContext().getResponse(); + } + +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/bean/RequestMode.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/bean/RequestMode.java new file mode 100644 index 00000000..8fb274a8 --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/bean/RequestMode.java @@ -0,0 +1,17 @@ +package com.gitee.sop.gatewaycommon.bean; + +/** + * @author tanghc + */ +public enum RequestMode { + + /** + * 数字签名请求模式 + */ + SIGNATURE, + /** + * 公私钥加密模式,这样请求和返回的数据都经过加密处理 + */ + ENCRYPT + +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/bean/ServiceApiInfo.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/bean/ServiceApiInfo.java new file mode 100644 index 00000000..854ef7f0 --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/bean/ServiceApiInfo.java @@ -0,0 +1,29 @@ +package com.gitee.sop.gatewaycommon.bean; + +import lombok.Data; + +import java.util.List; + +/** + * @author tanghc + */ +@Data +public class ServiceApiInfo { + private String md5; + private String appName; + private List apis; + + @Data + public static class ApiMeta { + /** 接口名 */ + private String name; + /** 请求path */ + private String path; + /** 版本号 */ + private String version; + + public String fetchNameVersion() { + return name + version; + } + } +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/bean/SopConstants.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/bean/SopConstants.java new file mode 100644 index 00000000..d6fd6123 --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/bean/SopConstants.java @@ -0,0 +1,33 @@ +package com.gitee.sop.gatewaycommon.bean; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +/** + * @author tanghc + */ +public class SopConstants { + + public static final String NULL = "null"; + public static final String RANDOM_KEY_NAME = "ssl_randomKey"; + public static final Charset CHARSET_UTF8 = StandardCharsets.UTF_8; + public static final String FORMAT_JSON = "json"; + public static final String FORMAT_XML = "xml"; + public static final String AUTHORIZATION = "Authorization"; + public static final String PREFIX_BEARER = "Bearer "; + public static final String YMDHMS = "yyyy-MM-dd HH:mm:ss"; + + public static final String DEFAULT_SIGN_METHOD = "md5"; + + public static final String CONTENT_TYPE_NAME = "Content-Type"; + public static final String CONTENT_TYPE_JSON = "application/json;charset=UTF-8"; + + public static final String LINE = "\n"; + + public static final String EMPTY_JSON = "{}"; + + public static final String SORT_DESC = "DESC"; + + public static final String REST_PARAM_NAME = "_REST_PARAM_NAME_"; + public static final String REST_PARAM_VERSION = "_REST_PARAM_VERSION_"; +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/configuration/AlipayZuulConfiguration.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/configuration/AlipayZuulConfiguration.java new file mode 100644 index 00000000..0c341582 --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/configuration/AlipayZuulConfiguration.java @@ -0,0 +1,16 @@ +package com.gitee.sop.gatewaycommon.configuration; + +import com.gitee.sop.gatewaycommon.bean.ApiContext; +import com.gitee.sop.gatewaycommon.validate.alipay.AlipaySigner; + +/** + * 具备支付宝开放平台能力配置 https://docs.open.alipay.com/api + * + * @author tanghc + */ +public class AlipayZuulConfiguration extends BaseZuulConfiguration { + + static { + ApiContext.getApiConfig().setSigner(new AlipaySigner()); + } +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/configuration/BaseZuulConfiguration.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/configuration/BaseZuulConfiguration.java new file mode 100644 index 00000000..704ed496 --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/configuration/BaseZuulConfiguration.java @@ -0,0 +1,112 @@ +package com.gitee.sop.gatewaycommon.configuration; + +import com.gitee.sop.gatewaycommon.bean.ApiContext; +import com.gitee.sop.gatewaycommon.filter.ErrorFilter; +import com.gitee.sop.gatewaycommon.filter.PostResultFilter; +import com.gitee.sop.gatewaycommon.filter.PreValidateFilter; +import com.gitee.sop.gatewaycommon.manager.ApiMetaChangeListener; +import com.gitee.sop.gatewaycommon.manager.ApiMetaContext; +import com.gitee.sop.gatewaycommon.manager.ApiMetaManager; +import com.gitee.sop.gatewaycommon.manager.DefaultApiMetaContext; +import com.gitee.sop.gatewaycommon.manager.DefaultApiMetaManager; +import com.gitee.sop.gatewaycommon.manager.SopRouteLocator; +import com.gitee.sop.gatewaycommon.message.ErrorFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.cloud.netflix.zuul.filters.ProxyRequestHelper; +import org.springframework.cloud.netflix.zuul.filters.ZuulProperties; +import org.springframework.cloud.netflix.zuul.filters.pre.PreDecorationFilter; +import org.springframework.context.annotation.Bean; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.listener.PatternTopic; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; + +import javax.annotation.PostConstruct; + +/** + * @author tanghc + */ +public class BaseZuulConfiguration { + + public static final String API_CHANGE_CHANNEL = "channel.sop.api.change"; + + + @Autowired + protected ZuulProperties zuulProperties; + + @Autowired + protected ServerProperties server; + + @Autowired + protected ApiMetaManager apiMetaManager; + + @Bean + public ApiMetaContext apiMetaContext() { + return new DefaultApiMetaContext(); + } + + @Bean + public ApiMetaManager apiMetaManager(StringRedisTemplate stringRedisTemplate, ApiMetaContext apiMetaContext) { + return new DefaultApiMetaManager(stringRedisTemplate, apiMetaContext); + } + + @Bean + PreValidateFilter preValidateFilter() { + return new PreValidateFilter(); + } + + @Bean + public PreDecorationFilter preDecorationFilter(ApiMetaContext apiMetaContext, ProxyRequestHelper proxyRequestHelper) { + SopRouteLocator routeLocator = new SopRouteLocator(apiMetaContext); + return new PreDecorationFilter(routeLocator, + this.server.getServlet().getContextPath(), + this.zuulProperties, + proxyRequestHelper); + } + + @Bean + ErrorFilter errorFilter() { + return new ErrorFilter(); + } + + @Bean + PostResultFilter postResultFilter() { + return new PostResultFilter(); + } + + /** + * 配置redis事件订阅 + * + * @param apiMetaManager + * @param redisTemplate + * @return + */ + @Bean + RedisMessageListenerContainer container(ApiMetaManager apiMetaManager, StringRedisTemplate redisTemplate) { + RedisMessageListenerContainer container = new RedisMessageListenerContainer(); + container.setConnectionFactory(redisTemplate.getConnectionFactory()); + ApiMetaChangeListener apiMetaChangeListener = new ApiMetaChangeListener(apiMetaManager, redisTemplate); + container.addMessageListener(apiMetaChangeListener, new PatternTopic(API_CHANGE_CHANNEL)); + return container; + } + + @Bean + BaseZuulController baseZuulController() { + return ApiContext.getApiConfig().getBaseZuulController(); + } + + @PostConstruct + public void after() { + doAfter(); + } + + protected void doAfter() { + initMessage(); + apiMetaManager.refresh(); + } + + protected void initMessage() { + ErrorFactory.initMessageSource(ApiContext.getApiConfig().getI18nModules()); + } + +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/configuration/BaseZuulController.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/configuration/BaseZuulController.java new file mode 100644 index 00000000..399db035 --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/configuration/BaseZuulController.java @@ -0,0 +1,44 @@ +package com.gitee.sop.gatewaycommon.configuration; + +import com.gitee.sop.gatewaycommon.bean.ApiContext; +import com.gitee.sop.gatewaycommon.result.ResultExecutor; +import com.netflix.zuul.context.RequestContext; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.web.servlet.error.ErrorController; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * 处理网关自身异常 + * + * @author tanghc + */ +@Controller +@Slf4j +public class BaseZuulController implements ErrorController { + + public static final String ERROR_PATH = "/error"; + + // 错误最终会到这里来 + @RequestMapping(ERROR_PATH) + @ResponseBody + public Object error(HttpServletRequest request, HttpServletResponse response) { + RequestContext ctx = RequestContext.getCurrentContext(); + Throwable throwable = ctx.getThrowable(); + return this.buildResult(throwable); + } + + protected Object buildResult(Throwable throwable) { + ResultExecutor resultExecutor = ApiContext.getApiConfig().getResultExecutor(); + return resultExecutor.mergeError(throwable); + } + + @Override + public String getErrorPath() { + return ERROR_PATH; + } +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/configuration/TaobaoZuulConfiguration.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/configuration/TaobaoZuulConfiguration.java new file mode 100644 index 00000000..ef3dd3e2 --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/configuration/TaobaoZuulConfiguration.java @@ -0,0 +1,23 @@ +package com.gitee.sop.gatewaycommon.configuration; + +import com.gitee.sop.gatewaycommon.bean.ApiContext; +import com.gitee.sop.gatewaycommon.param.ParamNames; +import com.gitee.sop.gatewaycommon.validate.taobao.TaobaoSigner; + +/** + * 具备淘宝开放平台能力配置 + * 淘宝开放平台:http://open.taobao.com/doc.htm + * @author tanghc + */ +public class TaobaoZuulConfiguration extends BaseZuulConfiguration { + + static { + ParamNames.APP_KEY_NAME = "app_key"; + ParamNames.SIGN_TYPE_NAME = "sign_method"; + ParamNames.VERSION_NAME = "v"; + ParamNames.APP_AUTH_TOKEN_NAME = "session"; + + ApiContext.getApiConfig().setSigner(new TaobaoSigner()); + } + +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/easyopen/EasyopenRoute.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/easyopen/EasyopenRoute.java new file mode 100644 index 00000000..7a827c0f --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/easyopen/EasyopenRoute.java @@ -0,0 +1,20 @@ +package com.gitee.sop.gatewaycommon.easyopen; + +import org.springframework.cloud.netflix.zuul.filters.Route; + +import java.util.Collections; +import java.util.Set; + +/** + * @author tanghc + */ +public class EasyopenRoute extends Route { + public EasyopenRoute(String id, String path, String location, String prefix, Boolean retryable, Set ignoredHeaders) { + super(id, path, location, prefix, retryable, ignoredHeaders); + } + + public EasyopenRoute(String id, String location) { + this(id, "/" + location + "/", location, "", false, Collections.emptySet()); + } + +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/easyopen/EasyopenZuulConfig.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/easyopen/EasyopenZuulConfig.java new file mode 100644 index 00000000..50f42a78 --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/easyopen/EasyopenZuulConfig.java @@ -0,0 +1,74 @@ +package com.gitee.sop.gatewaycommon.easyopen; + +import com.gitee.sop.gatewaycommon.filter.ErrorFilter; +import com.gitee.sop.gatewaycommon.filter.PostResultFilter; +import com.gitee.sop.gatewaycommon.easyopen.filter.PostEasyopenResultFilter; +import com.gitee.sop.gatewaycommon.filter.PreValidateFilter; +import com.gitee.sop.gatewaycommon.manager.ApiMetaContext; +import com.gitee.sop.gatewaycommon.manager.ApiMetaManager; +import com.gitee.sop.gatewaycommon.manager.DefaultApiMetaContext; +import com.gitee.sop.gatewaycommon.manager.DefaultApiMetaManager; +import com.gitee.sop.gatewaycommon.manager.SopRouteLocator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.cloud.netflix.zuul.filters.ProxyRequestHelper; +import org.springframework.cloud.netflix.zuul.filters.ZuulProperties; +import org.springframework.cloud.netflix.zuul.filters.pre.PreDecorationFilter; +import org.springframework.context.annotation.Bean; +import org.springframework.data.redis.core.StringRedisTemplate; + +import javax.annotation.PostConstruct; + +/** + * @author tanghc + */ +public class EasyopenZuulConfig { + + @Autowired + protected ZuulProperties zuulProperties; + + @Autowired + protected ServerProperties server; + + @Autowired + protected ApiMetaManager apiMetaManager; + + @Bean + public ApiMetaContext apiMetaContext() { + return new DefaultApiMetaContext(); + } + + @Bean + public ApiMetaManager apiMetaManager(StringRedisTemplate stringRedisTemplate, ApiMetaContext apiMetaContext) { + return new DefaultApiMetaManager(stringRedisTemplate, apiMetaContext); + } + + @Bean + public PreDecorationFilter preDecorationFilter(ApiMetaContext apiMetaContext, ProxyRequestHelper proxyRequestHelper) { + SopRouteLocator routeLocator = new SopRouteLocator(apiMetaContext); + return new PreDecorationFilter(routeLocator, + this.server.getServlet().getContextPath(), + this.zuulProperties, + proxyRequestHelper); + } + + @Bean + PreValidateFilter preValidateFilter() { + return new PreValidateFilter(); + } + + @Bean + ErrorFilter errorFilter() { + return new ErrorFilter(); + } + + @Bean + PostResultFilter postResultFilter() { + return new PostEasyopenResultFilter(); + } + + @PostConstruct + public void after() { + apiMetaManager.refresh(); + } +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/easyopen/filter/PostEasyopenResultFilter.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/easyopen/filter/PostEasyopenResultFilter.java new file mode 100644 index 00000000..38316134 --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/easyopen/filter/PostEasyopenResultFilter.java @@ -0,0 +1,46 @@ +package com.gitee.sop.gatewaycommon.easyopen.filter; + +import com.gitee.sop.gatewaycommon.filter.PostResultFilter; + +/** + * 合并微服务结果,统一返回格式 + * + * @author tanghc + */ +public class PostEasyopenResultFilter extends PostResultFilter { + private static final String EASYOPEN_SUCCESS_CODE = "0"; +// +// protected Result processServiceResult(InputStream responseDataStream, ResultBuilder resultBuilder) throws IOException { +// String responseBody = IOUtils.toString(responseDataStream, SopConstants.CHARSET_UTF8); +// ApiResult result = this.parseApiResult(responseBody); +// Result finalResult; +// if (EASYOPEN_SUCCESS_CODE.equals(result.getCode())) { +// finalResult = resultBuilder.buildSuccessResult(GATEWAY_SUCCESS_CODE, null, result.getData()); +// } else { +// // 业务出错 +// finalResult = resultBuilder.buildServiceError(result.getCode(), result.getMsg()); +// } +// return finalResult; +// } +// +// protected ApiResult parseApiResult(String responseBody) { +// ApiParam apiParam = ApiContext.getApiParam(); +// String format = apiParam.fetchFormat(); +// return SopConstants.FORMAT_XML.equalsIgnoreCase(format) +// ? XmlUtil.unserialize(responseBody, ApiResult.class) +// : JSON.parseObject(responseBody, ApiResult.class); +// } +// +// protected ResultSerializer buildResultSerializer(HttpServletRequest request, ApiConfig apiConfig) { +// ApiParam apiParam = ApiContext.getApiParam(); +// String format = apiParam.fetchFormat(); +// if (SopConstants.FORMAT_JSON.equalsIgnoreCase(format)) { +// return apiConfig.getJsonResultSerializer(); +// } else if (SopConstants.FORMAT_XML.equalsIgnoreCase(format)) { +// // xml格式输出 +// return apiConfig.getXmlResultSerializer(); +// } else { +// throw ErrorEnum.isv_invalid_format.getErrorMeta().getException(); +// } +// } +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/exception/ApiException.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/exception/ApiException.java new file mode 100644 index 00000000..ae6bb9ac --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/exception/ApiException.java @@ -0,0 +1,26 @@ +package com.gitee.sop.gatewaycommon.exception; + + +import com.gitee.sop.gatewaycommon.message.Error; + +/** + * @author tanghc + */ +public class ApiException extends RuntimeException { + + private Error error; + + public ApiException(Throwable cause, Error error) { + super(cause); + this.error = error; + } + + public ApiException(Error error) { + super(error.toString()); + this.error = error; + } + + public Error getError() { + return error; + } +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/filter/BaseZuulFilter.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/filter/BaseZuulFilter.java new file mode 100644 index 00000000..78dbc74b --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/filter/BaseZuulFilter.java @@ -0,0 +1,96 @@ +package com.gitee.sop.gatewaycommon.filter; + +import com.alibaba.fastjson.JSON; +import com.gitee.sop.gatewaycommon.result.ApiResult; +import com.netflix.zuul.ZuulFilter; +import com.netflix.zuul.context.RequestContext; +import com.netflix.zuul.exception.ZuulException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author tanghc + */ +public abstract class BaseZuulFilter extends ZuulFilter { + + protected Logger log = LoggerFactory.getLogger(getClass()); + + private Integer filterOrder; + + protected abstract FilterType getFilterType(); + + protected abstract int getFilterOrder(); + + protected abstract Object doRun(RequestContext requestContext) throws ZuulException; + + /** + * 设置过滤器顺序 + * + * @param filterOrder 顺序,值越小优先执行 + * @return 返回自身对象 + */ + public BaseZuulFilter order(int filterOrder) { + this.filterOrder = filterOrder; + return this; + } + + @Override + public int filterOrder() { + return filterOrder != null ? filterOrder : this.getFilterOrder(); + } + + @Override + public String filterType() { + return this.getFilterType().getType(); + } + + @Override + public boolean shouldFilter() { + return true; + } + + @Override + public Object run() throws ZuulException { + return this.doRun(RequestContext.getCurrentContext()); + } + + /** + * 过滤该请求,不往下级服务去转发请求,到此结束。并填充responseBody + * + * @param requestContext + * @param result + */ + public static void stopRouteAndReturn(RequestContext requestContext, Object result) { + requestContext.setSendZuulResponse(false); + requestContext.setResponseBody(JSON.toJSONString(result)); + } + + public static void main(String[] args) { + System.out.println(JSON.toJSONString(new ApiResult())); + } + + /** + * to classify a filter by type. Standard types in Zuul are "pre" for pre-routing filtering, + * "route" for routing to an origin, "post" for post-routing filters, "error" for error handling. + * We also support a "static" type for static responses see StaticResponseFilter. + * Any filterType made be created or added and doRun by calling FilterProcessor.runFilters(type) + */ + public enum FilterType { + PRE("pre"), + ROUTE("route"), + POST("post"), + ERROR("error"), + STATIC("static"), + ; + + FilterType(String type) { + this.type = type; + } + + private String type; + + public String getType() { + return type; + } + } +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/filter/ErrorFilter.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/filter/ErrorFilter.java new file mode 100644 index 00000000..e476c7d6 --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/filter/ErrorFilter.java @@ -0,0 +1,59 @@ +package com.gitee.sop.gatewaycommon.filter; + +import com.netflix.zuul.FilterProcessor; +import com.netflix.zuul.ZuulFilter; +import com.netflix.zuul.context.RequestContext; +import com.netflix.zuul.exception.ZuulException; +import org.springframework.cloud.netflix.zuul.filters.post.SendErrorFilter; +import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants; + +/** + * @author tanghc + */ +public class ErrorFilter extends SendErrorFilter { + + public static final String FAILED_FILTER = "failed.filter"; + + private int filterOrder = 10; + + public ErrorFilter() { + initFilterProcessor(); + } + + public void initFilterProcessor() { + FilterProcessor instance = FilterProcessor.getInstance(); + if (!(instance instanceof MyFilterProcessor)) { + FilterProcessor.setProcessor(new MyFilterProcessor()); + } + } + + @Override + public int filterOrder() { + return filterOrder; + } + + @Override + public boolean shouldFilter() { + // 判断:仅处理来自post过滤器引起的异常 + RequestContext ctx = RequestContext.getCurrentContext(); + ZuulFilter failedFilter = (ZuulFilter) ctx.get(FAILED_FILTER); + return failedFilter != null && failedFilter.filterType().equals(FilterConstants.POST_TYPE); + } + + public static class MyFilterProcessor extends FilterProcessor { + @Override + public Object processZuulFilter(ZuulFilter filter) throws ZuulException { + try { + return super.processZuulFilter(filter); + } catch (ZuulException e) { + RequestContext ctx = RequestContext.getCurrentContext(); + ctx.set(FAILED_FILTER, filter); + throw e; + } + } + } + + public void setFilterOrder(int filterOrder) { + this.filterOrder = filterOrder; + } +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/filter/PostResultFilter.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/filter/PostResultFilter.java new file mode 100644 index 00000000..9bb819c7 --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/filter/PostResultFilter.java @@ -0,0 +1,49 @@ +package com.gitee.sop.gatewaycommon.filter; + +import com.gitee.sop.gatewaycommon.bean.ApiConfig; +import com.gitee.sop.gatewaycommon.bean.ApiContext; +import com.gitee.sop.gatewaycommon.bean.SopConstants; +import com.gitee.sop.gatewaycommon.result.ResultExecutor; +import com.netflix.zuul.context.RequestContext; +import com.netflix.zuul.exception.ZuulException; +import org.apache.commons.io.IOUtils; +import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants; + +import java.io.InputStream; + +/** + * 合并微服务结果,统一返回格式 + * + * @author tanghc + */ +public class PostResultFilter extends BaseZuulFilter { + + @Override + protected FilterType getFilterType() { + return FilterType.POST; + } + + @Override + protected int getFilterOrder() { + return FilterConstants.SEND_RESPONSE_FILTER_ORDER - 1; + } + + @Override + protected Object doRun(RequestContext requestContext) throws ZuulException { + int responseStatusCode = requestContext.getResponseStatusCode(); + InputStream responseDataStream = requestContext.getResponseDataStream(); + ApiConfig apiConfig = ApiContext.getApiConfig(); + ResultExecutor resultExecutor = apiConfig.getResultExecutor(); + String serviceResult; + try { + serviceResult = IOUtils.toString(responseDataStream, SopConstants.CHARSET_UTF8); + } catch (Exception e) { + log.error("业务方无数据返回", e); + serviceResult = SopConstants.EMPTY_JSON; + } + String finalResult = resultExecutor.mergeResult(responseStatusCode, serviceResult); + requestContext.setResponseBody(finalResult); + return null; + } + +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/filter/PreTokenFilter.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/filter/PreTokenFilter.java new file mode 100644 index 00000000..49fa27a9 --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/filter/PreTokenFilter.java @@ -0,0 +1,41 @@ +package com.gitee.sop.gatewaycommon.filter; + +import com.gitee.sop.gatewaycommon.message.ErrorEnum; +import com.netflix.zuul.context.RequestContext; +import com.netflix.zuul.exception.ZuulException; +import org.apache.commons.lang.StringUtils; +import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants; + +import javax.servlet.http.HttpServletRequest; + +/** + * @author tanghc + */ +public class PreTokenFilter extends BaseZuulFilter { + + @Override + protected FilterType getFilterType() { + return FilterType.PRE; + } + + @Override + protected int getFilterOrder() { + return FilterConstants.PRE_DECORATION_FILTER_ORDER + 1; + } + + @Override + protected Object doRun(RequestContext ctx) throws ZuulException { + Object serviceId = ctx.get(FilterConstants.SERVICE_ID_KEY); + log.info("serviceId:{}", serviceId); + HttpServletRequest request = ctx.getRequest(); + + log.info("send {} request to {}", request.getMethod(), request.getRequestURL().toString()); + + String accessToken = request.getParameter("access_token"); + if (StringUtils.isBlank(accessToken)) { + throw ErrorEnum.AOP_INVALID_APP_AUTH_TOKEN.getErrorMeta().getException(); + } + return null; + } + +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/filter/PreValidateFilter.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/filter/PreValidateFilter.java new file mode 100644 index 00000000..6f28cb11 --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/filter/PreValidateFilter.java @@ -0,0 +1,48 @@ +package com.gitee.sop.gatewaycommon.filter; + +import com.gitee.sop.gatewaycommon.bean.ApiContext; +import com.gitee.sop.gatewaycommon.bean.ApiConfig; +import com.gitee.sop.gatewaycommon.exception.ApiException; +import com.gitee.sop.gatewaycommon.param.ApiParam; +import com.gitee.sop.gatewaycommon.validate.Validator; +import com.netflix.zuul.context.RequestContext; +import com.netflix.zuul.exception.ZuulException; +import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants; + +import javax.servlet.http.HttpServletRequest; + +/** + * 前置校验 + * @author tanghc + */ +public class PreValidateFilter extends BaseZuulFilter { + @Override + protected FilterType getFilterType() { + return FilterType.PRE; + } + + @Override + protected int getFilterOrder() { + // 在org.springframework.cloud.netflix.zuul.filters.pre.PreDecorationFilter前面 + return FilterConstants.PRE_DECORATION_FILTER_ORDER - 1; + } + + @Override + protected Object doRun(RequestContext requestContext) throws ZuulException { + HttpServletRequest request = requestContext.getRequest(); + ApiConfig apiConfig = ApiContext.getApiConfig(); + // 解析参数 + ApiParam param = apiConfig.getParamParser().parse(request); + ApiContext.setApiParam(param); + // 验证操作,这里有负责验证签名参数 + Validator validator = apiConfig.getValidator(); + try { + validator.validate(param); + } catch (ApiException e) { + log.error("签名验证失败,params:{}", param.toJSONString(), e); + throw e; + } + return null; + } + +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/manager/ApiMetaChangeListener.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/manager/ApiMetaChangeListener.java new file mode 100644 index 00000000..e95baaf1 --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/manager/ApiMetaChangeListener.java @@ -0,0 +1,25 @@ +package com.gitee.sop.gatewaycommon.manager; + +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.connection.MessageListener; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.serializer.RedisSerializer; + +public class ApiMetaChangeListener implements MessageListener { + + private ApiMetaManager apiMetaManager; + private StringRedisTemplate redisTemplate; + + public ApiMetaChangeListener(ApiMetaManager apiMetaManager, StringRedisTemplate redisTemplate) { + this.apiMetaManager = apiMetaManager; + this.redisTemplate = redisTemplate; + } + + @Override + public void onMessage(Message message, byte[] bytes) { + RedisSerializer stringSerializer = redisTemplate.getStringSerializer(); + String msg = stringSerializer.deserialize(message.getBody()); + this.apiMetaManager.onChange(msg); + } + +} \ No newline at end of file diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/manager/ApiMetaConfig.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/manager/ApiMetaConfig.java new file mode 100644 index 00000000..da879a3a --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/manager/ApiMetaConfig.java @@ -0,0 +1,20 @@ +package com.gitee.sop.gatewaycommon.manager; + +import org.springframework.data.redis.core.StringRedisTemplate; + +/** + * @author tanghc + */ +public class ApiMetaConfig { + + private StringRedisTemplate redisTemplate; + + public ApiMetaConfig(StringRedisTemplate redisTemplate) { + this.redisTemplate = redisTemplate; + } + + public void loadApiMetas() { + + } + +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/manager/ApiMetaContext.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/manager/ApiMetaContext.java new file mode 100644 index 00000000..2e5ff58d --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/manager/ApiMetaContext.java @@ -0,0 +1,13 @@ +package com.gitee.sop.gatewaycommon.manager; + +import com.gitee.sop.gatewaycommon.bean.ServiceApiInfo; +import org.springframework.cloud.netflix.zuul.filters.Route; + +/** + * @author tanghc + */ +public interface ApiMetaContext { + void reload(String serviceId, ServiceApiInfo serviceApiInfo); + + Route getRoute(String nameVersion); +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/manager/ApiMetaManager.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/manager/ApiMetaManager.java new file mode 100644 index 00000000..855ccf70 --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/manager/ApiMetaManager.java @@ -0,0 +1,22 @@ +package com.gitee.sop.gatewaycommon.manager; + +/** + * 管理各服务接口信息 + * @author tanghc + */ +public interface ApiMetaManager { + + String API_STORE_KEY = "com.gitee.sop.api"; + + /** + * 刷新素有的微服务接口信息 + */ + void refresh(); + + /** + * 某个服务接口更改时触发 + * @param serviceApiInfoJson 接口信息 + */ + void onChange(String serviceApiInfoJson); + +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/manager/DefaultApiMetaContext.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/manager/DefaultApiMetaContext.java new file mode 100644 index 00000000..2565119b --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/manager/DefaultApiMetaContext.java @@ -0,0 +1,62 @@ +package com.gitee.sop.gatewaycommon.manager; + +import com.gitee.sop.gatewaycommon.bean.ServiceApiInfo; +import com.gitee.sop.gatewaycommon.message.ErrorEnum; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cloud.netflix.zuul.filters.Route; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * @author tanghc + */ +@Slf4j +public class DefaultApiMetaContext implements ApiMetaContext { + + // key:nameVersion + private Map nameVersionServiceIdMap = new HashMap<>(128); + + // key: serviceId , value: md5 + private Map serviceIdMd5Map = new HashMap<>(16); + + @Override + public void reload(String serviceId, ServiceApiInfo serviceApiInfo) { + String md5 = serviceIdMd5Map.get(serviceId); + if (md5 != null && md5.equals(serviceApiInfo.getMd5())) { + log.info("MD5相同,无需更改本地接口信息,appName:{}, md5:{}", serviceApiInfo.getAppName(), serviceApiInfo.getMd5()); + return; + } + log.info("更新本地接口信息,appName:{}, md5:{}", serviceApiInfo.getAppName(), serviceApiInfo.getMd5()); + // 移除原来的 + Iterator> iterator = nameVersionServiceIdMap.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + if (entry.getValue().getLocation().equals(serviceId)) { + iterator.remove(); + } + } + List apis = serviceApiInfo.getApis(); + for (ServiceApiInfo.ApiMeta apiMeta : apis) { + Route route = this.buildRoute(serviceId, apiMeta); + nameVersionServiceIdMap.put(apiMeta.fetchNameVersion(), route); + } + serviceIdMd5Map.put(serviceId, serviceApiInfo.getMd5()); + } + + @Override + public Route getRoute(String nameVersion) { + Route route = nameVersionServiceIdMap.get(nameVersion); + if (route == null) { + throw ErrorEnum.ISV_INVALID_METHOD.getErrorMeta().getException(); + } + return route; + } + + protected Route buildRoute(String serviceId, ServiceApiInfo.ApiMeta apiMeta) { + return new Route(apiMeta.getName(), apiMeta.getPath(), serviceId, null, false, null); + } + +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/manager/DefaultApiMetaManager.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/manager/DefaultApiMetaManager.java new file mode 100644 index 00000000..44ef79c1 --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/manager/DefaultApiMetaManager.java @@ -0,0 +1,51 @@ +package com.gitee.sop.gatewaycommon.manager; + +import com.alibaba.fastjson.JSON; +import com.gitee.sop.gatewaycommon.bean.ServiceApiInfo; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.StringRedisTemplate; + +import java.util.Map; + +/** + * 保存在redis中,结构为HSET。格式如下: + *

+ * com.gitee.sop.api
+ *     <serviceId>:{ md5:"xxx", apis:[{name:"", version:""}] }
+ * 
+ * @author tanghc + */ +@Getter +@Slf4j +public class DefaultApiMetaManager implements ApiMetaManager { + + private StringRedisTemplate redisTemplate; + private ApiMetaContext apiMetaContext; + + public DefaultApiMetaManager(StringRedisTemplate redisTemplate, ApiMetaContext apiMetaContext) { + this.redisTemplate = redisTemplate; + this.apiMetaContext = apiMetaContext; + } + + @Override + public void refresh() { + log.info("刷新本地接口信息"); + Map entries = redisTemplate.opsForHash().entries(API_STORE_KEY); + for (Map.Entry entry : entries.entrySet()) { + log.info("更新微服务接口,appName:{}", entry.getKey()); + String serviceId = entry.getKey().toString(); + String serviceApiInfoJson = entry.getValue().toString(); + ServiceApiInfo serviceApiInfo = JSON.parseObject(serviceApiInfoJson, ServiceApiInfo.class); + apiMetaContext.reload(serviceId, serviceApiInfo); + } + } + + @Override + public void onChange(String serviceApiInfoJson) { + ServiceApiInfo serviceApiInfo = JSON.parseObject(serviceApiInfoJson, ServiceApiInfo.class); + log.info("Redis订阅推送接口信息,appName:{}, md5:{}", serviceApiInfo.getAppName(), serviceApiInfo.getMd5()); + this.apiMetaContext.reload(serviceApiInfo.getAppName(), serviceApiInfo); + } + +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/manager/SopRouteLocator.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/manager/SopRouteLocator.java new file mode 100644 index 00000000..ffae417f --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/manager/SopRouteLocator.java @@ -0,0 +1,45 @@ +package com.gitee.sop.gatewaycommon.manager; + +import com.gitee.sop.gatewaycommon.bean.ApiContext; +import com.gitee.sop.gatewaycommon.param.ApiParam; +import org.springframework.cloud.netflix.zuul.filters.Route; +import org.springframework.cloud.netflix.zuul.filters.RouteLocator; +import org.springframework.core.Ordered; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * @author tanghc + */ +public class SopRouteLocator implements RouteLocator, Ordered { + + private ApiMetaContext apiMetaContext; + + public SopRouteLocator(ApiMetaContext apiMetaContext) { + this.apiMetaContext = apiMetaContext; + } + + @Override + public Collection getIgnoredPaths() { + return Collections.emptyList(); + } + + @Override + public List getRoutes() { + return null; + } + + @Override + public Route getMatchingRoute(String path) { + ApiParam param = ApiContext.getApiParam(); + String nameVersion = param.fetchNameVersion(); + return apiMetaContext.getRoute(nameVersion); + } + + @Override + public int getOrder() { + return 0; + } +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/message/Error.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/message/Error.java new file mode 100644 index 00000000..fe928099 --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/message/Error.java @@ -0,0 +1,48 @@ +package com.gitee.sop.gatewaycommon.message; + +/** + * 定义错误返回 + * code(返回码) + * msg(返回码描述) + * sub_code(明细返回码) + * sub_msg(明细返回码描述) + * 解决方案 + * @author tanghc + */ +public interface Error { + /** + * 获取网关状态码 + * + * @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-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/message/ErrorEnum.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/message/ErrorEnum.java new file mode 100644 index 00000000..e963f4f9 --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/message/ErrorEnum.java @@ -0,0 +1,66 @@ +package com.gitee.sop.gatewaycommon.message; + +/** + * @author tanghc + */ +public enum ErrorEnum { + SUCCESS("10000", ""), + + ISP_UNKNOW_ERROR("20000", "isp.unknow-error"), + AOP_UNKNOW_ERROR("20000", "aop.unknow-error"), + + AOP_INVALID_AUTH_TOKEN("20001", "aop.invalid-auth-token"), + AOP_INVALID_AUTH_OKEN("20001", "aop.invalid-auth-token"), + AOP_AUTH_TOKEN_TIME_OUT("20001", "aop.auth-token-time-out"), + AOP_INVALID_APP_AUTH_TOKEN("20001", "aop.invalid-app-auth-token"), + AOP_INVALID_APP_AUTH_TOKEN_NO_API("20001", "aop.invalid-app-auth-token-no-api"), + AOP_APP_AUTH_TOKEN_TIME_OUT("20001", "aop.app-auth-token-time-out"), + AOP_NO_PRODUCT_REG_BY_PARTNER("20001", "aop.no-product-reg-by-partner"), + + ISV_MISSING_METHOD("40001", "isv.missing-method"), + ISV_MISSING_SIGNATURE("40001", "isv.missing-signature"), + ISV_MISSING_SIGNATURE_TYPE("40001", "isv.missing-signature-type"), + ISV_MISSING_SIGNATURE_KEY("40001", "isv.missing-signature-key"), + ISV_MISSING_APP_ID("40001", "isv.missing-app-id"), + ISV_MISSING_TIMESTAMP("40001", "isv.missing-timestamp"), + ISV_MISSING_VERSION("40001", "isv.missing-version"), + ISV_DECRYPTION_ERROR_MISSING_ENCRYPT_TYPE("40001", "isv.decryption-error-missing-encrypt-type"), + + ISV_INVALID_PARAMETER("40002", "isv.invalid-parameter"), + ISV_UPLOAD_FAIL("40002", "isv.upload-fail"), + ISV_INVALID_FILE_EXTENSION("40002", "isv.invalid-file-extension"), + ISV_INVALID_FILE_SIZE("40002", "isv.invalid-file-size"), + ISV_INVALID_METHOD("40002", "isv.invalid-method"), + ISV_INVALID_FORMAT("40002", "isv.invalid-format"), + ISV_INVALID_SIGNATURE_TYPE("40002", "isv.invalid-signature-type"), + ISV_INVALID_SIGNATURE("40002", "isv.invalid-signature"), + ISV_INVALID_ENCRYPT_TYPE("40002", "isv.invalid-encrypt-type"), + ISV_INVALID_ENCRYPT("40002", "isv.invalid-encrypt"), + ISV_INVALID_APP_ID("40002", "isv.invalid-app-id"), + ISV_INVALID_TIMESTAMP("40002", "isv.invalid-timestamp"), + ISV_INVALID_CHARSET("40002", "isv.invalid-charset"), + ISV_INVALID_DIGEST("40002", "isv.invalid-digest"), + ISV_DECRYPTION_ERROR_NOT_VALID_ENCRYPT_TYPE("40002", "isv.decryption-error-not-valid-encrypt-type"), + ISV_DECRYPTION_ERROR_NOT_VALID_ENCRYPT_KEY("40002", "isv.decryption-error-not-valid-encrypt-key"), + ISV_DECRYPTION_ERROR_UNKNOWN("40002", "isv.decryption-error-unknown"), + ISV_MISSING_SIGNATURE_CONFIG("40002", "isv.missing-signature-config"), + ISV_NOT_SUPPORT_APP_AUTH("40002", "isv.not-support-app-auth"), + ISV_SUSPECTED_ATTACK("40002", "isv.suspected-attack"), + ISV_INVALID_CONTENT_TYPE("40002", "isv.invalid-content-type"), + + BIZ_ERROR("40004", ""), + + ISV_INSUFFICIENT_ISV_PERMISSIONS("40006", "isv.insufficient-isv-permissions"), + ISV_INSUFFICIENT_USER_PERMISSIONS("40006", "isv.insufficient-user-permissions"), + + ; + private ErrorMeta errorMeta; + + ErrorEnum(String code, String sub_code) { + this.errorMeta = new ErrorMeta("open.error_", code, sub_code); + } + + public ErrorMeta getErrorMeta() { + return errorMeta; + } +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/message/ErrorFactory.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/message/ErrorFactory.java new file mode 100644 index 00000000..c6df7fbc --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/message/ErrorFactory.java @@ -0,0 +1,120 @@ +package com.gitee.sop.gatewaycommon.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.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 { + + 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 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 Error getError(ErrorMeta errorMeta, Locale locale, Object... params) { + String key = errorMeta.getModulePrefix() + errorMeta.getCode() + errorMeta.getSubCode() + locale.toString(); + Error 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.unknow-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.unknow-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-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/message/ErrorImpl.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/message/ErrorImpl.java new file mode 100644 index 00000000..6fb66b87 --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/message/ErrorImpl.java @@ -0,0 +1,23 @@ +package com.gitee.sop.gatewaycommon.message; + +import lombok.Data; + +@Data +public class ErrorImpl implements Error { + private String code; + private String msg; + private String sub_code; + private String sub_msg; + private String solution; + + public ErrorImpl() { + } + + 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; + } +} \ No newline at end of file diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/message/ErrorMeta.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/message/ErrorMeta.java new file mode 100644 index 00000000..5bb5bc8a --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/message/ErrorMeta.java @@ -0,0 +1,47 @@ +package com.gitee.sop.gatewaycommon.message; + +import com.gitee.sop.gatewaycommon.bean.ApiContext; +import com.gitee.sop.gatewaycommon.exception.ApiException; +import lombok.Getter; + +/** + * 错误对象 + * + * @author tanghc + */ +@Getter +public class ErrorMeta { + + private String modulePrefix; + private String code; + private String subCode; + + public ErrorMeta(String modulePrefix, String code, String subCode) { + this.modulePrefix = modulePrefix; + this.code = code; + this.subCode = subCode; + } + + public Error getError() { + return ErrorFactory.getError(this, ApiContext.getLocale()); + } + + /** + * 返回网关exception + * + * @param params 参数 + * @return 返回exception + */ + public ApiException getException(Object... params) { + if (params != null && params.length == 1) { + Object param = params[0]; + if (param instanceof Throwable) { + Error error = ErrorFactory.getError(this, ApiContext.getLocale()); + return new ApiException((Throwable) param, error); + } + } + Error error = ErrorFactory.getError(this, ApiContext.getLocale(), params); + return new ApiException(error); + } + +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/param/ApiParam.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/param/ApiParam.java new file mode 100644 index 00000000..d2433980 --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/param/ApiParam.java @@ -0,0 +1,197 @@ +package com.gitee.sop.gatewaycommon.param; + +import com.alibaba.fastjson.JSONObject; +import com.gitee.sop.gatewaycommon.bean.SopConstants; +import com.netflix.zuul.context.RequestContext; +import org.apache.commons.lang.StringUtils; + +import javax.servlet.http.HttpServletRequest; +import java.util.Map; + +/** + * 客户端传来的参数放在这里. + * + * @author tanghc + */ +public class ApiParam extends JSONObject implements Param { + public ApiParam(Map map) { + super(map); + } + + private boolean ignoreSign; + private boolean ignoreValidate; + + private String restName; + private String restVersion; + + /** + * 获取sign,并从param中删除 + * + * @return 返回sign内容 + */ + public String fetchSignAndRemove() { + String sign = this.fetchSign(); + this.remove(ParamNames.SIGN_NAME); + return sign; + } + + public HttpServletRequest fetchRequest() { + return RequestContext.getCurrentContext().getRequest(); + } + + /** + * 是否忽略验证签名 + * + * @return 返回true,忽略签名 + */ + public boolean fetchIgnoreSign() { + return ignoreSign; + } + + public void setIgnoreSign(boolean ignoreSign) { + this.ignoreSign = ignoreSign; + } + + public boolean fetchIgnoreValidate() { + return ignoreValidate; + } + + public void setIgnoreValidate(boolean ignoreValidate) { + this.ignoreValidate = ignoreValidate; + } + + /** + * 接口名,如:goods.list + */ + @Override + public String fetchName() { + String name = getString(ParamNames.API_NAME); + if (name == null) { + name = this.restName; + } + return name; + } + + public void setName(String name) { + this.restName = name; + } + + public String fetchNameVersion() { + return buildNameVersion(this.fetchName(), this.fetchVersion()); + } + + public static String buildNameVersion(String name, String version) { + if (StringUtils.isEmpty(version)) { + return name; + } else { + return name + version; + } + } + + /** + * 版本号 + */ + @Override + public String fetchVersion() { + String version = getString(ParamNames.VERSION_NAME); + if (version == null) { + version = this.restVersion; + } + return version; + } + + public void setVersion(String version) { + this.restVersion = version; + } + + /** + * 接入应用ID + */ + @Override + public String fetchAppKey() { + return getString(ParamNames.APP_KEY_NAME); + } + + public void setAppKey(String appKey) { + put(ParamNames.APP_KEY_NAME, appKey); + } + + /** + * 参数,urlencode后的 + */ + @Override + public String fetchData() { + return getString(ParamNames.BIZ_CONTENT_NAME); + } + + public void setData(String json) { + put(ParamNames.BIZ_CONTENT_NAME, json); + } + + /** + * 时间戳,格式为yyyy-MM-dd HH:mm:ss,例如:2015-01-01 12:00:00 + */ + @Override + public String fetchTimestamp() { + return getString(ParamNames.TIMESTAMP_NAME); + } + + public void setTimestamp(String timestamp) { + put(ParamNames.TIMESTAMP_NAME, timestamp); + } + + /** + * 签名串 + */ + @Override + public String fetchSign() { + return getString(ParamNames.SIGN_NAME); + } + + public void setSign(String sign) { + put(ParamNames.SIGN_NAME, sign); + } + + @Override + public String fetchFormat() { + String format = getString(ParamNames.FORMAT_NAME); + if (format == null || "".equals(format)) { + return SopConstants.FORMAT_JSON; + } + return format; + } + + public void setFormat(String format) { + put(ParamNames.FORMAT_NAME, format); + } + + @Override + public String fetchAccessToken() { + return getString(ParamNames.APP_AUTH_TOKEN_NAME); + } + + @Override + public String fetchSignMethod() { + String signMethod = getString(ParamNames.SIGN_TYPE_NAME); + if (signMethod == null) { + return SopConstants.DEFAULT_SIGN_METHOD; + } else { + return signMethod; + } + } + + @Override + public String fetchCharset() { + return getString(ParamNames.CHARSET_NAME); + } + + @Override + public ApiParam clone() { + ApiParam param = new ApiParam(this); + param.ignoreSign = this.ignoreSign; + param.ignoreValidate = this.ignoreValidate; + return param; + } + + +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/param/ApiParamParser.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/param/ApiParamParser.java new file mode 100644 index 00000000..d2834b50 --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/param/ApiParamParser.java @@ -0,0 +1,104 @@ +package com.gitee.sop.gatewaycommon.param; + +import com.alibaba.fastjson.JSON; +import com.gitee.sop.gatewaycommon.bean.ApiContext; +import com.gitee.sop.gatewaycommon.message.ErrorEnum; +import com.gitee.sop.gatewaycommon.util.RequestUtil; +import com.netflix.zuul.http.HttpServletRequestWrapper; +import org.springframework.http.MediaType; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.multipart.MultipartHttpServletRequest; +import org.springframework.web.multipart.commons.CommonsMultipartResolver; + +import javax.servlet.http.HttpServletRequest; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * 参数解析默认实现 + * + * @author tanghc + */ +public class ApiParamParser implements ParamParser { + + private static final String CONTENT_TYPE_MULTIPART = MediaType.MULTIPART_FORM_DATA_VALUE; + private static final String CONTENT_TYPE_JSON = MediaType.APPLICATION_JSON_VALUE; + private static final String CONTENT_TYPE_TEXT = MediaType.TEXT_PLAIN_VALUE; + + @Override + public ApiParam parse(HttpServletRequest request) { + try { + Map params = this.getJson(request); + return new ApiParam(params); + } catch (Exception e) { + throw ErrorEnum.ISV_INVALID_PARAMETER.getErrorMeta().getException(); + } + } + + public Map getJson(HttpServletRequest request) throws Exception { + // zuul会做一层包装 + if (request instanceof HttpServletRequestWrapper) { + HttpServletRequestWrapper req = (HttpServletRequestWrapper) request; + request = req.getRequest(); + } + Map params = null; + + if (RequestUtil.isGetRequest(request)) { + params = RequestUtil.convertRequestParamsToMap(request); + } else { + String contectType = request.getContentType(); + + if (contectType == null) { + contectType = ""; + } + + contectType = contectType.toLowerCase(); + + // json或者纯文本形式 + if (contectType.contains(CONTENT_TYPE_JSON) || contectType.contains(CONTENT_TYPE_TEXT)) { + String txt = RequestUtil.getText(request); + params = JSON.parseObject(txt); + } else if (contectType.contains(CONTENT_TYPE_MULTIPART)) { + // 上传文件形式 + params = this.parseUploadRequest(request); + } else { + params = RequestUtil.convertRequestParamsToMap(request); + } + } + + return params; + } + + /** + * 解析文件上传请求 + * + * @param request + * @return 返回json字符串 + */ + protected Map parseUploadRequest(HttpServletRequest request) { + CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver( + request.getSession().getServletContext()); + // 检查form中是否有enctype="multipart/form-data" + if (multipartResolver.isMultipart(request)) { + MultipartHttpServletRequest multiRequest = (MultipartHttpServletRequest) request; + Map fileMap = multiRequest.getFileMap(); + Map finalMap = new HashMap<>(fileMap.size()); + + Set keys = fileMap.keySet(); + for (String name : keys) { + MultipartFile file = fileMap.get(name); + if (file.getSize() > 0) { + finalMap.put(name, file); + } + } + if (finalMap.size() > 0) { + // 保存上传文件 + ApiContext.setUploadContext(new ApiUploadContext(finalMap)); + } + } + + return RequestUtil.convertRequestParamsToMap(request); + } + +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/param/ApiUploadContext.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/param/ApiUploadContext.java new file mode 100644 index 00000000..6fb56403 --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/param/ApiUploadContext.java @@ -0,0 +1,42 @@ +package com.gitee.sop.gatewaycommon.param; + +import org.springframework.web.multipart.MultipartFile; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * 存放上传文件 + * + * @author tanghc + */ +public class ApiUploadContext implements UploadContext { + + private Map fileMap; + private List allFile; + + public ApiUploadContext(Map map) { + if (map == null) { + map = Collections.emptyMap(); + } + this.fileMap = map; + this.allFile = new ArrayList<>(map.values()); + } + + @Override + public MultipartFile getFile(int index) { + return this.allFile.get(index); + } + + @Override + public MultipartFile getFile(String name) { + return fileMap.get(name); + } + + @Override + public List getAllFile() { + return this.allFile; + } +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/param/Param.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/param/Param.java new file mode 100644 index 00000000..b2a6387c --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/param/Param.java @@ -0,0 +1,70 @@ +package com.gitee.sop.gatewaycommon.param; + +import java.io.Serializable; + +/** + * @author tanghc + */ +public interface Param extends Serializable { + + /** + * 获取接口名 + * @return 返回接口名 + */ + String fetchName(); + + /** + * 获取版本号 + * @return 返回版本号 + */ + String fetchVersion(); + + /** + * 获取appKey + * @return 返回appKey + */ + String fetchAppKey(); + + /** + * 获取业务参数 + * @return 返回业务参数 + */ + String fetchData(); + + /** + * 获取时间戳 + * @return 返回时间戳 + */ + String fetchTimestamp(); + + /** + * 获取签名串 + * @return 返回签名串 + */ + String fetchSign(); + + /** + * 获取格式化类型 + * @return 返回格式化类型 + */ + String fetchFormat(); + + /** + * 获取accessToken + * @return 返回accessToken + */ + String fetchAccessToken(); + + /** + * 获取签名方式 + * @return 返回签名方式 + */ + String fetchSignMethod(); + + /** + * 请求使用的编码格式,如utf-8,gbk,gb2312等 + * @return 请求使用的编码格式,如utf-8,gbk,gb2312等 + */ + String fetchCharset(); + +} \ No newline at end of file diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/param/ParamNames.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/param/ParamNames.java new file mode 100644 index 00000000..5d5e0928 --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/param/ParamNames.java @@ -0,0 +1,46 @@ +package com.gitee.sop.gatewaycommon.param; + +/** + * 请求参数名定义 + * + * 参数 类型 是否必填 最大长度 描述 示例值 + * 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 + * 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"; + /** 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"; + + +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/param/ParamParser.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/param/ParamParser.java new file mode 100644 index 00000000..3edceb3d --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/param/ParamParser.java @@ -0,0 +1,19 @@ +package com.gitee.sop.gatewaycommon.param; + +import javax.servlet.http.HttpServletRequest; + +/** + * 负责解析参数 + * @author tanghc + * + */ +public interface ParamParser { + + /** + * 从request提取参数 + * @param request + * @return 返回ApiParam + * @throws Exception + */ + ApiParam parse(HttpServletRequest request); +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/param/UploadContext.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/param/UploadContext.java new file mode 100644 index 00000000..bb72a2d7 --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/param/UploadContext.java @@ -0,0 +1,36 @@ +package com.gitee.sop.gatewaycommon.param; + +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +/** + * 获取上传文件 + * + * @author tanghc + */ +public interface UploadContext { + /** + * 根据索引获取上传文件,从0开始 + * + * @param index + * @return 返回上传文件信息 + */ + MultipartFile getFile(int index); + + /** + * 根据表单名获取上传文件 + * + * @param name + * 表单名称 + * @return 返回上传文件信息 + */ + MultipartFile getFile(String name); + + /** + * 获取所有的上传文件 + * + * @return 返回所有的上传文件 + */ + List getAllFile(); +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/result/ApiResult.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/result/ApiResult.java new file mode 100644 index 00000000..9247cc76 --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/result/ApiResult.java @@ -0,0 +1,77 @@ +package com.gitee.sop.gatewaycommon.result; + +import com.gitee.sop.gatewaycommon.message.ErrorEnum; +import com.thoughtworks.xstream.annotations.XStreamAlias; +import lombok.Data; + +/** + * 默认的结果封装类. + *
+ *
+ * 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 + */ +@XStreamAlias("response") +@Data +public class ApiResult implements Result { + + /** + * 网关异常码,范围0~100 成功返回"0" + */ + private String code = ErrorEnum.SUCCESS.getErrorMeta().getCode(); + + /** + * 网关异常信息 + */ + private String msg; + + /** + * 业务异常码 + */ + private String sub_msg; + + /** + * 业务异常信息 + */ + private String sub_code; + + private String sign; +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/result/ApiResultExecutor.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/result/ApiResultExecutor.java new file mode 100644 index 00000000..f8bf8c7e --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/result/ApiResultExecutor.java @@ -0,0 +1,125 @@ +package com.gitee.sop.gatewaycommon.result; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.gitee.sop.gatewaycommon.bean.ApiContext; +import com.gitee.sop.gatewaycommon.exception.ApiException; +import com.gitee.sop.gatewaycommon.message.Error; +import com.gitee.sop.gatewaycommon.message.ErrorEnum; +import com.gitee.sop.gatewaycommon.message.ErrorMeta; +import com.gitee.sop.gatewaycommon.param.ApiParam; +import com.gitee.sop.gatewaycommon.param.ParamNames; +import com.netflix.zuul.exception.ZuulException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; + +/** + * @author tanghc + */ +@Slf4j +public class ApiResultExecutor implements ResultExecutor { + + private static final ErrorMeta SUCCESS_META = ErrorEnum.SUCCESS.getErrorMeta(); + private static final ErrorMeta ISP_UNKNOW_ERROR_META = ErrorEnum.ISP_UNKNOW_ERROR.getErrorMeta(); + private static final ErrorMeta ISP_BIZ_ERROR = ErrorEnum.BIZ_ERROR.getErrorMeta(); + + + public static final int BIZ_ERROR_STATUS = 4000; + private static final char DOT = '.'; + private static final char UNDERLINE = '_'; + public static final String GATEWAY_CODE_NAME = "code"; + public static final String GATEWAY_MSG_NAME = "msg"; + public static final String DATA_SUFFIX = "_response"; + + @Override + public String mergeResult(int responseStatus, String responseData) { + if (responseStatus == HttpStatus.OK.value() || responseStatus == BIZ_ERROR_STATUS) { + return mergeSuccess(responseStatus, responseData); + } else { + // 微服务端有可能返回500错误 + // {"path":"/book/getBook3","error":"Internal Server Error","message":"id不能为空","timestamp":"2019-02-13T07:41:00.495+0000","status":500} + return mergeError(responseData); + } + } + + @Override + public String mergeError(Throwable throwable) { + Error error = null; + if (throwable instanceof ZuulException) { + ZuulException ex = (ZuulException) throwable; + Throwable cause = ex.getCause(); + if (cause instanceof ApiException) { + ApiException apiException = (ApiException) cause; + error = apiException.getError(); + } + } + if (error == null) { + error = ErrorEnum.AOP_UNKNOW_ERROR.getErrorMeta().getError(); + } + JSONObject jsonObject = (JSONObject)JSON.toJSON(error); + return this.merge(jsonObject); + } + + /* + 成功示例 + { + "alipay_trade_fastpay_refund_query_response": { + "code": "10000", + "msg": "Success", + "trade_no": "2014112611001004680073956707", + "out_trade_no": "20150320010101001", + "out_request_no": "20150320010101001", + "refund_reason": "用户退款请求", + "total_amount": 100.2, + "refund_amount": 12.33 + }, + "sign": "ERITJKEIJKJHKKKKKKKHJEREEEEEEEEEEE" + } + */ + public String mergeSuccess(int responseStatus, String serviceResult) { + JSONObject jsonObjectService; + // 如果是业务出错 + if (responseStatus == BIZ_ERROR_STATUS) { + jsonObjectService = JSON.parseObject(serviceResult); + jsonObjectService.put(GATEWAY_CODE_NAME, ISP_BIZ_ERROR.getCode()); + jsonObjectService.put(GATEWAY_MSG_NAME, ISP_BIZ_ERROR.getError().getMsg()); + } else { + // 200正常返回 + jsonObjectService = JSON.parseObject(serviceResult); + jsonObjectService.put(GATEWAY_CODE_NAME, SUCCESS_META.getCode()); + jsonObjectService.put(GATEWAY_MSG_NAME, SUCCESS_META.getError().getMsg()); + } + return this.merge(jsonObjectService); + } + + /* + 异常示例 + { + "alipay_trade_fastpay_refund_query_response": { + "code": "20000", + "msg": "Service Currently Unavailable", + "sub_code": "isp.unknow-error", + "sub_msg": "系统繁忙" + }, + "sign": "ERITJKEIJKJHKKKKKKKHJEREEEEEEEEEEE" + } + */ + public String mergeError(String serviceResult) { + JSONObject jsonObjectService = new JSONObject(); + jsonObjectService.put(GATEWAY_CODE_NAME, ISP_UNKNOW_ERROR_META.getCode()); + jsonObjectService.put(GATEWAY_MSG_NAME, ISP_UNKNOW_ERROR_META.getError().getMsg()); + return this.merge(jsonObjectService); + } + + private String merge(JSONObject jsonObjectService) { + JSONObject ret = new JSONObject(); + ApiParam apiParam = ApiContext.getApiParam(); + // 点换成下划线 + String apiName = apiParam.fetchName().replace(DOT, UNDERLINE); + ret.put(apiName + DATA_SUFFIX, jsonObjectService); + ret.put(ParamNames.SIGN_NAME, apiParam.fetchSign()); + return ret.toJSONString(); + } + + +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/result/JsonResultSerializer.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/result/JsonResultSerializer.java new file mode 100644 index 00000000..e1d14797 --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/result/JsonResultSerializer.java @@ -0,0 +1,16 @@ +package com.gitee.sop.gatewaycommon.result; + +import com.alibaba.fastjson.JSON; + +/** + * 序列化json + * @author tanghc + */ +public class JsonResultSerializer implements ResultSerializer { + + @Override + public String serialize(Object obj) { + return JSON.toJSONString(obj); + } + +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/result/Result.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/result/Result.java new file mode 100644 index 00000000..8c1a7a7d --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/result/Result.java @@ -0,0 +1,12 @@ +package com.gitee.sop.gatewaycommon.result; + +import java.io.Serializable; + +/** + * 返回结果,后续自定义的返回类需要实现这个接口。 + * + * @author tanghc + */ +public interface Result extends Serializable { + +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/result/ResultBuilder.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/result/ResultBuilder.java new file mode 100644 index 00000000..a2bf66e5 --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/result/ResultBuilder.java @@ -0,0 +1,27 @@ +package com.gitee.sop.gatewaycommon.result; + +import com.gitee.sop.gatewaycommon.message.Error; + +/** + * @author tanghc + */ +public interface ResultBuilder { + + + /** + * 构建网关错误返回结果 + * + * @param throwable 异常 + * @return 返回最终结果 + */ + Result buildGatewayError(Throwable throwable); + + /** + * 构建网关错误返回结果 + * + * @param error error + * @return 返回最终结果 + */ + Result buildGatewayError(Error error); + +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/result/ResultExecutor.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/result/ResultExecutor.java new file mode 100644 index 00000000..afdd2a5f --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/result/ResultExecutor.java @@ -0,0 +1,10 @@ +package com.gitee.sop.gatewaycommon.result; + +/** + * @author tanghc + */ +public interface ResultExecutor { + String mergeResult(int responseStatus, String responseData); + + String mergeError(Throwable throwable); +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/result/ResultSerializer.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/result/ResultSerializer.java new file mode 100644 index 00000000..a92efffb --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/result/ResultSerializer.java @@ -0,0 +1,16 @@ +package com.gitee.sop.gatewaycommon.result; + +/** + * 对象序列化 + * @author tanghc + * + */ +public interface ResultSerializer { + /** + * 序列化 + * + * @param obj + * @return 返回序列化后的结果 + */ + String serialize(Object obj); +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/result/XmlResultSerializer.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/result/XmlResultSerializer.java new file mode 100644 index 00000000..fdacbf55 --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/result/XmlResultSerializer.java @@ -0,0 +1,16 @@ +package com.gitee.sop.gatewaycommon.result; + +import com.gitee.sop.gatewaycommon.util.XmlUtil; + +/** + * 序列化成xml + * @author tanghc + */ +public class XmlResultSerializer implements ResultSerializer { + + @Override + public String serialize(Object obj) { + return XmlUtil.serialize(obj); + } + +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/secret/AppSecretManager.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/secret/AppSecretManager.java new file mode 100644 index 00000000..3777215b --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/secret/AppSecretManager.java @@ -0,0 +1,32 @@ +package com.gitee.sop.gatewaycommon.secret; + +import java.util.Map; + +/** + * 负责秘钥管理 + * @author tanghc + */ +public interface AppSecretManager { + + /** + * 初始化秘钥数据 + * @param appSecretStore + */ + void addAppSecret(Map appSecretStore); + + /** + * 获取应用程序的密钥 + * + * @param appKey + * @return 返回秘钥 + */ + String getSecret(String appKey); + + /** + * 是否是合法的appKey + * + * @param appKey + * @return 返回appKey + */ + boolean isValidAppKey(String appKey); +} \ No newline at end of file diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/secret/CacheAppSecretManager.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/secret/CacheAppSecretManager.java new file mode 100644 index 00000000..59060361 --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/secret/CacheAppSecretManager.java @@ -0,0 +1,33 @@ +package com.gitee.sop.gatewaycommon.secret; + +import java.util.HashMap; +import java.util.Map; + +/** + * appkey,secret默认管理,简单放在map中,如果要放在redis中,可以参照此方式实现AppSecretManager,然后在ApiConfig中setAppSecretManager() + * @author tanghc + * + */ +public class CacheAppSecretManager implements AppSecretManager { + + private Map secretMap = new HashMap(64); + + @Override + public void addAppSecret(Map appSecretStore) { + secretMap.putAll(appSecretStore); + } + + @Override + public String getSecret(String appKey) { + return secretMap.get(appKey); + } + + @Override + public boolean isValidAppKey(String appKey) { + if (appKey == null) { + return false; + } + return getSecret(appKey) != null; + } + +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/secret/FileAppSecretManager.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/secret/FileAppSecretManager.java new file mode 100644 index 00000000..bfbc46ff --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/secret/FileAppSecretManager.java @@ -0,0 +1,56 @@ +package com.gitee.sop.gatewaycommon.secret; + +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PropertiesLoaderUtils; + +import java.io.IOException; +import java.util.Map; +import java.util.Properties; + +/** + * appkey,secret文件管理,功能同CacheAppSecretManager,这个是将appKey,secret放在属性文件中
+ * key为appKey,value为secret + * @author tanghc + * + */ +public class FileAppSecretManager implements AppSecretManager { + + private String appSecretFile = "appSecret.properties"; + + private Properties properties; + + @Override + public void addAppSecret(Map appSecretStore) { + properties.putAll(appSecretStore); + } + + @Override + public String getSecret(String appKey) { + if (properties == null) { + try { + // 默认加载class根目录的appSecret.properties文件 + DefaultResourceLoader resourceLoader = new DefaultResourceLoader(); + Resource resource = resourceLoader.getResource(appSecretFile); + properties = PropertiesLoaderUtils.loadProperties(resource); + } catch (IOException e) { + throw new RuntimeException("在类路径下找不到appSecret.properties的应用密钥的属性文件"); + } + } + + return properties.getProperty(appKey); + } + + public void setAppSecretFile(String appSecretFile) { + this.appSecretFile = appSecretFile; + } + + @Override + public boolean isValidAppKey(String appKey) { + if(appKey == null){ + return false; + } + return getSecret(appKey) != null; + } +} + diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/session/ApiHttpSession.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/session/ApiHttpSession.java new file mode 100644 index 00000000..9cf28712 --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/session/ApiHttpSession.java @@ -0,0 +1,242 @@ +package com.gitee.sop.gatewaycommon.session; + +import org.springframework.util.Assert; + +import javax.servlet.ServletContext; +import javax.servlet.http.HttpSession; +import javax.servlet.http.HttpSessionBindingEvent; +import javax.servlet.http.HttpSessionBindingListener; +import javax.servlet.http.HttpSessionContext; +import java.io.Serializable; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.UUID; +import java.util.Vector; + +/** + * @author tanghc + */ +@SuppressWarnings("deprecation") +public class ApiHttpSession implements HttpSession, Serializable { + private static final long serialVersionUID = 946272038219216222L; + + private final String id; + + private final long creationTime = System.currentTimeMillis(); + + private int maxInactiveInterval; + + private long lastAccessedTime = System.currentTimeMillis(); + + private final ServletContext servletContext; + + private final Map attributes = new LinkedHashMap(); + + private boolean invalid = false; + + private boolean isNew = true; + + /** + * Create a new ApiHttpSession + */ + public ApiHttpSession() { + this(null); + } + + /** + * Create a new ApiHttpSession. + * + * @param servletContext + * the ServletContext that the session runs in + */ + public ApiHttpSession(ServletContext servletContext) { + this(servletContext, null); + } + + /** + * Create a new ApiHttpSession. + * + * @param servletContext + * the ServletContext that the session runs in + * @param id + * a unique identifier for this session + */ + public ApiHttpSession(ServletContext servletContext, String id) { + this.servletContext = servletContext; + this.id = this.buildId(id); + } + + protected String buildId(String id) { + return (id != null ? id : UUID.randomUUID().toString().replace("-", "").toUpperCase()); + } + + @Override + public long getCreationTime() { + return this.creationTime; + } + + @Override + public String getId() { + return this.id; + } + + public void access() { + this.lastAccessedTime = System.currentTimeMillis(); + this.isNew = false; + } + + @Override + public long getLastAccessedTime() { + return this.lastAccessedTime; + } + + @Override + public ServletContext getServletContext() { + return this.servletContext; + } + + @Override + public void setMaxInactiveInterval(int interval) { + this.maxInactiveInterval = interval; + } + + @Override + public int getMaxInactiveInterval() { + return this.maxInactiveInterval; + } + + @Override + public HttpSessionContext getSessionContext() { + throw new UnsupportedOperationException("getSessionContext"); + } + + @Override + public Object getAttribute(String name) { + Assert.notNull(name, "Attribute name must not be null"); + return this.attributes.get(name); + } + + @Override + public Object getValue(String name) { + return getAttribute(name); + } + + @Override + public Enumeration getAttributeNames() { + return new Vector(this.attributes.keySet()).elements(); + } + + @Override + public String[] getValueNames() { + return this.attributes.keySet().toArray(new String[this.attributes.size()]); + } + + @Override + public void setAttribute(String name, Object value) { + Assert.notNull(name, "Attribute name must not be null"); + if (value != null) { + this.attributes.put(name, value); + if (value instanceof HttpSessionBindingListener) { + ((HttpSessionBindingListener) value).valueBound(new HttpSessionBindingEvent(this, name, value)); + } + } else { + removeAttribute(name); + } + } + + @Override + public void putValue(String name, Object value) { + setAttribute(name, value); + } + + @Override + public void removeAttribute(String name) { + Assert.notNull(name, "Attribute name must not be null"); + Object value = this.attributes.remove(name); + if (value instanceof HttpSessionBindingListener) { + ((HttpSessionBindingListener) value).valueUnbound(new HttpSessionBindingEvent(this, name, value)); + } + } + + @Override + public void removeValue(String name) { + removeAttribute(name); + } + + /** + * Clear all of this session's attributes. + */ + public void clearAttributes() { + for (Iterator> it = this.attributes.entrySet().iterator(); it.hasNext();) { + Map.Entry entry = it.next(); + String name = entry.getKey(); + Object value = entry.getValue(); + it.remove(); + if (value instanceof HttpSessionBindingListener) { + ((HttpSessionBindingListener) value).valueUnbound(new HttpSessionBindingEvent(this, name, value)); + } + } + } + + @Override + public void invalidate() { + this.invalid = true; + clearAttributes(); + } + + public boolean isInvalid() { + return this.invalid; + } + + public void setNew(boolean value) { + this.isNew = value; + } + + @Override + public boolean isNew() { + return this.isNew; + } + + /** + * Serialize the attributes of this session into an object that can be + * turned into a byte array with standard Java serialization. + * + * @return a representation of this session's serialized state + */ + public Serializable serializeState() { + HashMap state = new HashMap(); + for (Iterator> it = this.attributes.entrySet().iterator(); it.hasNext();) { + Map.Entry entry = it.next(); + String name = entry.getKey(); + Object value = entry.getValue(); + it.remove(); + if (value instanceof Serializable) { + state.put(name, (Serializable) value); + } else { + // Not serializable... Servlet containers usually automatically + // unbind the attribute in this case. + if (value instanceof HttpSessionBindingListener) { + ((HttpSessionBindingListener) value).valueUnbound(new HttpSessionBindingEvent(this, name, value)); + } + } + } + return state; + } + + /** + * Deserialize the attributes of this session from a state object created by + * {@link #serializeState()}. + * + * @param state + * a representation of this session's serialized state + */ + @SuppressWarnings("unchecked") + public void deserializeState(Serializable state) { + Assert.isTrue(state instanceof Map, "Serialized state needs to be of type [java.util.Map]"); + this.attributes.putAll((Map) state); + } + +} \ No newline at end of file diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/session/ApiRedisTemplate.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/session/ApiRedisTemplate.java new file mode 100644 index 00000000..b9295f44 --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/session/ApiRedisTemplate.java @@ -0,0 +1,30 @@ +package com.gitee.sop.gatewaycommon.session; + +import org.springframework.data.redis.connection.DefaultStringRedisConnection; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +/** + * @author tanghc + */ +public class ApiRedisTemplate extends RedisTemplate { + public ApiRedisTemplate() { + RedisSerializer stringSerializer = new StringRedisSerializer(); + setKeySerializer(stringSerializer); + setHashKeySerializer(stringSerializer); + } + + public ApiRedisTemplate(RedisConnectionFactory connectionFactory) { + this(); + setConnectionFactory(connectionFactory); + afterPropertiesSet(); + } + + @Override + protected RedisConnection preProcessConnection(RedisConnection connection, boolean existingConnection) { + return new DefaultStringRedisConnection(connection); + } +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/session/ApiSessionManager.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/session/ApiSessionManager.java new file mode 100644 index 00000000..9246a5d0 --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/session/ApiSessionManager.java @@ -0,0 +1,100 @@ +package com.gitee.sop.gatewaycommon.session; + +import com.gitee.sop.gatewaycommon.bean.ApiContext; +import com.gitee.sop.gatewaycommon.message.ErrorEnum; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.ServletContext; +import javax.servlet.http.HttpSession; +import java.util.concurrent.TimeUnit; + +/** + * session管理,默认存放的是{@link ApiHttpSession}。采用谷歌guava缓存实现。 + * + * @author tanghc + * + */ +public class ApiSessionManager implements SessionManager { + private static Logger logger = LoggerFactory.getLogger(ApiSessionManager.class); + + private int sessionTimeout = 20; + + private LoadingCache cache; + + public ApiSessionManager() { + cache = this.buildCache(); + } + + @Override + public HttpSession getSession(String sessionId) { + if(sessionId == null) { + return this.createSession(sessionId); + } + try { + HttpSession session = cache.get(sessionId); + return session; + } catch (Exception e) { + logger.error(e.getMessage(), e); + throw ErrorEnum.AOP_UNKNOW_ERROR.getErrorMeta().getException(); + } + } + + /** + * 创建一个session + * + * @param sessionId 传null将返回一个新session + * @return 返回session + */ + protected HttpSession createSession(String sessionId) { + ServletContext servletContext = getServletContext(); + HttpSession session = this.newSession(sessionId, servletContext); + session.setMaxInactiveInterval(getSessionTimeout()); + this.cache.put(session.getId(), session); + return session; + } + + /** + * 返回新的session实例 + * + * @param sessionId + * @param servletContext + * @return 返回session + */ + protected HttpSession newSession(String sessionId, ServletContext servletContext) { + return new ApiHttpSession(servletContext, sessionId); + } + + protected ServletContext getServletContext() { + return ApiContext.getServletContext(); + } + + protected LoadingCache buildCache() { + return CacheBuilder.newBuilder().expireAfterAccess(getSessionTimeout(), TimeUnit.MINUTES) + .build(new CacheLoader() { + // 找不到sessionId对应的HttpSession时,进入这个方法 + // 找不到就新建一个 + @Override + public HttpSession load(String sessionId) throws Exception { + return createSession(sessionId); + } + }); + } + + public void setSessionTimeout(int sessionTimeout) { + this.sessionTimeout = sessionTimeout; + } + + /** + * 过期时间,分钟,默认20分钟 + * + * @return 返回过期时间 + */ + public int getSessionTimeout() { + return sessionTimeout; + } + +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/session/RedisHttpSession.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/session/RedisHttpSession.java new file mode 100644 index 00000000..ac23f5fe --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/session/RedisHttpSession.java @@ -0,0 +1,264 @@ +package com.gitee.sop.gatewaycommon.session; + +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.util.Assert; + +import javax.servlet.ServletContext; +import javax.servlet.http.HttpSession; +import javax.servlet.http.HttpSessionContext; +import java.io.Serializable; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +/** + * RedisHttpSession + * + * @author tanghc + */ +@SuppressWarnings("deprecation") +public class RedisHttpSession implements HttpSession, Serializable { + private static final long serialVersionUID = -8081963657251144855L; + + private static final int SEC60 = 60; + + private static final String SESSION_ATTR = "session_attr:"; + private static final String CREATION_TIME = "creationTime"; + private static final String LAST_ACCESSED_TIME = "lastAccessedTime"; + private static final String MAX_INACTIVE_INTERVAL = "maxInactiveInterval"; + + /** + * 存入redis的key + */ + private String key; + + /** + * sessionId + */ + private String id; + + private ServletContext servletContext; + + private RedisTemplate redisTemplate; + + private RedisHttpSession() { + } + + + protected static String buildId(String id) { + return (id != null ? id : UUID.randomUUID().toString().replace("-", "").toUpperCase()); + } + + public static String buildKey(String keyPrefix, String sessionId) { + Assert.notNull(keyPrefix, "sessionPrefix不能为null"); + return keyPrefix + sessionId; + } + + /** + * 创建新的session + * + * @param servletContext + * @param sessionId + * @param sessionTimeout 过期时间,单位秒 + * @param redisTemplate redis客户端 + * @param keyPrefix 存入的key前缀 + * @return 返回session + */ + public static RedisHttpSession createNewSession(ServletContext servletContext, String sessionId, int sessionTimeout, RedisTemplate redisTemplate, String keyPrefix) { + Assert.notNull(redisTemplate, "redisTemplate can not null."); + Assert.notNull(sessionId, "sessionId can not null."); + Assert.notNull(keyPrefix, "keyPrefix can not null."); + RedisHttpSession redisHttpSession = new RedisHttpSession(); + redisHttpSession.setId(sessionId); + redisHttpSession.setKey(buildKey(keyPrefix, sessionId)); + redisHttpSession.setRedisTemplate(redisTemplate); + redisHttpSession.setServletContext(servletContext); + + long creationTime = System.currentTimeMillis(); + // 过期时间,分转换成秒 + int maxInactiveInterval = sessionTimeout * SEC60; + + redisHttpSession.setCreationTime(creationTime); + redisHttpSession.setLastAccessedTime(creationTime); + redisHttpSession.setMaxInactiveInterval(maxInactiveInterval); + + redisHttpSession.refresh(); + + return redisHttpSession; + } + + /** + * 创建已经存在的session,数据在redis里面 + * + * @param sessionId + * @param servletContext + * @param redisTemplate redis客户端 + * @param keyPrefix 存入的key前缀 + * @return 返回session + */ + public static RedisHttpSession createExistSession(String sessionId, ServletContext servletContext, RedisTemplate redisTemplate, String keyPrefix) { + Assert.notNull(redisTemplate, "redisTemplate can not null."); + Assert.notNull(sessionId, "sessionId can not null."); + Assert.notNull(keyPrefix, "keyPrefix can not null."); + RedisHttpSession redisHttpSession = new RedisHttpSession(); + redisHttpSession.setId(sessionId); + redisHttpSession.setKey(buildKey(keyPrefix, sessionId)); + redisHttpSession.setRedisTemplate(redisTemplate); + redisHttpSession.setServletContext(servletContext); + + redisHttpSession.refresh(); + + return redisHttpSession; + } + + public void setId(String id) { + this.id = id; + } + + + public void setServletContext(ServletContext servletContext) { + this.servletContext = servletContext; + } + + public void setCreationTime(long creationTime) { + this.redisTemplate.opsForHash().put(key, CREATION_TIME, String.valueOf(creationTime)); + } + + @Override + public long getCreationTime() { + Object createTime = this.redisTemplate.opsForHash().get(key, CREATION_TIME); + return Long.valueOf(String.valueOf(createTime)); + } + + @Override + public String getId() { + return id; + } + + @Override + public long getLastAccessedTime() { + Object lastAccessedTime = this.redisTemplate.opsForHash().get(key, LAST_ACCESSED_TIME); + return Long.valueOf(String.valueOf(lastAccessedTime)); + } + + @Override + public ServletContext getServletContext() { + return servletContext; + } + + @Override + public void setMaxInactiveInterval(int interval) { + this.redisTemplate.opsForHash().put(key, MAX_INACTIVE_INTERVAL, String.valueOf(interval)); + } + + @Override + public int getMaxInactiveInterval() { + Object maxInactiveInterval = this.redisTemplate.opsForHash().get(key, MAX_INACTIVE_INTERVAL); + return Integer.valueOf(String.valueOf(maxInactiveInterval)); + } + + @Override + public HttpSessionContext getSessionContext() { + return null; + } + + @Override + public Object getAttribute(String name) { + return this.redisTemplate.opsForHash().get(key, SESSION_ATTR + name); + } + + @Override + public Object getValue(String name) { + return getAttribute(name); + } + + @Override + public Enumeration getAttributeNames() { + return Collections.enumeration(getAttributeKeys()); + } + + private Set getAttributeKeys() { + Set keys = this.redisTemplate.opsForHash().keys(key); + Set attrNames = new HashSet<>(); + for (Object key : keys) { + String k = String.valueOf(key); + if (k.startsWith(SESSION_ATTR)) { + attrNames.add(k.substring(SESSION_ATTR.length())); + } + } + return attrNames; + } + + + @Override + public String[] getValueNames() { + return getAttributeKeys().toArray(new String[0]); + } + + @Override + public void setAttribute(String name, Object value) { + this.redisTemplate.opsForHash().put(key, SESSION_ATTR + name, value); + } + + @Override + public void putValue(String name, Object value) { + setAttribute(name, value); + } + + @Override + public void removeAttribute(String name) { + this.redisTemplate.opsForHash().delete(key, name); + } + + @Override + public void removeValue(String name) { + removeAttribute(name); + } + + @Override + public void invalidate() { + this.redisTemplate.delete(key); + } + + @Override + public boolean isNew() { + return false; + } + + /** + * update expireTime,accessTime + */ + public void refresh() { + // token更新过期时间 + this.redisTemplate.expire(key, getMaxInactiveInterval(), TimeUnit.SECONDS); + // 设置访问时间 + this.setLastAccessedTime(System.currentTimeMillis()); + } + + + public void setLastAccessedTime(long lastAccessedTime) { + this.redisTemplate.opsForHash().put(key, LAST_ACCESSED_TIME, String.valueOf(lastAccessedTime)); + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public boolean isInvalidated() { + return !this.redisTemplate.hasKey(key); + } + + + public void setRedisTemplate(RedisTemplate redisTemplate) { + this.redisTemplate = redisTemplate; + } + + +} \ No newline at end of file diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/session/RedisSessionManager.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/session/RedisSessionManager.java new file mode 100644 index 00000000..ebf96ceb --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/session/RedisSessionManager.java @@ -0,0 +1,100 @@ +package com.gitee.sop.gatewaycommon.session; + +import com.gitee.sop.gatewaycommon.bean.ApiContext; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.util.Assert; + +import javax.servlet.ServletContext; +import javax.servlet.http.HttpSession; +import java.util.UUID; + +/** + * SessionManager的redis实现,使用redis管理session + * + * @author tanghc + * + */ +public class RedisSessionManager implements SessionManager { + + private ApiRedisTemplate redisTemplate; + + /** 过期时间,30分钟 */ + private int sessionTimeout = 30; + /** 存入redis中key的前缀 */ + private String keyPrefix = "session:"; + + public RedisSessionManager(@SuppressWarnings("rawtypes") RedisTemplate redisTemplate) { + Assert.notNull(redisTemplate, "RedisSessionManager中的redisTemplate不能为null"); + this.redisTemplate = new ApiRedisTemplate(redisTemplate.getConnectionFactory()); + } + + @Override + public HttpSession getSession(String sessionId) { + return this.getSession(sessionId, this.keyPrefix); + } + + public HttpSession getSession(String sessionId, String keyPrefix) { + if (this.hasKey(sessionId)) { + return RedisHttpSession.createExistSession(sessionId, getServletContext(), redisTemplate, keyPrefix); + } else { + sessionId = this.buildSessionId(sessionId); + return RedisHttpSession.createNewSession(getServletContext(), sessionId, this.getSessionTimeout(), + redisTemplate, keyPrefix); + } + } + + /** + * 构建sessionId + * @param id + * @return 返回sessionid + */ + public String buildSessionId(String id) { + return (id != null ? id : UUID.randomUUID().toString().replace("-", "").toUpperCase()); + } + + public boolean hasKey(String sessionId) { + if (sessionId == null) { + return false; + } else { + String key = RedisHttpSession.buildKey(this.keyPrefix, sessionId); + return redisTemplate.hasKey(key); + } + } + + public ServletContext getServletContext() { + return ApiContext.getServletContext(); + } + + public int getSessionTimeout() { + return sessionTimeout; + } + + /** + * 设置session过期时间,单位分钟 + * @param sessionTimeout + */ + public void setSessionTimeout(int sessionTimeout) { + this.sessionTimeout = sessionTimeout; + } + + public ApiRedisTemplate getRedisTemplate() { + return redisTemplate; + } + + public void setRedisTemplate(ApiRedisTemplate redisTemplate) { + this.redisTemplate = redisTemplate; + } + + public String getKeyPrefix() { + return keyPrefix; + } + + /** + * 设置存入redis中key的前缀,默认为"session:" + * @param keyPrefix + */ + public void setKeyPrefix(String keyPrefix) { + this.keyPrefix = keyPrefix; + } + +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/session/SessionManager.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/session/SessionManager.java new file mode 100644 index 00000000..e899e725 --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/session/SessionManager.java @@ -0,0 +1,20 @@ +package com.gitee.sop.gatewaycommon.session; + +import javax.servlet.http.HttpSession; + +/** + * session管理 + * + * @author tanghc + * + */ +public interface SessionManager { + + /** + * 根据sessionId获取session + * + * @param sessionId 客户端传过来的sessionId,为null时创建一个新session + * @return 返回session + */ + HttpSession getSession(String sessionId); +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/util/AESUtil.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/util/AESUtil.java new file mode 100644 index 00000000..f8b6beb8 --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/util/AESUtil.java @@ -0,0 +1,146 @@ +package com.gitee.sop.gatewaycommon.util; + +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.codec.binary.Hex; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +/** + * AES-128 ECB加密.
+ * + *

+ * 字符集:UTF-8
+ * 算法模式:ECB
+ * 数据块:128位
+ * 补码方式:PKCS5Padding
+ * 加密结果编码方式:Base64
+ * 
+ * + * @author tanghc + * + */ +public class AESUtil { + private static final String UTF8 = "UTF-8"; + private static final String ALGORITHM = "AES"; + /** 默认的加密算法 */ + private static final String ALGORITHM_CIPHER = "AES/ECB/PKCS5Padding"; + + private static final int LIMIT_LEN = 16; + + /** + * 生成一个SecretKey + * @param password 长度必须小于等于16 + * @return 返回SecretKey + */ + public static SecretKey getSecretKey(String password) { + byte[] passwordData = password.getBytes(); + if(passwordData.length > LIMIT_LEN) { + throw new IllegalArgumentException("password 长度必须小于等于16"); + } + // 创建一个空的16位字节数组(默认值为0),16byte(128bit) + byte[] keyData = new byte[16]; + System.arraycopy(passwordData, 0, keyData, 0, passwordData.length); + + return new SecretKeySpec(keyData, ALGORITHM); + } + + /** + * 加密 + * @param data 待加密数据 + * @param password 密码 + * @return 返回加密成功后数据 + * @throws Exception + */ + public static byte[] encrypt(byte[] data, String password) throws Exception { + SecretKey secretKey = getSecretKey(password); + // Ciphr完成加密或解密工作类 + Cipher cipher = Cipher.getInstance(ALGORITHM_CIPHER); + // 对Cipher初始化,解密模式 + cipher.init(Cipher.ENCRYPT_MODE, secretKey); + // 加密data + return cipher.doFinal(data); + } + + /** + * 解密 + * @param data 待解密数据 + * @param password 密码 + * @return 返回解密后的数据 + * @throws Exception + */ + public static byte[] decrypt(byte[] data, String password) throws Exception { + SecretKey secretKey = getSecretKey(password); + // Cipher完成加密或解密工作类 + Cipher cipher = Cipher.getInstance(ALGORITHM_CIPHER); + // 对Cipher初始化,解密模式 + cipher.init(Cipher.DECRYPT_MODE, secretKey); + // 解密data + return cipher.doFinal(data); + } + + /** + * 文本加密 + * @param content 明文 + * @param password 密码 + * @return 返回base64内容 + * @throws Exception + */ + public static String encryptToBase64String(String content, String password) throws Exception { + byte[] data = content.getBytes(UTF8); + byte[] result = encrypt(data, password); + return Base64.encodeBase64String(result); + } + + /** + * 文本解密 + * @param base64String 待解密文本 + * @param password 密码 + * @return 返回明文 + * @throws Exception + */ + public static String decryptFromBase64String(String base64String, String password) throws Exception { + byte[] data = Base64.decodeBase64(base64String); + byte[] contentData = decrypt(data, password); + return new String(contentData, UTF8); + } + + /** + * 文本加密 + * @param content 明文 + * @param password 密码 + * @return 返回16进制内容 + * @throws Exception + */ + public static String encryptToHex(String content, String password) throws Exception { + byte[] data = content.getBytes(UTF8); + byte[] result = encrypt(data, password); + return Hex.encodeHexString(result); + } + + /** + * 文本解密 + * @param hex 待解密文本 + * @param password 密码 + * @return 返回明文 + * @throws Exception + */ + public static String decryptFromHex(String hex, String password) throws Exception { + byte[] data = Hex.decodeHex(hex); + byte[] contentData = decrypt(data, password); + return new String(contentData,UTF8); + } + + /*public static void main(String[] args) throws Exception { + String content = "我爱你"; + String password = "1234567890123456"; + System.out.println("password:" + password); + + String ret2 = encryptToBase64String(content, password); + System.out.println("密文:" + ret2); + String content3 = decryptFromBase64String(ret2, password); + System.out.println(content.equals(content3)); + }*/ + +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/util/KeyStore.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/util/KeyStore.java new file mode 100644 index 00000000..5e926ba9 --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/util/KeyStore.java @@ -0,0 +1,25 @@ +package com.gitee.sop.gatewaycommon.util; + +/** + * @author tanghc + */ +public class KeyStore { + private String publicKey; + private String privateKey; + + public String getPublicKey() { + return publicKey; + } + + public void setPublicKey(String publicKey) { + this.publicKey = publicKey; + } + + public String getPrivateKey() { + return privateKey; + } + + public void setPrivateKey(String privateKey) { + this.privateKey = privateKey; + } +} \ No newline at end of file diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/util/RSANewUtil.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/util/RSANewUtil.java new file mode 100644 index 00000000..3461e6cf --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/util/RSANewUtil.java @@ -0,0 +1,208 @@ +package com.gitee.sop.gatewaycommon.util; + +import org.apache.commons.codec.binary.Base64; + +import javax.crypto.Cipher; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; + +/** + * RSA加解密工具
+ * @author tanghc + */ +public class RSANewUtil { + public static final String RSA_ALGORITHM = "RSA"; + public static final String UTF8 = "UTF-8"; + + /** + * 创建公钥私钥 + * + * @return 返回公私钥对 + * @throws Exception + */ + public static KeyStore createKeys() throws Exception { + KeyPairGenerator keyPairGeno = KeyPairGenerator.getInstance(RSA_ALGORITHM); + keyPairGeno.initialize(1024); + KeyPair keyPair = keyPairGeno.generateKeyPair(); + + RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); + RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); + + KeyStore keyStore = new KeyStore(); + keyStore.setPublicKey(Base64.encodeBase64String(publicKey.getEncoded())); + keyStore.setPrivateKey(Base64.encodeBase64String(privateKey.getEncoded())); + return keyStore; + } + + /** + * 获取公钥对象 + * + * @param pubKeyData 公钥数据 + * @return 公钥对象 + * @throws Exception + */ + public static RSAPublicKey getPublicKey(byte[] pubKeyData) throws Exception { + X509EncodedKeySpec keySpec = new X509EncodedKeySpec(pubKeyData); + KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM); + return (RSAPublicKey) keyFactory.generatePublic(keySpec); + } + + /** + * 获取公钥对象 + * + * @param pubKey + * 公钥 + * @return 返回公钥对象 + * @throws Exception + */ + public static RSAPublicKey getPublicKey(String pubKey) throws Exception { + return getPublicKey(Base64.decodeBase64(pubKey)); + + } + + /** + * 获取私钥对象 + * + * @param priKey + * 私钥 + * @return 返回私钥对象 + * @throws Exception + */ + public static RSAPrivateKey getPrivateKey(String priKey) throws Exception { + return getPrivateKey(Base64.decodeBase64(priKey)); + } + + /** + * 通过私钥byte[]将公钥还原,适用于RSA算法 + * + * @param keyBytes 私钥数据 + * @return 返回公钥对象 + * @throws Exception + */ + public static RSAPrivateKey getPrivateKey(byte[] keyBytes) throws Exception { + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes); + KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM); + return (RSAPrivateKey) keyFactory.generatePrivate(keySpec); + + } + + public static String encryptByPublicKey(String data, String publicKey) throws Exception { + return encryptByPublicKey(data, getPublicKey(publicKey)); + } + + /** + * 公钥加密 + * + * @param data 内容 + * @param publicKey 公钥 + * @return 返回密文 + * @throws Exception + */ + public static String encryptByPublicKey(String data, RSAPublicKey publicKey) throws Exception { + Cipher cipher = Cipher.getInstance(RSA_ALGORITHM); + cipher.init(Cipher.ENCRYPT_MODE, publicKey); + byte[] bytes = cipher.doFinal(data.getBytes(UTF8)); + return Base64.encodeBase64String(bytes); + } + + public static String decryptByPublicKey(String data, String rsaPublicKey) throws Exception { + return decryptByPublicKey(data, getPublicKey(rsaPublicKey)); + } + + /** + * 公钥解密 + * + * @param data 待解密内容 + * @param rsaPublicKey 公钥 + * @return 返回明文 + * @throws Exception + */ + public static String decryptByPublicKey(String data, RSAPublicKey rsaPublicKey) throws Exception { + Cipher cipher = Cipher.getInstance(RSA_ALGORITHM); + cipher.init(Cipher.DECRYPT_MODE, rsaPublicKey); + byte[] inputData = Base64.decodeBase64(data); + byte[] bytes = cipher.doFinal(inputData); + return new String(bytes, UTF8); + } + + public static String encryptByPrivateKey(String data, String privateKey) throws Exception { + return encryptByPrivateKey(data, getPrivateKey(privateKey)); + } + + /** + * 私钥加密 + * + * @param data 内容 + * @param privateKey 私钥 + * @return 返回密文 + * @throws Exception + */ + public static String encryptByPrivateKey(String data, RSAPrivateKey privateKey) throws Exception { + Cipher cipher = Cipher.getInstance(RSA_ALGORITHM); + cipher.init(Cipher.ENCRYPT_MODE, privateKey); + byte[] bytes = cipher.doFinal(data.getBytes(UTF8)); + return Base64.encodeBase64String(bytes); + } + + public static String decryptByPrivateKey(String data, String privateKey) throws Exception { + return decryptByPrivateKey(data, getPrivateKey(privateKey)); + } + + /** + * 私钥解密 + * + * @param data 待解密内容 + * @param privateKey 私钥 + * @return 返回明文 + * @throws Exception + */ + public static String decryptByPrivateKey(String data, RSAPrivateKey privateKey) throws Exception { + Cipher cipher = Cipher.getInstance(RSA_ALGORITHM); + cipher.init(Cipher.DECRYPT_MODE, privateKey); + byte[] inputData = Base64.decodeBase64(data); + byte[] bytes = cipher.doFinal(inputData); + return new String(bytes, UTF8); + } + + + /* + pubKey: +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCG/iIZZzb16PxKqslkDMYa4tVFb3IVPBpLj4BgHQmDfe843sG4gkJIPXCm7+t6QxIbfDfynBpqZJLvu0c6E7TqlCtynBIlRFOBZrQVNEFkaanR2Kln3vd3CIidR571UstOC32XDyqAQNlvjD19zeIDVfmLa0Q+Or0zaxY99QwBHwIDAQAB +priKey: +MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAIb+IhlnNvXo/EqqyWQMxhri1UVvchU8GkuPgGAdCYN97zjewbiCQkg9cKbv63pDEht8N/KcGmpkku+7RzoTtOqUK3KcEiVEU4FmtBU0QWRpqdHYqWfe93cIiJ1HnvVSy04LfZcPKoBA2W+MPX3N4gNV+YtrRD46vTNrFj31DAEfAgMBAAECgYBiNPQdwwcq86rHr2QAE4L0AF3ju+YlKKqAmg9s3PMU5ENq/jO0xZ7u6zPPXu/S7IR51m7lY0ecazqyiW6SA9AzYH7ImWWkZ4stZ03beTB2US3cSeJIkugoexoN5fQRAGZiZezTLs91CeJivESOZyDKnnQdgJ49mveBV5OvievD8QJBAMztpqiWWavdR4tqQ+plat+rwYoXqejsK3Hyfg0pVJqEdazve2sr74rla7yI9P47ZAh1sklCv0CO//ctICv366UCQQCoop3T0FeZtbKJG+fHzZvpAe63tXpdhLMaQvTBuXLG8vi78Wyfhg5r7HOWR0Z1V7nzF1gzMywL53Pmkq9tB65zAkAiHu/A4kfL9ewTqn3kaT6CP3baJ1aDEc+qCVYzms4bbDKruLQ0A/y+g7SMj8E7E2h0gCRPTm3JsgWsgjb5Gy6BAkAA8mjQd6sGQe7utilnBdCKTmh4v5wgSk53J0kYjWIHm/WpmIFzo90Q3hMIFP5gSk3Q/6CPKQpmRrZv5QL3KcPhAkEAuMoQbij/7hyLlIxRHZs2SMXxfHPiZgDc6rVi1KNxeq8HXTlERi7Npc2Uz5TeWN4JwBBx9uA50zowk9iS05nclQ== +用公钥加密mi : c3B0jtMdvkqrgaPxHZCK2cXMUQC2QzLud2ouLMNx0nBAj9k2/ytOuVJViTGe/DozB/ky5jvl4spD9Ey6aTMrwLHfQVhn0gRJ+wHcmx/51dXQDIgsldt6bf7YpdPdnghBjQz2+P5RhqSkeFDbTZKkl2BNaLE78a/OyWWeCGwN+4s= +true +用私钥加密mi2 : QU5vDnQ1ukj8GsauokFlgcB/g61U882tj82wHGrrqHEnvaga+4cXjML9RhjpZtKqwDGZTCujsmpynDk4qek6IGOQ/oxdWLwV4ZNjfa/oqA8OFDothVUT8wpqCu9kOYHrTdGybmXD0dB2Iy1/AMQTAgPNNXXiRXdvsz9xWYTV6z8= +true + */ + /*public static void main(String[] args) throws Exception { + KeyStore keys = createKeys(); + String pubKey = keys.getPublicKey(); + System.out.println("pubKey:"); + System.out.println(pubKey); + String priKey = keys.getPrivateKey(); + System.out.println("priKey:"); + System.out.println(priKey); + + String ming = "1234567890123456"; + // 用公钥加密 + String mi = encryptByPublicKey(ming, pubKey); + System.out.println("用公钥加密mi : " + mi); + // 用私钥解密 + System.out.println(ming.equals(decryptByPrivateKey(mi, priKey))); + + // 用私钥加密 + String mi2 = encryptByPrivateKey(ming, priKey); + + System.out.println("用私钥加密mi2 : " + mi2); + // 用公钥解密 + String ming2 = decryptByPublicKey(mi2, pubKey); + System.out.println(ming.equals(ming2)); + }*/ + +} \ No newline at end of file diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/util/RSAUtil.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/util/RSAUtil.java new file mode 100644 index 00000000..4b36c739 --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/util/RSAUtil.java @@ -0,0 +1,312 @@ +package com.gitee.sop.gatewaycommon.util; + +import org.apache.commons.codec.binary.Base64; + +import javax.crypto.Cipher; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; + +/** + * RSA加解密工具
+ * @author tanghc + */ +public class RSAUtil { + public static String RSA_ALGORITHM = "RSA"; + + /** + * 创建公钥私钥 + * + * @return 返回公私钥对 + * @throws Exception + */ + public static KeyStore createKeys() throws Exception { + KeyPairGenerator keyPairGeno = KeyPairGenerator.getInstance(RSA_ALGORITHM); + keyPairGeno.initialize(1024); + KeyPair keyPair = keyPairGeno.generateKeyPair(); + + RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); + RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); + + KeyStore keyStore = new KeyStore(); + keyStore.setPublicKey(Base64.encodeBase64String(publicKey.getEncoded())); + keyStore.setPrivateKey(Base64.encodeBase64String(privateKey.getEncoded())); + return keyStore; + } + + /** + * 获取公钥对象 + * + * @param pubKeyData 公钥 + * @return 返回公钥对象 + * @throws Exception + */ + public static RSAPublicKey getPublicKey(byte[] pubKeyData) throws Exception { + X509EncodedKeySpec keySpec = new X509EncodedKeySpec(pubKeyData); + KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM); + return (RSAPublicKey) keyFactory.generatePublic(keySpec); + } + + /** + * 获取公钥对象 + * + * @param pubKey + * 公钥 + * @return 返回私钥对象 + * @throws Exception + */ + public static RSAPublicKey getPublicKey(String pubKey) throws Exception { + return getPublicKey(Base64.decodeBase64(pubKey)); + + } + + /** + * 获取私钥对象 + * + * @param priKey + * 私钥 + * @return 私钥对象 + * @throws Exception + */ + public static RSAPrivateKey getPrivateKey(String priKey) throws Exception { + return getPrivateKey(Base64.decodeBase64(priKey)); + } + + /** + * 通过私钥byte[]将公钥还原,适用于RSA算法 + * + * @param keyBytes + * @return 返回私钥 + * @throws Exception + */ + public static RSAPrivateKey getPrivateKey(byte[] keyBytes) throws Exception { + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes); + KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM); + return (RSAPrivateKey) keyFactory.generatePrivate(keySpec); + + } + + /** + * 公钥加密 + * + * @param data 待加密内容 + * @param publicKey 公钥 + * @return 返回密文 + * @throws Exception + */ + public static String encryptByPublicKey(String data, RSAPublicKey publicKey) throws Exception { + Cipher cipher = Cipher.getInstance(RSA_ALGORITHM); + cipher.init(Cipher.ENCRYPT_MODE, publicKey); + // 模长 + int key_len = publicKey.getModulus().bitLength() / 8; + // 加密数据长度 <= 模长-11 + String[] datas = splitString(data, key_len - 11); + String mi = ""; + // 如果明文长度大于模长-11则要分组加密 + for (String s : datas) { + mi += bcd2Str(cipher.doFinal(s.getBytes())); + } + return mi; + } + public static String encryptByPrivateKey(String data, String privateKey) throws Exception { + return encryptByPrivateKey(data, getPrivateKey(privateKey)); + } + + /** + * 私钥加密 + * + * @param data 待加密数据 + * @param privateKey 私钥 + * @return 返回密文 + * @throws Exception + */ + public static String encryptByPrivateKey(String data, RSAPrivateKey privateKey) throws Exception { + Cipher cipher = Cipher.getInstance(RSA_ALGORITHM); + cipher.init(Cipher.ENCRYPT_MODE, privateKey); + // 模长 + int key_len = privateKey.getModulus().bitLength() / 8; + // 加密数据长度 <= 模长-11 + String[] datas = splitString(data, key_len - 11); + String mi = ""; + // 如果明文长度大于模长-11则要分组加密 + for (String s : datas) { + mi += bcd2Str(cipher.doFinal(s.getBytes())); + } + return mi; + } + + public static String decryptByPrivateKey(String data, String privateKey) throws Exception { + return decryptByPrivateKey(data, getPrivateKey(privateKey)); + } + + /** + * 私钥解密 + * + * @param data 待解密内容 + * @param privateKey 私钥 + * @return 返回明文 + * @throws Exception + */ + public static String decryptByPrivateKey(String data, RSAPrivateKey privateKey) throws Exception { + Cipher cipher = Cipher.getInstance(RSA_ALGORITHM); + cipher.init(Cipher.DECRYPT_MODE, privateKey); + // 模长 + int key_len = privateKey.getModulus().bitLength() / 8; + byte[] bytes = data.getBytes(); + byte[] bcd = ASCII_To_BCD(bytes, bytes.length); + // 如果密文长度大于模长则要分组解密 + String ming = ""; + byte[][] arrays = splitArray(bcd, key_len); + for (byte[] arr : arrays) { + ming += new String(cipher.doFinal(arr)); + } + return ming; + } + + /** + * 公钥解密 + * + * @param data 待解密内容 + * @param rsaPublicKey 公钥 + * @return 返回明文 + * @throws Exception + */ + public static String decryptByPublicKey(String data, RSAPublicKey rsaPublicKey) throws Exception { + Cipher cipher = Cipher.getInstance(RSA_ALGORITHM); + cipher.init(Cipher.DECRYPT_MODE, rsaPublicKey); + // 模长 + int key_len = rsaPublicKey.getModulus().bitLength() / 8; + byte[] bytes = data.getBytes(); + byte[] bcd = ASCII_To_BCD(bytes, bytes.length); + // 如果密文长度大于模长则要分组解密 + String ming = ""; + byte[][] arrays = splitArray(bcd, key_len); + for (byte[] arr : arrays) { + ming += new String(cipher.doFinal(arr)); + } + return ming; + } + + + /** + * ASCII码转BCD码 + * + */ + public static byte[] ASCII_To_BCD(byte[] ascii, int asc_len) { + byte[] bcd = new byte[asc_len / 2]; + int j = 0; + for (int i = 0; i < (asc_len + 1) / 2; i++) { + bcd[i] = asc_to_bcd(ascii[j++]); + bcd[i] = (byte) (((j >= asc_len) ? 0x00 : asc_to_bcd(ascii[j++]) & 0xff) + (bcd[i] << 4)); + } + return bcd; + } + + public static byte asc_to_bcd(byte asc) { + byte bcd; + + if ((asc >= '0') && (asc <= '9')) { + bcd = (byte) (asc - '0'); + } else if ((asc >= 'A') && (asc <= 'F')) { + bcd = (byte) (asc - 'A' + 10); + } else if ((asc >= 'a') && (asc <= 'f')) { + bcd = (byte) (asc - 'a' + 10); + } else { + bcd = (byte) (asc - 48); + } + return bcd; + } + + /** + * BCD转字符串 + */ + public static String bcd2Str(byte[] bytes) { + char[] temp = new char[bytes.length * 2]; + char val; + + for (int i = 0; i < bytes.length; i++) { + val = (char) (((bytes[i] & 0xf0) >> 4) & 0x0f); + temp[i * 2] = (char) (val > 9 ? val + 'A' - 10 : val + '0'); + + val = (char) (bytes[i] & 0x0f); + temp[i * 2 + 1] = (char) (val > 9 ? val + 'A' - 10 : val + '0'); + } + return new String(temp); + } + + /** + * 拆分字符串 + */ + public static String[] splitString(String string, int len) { + int x = string.length() / len; + int y = string.length() % len; + int z = 0; + if (y != 0) { + z = 1; + } + String[] strings = new String[x + z]; + String str = ""; + for (int i = 0; i < x + z; i++) { + if (i == x + z - 1 && y != 0) { + str = string.substring(i * len, i * len + y); + } else { + str = string.substring(i * len, i * len + len); + } + strings[i] = str; + } + return strings; + } + + /** + * 拆分数组 + */ + public static byte[][] splitArray(byte[] data, int len) { + int x = data.length / len; + int y = data.length % len; + int z = 0; + if (y != 0) { + z = 1; + } + byte[][] arrays = new byte[x + z][]; + byte[] arr; + for (int i = 0; i < x + z; i++) { + arr = new byte[len]; + if (i == x + z - 1 && y != 0) { + System.arraycopy(data, i * len, arr, 0, y); + } else { + System.arraycopy(data, i * len, arr, 0, len); + } + arrays[i] = arr; + } + return arrays; + } + + /*public static void main(String[] args) throws Exception { + KeyStore keys = createKeys(); + String pubKey = keys.getPublicKey(); + System.out.println("pubKey:"); + System.out.println(pubKey); + String priKey = keys.getPrivateKey(); + System.out.println("priKey:"); + System.out.println(priKey); + + String ming = "6460201d23954f8e90cf79b818844ca0"; + // 用公钥加密 + String mi = encryptByPublicKey(ming, getPublicKey(pubKey)); + System.out.println("mi : " + mi); + // 用私钥解密 + System.out.println("ming : " + decryptByPrivateKey(mi, getPrivateKey(priKey))); + + // 用私钥加密 + String mi2 = encryptByPrivateKey(ming, getPrivateKey(priKey)); + + System.out.println("mi2 : " + mi2); + // 用公钥解密 + System.out.println("ming2 : " + decryptByPublicKey(mi2, getPublicKey(pubKey))); + }*/ + +} \ No newline at end of file diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/util/RedisLockUtil.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/util/RedisLockUtil.java new file mode 100644 index 00000000..2221badc --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/util/RedisLockUtil.java @@ -0,0 +1,139 @@ +package com.gitee.sop.gatewaycommon.util; + +import org.apache.commons.codec.digest.DigestUtils; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.script.RedisScript; + +import java.util.Collections; + +/** + * redis分布式锁,详见:https://blog.csdn.net/thc1987/article/details/80355155
+ * 思路: + *
+ * 用SETNX命令,SETNX只有在key不存在时才返回成功。这意味着只有一个线程可以成功运行SETNX命令,而其他线程会失败,然后不断重试,直到它们能建立锁。
+ * 然后使用脚本来创建锁,因为一个redis脚本同一时刻只能运行一次。
+ * 创建锁代码:
+ * 
+-- KEYS[1] key,
+-- ARGV[1] value,
+-- ARGV[2] expireTimeMilliseconds
+
+if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then 
+    redis.call('pexpire', KEYS[1], ARGV[2]) 
+    return 1 
+else 
+    return 0 
+end
+ * 
+ * 最后使用脚本来解锁。
+ * 解锁代码:
+ * 
+ * 
+-- KEYS[1] key,
+-- ARGV[1] value
+if redis.call("get", KEYS[1]) == ARGV[1]
+then
+    return redis.call("del", KEYS[1])
+else
+    return 0
+end
+ * 
+ * 
+ * + * @author tanghc + */ +public class RedisLockUtil { + + private static final Long SUCCESS = 1L; + + /** 加锁脚本 */ + private static final String SCRIPT_LOCK = "if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then redis.call('pexpire', KEYS[1], ARGV[2]) return 1 else return 0 end"; + /** 解锁脚本 */ + private static final String SCRIPT_UNLOCK = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; + /** 加锁脚本sha1值 */ + private static final String SCRIPT_LOCK_SHA1 = DigestUtils.sha1Hex(SCRIPT_LOCK); + /** 解锁脚本sha1值 */ + private static final String SCRIPT_UNLOCK_SHA1 = DigestUtils.sha1Hex(SCRIPT_UNLOCK); + + /** + * 尝试获取分布式锁 + * + * @param redisTemplate + * Redis客户端 + * @param lockKey + * 锁 + * @param requestId + * 请求标识 + * @param expireTimeMilliseconds + * 超期时间,多少毫秒后这把锁自动释放 + * @return 返回true表示拿到锁 + */ + @SuppressWarnings("unchecked") + public static boolean tryGetDistributedLock(@SuppressWarnings("rawtypes") final RedisTemplate redisTemplate, + final String lockKey, final String requestId, final int expireTimeMilliseconds) { + + Object result = redisTemplate.execute(new RedisScript() { + @Override + public String getSha1() { + return SCRIPT_LOCK_SHA1; + } + + @Override + public Class getResultType() { + return Long.class; + } + + @Override + public String getScriptAsString() { + return SCRIPT_LOCK; + } + + }, + // KEYS[1] + Collections.singletonList(lockKey), + // ARGV[1] + requestId, + // ARGV[2] + String.valueOf(expireTimeMilliseconds) + ); + + return SUCCESS.equals(result); + } + + /** + * 释放分布式锁 + * + * @param redisTemplate + * Redis客户端 + * @param lockKey + * 锁 + * @param requestId + * 请求标识 + * @return 返回true表示释放锁成功 + */ + @SuppressWarnings("unchecked") + public static boolean releaseDistributedLock(@SuppressWarnings("rawtypes") RedisTemplate redisTemplate, + String lockKey, String requestId) { + + Object result = redisTemplate.execute(new RedisScript() { + @Override + public String getSha1() { + return SCRIPT_UNLOCK_SHA1; + } + + @Override + public Class getResultType() { + return Long.class; + } + + @Override + public String getScriptAsString() { + return SCRIPT_UNLOCK; + } + + }, Collections.singletonList(lockKey), requestId); + + return SUCCESS.equals(result); + } + +} \ No newline at end of file diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/util/RequestUtil.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/util/RequestUtil.java new file mode 100644 index 00000000..aec2b561 --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/util/RequestUtil.java @@ -0,0 +1,137 @@ +package com.gitee.sop.gatewaycommon.util; + +import com.alibaba.fastjson.JSON; +import com.gitee.sop.gatewaycommon.message.ErrorEnum; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.StringUtils; + +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +/** + * @author tanghc + */ +public class RequestUtil { + + private static final String CONTENT_TYPE_URLENCODED = "application/x-www-form-urlencoded"; + private static final String CONTENT_TYPE_JSON = "application/json"; + private static final String CONTENT_TYPE_TEXT = "text/plain"; + + private static final String UTF8 = "UTF-8"; + private static final String GET = "get"; + + private static final String UNKOWN = "unknown"; + private static final String LOCAL_IP = "127.0.0.1"; + private static final int IP_LEN = 15; + + public static String getText(HttpServletRequest request) throws Exception { + return IOUtils.toString(request.getInputStream(), UTF8); + } + + /** + * 从request中获取json。如果提交方式是application/x-www-form-urlencoded,则组装成json格式。 + * + * @param request request对象 + * @return 返回json + * @throws IOException + */ + public static String getJson(HttpServletRequest request) throws Exception { + String requestJson = null; + String contectType = request.getContentType(); + if (StringUtils.isBlank(contectType)) { + throw ErrorEnum.ISV_INVALID_CONTENT_TYPE.getErrorMeta().getException(contectType); + } + + contectType = contectType.toLowerCase(); + + if (contectType.contains(CONTENT_TYPE_JSON) || contectType.contains(CONTENT_TYPE_TEXT)) { + requestJson = getText(request); + } else if (contectType.contains(CONTENT_TYPE_URLENCODED)) { + Map params = convertRequestParamsToMap(request); + requestJson = JSON.toJSONString(params); + } else { + throw ErrorEnum.ISV_INVALID_CONTENT_TYPE.getErrorMeta().getException(contectType); + } + return requestJson; + } + + /** + * request中的参数转换成map + * + * @param request request对象 + * @return 返回参数键值对 + */ + public static Map convertRequestParamsToMap(HttpServletRequest request) { + Map paramMap = request.getParameterMap(); + if(paramMap == null || paramMap.isEmpty()) { + return Collections.emptyMap(); + } + Map retMap = new HashMap(paramMap.size()); + + Set> entrySet = paramMap.entrySet(); + + for (Entry entry : entrySet) { + String name = entry.getKey(); + String[] values = entry.getValue(); + if (values.length == 1) { + retMap.put(name, values[0]); + } + } + return retMap; + } + + /** + * 获取客户端真实IP + * + * @param request request对象 + * @return 返回ip + */ + public static String getClientIP(HttpServletRequest request) { + String ipAddress = request.getHeader("x-forwarded-for"); + if (ipAddress == null || ipAddress.length() == 0 || UNKOWN.equalsIgnoreCase(ipAddress)) { + ipAddress = request.getHeader("Proxy-Client-IP"); + } + if (ipAddress == null || ipAddress.length() == 0 || UNKOWN.equalsIgnoreCase(ipAddress)) { + ipAddress = request.getHeader("WL-Proxy-Client-IP"); + } + if (ipAddress == null || ipAddress.length() == 0 || UNKOWN.equalsIgnoreCase(ipAddress)) { + ipAddress = request.getRemoteAddr(); + if (LOCAL_IP.equals(ipAddress)) { + // 根据网卡取本机配置的IP + try { + InetAddress inet = InetAddress.getLocalHost(); + ipAddress = inet.getHostAddress(); + } catch (UnknownHostException e) { + // ignore + } + } + + } + + // 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割 + if (ipAddress != null && ipAddress.length() > IP_LEN) { + if (ipAddress.indexOf(",") > 0) { + ipAddress = ipAddress.substring(0, ipAddress.indexOf(",")); + } + } + return ipAddress; + + } + + /** + * 是否是get请求 + * @param request request对象 + * @return true,是 + */ + public static boolean isGetRequest(HttpServletRequest request) { + return GET.equalsIgnoreCase(request.getMethod()); + } + +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/util/ResponseUtil.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/util/ResponseUtil.java new file mode 100644 index 00000000..5169243a --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/util/ResponseUtil.java @@ -0,0 +1,119 @@ +package com.gitee.sop.gatewaycommon.util; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import lombok.Data; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.MediaType; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * @author tanghc + */ +public class ResponseUtil { + public static final String UTF_8 = "UTF-8"; + + private static Logger log = LoggerFactory.getLogger(ResponseUtil.class); + + public static void writeJson(HttpServletResponse response, Object result) { + response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); + response.setCharacterEncoding(UTF_8); + try { + response.getWriter().write(JSON.toJSONString(result)); + } catch (IOException e) { + log.error("doWriter", e); + } + } + + /** + * map转成xml + * + * @param parameters + * @return + */ + public static String mapToXml(JSONObject parameters) { + String content = doMap2xml(parameters); + return content; + } + + private static String doMap2xml(JSONObject parameters) { + StringBuffer sb = new StringBuffer(); + Set es = parameters.entrySet(); + Iterator it = es.iterator(); + while (it.hasNext()) { + Map.Entry entry = (Map.Entry) it.next(); + String k = (String) entry.getKey(); + Object v = entry.getValue(); + if (v instanceof JSONObject) { + sb.append("<").append(k).append(">") + .append(doMap2xml((JSONObject) v)) + .append(""); + } else if (v instanceof JSONArray) { + JSONArray collection = (JSONArray) v; + String items = buildItems(k + "_item", collection); + sb.append("<").append(k).append(">") + .append(items) + .append(""); + } else { + sb.append("<").append(k).append(">"); + } + } + return sb.toString(); + } + + private static String buildItems(String key, JSONArray collection) { + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < collection.size(); i++) { + Object jsonObject = collection.get(i); + sb.append("<").append(key).append(">"); + if (jsonObject instanceof JSONObject) { + sb.append(doMap2xml((JSONObject) jsonObject)); + } else { + sb.append(jsonObject); + } + sb.append(""); + } + return sb.toString(); + } + + public static void main(String[] args) { + Persion persion = new Persion(); + persion.setId(1); + persion.setName("aaa"); + + String jsonString = JSON.toJSONString(persion); + JSONObject jsonObject = JSON.parseObject(jsonString); + + String xml = mapToXml(jsonObject); + System.out.println(xml); + + } + + @Data + public static class Persion { + int id; + String name; + List items = Arrays.asList("item1", "item2"); + List child = Arrays.asList(new Man("Jim"), new Man("Tom")); + } + + @Data + public static class Man{ + String name; + + public Man(String name) { + this.name = name; + } + } +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/util/XmlUtil.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/util/XmlUtil.java new file mode 100644 index 00000000..7c62ff1b --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/util/XmlUtil.java @@ -0,0 +1,32 @@ +package com.gitee.sop.gatewaycommon.util; + +import com.gitee.sop.gatewaycommon.result.ApiResult; +import com.thoughtworks.xstream.XStream; +import com.thoughtworks.xstream.io.naming.NoNameCoder; +import com.thoughtworks.xstream.io.xml.StaxDriver; + +/** + * @author tanghc + */ +public class XmlUtil { + private static XStream xStream = new XStream(new StaxDriver(new NoNameCoder())); + static { + xStream.processAnnotations(ApiResult.class); + xStream.aliasSystemAttribute(null, "class"); + } + + public static String serialize(Object obj) { + return getXStream().toXML(obj); + } + + public static T unserialize(String xml, Class clazz) { + xStream.processAnnotations(clazz); + Object object = xStream.fromXML(xml); + T cast = clazz.cast(object); + return cast; + } + + public static XStream getXStream() { + return xStream; + } +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/AbstractSigner.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/AbstractSigner.java new file mode 100644 index 00000000..e06236a0 --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/AbstractSigner.java @@ -0,0 +1,36 @@ +package com.gitee.sop.gatewaycommon.validate; + +import com.gitee.sop.gatewaycommon.bean.ApiContext; +import com.gitee.sop.gatewaycommon.message.ErrorEnum; +import com.gitee.sop.gatewaycommon.param.ApiParam; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; + +import javax.servlet.http.HttpServletRequest; + +/** + * @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(HttpServletRequest request, String secret) { + ApiParam apiParam = ApiContext.getApiParam(); + String clientSign = apiParam.fetchSignAndRemove(); + if (StringUtils.isBlank(clientSign)) { + throw ErrorEnum.ISV_MISSING_SIGNATURE.getErrorMeta().getException(); + } + String serverSign = buildServerSign(apiParam, secret); + return clientSign.equals(serverSign); + } +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/ApiEncrypter.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/ApiEncrypter.java new file mode 100644 index 00000000..e41c9372 --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/ApiEncrypter.java @@ -0,0 +1,60 @@ +package com.gitee.sop.gatewaycommon.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-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/ApiSigner.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/ApiSigner.java new file mode 100644 index 00000000..133fa1f0 --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/ApiSigner.java @@ -0,0 +1,56 @@ +package com.gitee.sop.gatewaycommon.validate; + + +import com.gitee.sop.gatewaycommon.message.ErrorEnum; +import com.gitee.sop.gatewaycommon.param.ApiParam; +import org.apache.tomcat.util.buf.HexUtils; + +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 HexUtils.toHexString(bytes).toUpperCase(); + } +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/ApiValidator.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/ApiValidator.java new file mode 100644 index 00000000..38e937ca --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/ApiValidator.java @@ -0,0 +1,150 @@ +package com.gitee.sop.gatewaycommon.validate; + +import com.gitee.sop.gatewaycommon.bean.ApiConfig; +import com.gitee.sop.gatewaycommon.bean.ApiContext; +import com.gitee.sop.gatewaycommon.message.ErrorEnum; +import com.gitee.sop.gatewaycommon.param.ApiParam; +import com.gitee.sop.gatewaycommon.param.ParamNames; +import com.gitee.sop.gatewaycommon.param.UploadContext; +import com.gitee.sop.gatewaycommon.secret.AppSecretManager; +import org.apache.commons.codec.digest.DigestUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +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 + */ +public class ApiValidator implements Validator { + private static final Logger logger = LoggerFactory.getLogger(ApiValidator.class); + + private static final int MILLISECOND_OF_ONE_SECOND = 1000; + + + private static List FORMAT_LIST = Arrays.asList("json", "xml"); + + + @Override + public void validate(ApiParam param) { + ApiConfig apiConfig = ApiContext.getApiConfig(); + if (apiConfig.isIgnoreValidate() || param.fetchIgnoreValidate()) { + logger.debug("忽略所有验证(ignoreValidate=true), name:{}, version:{}", param.fetchName(), param.fetchVersion()); + return; + } + if (param.fetchIgnoreSign()) { + logger.debug("忽略签名验证, name:{}, version:{}", param.fetchName(), param.fetchVersion()); + } else { + // 需要验证签名 + checkAppKey(param); + checkSign(param); + } + checkUploadFile(param); + checkTimeout(param); + checkFormat(param); + } + + /** + * 校验上传文件内容 + * + * @param param + */ + protected void checkUploadFile(ApiParam param) { + UploadContext uploadContext = ApiContext.getUploadContext(); + if (uploadContext != null) { + try { + List files = uploadContext.getAllFile(); + for (MultipartFile file : files) { + // 客户端传来的文件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(); + } + } + } + } catch (IOException e) { + logger.error("验证上传文件MD5错误", e); + throw ErrorEnum.ISV_UPLOAD_FAIL.getErrorMeta().getException(); + } + } + + } + + protected void checkTimeout(ApiParam 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(ApiParam param) { + if (StringUtils.isEmpty(param.fetchAppKey())) { + throw ErrorEnum.ISV_MISSING_APP_ID.getErrorMeta().getException(); + } + AppSecretManager appSecretManager = ApiContext.getApiConfig().getAppSecretManager(); + Assert.notNull(appSecretManager, "appSecretManager未初始化"); + boolean isTrueAppKey = appSecretManager.isValidAppKey(param.fetchAppKey()); + if (!isTrueAppKey) { + throw ErrorEnum.ISV_INVALID_APP_ID.getErrorMeta().getException(); + } + } + + protected void checkSign(ApiParam param) { + String clientSign = param.fetchSign(); + try { + if (StringUtils.isEmpty(param.fetchSign())) { + throw ErrorEnum.ISV_MISSING_SIGNATURE.getErrorMeta().getException(param.fetchNameVersion(), ParamNames.SIGN_NAME); + } + String secret = ApiContext.getApiConfig().getAppSecretManager().getSecret(param.fetchAppKey()); + if (StringUtils.isEmpty(secret)) { + throw ErrorEnum.ISV_MISSING_SIGNATURE_CONFIG.getErrorMeta().getException(); + } + Signer signer = ApiContext.getApiConfig().getSigner(); + boolean isRightSign = signer.checkSign(ApiContext.getRequest(), secret); + // 错误的sign + if (!isRightSign) { + throw ErrorEnum.ISV_INVALID_SIGNATURE.getErrorMeta().getException(param.fetchNameVersion()); + } + } finally { + // 校验过程中会移除sign,这里需要重新设置进去 + param.setSign(clientSign); + } + } + + + protected void checkFormat(ApiParam param) { + String format = param.fetchFormat(); + boolean contains = FORMAT_LIST.contains(format.toLowerCase()); + + if (!contains) { + throw ErrorEnum.ISV_INVALID_FORMAT.getErrorMeta().getException(param.fetchNameVersion(), format); + } + } + +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/Encrypter.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/Encrypter.java new file mode 100644 index 00000000..649304a2 --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/Encrypter.java @@ -0,0 +1,95 @@ +package com.gitee.sop.gatewaycommon.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-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/SignEncipher.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/SignEncipher.java new file mode 100644 index 00000000..b9aaf532 --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/SignEncipher.java @@ -0,0 +1,13 @@ +package com.gitee.sop.gatewaycommon.validate; + +/** + * @author tanghc + */ +public interface SignEncipher { + /** + * 签名的摘要算法 + * @param input 待签名数据 + * @return 返回加密后的数据 + */ + byte[] encrypt(String input, String secret); +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/SignEncipherHMAC_MD5.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/SignEncipherHMAC_MD5.java new file mode 100644 index 00000000..a08fb7a4 --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/SignEncipherHMAC_MD5.java @@ -0,0 +1,37 @@ +package com.gitee.sop.gatewaycommon.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-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/SignEncipherMD5.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/SignEncipherMD5.java new file mode 100644 index 00000000..b5b88503 --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/SignEncipherMD5.java @@ -0,0 +1,16 @@ +package com.gitee.sop.gatewaycommon.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-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/Signer.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/Signer.java new file mode 100644 index 00000000..53d27a1c --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/Signer.java @@ -0,0 +1,20 @@ +package com.gitee.sop.gatewaycommon.validate; + +import javax.servlet.http.HttpServletRequest; + +/** + * 负责签名校验 + * @author tanghc + * + */ +public interface Signer { + + /** + * 签名校验 + * @param request + * @param secret 秘钥 + * @return true签名正确 + */ + boolean checkSign(HttpServletRequest request, String secret); + +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/Validator.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/Validator.java new file mode 100644 index 00000000..de7e194e --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/Validator.java @@ -0,0 +1,18 @@ +package com.gitee.sop.gatewaycommon.validate; + +import com.gitee.sop.gatewaycommon.param.ApiParam; + +/** + * 校验接口 + * + * @author tanghc + * + */ +public interface Validator { + /** + * 接口验证 + * @param param 接口参数 + */ + void validate(ApiParam param); + +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/alipay/AlipayConstants.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/alipay/AlipayConstants.java new file mode 100644 index 00000000..ceb140f8 --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/alipay/AlipayConstants.java @@ -0,0 +1,97 @@ +/** + * Alipay.com Inc. + * Copyright (c) 2004-2012 All Rights Reserved. + */ +package com.gitee.sop.gatewaycommon.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-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/alipay/AlipaySignature.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/alipay/AlipaySignature.java new file mode 100644 index 00000000..7ba2f51e --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/alipay/AlipaySignature.java @@ -0,0 +1,616 @@ +/** + * Alipay.com Inc. + * Copyright (c) 2004-2012 All Rights Reserved. + */ +package com.gitee.sop.gatewaycommon.validate.alipay; + +import com.gitee.sop.gatewaycommon.message.ErrorEnum; +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 publicKey + * @param charset + * @return + */ + public static String rsa256Sign(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_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"); + + StringBuffer content = new StringBuffer(); + List keys = new ArrayList(params.keySet()); + Collections.sort(keys); + + for (int i = 0; i < keys.size(); i++) { + String key = keys.get(i); + String value = 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"); + + StringBuffer content = new StringBuffer(); + List keys = new ArrayList(params.keySet()); + Collections.sort(keys); + + for (int i = 0; i < keys.size(); i++) { + String key = keys.get(i); + String value = String.valueOf(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-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/alipay/AlipaySigner.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/alipay/AlipaySigner.java new file mode 100644 index 00000000..6f690e55 --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/alipay/AlipaySigner.java @@ -0,0 +1,34 @@ +package com.gitee.sop.gatewaycommon.validate.alipay; + +import com.gitee.sop.gatewaycommon.bean.ApiContext; +import com.gitee.sop.gatewaycommon.message.ErrorEnum; +import com.gitee.sop.gatewaycommon.param.ApiParam; +import com.gitee.sop.gatewaycommon.validate.Signer; + +import javax.servlet.http.HttpServletRequest; + +/** + * 支付宝签名验证实现。 + * + * @author tanghc + * @see 支付宝签名 + */ +public class AlipaySigner implements Signer { + + @Override + public boolean checkSign(HttpServletRequest request, String secret) { + // 服务端存的是公钥 + String publicKey = secret; + ApiParam apiParam = ApiContext.getApiParam(); + String charset = apiParam.fetchCharset(); + String signType = apiParam.fetchSignMethod(); + if (signType == null) { + throw ErrorEnum.ISV_DECRYPTION_ERROR_MISSING_ENCRYPT_TYPE.getErrorMeta().getException(); + } + if (charset == null) { + throw ErrorEnum.ISV_INVALID_CHARSET.getErrorMeta().getException(); + } + return AlipaySignature.rsaCheckV2(apiParam, publicKey, charset, signType); + } + +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/alipay/StreamUtil.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/alipay/StreamUtil.java new file mode 100644 index 00000000..343b0ae5 --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/alipay/StreamUtil.java @@ -0,0 +1,134 @@ +/** + * Alipay.com Inc. + * Copyright (c) 2004-2012 All Rights Reserved. + */ +package com.gitee.sop.gatewaycommon.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 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; + } + + public void write(int datum) throws IOException { + synchronized (lock) { + out.write(datum); + } + } + + public void write(byte[] data) throws IOException { + synchronized (lock) { + out.write(data); + } + } + + public void write(byte[] data, int offset, int length) throws IOException { + synchronized (lock) { + out.write(data, offset, length); + } + } + + public void flush() throws IOException { + synchronized (lock) { + out.flush(); + } + } + + public void close() throws IOException { + synchronized (lock) { + out.close(); + } + } + } +} diff --git a/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/alipay/StringUtils.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/alipay/StringUtils.java new file mode 100644 index 00000000..0b3c01e7 --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/alipay/StringUtils.java @@ -0,0 +1,167 @@ +package com.gitee.sop.gatewaycommon.validate.alipay; + +/** + * 字符串工具类。 + * + * @author carver.gu + * @since 1.0, Sep 12, 2009 + */ +public abstract 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) throws IndexOutOfBoundsException { + 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-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/taobao/TaobaoSigner.java b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/taobao/TaobaoSigner.java new file mode 100644 index 00000000..21a1a620 --- /dev/null +++ b/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/taobao/TaobaoSigner.java @@ -0,0 +1,64 @@ +package com.gitee.sop.gatewaycommon.validate.taobao; + + +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.SignEncipher; +import com.gitee.sop.gatewaycommon.validate.SignEncipherHMAC_MD5; +import com.gitee.sop.gatewaycommon.validate.SignEncipherMD5; +import org.apache.commons.lang.StringUtils; +import org.apache.tomcat.util.buf.HexUtils; + +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(); + 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) { + Object val = param.get(paramName); + if (val != null && StringUtils.isNotBlank(String.valueOf(val))) { + paramNameValue.append(paramName).append(val); + } + } + + // 第三步:使用MD5/HMAC加密 + String source = paramNameValue.toString(); + byte[] bytes = signEncipher.encrypt(source, secret); + + // 第四步:把二进制转化为大写的十六进制 + return HexUtils.toHexString(bytes).toUpperCase(); + } +} diff --git a/sop-gateway-common/src/main/resources/i18n/open/error_en.properties b/sop-gateway-common/src/main/resources/i18n/open/error_en.properties new file mode 100644 index 00000000..6f2ed50d --- /dev/null +++ b/sop-gateway-common/src/main/resources/i18n/open/error_en.properties @@ -0,0 +1,55 @@ +# 网关错误配置 + +open.error_10000=Success + +open.error_20000=Service is temporarily unavailable +open.error_20000_isp.unknow-error=Service is temporarily unavailable +open.error_20000_aop.unknow-error=Service is temporarily unavailable + +open.error_20001=Insufficient authorization authority +open.error_20001_aop.invalid-auth-token=Invalid access token +open.error_20001_aop.auth-token-time-out=The access token has expired +open.error_20001_aop.invalid-app-auth-token=Invalid application authorization token +open.error_20001_aop.invalid-app-auth-token-no-api=Merchant does not authorize current interface +open.error_20001_aop.app-auth-token-time-out=The application authorization token has expired +open.error_20001_aop.no-product-reg-by-partner=The merchant has not signed any product + +open.error_40001=Missing required parameters +open.error_40001_isv.missing-method=Method name parameter is missing +open.error_40001_isv.missing-signature=Lack of signature parameter +open.error_40001_isv.missing-signature-type=Missing signature type parameter +open.error_40001_isv.missing-signature-key=Lack of signature configuration +open.error_40001_isv.missing-app-id=Missing appId parameter +open.error_40001_isv.missing-timestamp=Missing timestamp parameter +open.error_40001_isv.missing-version=Missing version parameter +open.error_40001_isv.decryption-error-missing-encrypt-type=Decryption error, no encryption algorithm specified + +open.error_40002=Invalid parameter +open.error_40002_isv.invalid-parameter=Parameter is invalid +open.error_40002_isv.upload-fail=File upload failed +open.error_40002_isv.invalid-file-extension=Invalid file extension +open.error_40002_isv.invalid-file-size=Invalid file size +open.error_40002_isv.invalid-method=Nonexistent method name +open.error_40002_isv.invalid-format=Invalid data format +open.error_40002_isv.invalid-signature-type=Invalid signature type +open.error_40002_isv.invalid-signature=Invalid signature +open.error_40002_isv.invalid-encrypt-type=Invalid encryption type +open.error_40002_isv.invalid-encrypt=Decryption exception +open.error_40002_isv.invalid-app-id=Invalid appId parameter +open.error_40002_isv.invalid-timestamp=Invalid timestamp parameter +open.error_40002_isv.invalid-charset=Character set error +open.error_40002_isv.invalid-digest=Digest error +open.error_40002_isv.decryption-error-not-valid-encrypt-type=Decryption error, unsupported encryption algorithm +open.error_40002_isv.decryption-error-not-valid-encrypt-key=Decryption error, unconfigured encryption key or encryption key format error +open.error_40002_isv.decryption-error-unknown=Decryption error, unknown exception +open.error_40002_isv.missing-signature-config=Signature verification error, no public key or certificate of the corresponding signature algorithm is configured +open.error_40002_isv.not-support-app-auth=This interface does not support third-party proxy calls +open.error_40002_isv.suspected-attack=Suspicious attack requests +open.error_40002_isv.invalid-content-type=Invalid content type + +open.error_40004=Business processing failure +open.error_40004_=Business processing failure + +open.error_40006=Insufficient permissions +open.error_40006_isv.insufficient-isv-permissions=Insufficient ISV permissions +open.error_40006_isv.insufficient-user-permissions=Insufficient user permissions \ No newline at end of file diff --git a/sop-gateway-common/src/main/resources/i18n/open/error_zh_CN.properties b/sop-gateway-common/src/main/resources/i18n/open/error_zh_CN.properties new file mode 100644 index 00000000..49e4969e --- /dev/null +++ b/sop-gateway-common/src/main/resources/i18n/open/error_zh_CN.properties @@ -0,0 +1,107 @@ +# 网关错误配置 + +#open.error_20000=服务不可用 +#open.error_20000_isp.unknow-error=服务暂不可用 +#open.error_20000_aop.unknow-error=服务暂不可用 +# +#open.error_20001=授权权限不足 +#open.error_20001_aop.invalid-auth-token=无效的访问令牌 +#open.error_20001_aop.auth-token-time-out=访问令牌已过期 +#open.error_20001_aop.invalid-app-auth-token=无效的应用授权令牌 +#open.error_20001_aop.invalid-app-auth-token-no-api=商户未授权当前接口 +#open.error_20001_aop.app-auth-token-time-out=应用授权令牌已过期 +#open.error_20001_aop.no-product-reg-by-partner=商户未签约任何产品 +# +#open.error_40001=缺少必选参数 +#open.error_40001_isv.missing-method=缺少方法名参数 +#open.error_40001_isv.missing-signature=缺少签名参数 +#open.error_40001_isv.missing-signature-type=缺少签名类型参数 +#open.error_40001_isv.missing-signature-key=缺少签名配置 +#open.error_40001_isv.missing-app-id=缺少appId参数 +#open.error_40001_isv.missing-timestamp=缺少时间戳参数 +#open.error_40001_isv.missing-version=缺少版本参数 +#open.error_40001_isv.decryption-error-missing-encrypt-type=解密出错, 未指定加密算法 +# +#open.error_40002=非法的参数 +#open.error_40002_isv.invalid-parameter=参数无效 +#open.error_40002_isv.upload-fail=文件上传失败 +#open.error_40002_isv.invalid-file-extension=文件扩展名无效 +#open.error_40002_isv.invalid-file-size=文件大小无效 +#open.error_40002_isv.invalid-method=不存在的方法名 +#open.error_40002_isv.invalid-format=无效的数据格式 +#open.error_40002_isv.invalid-signature-type=无效的签名类型 +#open.error_40002_isv.invalid-signature=无效签名 +#open.error_40002_isv.invalid-encrypt-type=无效的加密类型 +#open.error_40002_isv.invalid-encrypt=解密异常 +#open.error_40002_isv.invalid-app-id=无效的appId参数 +#open.error_40002_isv.invalid-timestamp=非法的时间戳参数 +#open.error_40002_isv.invalid-charset=字符集错误 +#open.error_40002_isv.invalid-digest=摘要错误 +#open.error_40002_isv.decryption-error-not-valid-encrypt-type=解密出错,不支持的加密算法 +#open.error_40002_isv.decryption-error-not-valid-encrypt-key=解密出错, 未配置加密密钥或加密密钥格式错误 +#open.error_40002_isv.decryption-error-unknown=解密出错,未知异常 +#open.error_40002_isv.missing-signature-config=验签出错, 未配置对应签名算法的公钥或者证书 +#open.error_40002_isv.not-support-app-auth=本接口不支持第三方代理调用 +#open.error_40002_isv.suspected-attack=可疑的攻击请求 +#open.error_40002_isv.invalid-content-type=无效的content-type +# +#open.error_40004=业务处理失败 +#open.error_40004_=业务处理失败 +# +#open.error_40006=权限不足 +#open.error_40006_isv.insufficient-isv-permissions=请检查配置的账户是否有当前接口权限 +#open.error_40006_isv.insufficient-user-permissions=代理的商户没有当前接口权限 + +open.error_10000=Success + +open.error_20000=\u670d\u52a1\u4e0d\u53ef\u7528 +open.error_20000_isp.unknow-error=\u670d\u52a1\u6682\u4e0d\u53ef\u7528 +open.error_20000_aop.unknow-error=\u670d\u52a1\u6682\u4e0d\u53ef\u7528 + +open.error_20001=\u6388\u6743\u6743\u9650\u4e0d\u8db3 +open.error_20001_aop.invalid-auth-token=\u65e0\u6548\u7684\u8bbf\u95ee\u4ee4\u724c +open.error_20001_aop.auth-token-time-out=\u8bbf\u95ee\u4ee4\u724c\u5df2\u8fc7\u671f +open.error_20001_aop.invalid-app-auth-token=\u65e0\u6548\u7684\u5e94\u7528\u6388\u6743\u4ee4\u724c +open.error_20001_aop.invalid-app-auth-token-no-api=\u5546\u6237\u672a\u6388\u6743\u5f53\u524d\u63a5\u53e3 +open.error_20001_aop.app-auth-token-time-out=\u5e94\u7528\u6388\u6743\u4ee4\u724c\u5df2\u8fc7\u671f +open.error_20001_aop.no-product-reg-by-partner=\u5546\u6237\u672a\u7b7e\u7ea6\u4efb\u4f55\u4ea7\u54c1 + +open.error_40001=\u7f3a\u5c11\u5fc5\u9009\u53c2\u6570 +open.error_40001_isv.missing-method=\u7f3a\u5c11\u65b9\u6cd5\u540d\u53c2\u6570 +open.error_40001_isv.missing-signature=\u7f3a\u5c11\u7b7e\u540d\u53c2\u6570 +open.error_40001_isv.missing-signature-type=\u7f3a\u5c11\u7b7e\u540d\u7c7b\u578b\u53c2\u6570 +open.error_40001_isv.missing-signature-key=\u7f3a\u5c11\u7b7e\u540d\u914d\u7f6e +open.error_40001_isv.missing-app-id=\u7f3a\u5c11appId\u53c2\u6570 +open.error_40001_isv.missing-timestamp=\u7f3a\u5c11\u65f6\u95f4\u6233\u53c2\u6570 +open.error_40001_isv.missing-version=\u7f3a\u5c11\u7248\u672c\u53c2\u6570 +open.error_40001_isv.decryption-error-missing-encrypt-type=\u89e3\u5bc6\u51fa\u9519, \u672a\u6307\u5b9a\u52a0\u5bc6\u7b97\u6cd5 + +open.error_40002=\u975e\u6cd5\u7684\u53c2\u6570 +open.error_40002_isv.invalid-parameter=\u53c2\u6570\u65e0\u6548 +open.error_40002_isv.upload-fail=\u6587\u4ef6\u4e0a\u4f20\u5931\u8d25 +open.error_40002_isv.invalid-file-extension=\u6587\u4ef6\u6269\u5c55\u540d\u65e0\u6548 +open.error_40002_isv.invalid-file-size=\u6587\u4ef6\u5927\u5c0f\u65e0\u6548 +open.error_40002_isv.invalid-method=\u4e0d\u5b58\u5728\u7684\u65b9\u6cd5\u540d +open.error_40002_isv.invalid-format=\u65e0\u6548\u7684\u6570\u636e\u683c\u5f0f +open.error_40002_isv.invalid-signature-type=\u65e0\u6548\u7684\u7b7e\u540d\u7c7b\u578b +open.error_40002_isv.invalid-signature=\u65e0\u6548\u7b7e\u540d +open.error_40002_isv.invalid-encrypt-type=\u65e0\u6548\u7684\u52a0\u5bc6\u7c7b\u578b +open.error_40002_isv.invalid-encrypt=\u89e3\u5bc6\u5f02\u5e38 +open.error_40002_isv.invalid-app-id=\u65e0\u6548\u7684appId\u53c2\u6570 +open.error_40002_isv.invalid-timestamp=\u975e\u6cd5\u7684\u65f6\u95f4\u6233\u53c2\u6570 +open.error_40002_isv.invalid-charset=\u5b57\u7b26\u96c6\u9519\u8bef +open.error_40002_isv.invalid-digest=\u6458\u8981\u9519\u8bef +open.error_40002_isv.decryption-error-not-valid-encrypt-type=\u89e3\u5bc6\u51fa\u9519\uff0c\u4e0d\u652f\u6301\u7684\u52a0\u5bc6\u7b97\u6cd5 +open.error_40002_isv.decryption-error-not-valid-encrypt-key=\u89e3\u5bc6\u51fa\u9519, \u672a\u914d\u7f6e\u52a0\u5bc6\u5bc6\u94a5\u6216\u52a0\u5bc6\u5bc6\u94a5\u683c\u5f0f\u9519\u8bef +open.error_40002_isv.decryption-error-unknown=\u89e3\u5bc6\u51fa\u9519\uff0c\u672a\u77e5\u5f02\u5e38 +open.error_40002_isv.missing-signature-config=\u9a8c\u7b7e\u51fa\u9519, \u672a\u914d\u7f6e\u5bf9\u5e94\u7b7e\u540d\u7b97\u6cd5\u7684\u516c\u94a5\u6216\u8005\u8bc1\u4e66 +open.error_40002_isv.not-support-app-auth=\u672c\u63a5\u53e3\u4e0d\u652f\u6301\u7b2c\u4e09\u65b9\u4ee3\u7406\u8c03\u7528 +open.error_40002_isv.suspected-attack=\u53ef\u7591\u7684\u653b\u51fb\u8bf7\u6c42 +open.error_40002_isv.invalid-content-type=\u65e0\u6548\u7684content-type + +open.error_40004=\u4e1a\u52a1\u5904\u7406\u5931\u8d25 +open.error_40004_=\u4e1a\u52a1\u5904\u7406\u5931\u8d25 + +open.error_40006=\u6743\u9650\u4e0d\u8db3 +open.error_40006_isv.insufficient-isv-permissions=\u8bf7\u68c0\u67e5\u914d\u7f6e\u7684\u8d26\u6237\u662f\u5426\u6709\u5f53\u524d\u63a5\u53e3\u6743\u9650 +open.error_40006_isv.insufficient-user-permissions=\u4ee3\u7406\u7684\u5546\u6237\u6ca1\u6709\u5f53\u524d\u63a5\u53e3\u6743\u9650 diff --git a/sop-gateway/.gitignore b/sop-gateway/.gitignore new file mode 100644 index 00000000..c456c4a3 --- /dev/null +++ b/sop-gateway/.gitignore @@ -0,0 +1,25 @@ +/target/ +!.mvn/wrapper/maven-wrapper.jar + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +/build/ diff --git a/sop-gateway/.mvn/wrapper/maven-wrapper.jar b/sop-gateway/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..01e67997377a393fd672c7dcde9dccbedf0cb1e9 GIT binary patch literal 48337 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 \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" + # TODO classpath? +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + wget "$jarUrl" -O "$wrapperJarPath" + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + curl -o "$wrapperJarPath" "$jarUrl" + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/sop-gateway/mvnw.cmd b/sop-gateway/mvnw.cmd new file mode 100644 index 00000000..e5cfb0ae --- /dev/null +++ b/sop-gateway/mvnw.cmd @@ -0,0 +1,161 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven2 Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" +FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + echo Found %WRAPPER_JAR% +) else ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" + echo Finished downloading %WRAPPER_JAR% +) +@REM End of extension + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/sop-gateway/pom.xml b/sop-gateway/pom.xml new file mode 100644 index 00000000..c1a78a50 --- /dev/null +++ b/sop-gateway/pom.xml @@ -0,0 +1,74 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.1.2.RELEASE + + + com.gitee.sop + sop-gateway + 1.0.0-SNAPSHOT + sop-gateway + Demo project for Spring Boot + + + 1.8 + Greenwich.RELEASE + + + + + com.gitee.sop + sop-gateway-common + 1.0.0-SNAPSHOT + + + + org.springframework.cloud + spring-cloud-starter-netflix-zuul + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + + + diff --git a/sop-gateway/src/main/java/com/gitee/sop/gateway/SopGatewayApplication.java b/sop-gateway/src/main/java/com/gitee/sop/gateway/SopGatewayApplication.java new file mode 100644 index 00000000..cc7af2ed --- /dev/null +++ b/sop-gateway/src/main/java/com/gitee/sop/gateway/SopGatewayApplication.java @@ -0,0 +1,17 @@ +package com.gitee.sop.gateway; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.netflix.zuul.EnableZuulProxy; + +// 开启网关功能 +@EnableZuulProxy +@SpringBootApplication +public class SopGatewayApplication { + + public static void main(String[] args) { + SpringApplication.run(SopGatewayApplication.class, args); + } + +} + diff --git a/sop-gateway/src/main/java/com/gitee/sop/gateway/config/ZuulConfig.java b/sop-gateway/src/main/java/com/gitee/sop/gateway/config/ZuulConfig.java new file mode 100644 index 00000000..e9ccd367 --- /dev/null +++ b/sop-gateway/src/main/java/com/gitee/sop/gateway/config/ZuulConfig.java @@ -0,0 +1,35 @@ +package com.gitee.sop.gateway.config; + +import com.gitee.sop.gatewaycommon.bean.ApiContext; +import com.gitee.sop.gatewaycommon.configuration.AlipayZuulConfiguration; +import org.springframework.context.annotation.Configuration; + +import java.util.HashMap; +import java.util.Map; + +/** + * 开通支付宝开放平台能力 + * @author tanghc + */ +@Configuration +public class ZuulConfig extends AlipayZuulConfiguration { + + { + Map appSecretStore = new HashMap(); + appSecretStore.put("alipay_test", "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlyb9aUBaljQP/vjmBFe1mF8HsWSvyfC2NTlpT/V9E+sBxTr8TSkbzJCeeeOEm4LCaVXL0Qz63MZoT24v7AIXTuMdj4jyiM/WJ4tjrWAgnmohNOegfntTto16C3l234vXz4ryWZMR/7W+MXy5B92wPGQEJ0LKFwNEoLspDEWZ7RdE53VH7w6y6sIZUfK+YkXWSwehfKPKlx+lDw3zRJ3/yvMF+U+BAdW/MfECe1GuBnCFKnlMRh3UKczWyXWkL6ItOpYHHJi/jx85op5BWDje2pY9QowzfN94+0DB3T7UvZeweu3zlP6diwAJDzLaFQX8ULfWhY+wfKxIRgs9NoiSAQIDAQAB"); + ApiContext.getApiConfig().addAppSecret(appSecretStore); + } +} + +/** + * 开通淘宝开放平能力 + */ +//@Configuration +//public class ZuulConfig extends TaobaoZuulConfiguration { +// +// { +// Map appSecretStore = new HashMap(); +// appSecretStore.put("taobao_test", "G9w0BAQEFAAOCAQ8AMIIBCgKCA"); +// ApiContext.getApiConfig().addAppSecret(appSecretStore); +// } +//} diff --git a/sop-gateway/src/main/resources/application.properties b/sop-gateway/src/main/resources/application.properties new file mode 100644 index 00000000..85c1a89c --- /dev/null +++ b/sop-gateway/src/main/resources/application.properties @@ -0,0 +1,28 @@ +server.port=8081 +spring.application.name=api-gateway + +# 注册中心 +eureka.host=localhost +eureka.port=1111 +eureka.client.serviceUrl.defaultZone=http://${eureka.host}:${eureka.port}/eureka/ + +# 入口地址,默认是/zuul +zuul.servlet-path=/api + +# 路由配置 +# story服务路由配置 +# 内置有个默认的: +# zuul.routes..path=/<服务名>/** +# zuul.routes..serviceId=<服务名> +# 等同于: +# zuul.routes.<服务名>=/<服务名>/** + + +# Redis数据库索引(默认为0) +spring.redis.database=0 +# Redis服务器地址 +spring.redis.host=127.0.0.1 +# Redis服务器连接端口 +spring.redis.port=6379 +# Redis服务器连接密码(默认为空) +spring.redis.password= diff --git a/sop-gateway/src/test/java/com/gitee/sop/gateway/PostTest.java b/sop-gateway/src/test/java/com/gitee/sop/gateway/PostTest.java new file mode 100644 index 00000000..13879db2 --- /dev/null +++ b/sop-gateway/src/test/java/com/gitee/sop/gateway/PostTest.java @@ -0,0 +1,127 @@ +package com.gitee.sop.gateway; + +import com.alibaba.fastjson.JSON; +import org.junit.Test; + +import java.io.IOException; +import java.net.URLEncoder; +import java.security.MessageDigest; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class PostTest extends TestBase { + + String url = "http://localhost:8081/zuul"; + String appKey = "test"; + String secret = "123456"; + + @Test + public void testPost() throws Exception { + // 业务参数 + Map jsonMap = new HashMap(); + jsonMap.put("id", "1"); + jsonMap.put("id", "葫芦娃"); + + String json = JSON.toJSONString(jsonMap); + json = URLEncoder.encode(json, "utf-8"); + + // 系统参数 + Map param = new HashMap(); + param.put("name", "story.get"); + param.put("app_key", appKey); + param.put("data", json); + param.put("timestamp", getTime()); + param.put("version", "2.0"); + + String sign = buildSign(param, secret); + + param.put("sign", sign); + + System.out.println("=====请求数据====="); + String postJson = JSON.toJSONString(param); + System.out.println(postJson); + + post(url, param); // 发送请求 + } + + + + /** + * 构建签名 + * + * @param paramsMap + * 参数 + * @param secret + * 密钥 + * @return + * @throws IOException + */ + public static String buildSign(Map paramsMap, String secret) throws IOException { + Set keySet = paramsMap.keySet(); + List paramNames = new ArrayList(keySet); + + Collections.sort(paramNames); + + StringBuilder paramNameValue = new StringBuilder(); + + for (String paramName : paramNames) { + paramNameValue.append(paramName).append(paramsMap.get(paramName)); + } + + String source = secret + paramNameValue.toString() + secret; + + return md5(source); + } + + /** + * 生成md5,全部大写 + * + * @param message + * @return + */ + public static String md5(String message) { + try { + // 1 创建一个提供信息摘要算法的对象,初始化为md5算法对象 + MessageDigest md = MessageDigest.getInstance("MD5"); + + // 2 将消息变成byte数组 + byte[] input = message.getBytes(); + + // 3 计算后获得字节数组,这就是那128位了 + byte[] buff = md.digest(input); + + // 4 把数组每一字节(一个字节占八位)换成16进制连成md5字符串 + return byte2hex(buff); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * 二进制转十六进制字符串 + * + * @param bytes + * @return + */ + private 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.toUpperCase()); + } + return sign.toString(); + } + + public String getTime() { + return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); + } +} diff --git a/sop-gateway/src/test/java/com/gitee/sop/gateway/SopGatewayApplicationTests.java b/sop-gateway/src/test/java/com/gitee/sop/gateway/SopGatewayApplicationTests.java new file mode 100644 index 00000000..8db99012 --- /dev/null +++ b/sop-gateway/src/test/java/com/gitee/sop/gateway/SopGatewayApplicationTests.java @@ -0,0 +1,17 @@ +package com.gitee.sop.gateway; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +@SpringBootTest +public class SopGatewayApplicationTests { + + @Test + public void contextLoads() { + } + +} + diff --git a/sop-gateway/src/test/java/com/gitee/sop/gateway/SopPostTest.java b/sop-gateway/src/test/java/com/gitee/sop/gateway/SopPostTest.java new file mode 100644 index 00000000..ce374de7 --- /dev/null +++ b/sop-gateway/src/test/java/com/gitee/sop/gateway/SopPostTest.java @@ -0,0 +1,121 @@ +package com.gitee.sop.gateway; + +import com.alibaba.fastjson.JSON; +import org.junit.Test; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class SopPostTest extends TestBase { + + + String url = "http://localhost:8081/zuul"; + String appKey = "test"; + String secret = "123456"; + + @Test + public void testPost() throws Exception { + + // 系统参数 + Map param = new HashMap(); + param.put("method", "story.get"); + param.put("app_key", appKey); + param.put("timestamp", getTime()); + param.put("version", "2.0"); + + // 业务参数 + param.put("id", "1"); + param.put("name", "葫芦娃"); + + String sign = buildSign(param, secret); + + param.put("sign", sign); + + System.out.println("=====请求数据====="); + String postJson = JSON.toJSONString(param); + System.out.println(postJson); + + post(url, param); // 发送请求 + } + + /** + * 构建签名 + * + * @param paramsMap 参数 + * @param secret 密钥 + * @return + * @throws IOException + */ + public static String buildSign(Map paramsMap, String secret) throws IOException { + Set keySet = paramsMap.keySet(); + List paramNames = new ArrayList(keySet); + + Collections.sort(paramNames); + + StringBuilder paramNameValue = new StringBuilder(); + + for (String paramName : paramNames) { + paramNameValue.append(paramName).append(paramsMap.get(paramName)); + } + + String source = secret + paramNameValue.toString() + secret; + + return md5(source); + } + + /** + * 生成md5,全部大写 + * + * @param message + * @return + */ + public static String md5(String message) { + try { + // 1 创建一个提供信息摘要算法的对象,初始化为md5算法对象 + MessageDigest md = MessageDigest.getInstance("MD5"); + + // 2 将消息变成byte数组 + byte[] input = message.getBytes(StandardCharsets.UTF_8); + + // 3 计算后获得字节数组,这就是那128位了 + byte[] buff = md.digest(input); + + // 4 把数组每一字节(一个字节占八位)换成16进制连成md5字符串 + return byte2hex(buff); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * 二进制转十六进制字符串 + * + * @param bytes + * @return + */ + private 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.toUpperCase()); + } + return sign.toString(); + } + + public String getTime() { + return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); + } + +} diff --git a/sop-gateway/src/test/java/com/gitee/sop/gateway/TestBase.java b/sop-gateway/src/test/java/com/gitee/sop/gateway/TestBase.java new file mode 100644 index 00000000..16593289 --- /dev/null +++ b/sop-gateway/src/test/java/com/gitee/sop/gateway/TestBase.java @@ -0,0 +1,102 @@ +package com.gitee.sop.gateway; + +import junit.framework.TestCase; +import org.apache.commons.io.IOUtils; +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.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.message.BasicHeader; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.util.EntityUtils; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * @author tanghc + */ +public class TestBase extends TestCase { + public void post(String url, String postJson) throws IOException { + HttpClient httpClient = HttpClientBuilder.create().build(); + HttpPost post = new HttpPost(url); + // 构造消息头 + + post.setHeader("Content-Type", "application/x-www-form-urlencoded; charset=utf-8"); + // 构建消息实体 + StringEntity entity = new StringEntity(postJson, Charset.forName("UTF-8")); + entity.setContentEncoding("UTF-8"); + // 发送Json格式的数据请求 + entity.setContentType("application/json"); + post.setEntity(entity); + + HttpResponse response = httpClient.execute(post); + HttpEntity responseEntity = response.getEntity(); + String content = IOUtils.toString(responseEntity.getContent(), "UTF-8"); + System.out.println(content); + } + + /** + * 发送POST请求 + * @param url + * @return JSON或者字符串 + * @throws Exception + */ + public static Object post(String url, Map params) throws Exception{ + CloseableHttpClient client = null; + CloseableHttpResponse response = null; + try{ + /** + * 创建一个httpclient对象 + */ + client = HttpClients.createDefault(); + /** + * 创建一个post对象 + */ + HttpPost post = new HttpPost(url); + List nameValuePairs = params.entrySet().stream().map(entry -> { + return new BasicNameValuePair(entry.getKey(), String.valueOf(entry.getValue())); + }).collect(Collectors.toList()); + /** + * 包装成一个Entity对象 + */ + UrlEncodedFormEntity entity = new UrlEncodedFormEntity(nameValuePairs, "UTF-8"); + /** + * 设置请求的内容 + */ + post.setEntity(entity); + /** + * 设置请求的报文头部的编码 + */ + post.setHeader(new BasicHeader("Content-Type", "application/x-www-form-urlencoded; charset=utf-8")); + /** + * 执行post请求 + */ + response = client.execute(post); + /** + * 通过EntityUitls获取返回内容 + */ + String result = EntityUtils.toString(response.getEntity(),"UTF-8"); + System.out.println(result); + return result; + }catch (Exception e){ + e.printStackTrace(); + }finally { + IOUtils.closeQuietly(client); + IOUtils.closeQuietly(response); + } + return null; + } + + +} diff --git a/sop-registry/.gitignore b/sop-registry/.gitignore new file mode 100644 index 00000000..c456c4a3 --- /dev/null +++ b/sop-registry/.gitignore @@ -0,0 +1,25 @@ +/target/ +!.mvn/wrapper/maven-wrapper.jar + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +/build/ diff --git a/sop-registry/.mvn/wrapper/maven-wrapper.jar b/sop-registry/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..01e67997377a393fd672c7dcde9dccbedf0cb1e9 GIT binary patch literal 48337 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 \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" + # TODO classpath? +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + wget "$jarUrl" -O "$wrapperJarPath" + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + curl -o "$wrapperJarPath" "$jarUrl" + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/sop-registry/mvnw.cmd b/sop-registry/mvnw.cmd new file mode 100644 index 00000000..e5cfb0ae --- /dev/null +++ b/sop-registry/mvnw.cmd @@ -0,0 +1,161 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven2 Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" +FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + echo Found %WRAPPER_JAR% +) else ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" + echo Finished downloading %WRAPPER_JAR% +) +@REM End of extension + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/sop-registry/pom.xml b/sop-registry/pom.xml new file mode 100644 index 00000000..2518b0e9 --- /dev/null +++ b/sop-registry/pom.xml @@ -0,0 +1,64 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.1.2.RELEASE + + + com.gitee.sop + sop-registry + 1.0.0-SNAPSHOT + sop-registry + 注册中心 + + + 1.8 + Greenwich.RELEASE + + + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-server + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + + + diff --git a/sop-registry/src/main/java/com/gitee/sop/registry/SopRegistryApplication.java b/sop-registry/src/main/java/com/gitee/sop/registry/SopRegistryApplication.java new file mode 100644 index 00000000..42662cd4 --- /dev/null +++ b/sop-registry/src/main/java/com/gitee/sop/registry/SopRegistryApplication.java @@ -0,0 +1,16 @@ +package com.gitee.sop.registry; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; + +@EnableEurekaServer +@SpringBootApplication +public class SopRegistryApplication { + + public static void main(String[] args) { + SpringApplication.run(SopRegistryApplication.class, args); + } + +} + diff --git a/sop-registry/src/main/resources/application.properties b/sop-registry/src/main/resources/application.properties new file mode 100644 index 00000000..da1d10a4 --- /dev/null +++ b/sop-registry/src/main/resources/application.properties @@ -0,0 +1,12 @@ +spring.application.name=sop-registry +server.port=1111 + +# ---- eureka注册中心 ---- +# 不注册自己 +eureka.client.register-with-eureka=false +eureka.client.fetch-registry=false +# 注册中心地址 +eureka.host=localhost +eureka.port=1111 +eureka.client.serviceUrl.defaultZone=http://${eureka.host}:${eureka.port}/eureka/ + diff --git a/sop-registry/src/test/java/com/gitee/sop/registry/SopRegistryApplicationTests.java b/sop-registry/src/test/java/com/gitee/sop/registry/SopRegistryApplicationTests.java new file mode 100644 index 00000000..da1deda9 --- /dev/null +++ b/sop-registry/src/test/java/com/gitee/sop/registry/SopRegistryApplicationTests.java @@ -0,0 +1,17 @@ +package com.gitee.sop.registry; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +@SpringBootTest +public class SopRegistryApplicationTests { + + @Test + public void contextLoads() { + } + +} + diff --git a/sop-server-common/.gitignore b/sop-server-common/.gitignore new file mode 100644 index 00000000..c456c4a3 --- /dev/null +++ b/sop-server-common/.gitignore @@ -0,0 +1,25 @@ +/target/ +!.mvn/wrapper/maven-wrapper.jar + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +/build/ diff --git a/sop-server-common/pom.xml b/sop-server-common/pom.xml new file mode 100644 index 00000000..6843c42d --- /dev/null +++ b/sop-server-common/pom.xml @@ -0,0 +1,92 @@ + + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.1.2.RELEASE + + + com.gitee.sop + sop-server-common + sop-server-common + 1.0.0-SNAPSHOT + + + UTF-8 + Greenwich.RELEASE + 1.8 + 1.8 + 1.8.8.RELEASE + 1.2.15 + + + + + com.alibaba + fastjson + ${fastjson.version} + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-data-redis + + + + org.apache.tomcat.embed + tomcat-embed-core + 9.0.14 + provided + + + org.projectlombok + lombok + 1.18.4 + provided + + + + junit + junit + 4.11 + test + + + + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + + diff --git a/sop-server-common/readme.md b/sop-server-common/readme.md new file mode 100644 index 00000000..1335082e --- /dev/null +++ b/sop-server-common/readme.md @@ -0,0 +1,3 @@ +# sop-server-common + +微服务端公共组件,每个微服务需要依赖这个jar,先把这个工程mvn deploy到maven私服。 diff --git a/sop-server-common/src/main/java/com/gitee/sop/servercommon/annotation/ApiAbility.java b/sop-server-common/src/main/java/com/gitee/sop/servercommon/annotation/ApiAbility.java new file mode 100644 index 00000000..91f2af05 --- /dev/null +++ b/sop-server-common/src/main/java/com/gitee/sop/servercommon/annotation/ApiAbility.java @@ -0,0 +1,23 @@ +package com.gitee.sop.servercommon.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 设置接口能力。 + * 如果想把已经存在的接口开放出去,可用此注解。
+ * 作用于Controller类上或方法上。如果作用在类上,则类中的所有方法将具备开放平台接口提供能力。 + * @author tanghc + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface ApiAbility { + /** + * 版本号,如:1.0 + */ + String version() default ""; +} diff --git a/sop-server-common/src/main/java/com/gitee/sop/servercommon/annotation/ApiMapping.java b/sop-server-common/src/main/java/com/gitee/sop/servercommon/annotation/ApiMapping.java new file mode 100644 index 00000000..5a5a16d0 --- /dev/null +++ b/sop-server-common/src/main/java/com/gitee/sop/servercommon/annotation/ApiMapping.java @@ -0,0 +1,64 @@ +package com.gitee.sop.servercommon.annotation; + +import org.springframework.core.annotation.AliasFor; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 接口申明注解,使用方式同RequestMapping一样,多了一个版本号属性, + * 用了此注解具备开放平台接口提供能力。 + * @author tanghc + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@RequestMapping +public @interface ApiMapping { + + /** + * 版本号,默认版本号是""
+ * 改默认版本号:ServiceContext.getSopServerConfig().setDefaultVersion("1.0"); + */ + String version() default ""; + + /** + * 接口名 + * Alias for {@link RequestMapping#value}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] value() default {}; + + + @AliasFor(annotation = RequestMapping.class) + RequestMethod[] method() default {}; + + /** + * Alias for {@link RequestMapping#params}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] params() default {}; + + /** + * Alias for {@link RequestMapping#headers}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] headers() default {}; + + /** + * Alias for {@link RequestMapping#consumes}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] consumes() default {}; + + /** + * Alias for {@link RequestMapping#produces}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] produces() default {}; +} diff --git a/sop-server-common/src/main/java/com/gitee/sop/servercommon/bean/ParamNames.java b/sop-server-common/src/main/java/com/gitee/sop/servercommon/bean/ParamNames.java new file mode 100644 index 00000000..8694daf2 --- /dev/null +++ b/sop-server-common/src/main/java/com/gitee/sop/servercommon/bean/ParamNames.java @@ -0,0 +1,46 @@ +package com.gitee.sop.servercommon.bean; + +/** + * 请求参数名定义 + * + * 参数 类型 是否必填 最大长度 描述 示例值 + * 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 + * 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"; + /** 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"; + + +} diff --git a/sop-server-common/src/main/java/com/gitee/sop/servercommon/bean/ServiceApiInfo.java b/sop-server-common/src/main/java/com/gitee/sop/servercommon/bean/ServiceApiInfo.java new file mode 100644 index 00000000..3b67f8a3 --- /dev/null +++ b/sop-server-common/src/main/java/com/gitee/sop/servercommon/bean/ServiceApiInfo.java @@ -0,0 +1,39 @@ +package com.gitee.sop.servercommon.bean; + +import com.alibaba.fastjson.annotation.JSONField; +import lombok.Data; + +import java.util.List; + +/** + * @author tanghc + */ +@Data +public class ServiceApiInfo { + private String md5; + private String appName; + private List apis; + + @Data + public static class ApiMeta { + /** 接口名 */ + private String name; + /** 请求path */ + private String path; + /** 版本号 */ + private String version; + + public ApiMeta() { + } + + public ApiMeta(String name, String path, String version) { + this.name = name; + this.path = path; + this.version = version; + } + + public String fetchNameVersion() { + return this.name + this.version; + } + } +} diff --git a/sop-server-common/src/main/java/com/gitee/sop/servercommon/bean/ServiceConfig.java b/sop-server-common/src/main/java/com/gitee/sop/servercommon/bean/ServiceConfig.java new file mode 100644 index 00000000..a6292458 --- /dev/null +++ b/sop-server-common/src/main/java/com/gitee/sop/servercommon/bean/ServiceConfig.java @@ -0,0 +1,57 @@ +package com.gitee.sop.servercommon.bean; + +import com.gitee.sop.servercommon.configuration.DefaultGlobalExceptionHandler; +import com.gitee.sop.servercommon.configuration.GlobalExceptionHandler; +import com.gitee.sop.servercommon.param.ApiArgumentResolver; +import com.gitee.sop.servercommon.result.DefaultServiceResultBuilder; +import com.gitee.sop.servercommon.result.ServiceResultBuilder; +import lombok.Data; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author tanghc + */ +@Data +public class ServiceConfig { + + private static ServiceConfig instance = new ServiceConfig(); + + private ServiceConfig() { + } + + /** + * 默认版本号 + */ + private String defaultVersion = ""; + + /** + * 错误模块 + */ + private List i18nModules = new ArrayList(); + + /** + * 解析业务参数 + */ + private HandlerMethodArgumentResolver methodArgumentResolver = new ApiArgumentResolver(); + + /** + * 返回结果处理 + */ + private ServiceResultBuilder serviceResultBuilder = new DefaultServiceResultBuilder(); + + /** + * 全局异常处理 + */ + private GlobalExceptionHandler globalExceptionHandler = new DefaultGlobalExceptionHandler(); + + public static ServiceConfig getInstance() { + return instance; + } + + public static void setInstance(ServiceConfig instance) { + ServiceConfig.instance = instance; + } +} diff --git a/sop-server-common/src/main/java/com/gitee/sop/servercommon/bean/ServiceContext.java b/sop-server-common/src/main/java/com/gitee/sop/servercommon/bean/ServiceContext.java new file mode 100644 index 00000000..aba95ea6 --- /dev/null +++ b/sop-server-common/src/main/java/com/gitee/sop/servercommon/bean/ServiceContext.java @@ -0,0 +1,139 @@ +package com.gitee.sop.servercommon.bean; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Locale; +import java.util.concurrent.ConcurrentHashMap; + +public class ServiceContext extends ConcurrentHashMap { + + public static final String REQUEST_KEY = "request"; + public static final String RESPONSE_KEY = "response"; + protected static Class contextClass = ServiceContext.class; + + protected static final ThreadLocal threadLocal = new ThreadLocal() { + @Override + protected ServiceContext initialValue() { + try { + return contextClass.newInstance(); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + }; + + + public ServiceContext() { + super(); + } + + public Locale getLocale() { + return getRequest().getLocale(); + } + + /** + * Override the default ServiceContext + * + * @param clazz + */ + public static void setContextClass(Class clazz) { + contextClass = clazz; + } + + + /** + * Get the current ServiceContext + * + * @return the current ServiceContext + */ + public static ServiceContext getCurrentContext() { + return threadLocal.get(); + } + + /** + * Convenience method to return a boolean value for a given key + * + * @param key + * @return true or false depending what was set. default is false + */ + public boolean getBoolean(String key) { + return getBoolean(key, false); + } + + /** + * Convenience method to return a boolean value for a given key + * + * @param key + * @param defaultResponse + * @return true or false depending what was set. default defaultResponse + */ + public boolean getBoolean(String key, boolean defaultResponse) { + Boolean b = (Boolean) get(key); + if (b != null) { + return b.booleanValue(); + } + return defaultResponse; + } + + /** + * sets a key value to Boolen.TRUE + * + * @param key + */ + public void set(String key) { + put(key, Boolean.TRUE); + } + + /** + * puts the key, value into the map. a null value will remove the key from the map + * + * @param key + * @param value + */ + public void set(String key, Object value) { + if (value != null) put(key, value); + else remove(key); + } + + /** + * @return the HttpServletRequest from the "request" key + */ + public HttpServletRequest getRequest() { + return (HttpServletRequest) get(REQUEST_KEY); + } + + /** + * sets the HttpServletRequest into the "request" key + * + * @param request + */ + public void setRequest(HttpServletRequest request) { + put(REQUEST_KEY, request); + } + + /** + * @return the HttpServletResponse from the "response" key + */ + public HttpServletResponse getResponse() { + return (HttpServletResponse) get(RESPONSE_KEY); + } + + /** + * sets the "response" key to the HttpServletResponse passed in + * + * @param response + */ + public void setResponse(HttpServletResponse response) { + set(RESPONSE_KEY, response); + } + + + /** + * unsets the threadLocal context. Done at the end of the request. + */ + public void unset() { + threadLocal.remove(); + } + + +} \ No newline at end of file diff --git a/sop-server-common/src/main/java/com/gitee/sop/servercommon/configuration/AlipayServerConfiguration.java b/sop-server-common/src/main/java/com/gitee/sop/servercommon/configuration/AlipayServerConfiguration.java new file mode 100644 index 00000000..e6623169 --- /dev/null +++ b/sop-server-common/src/main/java/com/gitee/sop/servercommon/configuration/AlipayServerConfiguration.java @@ -0,0 +1,24 @@ +package com.gitee.sop.servercommon.configuration; + +import com.gitee.sop.servercommon.bean.ServiceConfig; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; + +import java.util.List; + +/** + * 具备支付宝开放平台服务提供能力 + * @author tanghc + */ +public class AlipayServerConfiguration extends BaseServerConfiguration { + + public AlipayServerConfiguration() { + // 默认版本号为1.0 + ServiceConfig.getInstance().setDefaultVersion("1.0"); + } + + @Override + protected void addArgumentResolvers(List argumentResolvers) { + argumentResolvers.add(ServiceConfig.getInstance().getMethodArgumentResolver()); + } + +} diff --git a/sop-server-common/src/main/java/com/gitee/sop/servercommon/configuration/BaseServerConfiguration.java b/sop-server-common/src/main/java/com/gitee/sop/servercommon/configuration/BaseServerConfiguration.java new file mode 100644 index 00000000..f4c32c01 --- /dev/null +++ b/sop-server-common/src/main/java/com/gitee/sop/servercommon/configuration/BaseServerConfiguration.java @@ -0,0 +1,78 @@ +package com.gitee.sop.servercommon.configuration; + +import com.gitee.sop.servercommon.bean.ServiceConfig; +import com.gitee.sop.servercommon.exception.ServiceException; +import com.gitee.sop.servercommon.interceptor.ServiceContextInterceptor; +import com.gitee.sop.servercommon.manager.ApiMetaManager; +import com.gitee.sop.servercommon.manager.DefaultRequestMappingEvent; +import com.gitee.sop.servercommon.manager.RedisApiMetaManager; +import com.gitee.sop.servercommon.mapping.ApiMappingHandlerMapping; +import com.gitee.sop.servercommon.message.ServiceErrorFactory; +import com.gitee.sop.servercommon.result.ServiceResultBuilder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.web.servlet.error.ErrorController; +import org.springframework.context.annotation.Bean; +import org.springframework.core.env.Environment; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; + +import javax.annotation.PostConstruct; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * @author tanghc + */ +public class BaseServerConfiguration extends WebMvcConfigurationSupport { + + public BaseServerConfiguration() { + ServiceConfig.getInstance().getI18nModules().add("i18n/isp/bizerror"); + } + + @Autowired + private StringRedisTemplate redisTemplate; + + @Autowired + private Environment environment; + + @Override + protected void addInterceptors(InterceptorRegistry registry) { + // 添加拦截器 + registry.addInterceptor(new ServiceContextInterceptor()); + super.addInterceptors(registry); + } + + // 自定义Mapping,详见@ApiMapping + @Override + protected RequestMappingHandlerMapping createRequestMappingHandlerMapping() { + ApiMetaManager apiMetaManager = new RedisApiMetaManager(redisTemplate); + DefaultRequestMappingEvent requestMappingEvent = new DefaultRequestMappingEvent(apiMetaManager, environment); + return new ApiMappingHandlerMapping(requestMappingEvent); + } + + @Bean + GlobalExceptionHandler globalExceptionHandler() { + return ServiceConfig.getInstance().getGlobalExceptionHandler(); + } + + @PostConstruct + public void after() { + doAfter(); + } + + protected void doAfter() { + initMessage(); + } + + protected void initMessage() { + ServiceErrorFactory.initMessageSource(ServiceConfig.getInstance().getI18nModules()); + } + +} diff --git a/sop-server-common/src/main/java/com/gitee/sop/servercommon/configuration/DefaultGlobalExceptionHandler.java b/sop-server-common/src/main/java/com/gitee/sop/servercommon/configuration/DefaultGlobalExceptionHandler.java new file mode 100644 index 00000000..9395fc41 --- /dev/null +++ b/sop-server-common/src/main/java/com/gitee/sop/servercommon/configuration/DefaultGlobalExceptionHandler.java @@ -0,0 +1,46 @@ +package com.gitee.sop.servercommon.configuration; + +import com.gitee.sop.servercommon.bean.ServiceConfig; +import com.gitee.sop.servercommon.exception.ServiceException; +import com.gitee.sop.servercommon.result.ServiceResultBuilder; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * 全局异常处理 + * @author tanghc + */ +@ControllerAdvice +public class DefaultGlobalExceptionHandler implements GlobalExceptionHandler { + + @RequestMapping("/error") + @ResponseBody + public Object error(HttpServletRequest request, HttpServletResponse response) { + ServiceResultBuilder serviceResultBuilder = ServiceConfig.getInstance().getServiceResultBuilder(); + return serviceResultBuilder.buildError(request, response, new RuntimeException("系统繁忙")); + } + + @ExceptionHandler(ServiceException.class) + @ResponseBody + public Object exceptionHandler(HttpServletRequest request, HttpServletResponse response, Exception exception) throws Exception { + return this.processError(request, response, exception); + } + + /** + * 处理异常 + * + * @param request + * @param response + * @param exception + * @return + */ + protected Object processError(HttpServletRequest request, HttpServletResponse response, Exception exception) { + ServiceResultBuilder serviceResultBuilder = ServiceConfig.getInstance().getServiceResultBuilder(); + return serviceResultBuilder.buildError(request, response, exception); + } +} diff --git a/sop-server-common/src/main/java/com/gitee/sop/servercommon/configuration/GlobalExceptionHandler.java b/sop-server-common/src/main/java/com/gitee/sop/servercommon/configuration/GlobalExceptionHandler.java new file mode 100644 index 00000000..822ae86d --- /dev/null +++ b/sop-server-common/src/main/java/com/gitee/sop/servercommon/configuration/GlobalExceptionHandler.java @@ -0,0 +1,7 @@ +package com.gitee.sop.servercommon.configuration; + +/** + * @author tanghc + */ +public interface GlobalExceptionHandler { +} diff --git a/sop-server-common/src/main/java/com/gitee/sop/servercommon/configuration/TaobaoServerConfiguration.java b/sop-server-common/src/main/java/com/gitee/sop/servercommon/configuration/TaobaoServerConfiguration.java new file mode 100644 index 00000000..50990e0d --- /dev/null +++ b/sop-server-common/src/main/java/com/gitee/sop/servercommon/configuration/TaobaoServerConfiguration.java @@ -0,0 +1,9 @@ +package com.gitee.sop.servercommon.configuration; + +/** + * 具备淘宝开放平台服务提供能力 + * @author tanghc + */ +public class TaobaoServerConfiguration extends BaseServerConfiguration { + +} diff --git a/sop-server-common/src/main/java/com/gitee/sop/servercommon/exception/ServiceException.java b/sop-server-common/src/main/java/com/gitee/sop/servercommon/exception/ServiceException.java new file mode 100644 index 00000000..86d5d84e --- /dev/null +++ b/sop-server-common/src/main/java/com/gitee/sop/servercommon/exception/ServiceException.java @@ -0,0 +1,26 @@ +package com.gitee.sop.servercommon.exception; + +import com.gitee.sop.servercommon.message.ServiceError; +import com.gitee.sop.servercommon.message.ServiceErrorImpl; + +/** + * @author tanghc + */ +public class ServiceException extends RuntimeException { + + private ServiceError error; + + public ServiceException(String subCode, String subMsg) { + super(subMsg); + this.error = new ServiceErrorImpl(subCode, subMsg); + } + + public ServiceException(ServiceError error) { + super(error.toString()); + this.error = error; + } + + public ServiceError getError() { + return error; + } +} diff --git a/sop-server-common/src/main/java/com/gitee/sop/servercommon/interceptor/ServiceContextInterceptor.java b/sop-server-common/src/main/java/com/gitee/sop/servercommon/interceptor/ServiceContextInterceptor.java new file mode 100644 index 00000000..7ff9495f --- /dev/null +++ b/sop-server-common/src/main/java/com/gitee/sop/servercommon/interceptor/ServiceContextInterceptor.java @@ -0,0 +1,25 @@ +package com.gitee.sop.servercommon.interceptor; + +import com.gitee.sop.servercommon.bean.ServiceContext; +import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * @author tanghc + */ +public class ServiceContextInterceptor extends HandlerInterceptorAdapter { + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + ServiceContext context = ServiceContext.getCurrentContext(); + context.setRequest(request); + context.setResponse(response); + return true; + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { + ServiceContext.getCurrentContext().unset(); + } +} diff --git a/sop-server-common/src/main/java/com/gitee/sop/servercommon/manager/ApiMetaManager.java b/sop-server-common/src/main/java/com/gitee/sop/servercommon/manager/ApiMetaManager.java new file mode 100644 index 00000000..225074ce --- /dev/null +++ b/sop-server-common/src/main/java/com/gitee/sop/servercommon/manager/ApiMetaManager.java @@ -0,0 +1,12 @@ +package com.gitee.sop.servercommon.manager; + +import com.gitee.sop.servercommon.bean.ServiceApiInfo; + +/** + * @author tanghc + */ +public interface ApiMetaManager { + String API_STORE_KEY = "com.gitee.sop.api"; + + void uploadApi(ServiceApiInfo serviceApiInfo); +} diff --git a/sop-server-common/src/main/java/com/gitee/sop/servercommon/manager/DefaultRequestMappingEvent.java b/sop-server-common/src/main/java/com/gitee/sop/servercommon/manager/DefaultRequestMappingEvent.java new file mode 100644 index 00000000..06d29b3f --- /dev/null +++ b/sop-server-common/src/main/java/com/gitee/sop/servercommon/manager/DefaultRequestMappingEvent.java @@ -0,0 +1,100 @@ +package com.gitee.sop.servercommon.manager; + +import com.gitee.sop.servercommon.bean.ServiceApiInfo; +import com.gitee.sop.servercommon.mapping.ApiMappingHandlerMapping; +import com.gitee.sop.servercommon.mapping.ApiMappingInfo; +import com.gitee.sop.servercommon.mapping.ApiMappingRequestCondition; +import lombok.Getter; +import org.springframework.core.env.Environment; +import org.springframework.util.DigestUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.mvc.condition.RequestCondition; +import org.springframework.web.servlet.mvc.method.RequestMappingInfo; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * @author tanghc + */ +@Getter +public class DefaultRequestMappingEvent implements RequestMappingEvent { + + private ApiMetaManager apiMetaManager; + private Environment environment; + + public DefaultRequestMappingEvent(ApiMetaManager apiMetaManager, Environment environment) { + this.apiMetaManager = apiMetaManager; + this.environment = environment; + } + + @Override + public void onRegisterSuccess(ApiMappingHandlerMapping apiMappingHandlerMapping) { + Map handlerMethods = apiMappingHandlerMapping.getHandlerMethods(); + Set requestMappingInfos = handlerMethods.keySet(); + List store = new ArrayList<>(requestMappingInfos.size()); + List apis = new ArrayList<>(requestMappingInfos.size()); + + for (RequestMappingInfo requestMappingInfo : requestMappingInfos) { + Set patterns = requestMappingInfo.getPatternsCondition().getPatterns(); + RequestCondition customCondition = requestMappingInfo.getCustomCondition(); + if (customCondition instanceof ApiMappingRequestCondition) { + ApiMappingRequestCondition condition = (ApiMappingRequestCondition) customCondition; + ApiMappingInfo apiMappingInfo = condition.getApiMappingInfo(); + String name = apiMappingInfo.getName(); + String version = apiMappingInfo.getVersion(); + String path = patterns.iterator().next(); + // 不是ApiMapping注解的接口,name属性是null + if (name == null) { + name = buildName(path); + } + String key = path + version; + if (store.contains(key)) { + throw new RuntimeException("重复申明接口,请检查path和version,path:" + path + ", version:" + version); + } else { + store.add(key); + } + apis.add(new ServiceApiInfo.ApiMeta(name, path, version)); + } + } + + String appName = environment.getProperty("spring.application.name"); + if (appName == null) { + throw new RuntimeException("请在application.properties中指定spring.application.name属性"); + } + // 排序 + Collections.sort(apis, new Comparator() { + @Override + public int compare(ServiceApiInfo.ApiMeta o1, ServiceApiInfo.ApiMeta o2) { + return o1.fetchNameVersion().compareTo(o2.fetchNameVersion()); + } + }); + + ServiceApiInfo serviceApiInfo = new ServiceApiInfo(); + serviceApiInfo.setAppName(appName); + serviceApiInfo.setApis(apis); + serviceApiInfo.setMd5(getMd5(apis)); + + apiMetaManager.uploadApi(serviceApiInfo); + } + + protected String buildName(String path) { + path = StringUtils.trimLeadingCharacter(path, '/'); + path = StringUtils.trimTrailingCharacter(path, '/'); + path = path.replace("/", "."); + return path; + } + + protected String getMd5(List apis) { + StringBuilder sb = new StringBuilder(); + for (ServiceApiInfo.ApiMeta api : apis) { + sb.append(api.fetchNameVersion()); + } + return DigestUtils.md5DigestAsHex(sb.toString().getBytes()); + } +} diff --git a/sop-server-common/src/main/java/com/gitee/sop/servercommon/manager/RedisApiMetaManager.java b/sop-server-common/src/main/java/com/gitee/sop/servercommon/manager/RedisApiMetaManager.java new file mode 100644 index 00000000..7deb94c9 --- /dev/null +++ b/sop-server-common/src/main/java/com/gitee/sop/servercommon/manager/RedisApiMetaManager.java @@ -0,0 +1,29 @@ +package com.gitee.sop.servercommon.manager; + +import com.alibaba.fastjson.JSON; +import com.gitee.sop.servercommon.bean.ServiceApiInfo; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.StringRedisTemplate; + +/** + * @author tanghc + */ +@Slf4j +public class RedisApiMetaManager implements ApiMetaManager { + public static final String API_CHANGE_CHANNEL = "channel.sop.api.change"; + + private StringRedisTemplate redisTemplate; + + public RedisApiMetaManager(StringRedisTemplate redisTemplate) { + this.redisTemplate = redisTemplate; + } + + @Override + public void uploadApi(ServiceApiInfo serviceApiInfo) { + log.info("上传接口信息到Redis,appName:{}, md5:{}, 接口数量:{}", serviceApiInfo.getAppName(), serviceApiInfo.getMd5(), serviceApiInfo.getApis().size()); + String serviceApiInfoJson = JSON.toJSONString(serviceApiInfo); + redisTemplate.opsForHash().put(API_STORE_KEY, serviceApiInfo.getAppName(), serviceApiInfoJson); + // 发送订阅事件 + redisTemplate.convertAndSend(API_CHANGE_CHANNEL, serviceApiInfoJson); + } +} diff --git a/sop-server-common/src/main/java/com/gitee/sop/servercommon/manager/RequestMappingEvent.java b/sop-server-common/src/main/java/com/gitee/sop/servercommon/manager/RequestMappingEvent.java new file mode 100644 index 00000000..e9031dcd --- /dev/null +++ b/sop-server-common/src/main/java/com/gitee/sop/servercommon/manager/RequestMappingEvent.java @@ -0,0 +1,10 @@ +package com.gitee.sop.servercommon.manager; + +import com.gitee.sop.servercommon.mapping.ApiMappingHandlerMapping; + +/** + * @author tanghc + */ +public interface RequestMappingEvent { + void onRegisterSuccess(ApiMappingHandlerMapping apiMappingHandlerMapping); +} diff --git a/sop-server-common/src/main/java/com/gitee/sop/servercommon/mapping/ApiMappingHandlerMapping.java b/sop-server-common/src/main/java/com/gitee/sop/servercommon/mapping/ApiMappingHandlerMapping.java new file mode 100644 index 00000000..8f9f7a96 --- /dev/null +++ b/sop-server-common/src/main/java/com/gitee/sop/servercommon/mapping/ApiMappingHandlerMapping.java @@ -0,0 +1,76 @@ +package com.gitee.sop.servercommon.mapping; + +import com.gitee.sop.servercommon.annotation.ApiMapping; +import com.gitee.sop.servercommon.annotation.ApiAbility; +import com.gitee.sop.servercommon.bean.ServiceConfig; +import com.gitee.sop.servercommon.manager.RequestMappingEvent; +import org.springframework.core.PriorityOrdered; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.StringValueResolver; +import org.springframework.web.servlet.mvc.condition.RequestCondition; +import org.springframework.web.servlet.mvc.method.RequestMappingInfo; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; + +import java.lang.reflect.Method; + +public class ApiMappingHandlerMapping extends RequestMappingHandlerMapping implements PriorityOrdered { + + private static StringValueResolver stringValueResolver = new ApiMappingStringValueResolver(); + + private RequestMappingEvent requestMappingEvent; + + public ApiMappingHandlerMapping(RequestMappingEvent requestMappingEvent) { + this.requestMappingEvent = requestMappingEvent; + } + + @Override + protected RequestMappingInfo getMappingForMethod(Method method, Class handlerType) { + ApiMapping apiMapping = method.getAnnotation(ApiMapping.class); + StringValueResolver valueResolver = null; + if (apiMapping != null) { + valueResolver = stringValueResolver; + } + this.setEmbeddedValueResolver(valueResolver); + return super.getMappingForMethod(method, handlerType); + } + + @Override + protected RequestCondition getCustomMethodCondition(Method method) { + method.setAccessible(true); + String name = null; + String version; + ApiMapping apiMapping = method.getAnnotation(ApiMapping.class); + if (apiMapping != null) { + name = apiMapping.value()[0]; + version = apiMapping.version(); + } else { + ApiAbility apiAbility = this.findApiAbilityAnnotation(method); + if (apiAbility != null) { + version = apiAbility.version(); + } else { + return super.getCustomMethodCondition(method); + } + } + if ("".equals(version)) { + version = ServiceConfig.getInstance().getDefaultVersion(); + } + ApiMappingInfo apiMappingInfo = new ApiMappingInfo(name, version); + logger.info("注册接口,method:" + method + ", version:" + version); + return new ApiMappingRequestCondition(apiMappingInfo); + } + + protected ApiAbility findApiAbilityAnnotation(Method method) { + ApiAbility apiAbility = method.getAnnotation(ApiAbility.class); + if (apiAbility == null) { + Class controllerClass = method.getDeclaringClass(); + apiAbility = AnnotationUtils.findAnnotation(controllerClass, ApiAbility.class); + } + return apiAbility; + } + + @Override + public void afterPropertiesSet() { + super.afterPropertiesSet(); + this.requestMappingEvent.onRegisterSuccess(this); + } +} \ No newline at end of file diff --git a/sop-server-common/src/main/java/com/gitee/sop/servercommon/mapping/ApiMappingInfo.java b/sop-server-common/src/main/java/com/gitee/sop/servercommon/mapping/ApiMappingInfo.java new file mode 100644 index 00000000..3f14ba6c --- /dev/null +++ b/sop-server-common/src/main/java/com/gitee/sop/servercommon/mapping/ApiMappingInfo.java @@ -0,0 +1,17 @@ +package com.gitee.sop.servercommon.mapping; + +import lombok.Data; + +/** + * @author tanghc + */ +@Data +public class ApiMappingInfo { + private String name; + private String version; + + public ApiMappingInfo(String name, String version) { + this.name = name; + this.version = version; + } +} diff --git a/sop-server-common/src/main/java/com/gitee/sop/servercommon/mapping/ApiMappingRequestCondition.java b/sop-server-common/src/main/java/com/gitee/sop/servercommon/mapping/ApiMappingRequestCondition.java new file mode 100644 index 00000000..c11c32f7 --- /dev/null +++ b/sop-server-common/src/main/java/com/gitee/sop/servercommon/mapping/ApiMappingRequestCondition.java @@ -0,0 +1,54 @@ +package com.gitee.sop.servercommon.mapping; + +import com.gitee.sop.servercommon.bean.ServiceConfig; +import com.gitee.sop.servercommon.bean.ParamNames; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.servlet.mvc.condition.RequestCondition; + +import javax.servlet.http.HttpServletRequest; + +@Getter +@Slf4j +public class ApiMappingRequestCondition implements RequestCondition { + + public static final String UTF_8 = "UTF-8"; + private String defaultVersion = ServiceConfig.getInstance().getDefaultVersion(); + + private ApiMappingInfo apiMappingInfo; + + public ApiMappingRequestCondition(ApiMappingInfo apiMappingInfo) { + this.apiMappingInfo = apiMappingInfo; + } + + @Override + public ApiMappingRequestCondition combine(ApiMappingRequestCondition other) { + return this; + } + + // 如果版本号跟当前对象中的版本号匹配,则表是命中的对应的方法 + // 否则返回null,表示不匹配 + @Override + public ApiMappingRequestCondition getMatchingCondition(HttpServletRequest request) { + String version = this.getVersion(request); + if (version.equals(this.apiMappingInfo.getVersion())) { + return this; + } + return null; + } + + protected String getVersion(HttpServletRequest request) { + String version = request.getParameter(ParamNames.VERSION_NAME); + return version == null ? defaultVersion : version; + } + + + // 对两个RequestCondition对象进行比较,这里主要是如果存在两个注册的一样的Mapping,那么就会对 + // 这两个Mapping进行排序,以判断哪个Mapping更适合处理当前request请求 + @Override + public int compareTo(ApiMappingRequestCondition other, HttpServletRequest request) { + return null != apiMappingInfo && null == other.apiMappingInfo ? 1 + : null == apiMappingInfo && null != other.apiMappingInfo ? -1 : 0; + } + +} diff --git a/sop-server-common/src/main/java/com/gitee/sop/servercommon/mapping/ApiMappingStringValueResolver.java b/sop-server-common/src/main/java/com/gitee/sop/servercommon/mapping/ApiMappingStringValueResolver.java new file mode 100644 index 00000000..d17ce61a --- /dev/null +++ b/sop-server-common/src/main/java/com/gitee/sop/servercommon/mapping/ApiMappingStringValueResolver.java @@ -0,0 +1,19 @@ +package com.gitee.sop.servercommon.mapping; + +import org.springframework.util.StringValueResolver; + +/** + * @author tanghc + */ +public class ApiMappingStringValueResolver implements StringValueResolver { + + private static final String END_CHAR = "/"; + + @Override + public String resolveStringValue(String strVal) { + if (strVal != null && !strVal.endsWith(END_CHAR)) { + return strVal + END_CHAR; + } + return null; + } +} diff --git a/sop-server-common/src/main/java/com/gitee/sop/servercommon/message/ServiceError.java b/sop-server-common/src/main/java/com/gitee/sop/servercommon/message/ServiceError.java new file mode 100644 index 00000000..13018f59 --- /dev/null +++ b/sop-server-common/src/main/java/com/gitee/sop/servercommon/message/ServiceError.java @@ -0,0 +1,25 @@ +package com.gitee.sop.servercommon.message; + +/** + * 定义错误返回 + * sub_code(明细返回码) + * sub_msg(明细返回码描述) + * 解决方案 + * @author tanghc + */ +public interface ServiceError { + + /** + * sub_code(明细返回码) + * @return sub_code(明细返回码) + */ + String getSub_code(); + + /** + * sub_msg(明细返回码描述) + * @return sub_msg(明细返回码描述) + */ + String getSub_msg(); + + +} diff --git a/sop-server-common/src/main/java/com/gitee/sop/servercommon/message/ServiceErrorEnum.java b/sop-server-common/src/main/java/com/gitee/sop/servercommon/message/ServiceErrorEnum.java new file mode 100644 index 00000000..2bcb981f --- /dev/null +++ b/sop-server-common/src/main/java/com/gitee/sop/servercommon/message/ServiceErrorEnum.java @@ -0,0 +1,20 @@ +package com.gitee.sop.servercommon.message; + +/** + * @author tanghc + */ +public enum ServiceErrorEnum { + /** 系统繁忙 */ + ISP_UNKNOW_ERROR("isp.unknow-error"), + ISP_PARAM_ERROR("isv.invalid-parameter"), + ; + private ServiceErrorMeta errorMeta; + + ServiceErrorEnum(String sub_code) { + this.errorMeta = new ServiceErrorMeta("isp.error_", sub_code); + } + + public ServiceErrorMeta getErrorMeta() { + return errorMeta; + } +} diff --git a/sop-server-common/src/main/java/com/gitee/sop/servercommon/message/ServiceErrorFactory.java b/sop-server-common/src/main/java/com/gitee/sop/servercommon/message/ServiceErrorFactory.java new file mode 100644 index 00000000..5553684e --- /dev/null +++ b/sop-server-common/src/main/java/com/gitee/sop/servercommon/message/ServiceErrorFactory.java @@ -0,0 +1,110 @@ +package com.gitee.sop.servercommon.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.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 ServiceErrorFactory { + + public static final String SYS_ERR = "系统错误"; + + private static final String I18N_OPEN_ERROR = "i18n/isp/error"; + + private static Set noModuleCache = new HashSet(); + + private static Map errorCache = new HashMap<>(64); + + /** + * 错误信息的国际化信息 + */ + 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 ServiceError getError(ServiceErrorMeta errorMeta, Locale locale, Object... params) { + String key = errorMeta.getModulePrefix() + errorMeta.getSubCode() + locale.toString(); + ServiceError error = errorCache.get(key); + if (error == null) { + Assert.notNull(locale, "未设置Locale"); + String modulePrefix = errorMeta.getModulePrefix(); + String subCode = errorMeta.getSubCode(); + // isp.error_sp.unknow-error=Service is temporarily unavailable + String subMsg = getErrorMessage(modulePrefix + subCode, locale, params); + if (StringUtils.isEmpty(subMsg)) { + subMsg = SYS_ERR; + } + error = new ServiceErrorImpl(subCode, subMsg); + errorCache.put(key, error); + } + return error; + } + + + public static void setErrorMessageSourceAccessor(MessageSourceAccessor errorMessageSourceAccessor) { + ServiceErrorFactory.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-server-common/src/main/java/com/gitee/sop/servercommon/message/ServiceErrorImpl.java b/sop-server-common/src/main/java/com/gitee/sop/servercommon/message/ServiceErrorImpl.java new file mode 100644 index 00000000..c8abba9f --- /dev/null +++ b/sop-server-common/src/main/java/com/gitee/sop/servercommon/message/ServiceErrorImpl.java @@ -0,0 +1,17 @@ +package com.gitee.sop.servercommon.message; + +import lombok.Data; + +@Data +public class ServiceErrorImpl implements ServiceError { + private String sub_code; + private String sub_msg; + + public ServiceErrorImpl() { + } + + public ServiceErrorImpl(String sub_code, String sub_msg) { + this.sub_code = sub_code; + this.sub_msg = sub_msg; + } +} \ No newline at end of file diff --git a/sop-server-common/src/main/java/com/gitee/sop/servercommon/message/ServiceErrorMeta.java b/sop-server-common/src/main/java/com/gitee/sop/servercommon/message/ServiceErrorMeta.java new file mode 100644 index 00000000..c8816bc8 --- /dev/null +++ b/sop-server-common/src/main/java/com/gitee/sop/servercommon/message/ServiceErrorMeta.java @@ -0,0 +1,38 @@ +package com.gitee.sop.servercommon.message; + +import com.gitee.sop.servercommon.bean.ServiceContext; +import com.gitee.sop.servercommon.exception.ServiceException; +import lombok.Getter; + +/** + * 错误对象 + * + * @author tanghc + */ +@Getter +public class ServiceErrorMeta { + + private String modulePrefix; + private String subCode; + + public ServiceErrorMeta(String modulePrefix, String subCode) { + this.modulePrefix = modulePrefix; + this.subCode = subCode; + } + + public ServiceError getError() { + return ServiceErrorFactory.getError(this, ServiceContext.getCurrentContext().getLocale()); + } + + /** + * 返回网关exception + * + * @param params 参数 + * @return 返回exception + */ + public ServiceException getException(Object... params) { + ServiceError error = ServiceErrorFactory.getError(this, ServiceContext.getCurrentContext().getLocale(), params); + return new ServiceException(error); + } + +} diff --git a/sop-server-common/src/main/java/com/gitee/sop/servercommon/param/ApiArgumentResolver.java b/sop-server-common/src/main/java/com/gitee/sop/servercommon/param/ApiArgumentResolver.java new file mode 100644 index 00000000..076367a8 --- /dev/null +++ b/sop-server-common/src/main/java/com/gitee/sop/servercommon/param/ApiArgumentResolver.java @@ -0,0 +1,39 @@ +package com.gitee.sop.servercommon.param; + +import com.alibaba.fastjson.JSON; +import com.gitee.sop.servercommon.bean.ParamNames; +import lombok.Data; +import org.springframework.core.MethodParameter; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +/** + * 解析request参数中的业务参数,隐射到方法参数上 + * @author tanghc + */ +@Data +public class ApiArgumentResolver implements HandlerMethodArgumentResolver { + + private ParamValidator paramValidator = new ServiceParamValidator(); + + @Override + public boolean supportsParameter(MethodParameter methodParameter) { + return methodParameter != null; + } + + @Override + public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception { + String bizContent = nativeWebRequest.getParameter(ParamNames.BIZ_CONTENT_NAME); + if (bizContent != null) { + Class parameterType = methodParameter.getParameterType(); + Object paramObj = JSON.parseObject(bizContent, parameterType); + // JSR-303验证 + paramValidator.validateBizParam(paramObj); + return paramObj; + } + return null; + } + +} \ No newline at end of file diff --git a/sop-server-common/src/main/java/com/gitee/sop/servercommon/param/ParamValidator.java b/sop-server-common/src/main/java/com/gitee/sop/servercommon/param/ParamValidator.java new file mode 100644 index 00000000..8fe5df53 --- /dev/null +++ b/sop-server-common/src/main/java/com/gitee/sop/servercommon/param/ParamValidator.java @@ -0,0 +1,12 @@ +package com.gitee.sop.servercommon.param; + +/** + * @author tanghc + */ +public interface ParamValidator { + /** + * 验证业务参数 + * @param obj + */ + void validateBizParam(Object obj); +} diff --git a/sop-server-common/src/main/java/com/gitee/sop/servercommon/param/ServiceParamValidator.java b/sop-server-common/src/main/java/com/gitee/sop/servercommon/param/ServiceParamValidator.java new file mode 100644 index 00000000..62f3b57a --- /dev/null +++ b/sop-server-common/src/main/java/com/gitee/sop/servercommon/param/ServiceParamValidator.java @@ -0,0 +1,67 @@ +package com.gitee.sop.servercommon.param; + +import com.gitee.sop.servercommon.bean.ServiceContext; +import com.gitee.sop.servercommon.exception.ServiceException; +import com.gitee.sop.servercommon.message.ServiceErrorEnum; +import com.gitee.sop.servercommon.message.ServiceErrorFactory; +import org.springframework.util.CollectionUtils; + +import javax.validation.ConstraintViolation; +import javax.validation.Validation; +import javax.validation.ValidatorFactory; +import java.util.Set; + +/** + * 参数验证,JSR-303 + * @author tanghc + */ +public class ServiceParamValidator implements ParamValidator { + private static final String LEFT_TOKEN = "{"; + private static final String RIGHT_TOKEN = "}"; + public static final String EQ = "="; + public static final String COMMA = ","; + private static Object[] EMPTY_OBJ_ARRAY = {}; + + private static ValidatorFactory factory; + private static javax.validation.Validator validator; + + static { + factory = Validation.buildDefaultValidatorFactory(); + validator = factory.getValidator(); + } + + @Override + public void validateBizParam(Object obj) { + if (obj == null) { + return; + } + Set> set = validator.validate(obj); + if (!CollectionUtils.isEmpty(set)) { + ConstraintViolation oneError = set.iterator().next(); + String errorMsg = oneError.getMessage(); + throw this.getValidateBizParamException(errorMsg); + } + } + + private RuntimeException getValidateBizParamException(String errorMsg) { + String subCode = ServiceErrorEnum.ISP_PARAM_ERROR.getErrorMeta().getSubCode(); + String[] msgToken = errorMsg.split(EQ); + String msg = msgToken[0]; + if (msg.startsWith(LEFT_TOKEN) && msg.endsWith(RIGHT_TOKEN)) { + String module = msg.substring(1, msg.length() - 1); + Object[] params = this.buildParams(msgToken); + String error = ServiceErrorFactory.getErrorMessage(module, ServiceContext.getCurrentContext().getLocale(), params); + return new ServiceException(subCode, error); + } else { + return new ServiceException(subCode, errorMsg); + } + } + + private Object[] buildParams(String[] msgToken) { + if (msgToken.length == 2) { + return msgToken[1].split(COMMA); + } else { + return EMPTY_OBJ_ARRAY; + } + } +} diff --git a/sop-server-common/src/main/java/com/gitee/sop/servercommon/result/DefaultServiceResultBuilder.java b/sop-server-common/src/main/java/com/gitee/sop/servercommon/result/DefaultServiceResultBuilder.java new file mode 100644 index 00000000..c5b909b1 --- /dev/null +++ b/sop-server-common/src/main/java/com/gitee/sop/servercommon/result/DefaultServiceResultBuilder.java @@ -0,0 +1,49 @@ +package com.gitee.sop.servercommon.result; + +import com.gitee.sop.servercommon.exception.ServiceException; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * 处理业务返回结果 + * @author tanghc + */ +@Slf4j +public class DefaultServiceResultBuilder implements ServiceResultBuilder { + + public static final String ISP_UNKNOWN_ERROR = "isp.unknown-error"; + /** 与网关约定好的状态码,表示业务出错 */ + public static final int BIZ_ERROR_CODE = 4000; + + @Override + public Object buildError(HttpServletRequest request, HttpServletResponse response, Throwable throwable) { + response.setStatus(BIZ_ERROR_CODE); + String subCode, subMsg; + if (throwable instanceof ServiceException) { + ServiceException ex = (ServiceException) throwable; + subCode = ex.getError().getSub_code(); + subMsg = ex.getError().getSub_msg(); + } else { + subCode = ISP_UNKNOWN_ERROR; + subMsg = throwable.getMessage(); + } + return this.buildError(subCode, subMsg); + } + + @Override + public Object buildError(String subCode, String subMsg) { + AlipayResult result = new AlipayResult(); + result.setSub_code(subCode); + result.setSub_msg(subMsg); + return result; + } + + @Data + public static class AlipayResult { + private String sub_code; + private String sub_msg; + } +} diff --git a/sop-server-common/src/main/java/com/gitee/sop/servercommon/result/ServiceResultBuilder.java b/sop-server-common/src/main/java/com/gitee/sop/servercommon/result/ServiceResultBuilder.java new file mode 100644 index 00000000..bab79e6c --- /dev/null +++ b/sop-server-common/src/main/java/com/gitee/sop/servercommon/result/ServiceResultBuilder.java @@ -0,0 +1,29 @@ +package com.gitee.sop.servercommon.result; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * @author tanghc + */ +public interface ServiceResultBuilder { + + + /** + * 构建错误返回结果 + * + * @param throwable 异常 + * @return 返回最终结果 + */ + Object buildError(HttpServletRequest request, HttpServletResponse response, Throwable throwable); + + + /** + * 构建错误返回结果 + * @param subCode + * @param subMsg + * @return + */ + Object buildError(String subCode, String subMsg); + +} diff --git a/sop-server-common/src/main/resources/i18n/isp/error_en.properties b/sop-server-common/src/main/resources/i18n/isp/error_en.properties new file mode 100644 index 00000000..8560312c --- /dev/null +++ b/sop-server-common/src/main/resources/i18n/isp/error_en.properties @@ -0,0 +1,4 @@ +# 错误配置 + +isp.error_isp.unknow-error=Service is temporarily unavailable +isp.error_isv.invalid-parameter=Invalid parameter \ No newline at end of file diff --git a/sop-server-common/src/main/resources/i18n/isp/error_zh_CN.properties b/sop-server-common/src/main/resources/i18n/isp/error_zh_CN.properties new file mode 100644 index 00000000..42616e36 --- /dev/null +++ b/sop-server-common/src/main/resources/i18n/isp/error_zh_CN.properties @@ -0,0 +1,5 @@ +# 错误配置 + +isp.error_isp.unknow-error=\u670d\u52a1\u6682\u4e0d\u53ef\u7528 +# 参数无效 +isp.error_isv.invalid-parameter=\u53c2\u6570\u65e0\u6548 diff --git a/sop-story/pom.xml b/sop-story/pom.xml new file mode 100644 index 00000000..b2da4384 --- /dev/null +++ b/sop-story/pom.xml @@ -0,0 +1,15 @@ + + + 4.0.0 + com.gitee.sop-story + sop-story-parent + 1.0.0-SNAPSHOT + pom + + + sop-story-api + sop-story-web + + \ No newline at end of file diff --git a/sop-story/sop-story-api/.gitignore b/sop-story/sop-story-api/.gitignore new file mode 100644 index 00000000..c456c4a3 --- /dev/null +++ b/sop-story/sop-story-api/.gitignore @@ -0,0 +1,25 @@ +/target/ +!.mvn/wrapper/maven-wrapper.jar + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +/build/ diff --git a/sop-story/sop-story-api/pom.xml b/sop-story/sop-story-api/pom.xml new file mode 100644 index 00000000..103848c1 --- /dev/null +++ b/sop-story/sop-story-api/pom.xml @@ -0,0 +1,46 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.1.2.RELEASE + + + com.gitee.sop + sop-story-api + 1.0-SNAPSHOT + + + 1.8 + Greenwich.RELEASE + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.projectlombok + lombok + 1.18.4 + provided + + + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + + \ No newline at end of file diff --git a/sop-story/sop-story-api/src/main/java/com/gitee/sop/story/api/domain/Story.java b/sop-story/sop-story-api/src/main/java/com/gitee/sop/story/api/domain/Story.java new file mode 100644 index 00000000..d2cfbae0 --- /dev/null +++ b/sop-story/sop-story-api/src/main/java/com/gitee/sop/story/api/domain/Story.java @@ -0,0 +1,18 @@ +package com.gitee.sop.story.api.domain; + +import lombok.Data; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.NotBlank; + +/** + * @author tanghc + */ +@Data +public class Story { + private int id; + + @NotBlank(message = "name不能为空") + @Length(max = 20, message = "name长度不能超过20") + private String name; +} diff --git a/sop-story/sop-story-api/src/main/java/com/gitee/sop/story/api/service/StoryService.java b/sop-story/sop-story-api/src/main/java/com/gitee/sop/story/api/service/StoryService.java new file mode 100644 index 00000000..0b3c9794 --- /dev/null +++ b/sop-story/sop-story-api/src/main/java/com/gitee/sop/story/api/service/StoryService.java @@ -0,0 +1,15 @@ +package com.gitee.sop.story.api.service; + +import com.gitee.sop.story.api.domain.Story; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; + +/** + * @author tanghc + */ +@RequestMapping("/story") +public interface StoryService { + @RequestMapping("/getStory") + Story getStory(@RequestParam("id")/* 必须指定@RequestParam,且value不能少 */ + int id); +} diff --git a/sop-story/sop-story-web/.gitignore b/sop-story/sop-story-web/.gitignore new file mode 100644 index 00000000..c456c4a3 --- /dev/null +++ b/sop-story/sop-story-web/.gitignore @@ -0,0 +1,25 @@ +/target/ +!.mvn/wrapper/maven-wrapper.jar + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +/build/ diff --git a/sop-story/sop-story-web/mvnw b/sop-story/sop-story-web/mvnw new file mode 100755 index 00000000..5551fde8 --- /dev/null +++ b/sop-story/sop-story-web/mvnw @@ -0,0 +1,286 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven2 Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" + # TODO classpath? +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + wget "$jarUrl" -O "$wrapperJarPath" + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + curl -o "$wrapperJarPath" "$jarUrl" + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/sop-story/sop-story-web/mvnw.cmd b/sop-story/sop-story-web/mvnw.cmd new file mode 100644 index 00000000..e5cfb0ae --- /dev/null +++ b/sop-story/sop-story-web/mvnw.cmd @@ -0,0 +1,161 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven2 Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" +FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + echo Found %WRAPPER_JAR% +) else ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" + echo Finished downloading %WRAPPER_JAR% +) +@REM End of extension + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/sop-story/sop-story-web/pom.xml b/sop-story/sop-story-web/pom.xml new file mode 100644 index 00000000..d6db8302 --- /dev/null +++ b/sop-story/sop-story-web/pom.xml @@ -0,0 +1,78 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.1.2.RELEASE + + + com.gitee.sop + sop-story-web + 0.0.1-SNAPSHOT + sop-story + Demo project for Spring Boot + + + 1.8 + Greenwich.RELEASE + + + + + com.gitee.sop + sop-server-common + 1.0.0-SNAPSHOT + + + com.gitee.sop + sop-story-api + 1.0-SNAPSHOT + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + + + diff --git a/sop-story/sop-story-web/src/main/java/com/gitee/sop/bookweb/SopStoryApplication.java b/sop-story/sop-story-web/src/main/java/com/gitee/sop/bookweb/SopStoryApplication.java new file mode 100644 index 00000000..16617166 --- /dev/null +++ b/sop-story/sop-story-web/src/main/java/com/gitee/sop/bookweb/SopStoryApplication.java @@ -0,0 +1,16 @@ +package com.gitee.sop.bookweb; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; + +@EnableDiscoveryClient +@SpringBootApplication +public class SopStoryApplication { + + public static void main(String[] args) { + SpringApplication.run(SopStoryApplication.class, args); + } + +} + diff --git a/sop-story/sop-story-web/src/main/java/com/gitee/sop/bookweb/config/OpenServerConfig.java b/sop-story/sop-story-web/src/main/java/com/gitee/sop/bookweb/config/OpenServerConfig.java new file mode 100644 index 00000000..6c1a7f1a --- /dev/null +++ b/sop-story/sop-story-web/src/main/java/com/gitee/sop/bookweb/config/OpenServerConfig.java @@ -0,0 +1,23 @@ +package com.gitee.sop.bookweb.config; + +import com.gitee.sop.servercommon.configuration.AlipayServerConfiguration; +import com.gitee.sop.servercommon.configuration.TaobaoServerConfiguration; +import org.springframework.context.annotation.Configuration; + +/** + * 使用支付宝开放平台功能 + * @author tanghc + */ +@Configuration +public class OpenServerConfig extends AlipayServerConfiguration { + +} + +/** + * 使用淘宝开放平台功能 + * @author tanghc + */ +//@Configuration +//public class OpenServerConfig extends TaobaoServerConfiguration { +// +//} diff --git a/sop-story/sop-story-web/src/main/java/com/gitee/sop/bookweb/controller/AlipayController.java b/sop-story/sop-story-web/src/main/java/com/gitee/sop/bookweb/controller/AlipayController.java new file mode 100644 index 00000000..7f9e86d2 --- /dev/null +++ b/sop-story/sop-story-web/src/main/java/com/gitee/sop/bookweb/controller/AlipayController.java @@ -0,0 +1,28 @@ +package com.gitee.sop.bookweb.controller; + +import com.gitee.sop.bookweb.message.StoryErrorEnum; +import com.gitee.sop.servercommon.annotation.ApiMapping; +import com.gitee.sop.story.api.domain.Story; +import org.springframework.web.bind.annotation.RestController; + +/** + * 支付宝服务端,假设签名验证通过后,到达这里进行具体的业务处理。 + * 这里演示如何接受业务参数。 + * @author tanghc + */ +@RestController +public class AlipayController { + + @ApiMapping(value = "alipay.story.get") + public Story getStory(Story story) { + if (story.getId() == 0) { + throw StoryErrorEnum.param_error.getErrorMeta().getException("id无效"); + } + return story; + } + + @ApiMapping(value = "alipay.story.get", version = "1.2") + public Story getStory11(Story story) { + return story; + } +} diff --git a/sop-story/sop-story-web/src/main/java/com/gitee/sop/bookweb/controller/Story2Controller.java b/sop-story/sop-story-web/src/main/java/com/gitee/sop/bookweb/controller/Story2Controller.java new file mode 100644 index 00000000..6f37e27a --- /dev/null +++ b/sop-story/sop-story-web/src/main/java/com/gitee/sop/bookweb/controller/Story2Controller.java @@ -0,0 +1,46 @@ +package com.gitee.sop.bookweb.controller; + +import com.gitee.sop.servercommon.annotation.ApiAbility; +import com.gitee.sop.servercommon.annotation.ApiMapping; +import com.gitee.sop.story.api.domain.Story; +import com.gitee.sop.story.api.service.StoryService; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author tanghc + */ +@ApiAbility // 放在这里,下面所有的接口都具备接口提供能力 +@RestController +@RequestMapping("story2") +public class Story2Controller{ + + @RequestMapping("getStory4") + public Story getStory4() { + Story story = new Story(); + story.setId(1); + story.setName("海底小纵队(getStory4)"); + return story; + } + + // 优先使用方法上@ApiAbility + @ApiAbility(version = "1.4") + @RequestMapping("getStory4") + public Story storyget() { + Story story = new Story(); + story.setId(1); + story.setName("海底小纵队(1.4)"); + return story; + } + + // 优先使用@ApiMapping + @ApiMapping(value = "story.get2") + public Story getStory2() { + Story story = new Story(); + story.setId(1); + story.setName("海底小纵队story.get2"); + return story; + } + + +} diff --git a/sop-story/sop-story-web/src/main/java/com/gitee/sop/bookweb/controller/StoryController.java b/sop-story/sop-story-web/src/main/java/com/gitee/sop/bookweb/controller/StoryController.java new file mode 100644 index 00000000..d2233e6f --- /dev/null +++ b/sop-story/sop-story-web/src/main/java/com/gitee/sop/bookweb/controller/StoryController.java @@ -0,0 +1,85 @@ +package com.gitee.sop.bookweb.controller; + +import com.gitee.sop.servercommon.annotation.ApiMapping; +import com.gitee.sop.servercommon.annotation.ApiAbility; +import com.gitee.sop.story.api.domain.Story; +import com.gitee.sop.story.api.service.StoryService; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author tanghc + */ +@RestController +public class StoryController implements StoryService { + + // 提供给Feign的服务 + @Override + public Story getStory(int id) { + Story story = new Story(); + story.setId(id); + story.setName("海底小纵队(Feign)"); + return story; + } + + // http://localhost:2222/story/story_get + // 原生的接口,可正常调用 + @RequestMapping("story_get") + public Story getStory4() { + Story story = new Story(); + story.setId(1); + story.setName("海底小纵队(原生)"); + return story; + } + + // http://localhost:2222/story/story.get/ + // 接口名,使用默认版本号 + @ApiMapping(value = "story.get") + public Story storyget() { + Story story = new Story(); + story.setId(1); + story.setName("海底小纵队(默认版本号)"); + return story; + } + + // http://localhost:2222/story/story.get/?version=1.1 + // 接口名 + 版本号 + @ApiMapping(value = "story.get", version = "1.1") + public Story getStory2() { + Story story = new Story(); + story.setId(1); + story.setName("海底小纵队1.0"); + return story; + } + + // http://localhost:2222/story/story.get/?version=2.0 + // 接口名 + 版本号 + @ApiMapping(value = "story.get", version = "2.0") + public Story getStory20(Story story) { + return story; + } + + // http://localhost:2222/story/getStory2 + // 遗留接口具备开放平台能力 + @ApiAbility + @RequestMapping("getStory2") + public Story getStory2_0() { + Story story = new Story(); + story.setId(1); + story.setName("海底小纵队(默认版本号)"); + return story; + } + + // http://localhost:2222/story/getStory2?version=2.1 + // 在原来的基础上加版本号 + @ApiAbility(version = "2.1") + @RequestMapping("getStory2") + public Story getStory2_1() { + Story story = new Story(); + story.setId(1); + story.setName("海底小纵队2.1"); + return story; + } + + +} diff --git a/sop-story/sop-story-web/src/main/java/com/gitee/sop/bookweb/message/StoryErrorEnum.java b/sop-story/sop-story-web/src/main/java/com/gitee/sop/bookweb/message/StoryErrorEnum.java new file mode 100644 index 00000000..5aae79ef --- /dev/null +++ b/sop-story/sop-story-web/src/main/java/com/gitee/sop/bookweb/message/StoryErrorEnum.java @@ -0,0 +1,21 @@ +package com.gitee.sop.bookweb.message; + +import com.gitee.sop.servercommon.message.ServiceErrorMeta; + +/** + * @author tanghc + */ +public enum StoryErrorEnum { + /** 参数错误 */ + param_error("isv.invalid-parameter"), + ; + private ServiceErrorMeta errorMeta; + + StoryErrorEnum(String sub_code) { + this.errorMeta = new ServiceErrorMeta("isp.error_", sub_code); + } + + public ServiceErrorMeta getErrorMeta() { + return errorMeta; + } +} diff --git a/sop-story/sop-story-web/src/main/resources/application.properties b/sop-story/sop-story-web/src/main/resources/application.properties new file mode 100644 index 00000000..cb731b6f --- /dev/null +++ b/sop-story/sop-story-web/src/main/resources/application.properties @@ -0,0 +1,15 @@ +server.port=2222 +spring.application.name=story-service + +eureka.host=localhost +eureka.port=1111 +eureka.client.serviceUrl.defaultZone=http://${eureka.host}:${eureka.port}/eureka/ + +# Redis数据库索引(默认为0) +spring.redis.database=0 +# Redis服务器地址 +spring.redis.host=127.0.0.1 +# Redis服务器连接端口 +spring.redis.port=6379 +# Redis服务器连接密码(默认为空) +spring.redis.password= diff --git a/sop-story/sop-story-web/src/main/resources/i18n/isp/bizerror_en.properties b/sop-story/sop-story-web/src/main/resources/i18n/isp/bizerror_en.properties new file mode 100644 index 00000000..76ef051e --- /dev/null +++ b/sop-story/sop-story-web/src/main/resources/i18n/isp/bizerror_en.properties @@ -0,0 +1,3 @@ +# 错误配置 + +isp.error_isv.invalid-parameter=Invalid parameter, {0} \ No newline at end of file diff --git a/sop-story/sop-story-web/src/main/resources/i18n/isp/bizerror_zh_CN.properties b/sop-story/sop-story-web/src/main/resources/i18n/isp/bizerror_zh_CN.properties new file mode 100644 index 00000000..4c215059 --- /dev/null +++ b/sop-story/sop-story-web/src/main/resources/i18n/isp/bizerror_zh_CN.properties @@ -0,0 +1,4 @@ +# 错误配置 + +# 参数无效 +isp.error_isv.invalid-parameter=\u53c2\u6570\u65e0\u6548, {0} diff --git a/sop-story/sop-story-web/src/test/java/com/gitee/sop/bookweb/SopStoryApplicationTests.java b/sop-story/sop-story-web/src/test/java/com/gitee/sop/bookweb/SopStoryApplicationTests.java new file mode 100644 index 00000000..ba442149 --- /dev/null +++ b/sop-story/sop-story-web/src/test/java/com/gitee/sop/bookweb/SopStoryApplicationTests.java @@ -0,0 +1,17 @@ +package com.gitee.sop.bookweb; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +@SpringBootTest +public class SopStoryApplicationTests { + + @Test + public void contextLoads() { + } + +} + diff --git a/sop-test/pom.xml b/sop-test/pom.xml new file mode 100644 index 00000000..3d508cb9 --- /dev/null +++ b/sop-test/pom.xml @@ -0,0 +1,102 @@ + + + + + sop-parent + com.gitee.sop + 1.0.0-SNAPSHOT + + 4.0.0 + + sop-test + + sop-test + + http://www.example.com + + + UTF-8 + 1.8 + 1.8 + + + + + junit + junit + 4.11 + test + + + org.apache.httpcomponents + httpclient + 4.5.6 + test + + + commons-io + commons-io + 2.5 + test + + + com.alibaba + fastjson + 1.2.47 + test + + + commons-codec + commons-codec + 1.11 + compile + + + + + + + + + maven-clean-plugin + 3.1.0 + + + + maven-resources-plugin + 3.0.2 + + + maven-compiler-plugin + 3.8.0 + + + maven-surefire-plugin + 2.22.1 + + + maven-jar-plugin + 3.0.2 + + + maven-install-plugin + 2.5.2 + + + maven-deploy-plugin + 2.8.2 + + + + maven-site-plugin + 3.7.1 + + + maven-project-info-reports-plugin + 3.0.0 + + + + + diff --git a/sop-test/readme.md b/sop-test/readme.md new file mode 100644 index 00000000..1dcc85cb --- /dev/null +++ b/sop-test/readme.md @@ -0,0 +1,3 @@ +# 测试 + +查看测试用例 \ No newline at end of file diff --git a/sop-test/src/main/java/com/gitee/sop/App.java b/sop-test/src/main/java/com/gitee/sop/App.java new file mode 100644 index 00000000..5712fbcd --- /dev/null +++ b/sop-test/src/main/java/com/gitee/sop/App.java @@ -0,0 +1,13 @@ +package com.gitee.sop; + +/** + * Hello world! + * + */ +public class App +{ + public static void main( String[] args ) + { + System.out.println( "Hello World!" ); + } +} diff --git a/sop-test/src/main/java/com/gitee/sop/alipay/AlipayApiException.java b/sop-test/src/main/java/com/gitee/sop/alipay/AlipayApiException.java new file mode 100644 index 00000000..18aa9ea8 --- /dev/null +++ b/sop-test/src/main/java/com/gitee/sop/alipay/AlipayApiException.java @@ -0,0 +1,49 @@ +/** + * Alipay.com Inc. + * Copyright (c) 2004-2012 All Rights Reserved. + */ +package com.gitee.sop.alipay; + + +/** + * + * @author runzhi + */ +public class AlipayApiException extends Exception { + + private static final long serialVersionUID = -238091758285157331L; + + private String errCode; + private String errMsg; + + public AlipayApiException() { + super(); + } + + public AlipayApiException(String message, Throwable cause) { + super(message, cause); + } + + public AlipayApiException(String message) { + super(message); + } + + public AlipayApiException(Throwable cause) { + super(cause); + } + + public AlipayApiException(String errCode, String errMsg) { + super(errCode + ":" + errMsg); + this.errCode = errCode; + this.errMsg = errMsg; + } + + public String getErrCode() { + return this.errCode; + } + + public String getErrMsg() { + return this.errMsg; + } + +} \ No newline at end of file diff --git a/sop-test/src/main/java/com/gitee/sop/alipay/AlipayConstants.java b/sop-test/src/main/java/com/gitee/sop/alipay/AlipayConstants.java new file mode 100644 index 00000000..276a3022 --- /dev/null +++ b/sop-test/src/main/java/com/gitee/sop/alipay/AlipayConstants.java @@ -0,0 +1,97 @@ +/** + * Alipay.com Inc. + * Copyright (c) 2004-2012 All Rights Reserved. + */ +package com.gitee.sop.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-test/src/main/java/com/gitee/sop/alipay/AlipaySignature.java b/sop-test/src/main/java/com/gitee/sop/alipay/AlipaySignature.java new file mode 100644 index 00000000..1eef3163 --- /dev/null +++ b/sop-test/src/main/java/com/gitee/sop/alipay/AlipaySignature.java @@ -0,0 +1,611 @@ +/** + * Alipay.com Inc. + * Copyright (c) 2004-2012 All Rights Reserved. + */ +package com.gitee.sop.alipay; + + +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 = sortedParams.get(key); + if (StringUtils.areNotEmpty(key, value)) { + content.append((index == 0 ? "" : "&") + key + "=" + value); + index++; + } + } + return content.toString(); + } + + /** + * rsa内容签名 + * + * @param content + * @param privateKey + * @param charset + * @return + * @throws AlipayApiException + */ + public static String rsaSign(String content, String privateKey, String charset, + String signType) throws AlipayApiException { + + if (AlipayConstants.SIGN_TYPE_RSA.equals(signType)) { + + return rsaSign(content, privateKey, charset); + } else if (AlipayConstants.SIGN_TYPE_RSA2.equals(signType)) { + + return rsa256Sign(content, privateKey, charset); + } else { + + throw new AlipayApiException("Sign Type is Not Support : signType=" + signType); + } + + } + + /** + * sha256WithRsa 加签 + * + * @param content + * @param privateKey + * @param charset + * @return + * @throws AlipayApiException + */ + public static String rsa256Sign(String content, String privateKey, + String charset) throws AlipayApiException { + + 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 new AlipayApiException("RSAcontent = " + content + "; charset = " + charset, e); + } + + } + + /** + * sha1WithRsa 加签 + * + * @param content + * @param privateKey + * @param charset + * @return + * @throws AlipayApiException + */ + public static String rsaSign(String content, String privateKey, + String charset) throws AlipayApiException { + try { + PrivateKey priKey = getPrivateKeyFromPKCS8(AlipayConstants.SIGN_TYPE_RSA, + new ByteArrayInputStream(privateKey.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 new AlipayApiException("RSA私钥格式不正确,请检查是否正确配置了PKCS8格式的私钥", ie); + } catch (Exception e) { + throw new AlipayApiException("RSAcontent = " + content + "; charset = " + charset, e); + } + } + + public static String rsaSign(Map params, String privateKey, + String charset) throws AlipayApiException { + String signContent = getSignContent(params); + + return rsaSign(signContent, privateKey, charset); + + } + + 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).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"); + + StringBuffer content = new StringBuffer(); + List keys = new ArrayList(params.keySet()); + Collections.sort(keys); + + for (int i = 0; i < keys.size(); i++) { + String key = keys.get(i); + String value = 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"); + + StringBuffer content = new StringBuffer(); + List keys = new ArrayList(params.keySet()); + Collections.sort(keys); + + for (int i = 0; i < keys.size(); i++) { + String key = keys.get(i); + String value = params.get(key); + content.append((i == 0 ? "" : "&") + key + "=" + value); + } + + return content.toString(); + } + + public static boolean rsaCheckV1(Map params, String publicKey, + String charset) throws AlipayApiException { + 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) throws AlipayApiException { + 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) throws AlipayApiException { + 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) throws AlipayApiException { + String sign = 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) throws AlipayApiException { + + 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 new AlipayApiException("Sign Type is Not Support : signType=" + signType); + } + + } + + public static boolean rsa256CheckContent(String content, String sign, String publicKey, + String charset) throws AlipayApiException { + 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 new AlipayApiException( + "RSAcontent = " + content + ",sign=" + sign + ",charset = " + charset, e); + } + } + + public static boolean rsaCheckContent(String content, String sign, String publicKey, + String charset) throws AlipayApiException { + 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 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 解密后明文,验签失败则异常抛出 + * @throws AlipayApiException + */ + public static String checkSignAndDecrypt(Map params, String alipayPublicKey, + String cusPrivateKey, boolean isCheckSign, + boolean isDecrypt) throws AlipayApiException { + String charset = params.get("charset"); + String bizContent = params.get("biz_content"); + if (isCheckSign) { + if (!rsaCheckV2(params, alipayPublicKey, charset)) { + 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 解密后明文,验签失败则异常抛出 + * @throws AlipayApiException + */ + public static String checkSignAndDecrypt(Map params, String alipayPublicKey, + String cusPrivateKey, boolean isCheckSign, + boolean isDecrypt, String signType) throws AlipayApiException { + String charset = params.get("charset"); + String bizContent = params.get("biz_content"); + if (isCheckSign) { + if (!rsaCheckV2(params, alipayPublicKey, charset,signType)) { + 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 + * + *

+ * @throws AlipayApiException + */ + public static String encryptAndSign(String bizContent, String alipayPublicKey, + String cusPrivateKey, String charset, boolean isEncrypt, + boolean isSign) throws AlipayApiException { + 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 + * + *

+ * @throws AlipayApiException + */ + public static String encryptAndSign(String bizContent, String alipayPublicKey, + String cusPrivateKey, String charset, boolean isEncrypt, + boolean isSign,String signType) throws AlipayApiException { + 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 密文内容 + * @throws AlipayApiException + */ + public static String rsaEncrypt(String content, String publicKey, + String charset) throws AlipayApiException { + 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 new AlipayApiException("EncryptContent = " + content + ",charset = " + charset, + e); + } + } + + /** + * 私钥解密 + * + * @param content 待解密内容 + * @param privateKey 私钥 + * @param charset 字符集,如UTF-8, GBK, GB2312 + * @return 明文内容 + * @throws AlipayApiException + */ + public static String rsaDecrypt(String content, String privateKey, + String charset) throws AlipayApiException { + try { + PrivateKey priKey = getPrivateKeyFromPKCS8(AlipayConstants.SIGN_TYPE_RSA, + new ByteArrayInputStream(privateKey.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 new AlipayApiException("EncodeContent = " + content + ",charset = " + charset, e); + } + } + +} diff --git a/sop-test/src/main/java/com/gitee/sop/alipay/StreamUtil.java b/sop-test/src/main/java/com/gitee/sop/alipay/StreamUtil.java new file mode 100644 index 00000000..23cddb1c --- /dev/null +++ b/sop-test/src/main/java/com/gitee/sop/alipay/StreamUtil.java @@ -0,0 +1,134 @@ +/** + * Alipay.com Inc. + * Copyright (c) 2004-2012 All Rights Reserved. + */ +package com.gitee.sop.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 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; + } + + public void write(int datum) throws IOException { + synchronized (lock) { + out.write(datum); + } + } + + public void write(byte[] data) throws IOException { + synchronized (lock) { + out.write(data); + } + } + + public void write(byte[] data, int offset, int length) throws IOException { + synchronized (lock) { + out.write(data, offset, length); + } + } + + public void flush() throws IOException { + synchronized (lock) { + out.flush(); + } + } + + public void close() throws IOException { + synchronized (lock) { + out.close(); + } + } + } +} diff --git a/sop-test/src/main/java/com/gitee/sop/alipay/StringUtils.java b/sop-test/src/main/java/com/gitee/sop/alipay/StringUtils.java new file mode 100644 index 00000000..f666f7a1 --- /dev/null +++ b/sop-test/src/main/java/com/gitee/sop/alipay/StringUtils.java @@ -0,0 +1,167 @@ +package com.gitee.sop.alipay; + +/** + * 字符串工具类。 + * + * @author carver.gu + * @since 1.0, Sep 12, 2009 + */ +public abstract 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) throws IndexOutOfBoundsException { + 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-test/src/main/java/com/gitee/sop/taobao/Constants.java b/sop-test/src/main/java/com/gitee/sop/taobao/Constants.java new file mode 100644 index 00000000..222f33b8 --- /dev/null +++ b/sop-test/src/main/java/com/gitee/sop/taobao/Constants.java @@ -0,0 +1,10 @@ +package com.gitee.sop.taobao; + +/** + * @author tanghc + */ +public class Constants { + public static final String SIGN_METHOD_HMAC = "hmac"; + public static final String SIGN_METHOD_MD5 = "md5"; + public static final String CHARSET_UTF8 = "UTF-8"; +} diff --git a/sop-test/src/main/java/com/gitee/sop/taobao/TaobaoSignature.java b/sop-test/src/main/java/com/gitee/sop/taobao/TaobaoSignature.java new file mode 100644 index 00000000..2f3719be --- /dev/null +++ b/sop-test/src/main/java/com/gitee/sop/taobao/TaobaoSignature.java @@ -0,0 +1,95 @@ +package com.gitee.sop.taobao; + +import com.gitee.sop.alipay.StringUtils; + +import javax.crypto.Mac; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.Map; + +/** + * @author tanghc + */ +public class TaobaoSignature { + + + public static String signTopRequest(Map params, String secret, String signMethod) throws IOException { + String content = getSignContent(params); + return doSign(content, secret, signMethod); + } + + public static String getSignContent(Map params) { + // 第一步:检查参数是否已经排序 + String[] keys = params.keySet().toArray(new String[0]); + Arrays.sort(keys); + + // 第二步:把所有参数名和参数值串在一起 + StringBuilder query = new StringBuilder(); + + for (String key : keys) { + String value = params.get(key); + if (StringUtils.areNotEmpty(key, value)) { + query.append(key).append(value); + } + } + + return query.toString(); + } + + public static String doSign(String content,String secret, String signMethod) throws IOException { + // 第三步:使用MD5/HMAC加密 + byte[] bytes; + if (Constants.SIGN_METHOD_HMAC.equals(signMethod)) { + bytes = encryptHMAC(content, secret); + } else { + bytes = encryptMD5(secret + content + secret); + } + + // 第四步:把二进制转化为大写的十六进制(正确签名应该为32大写字符串,此方法需要时使用) + return byte2hex(bytes); + } + + public static byte[] encryptHMAC(String data, String secret) throws IOException { + byte[] bytes = null; + try { + SecretKey secretKey = new SecretKeySpec(secret.getBytes(Constants.CHARSET_UTF8), "HmacMD5"); + Mac mac = Mac.getInstance(secretKey.getAlgorithm()); + mac.init(secretKey); + bytes = mac.doFinal(data.getBytes(Constants.CHARSET_UTF8)); + } catch (GeneralSecurityException gse) { + throw new IOException(gse.toString()); + } + return bytes; + } + + public static byte[] encryptMD5(String data) throws IOException { + return encryptMD5(data.getBytes(Constants.CHARSET_UTF8)); + } + + public static byte[] encryptMD5(byte[] data) { + try { + MessageDigest digest = MessageDigest.getInstance(Constants.SIGN_METHOD_MD5); + return digest.digest(data); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } + return null; + } + + public 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.toUpperCase()); + } + return sign.toString(); + } +} diff --git a/sop-test/src/test/java/com/gitee/sop/AlipayClientPostTest.java b/sop-test/src/test/java/com/gitee/sop/AlipayClientPostTest.java new file mode 100644 index 00000000..6c830627 --- /dev/null +++ b/sop-test/src/test/java/com/gitee/sop/AlipayClientPostTest.java @@ -0,0 +1,87 @@ +package com.gitee.sop; + +import com.alibaba.fastjson.JSON; +import com.gitee.sop.alipay.AlipaySignature; +import org.junit.Test; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * 模仿支付宝客户端请求接口 + */ +public class AlipayClientPostTest extends TestBase { + + + String url = "http://localhost:8081/api"; + String appId = "alipay_test"; + // 支付宝私钥 + String privateKey = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCXJv1pQFqWNA/++OYEV7WYXwexZK/J8LY1OWlP9X0T6wHFOvxNKRvMkJ5544SbgsJpVcvRDPrcxmhPbi/sAhdO4x2PiPKIz9Yni2OtYCCeaiE056B+e1O2jXoLeXbfi9fPivJZkxH/tb4xfLkH3bA8ZAQnQsoXA0SguykMRZntF0TndUfvDrLqwhlR8r5iRdZLB6F8o8qXH6UPDfNEnf/K8wX5T4EB1b8x8QJ7Ua4GcIUqeUxGHdQpzNbJdaQvoi06lgccmL+PHzminkFYON7alj1CjDN833j7QMHdPtS9l7B67fOU/p2LAAkPMtoVBfxQt9aFj7B8rEhGCz02iJIBAgMBAAECggEARqOuIpY0v6WtJBfmR3lGIOOokLrhfJrGTLF8CiZMQha+SRJ7/wOLPlsH9SbjPlopyViTXCuYwbzn2tdABigkBHYXxpDV6CJZjzmRZ+FY3S/0POlTFElGojYUJ3CooWiVfyUMhdg5vSuOq0oCny53woFrf32zPHYGiKdvU5Djku1onbDU0Lw8w+5tguuEZ76kZ/lUcccGy5978FFmYpzY/65RHCpvLiLqYyWTtaNT1aQ/9pw4jX9HO9NfdJ9gYFK8r/2f36ZE4hxluAfeOXQfRC/WhPmiw/ReUhxPznG/WgKaa/OaRtAx3inbQ+JuCND7uuKeRe4osP2jLPHPP6AUwQKBgQDUNu3BkLoKaimjGOjCTAwtp71g1oo+k5/uEInAo7lyEwpV0EuUMwLA/HCqUgR4K9pyYV+Oyb8d6f0+Hz0BMD92I2pqlXrD7xV2WzDvyXM3s63NvorRooKcyfd9i6ccMjAyTR2qfLkxv0hlbBbsPHz4BbU63xhTJp3Ghi0/ey/1HQKBgQC2VsgqC6ykfSidZUNLmQZe3J0p/Qf9VLkfrQ+xaHapOs6AzDU2H2osuysqXTLJHsGfrwVaTs00ER2z8ljTJPBUtNtOLrwNRlvgdnzyVAKHfOgDBGwJgiwpeE9voB1oAV/mXqSaUWNnuwlOIhvQEBwekqNyWvhLqC7nCAIhj3yvNQKBgQCqYbeec56LAhWP903Zwcj9VvG7sESqXUhIkUqoOkuIBTWFFIm54QLTA1tJxDQGb98heoCIWf5x/A3xNI98RsqNBX5JON6qNWjb7/dobitti3t99v/ptDp9u8JTMC7penoryLKK0Ty3bkan95Kn9SC42YxaSghzqkt+uvfVQgiNGQKBgGxU6P2aDAt6VNwWosHSe+d2WWXt8IZBhO9d6dn0f7ORvcjmCqNKTNGgrkewMZEuVcliueJquR47IROdY8qmwqcBAN7Vg2K7r7CPlTKAWTRYMJxCT1Hi5gwJb+CZF3+IeYqsJk2NF2s0w5WJTE70k1BSvQsfIzAIDz2yE1oPHvwVAoGAA6e+xQkVH4fMEph55RJIZ5goI4Y76BSvt2N5OKZKd4HtaV+eIhM3SDsVYRLIm9ZquJHMiZQGyUGnsvrKL6AAVNK7eQZCRDk9KQz+0GKOGqku0nOZjUbAu6A2/vtXAaAuFSFx1rUQVVjFulLexkXR3KcztL1Qu2k5pB6Si0K/uwQ="; + + /* + 参数 类型 是否必填 最大长度 描述 示例值 +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 +app_auth_token String 否 40 详见应用授权概述 +biz_content String 是 请求参数的集合,最大长度不限,除公共参数外所有请求参数都必须放在这个参数中传递,具体参照各产品快速接入文档 + */ + @Test + public void testPost() throws Exception { + + // 公共请求参数 + Map params = new HashMap(); + params.put("app_id", appId); + params.put("method", "alipay.story.get"); + params.put("format", "json"); + params.put("charset", "utf-8"); + params.put("sign_type", "RSA2"); + params.put("timestamp", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())); + params.put("version", "1.0"); + + // 业务参数 + Map bizContent = new HashMap<>(); + bizContent.put("id", "1"); + bizContent.put("name", "葫芦娃"); +// bizContent.put("name", "葫芦娃1234567890葫芦娃1234567890"); // 超出长度 + + params.put("biz_content", JSON.toJSONString(bizContent)); + + System.out.println("----------- 请求信息 -----------"); + System.out.println("请求参数:" + buildParamQuery(params)); + System.out.println("商户秘钥:" + privateKey); + String content = AlipaySignature.getSignContent(params); + System.out.println("待签名内容:" + content); + String sign = AlipaySignature.rsa256Sign(content, privateKey, "utf-8"); + System.out.println("签名(sign):" + sign); + + params.put("sign", sign); + + System.out.println("----------- 返回结果 -----------"); + String responseData = post(url, params);// 发送请求 + System.out.println(responseData); + } + + private String buildParamQuery(Map params) { + StringBuilder sb = new StringBuilder(); + for (Map.Entry entry : params.entrySet()) { + sb.append("&").append(entry.getKey()).append("=").append(entry.getValue()); + } + return sb.toString().substring(1); + } + +} diff --git a/sop-test/src/test/java/com/gitee/sop/TaobaoClientPostTest.java b/sop-test/src/test/java/com/gitee/sop/TaobaoClientPostTest.java new file mode 100644 index 00000000..e35315ad --- /dev/null +++ b/sop-test/src/test/java/com/gitee/sop/TaobaoClientPostTest.java @@ -0,0 +1,77 @@ +package com.gitee.sop; + +import com.alibaba.fastjson.JSON; +import com.gitee.sop.taobao.TaobaoSignature; +import org.junit.Test; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +/** + * 模仿淘宝客户端请求接口 + */ +public class TaobaoClientPostTest extends TestBase { + + + String url = "http://localhost:8081/api"; + String appId = "taobao_test"; + // 支付宝私钥 + String secret = "G9w0BAQEFAAOCAQ8AMIIBCgKCA"; + + /* +参数名称 参数类型 是否必须 参数描述 +method String 是 API接口名称。 +app_key String 是 TOP分配给应用的AppKey。这里要注意正式环境和沙箱环境的AppKey是不同的(包括AppSecret),使用时要注意区分;进入开放平台控制台“应用管理-概览” 和 “应用管理-沙箱环境管理”可分别查看正式环境及沙箱环境的AppKey、AppSecret +session String 否 用户登录授权成功后,TOP颁发给应用的授权信息,详细介绍请点击这里。当此API文档的标签上注明:“需要授权”,则此参数必传;“不需要授权”,则此参数不需要传;“可选授权”,则此参数为可选。 +timestamp String 是 时间戳,格式为yyyy-MM-dd HH:mm:ss,时区为GMT+8,例如:2016-01-01 12:00:00。淘宝API服务端允许客户端请求最大时间误差为10分钟。 +format String 否 响应格式。默认为xml格式,可选值:xml,json。 +v String 是 API协议版本,可选值:2.0。 +partner_id String 否 合作伙伴身份标识。 +target_app_key String 否 被调用的目标AppKey,仅当被调用的API为第三方ISV提供时有效。 +simplify Boolean 否 是否采用精简JSON返回格式,仅当format=json时有效,默认值为:false。 +sign_method String 是 签名的摘要算法,可选值为:hmac,md5。 +sign String 是 API输入参数签名结果,签名算法参照下面的介绍。 + */ + @Test + public void testPost() throws Exception { + + // 公共请求参数 + Map params = new HashMap(); + params.put("app_key", appId); + params.put("method", "alipay.story.get"); + params.put("format", "json"); + params.put("sign_method", "md5"); + params.put("timestamp", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())); + params.put("v", ""); + + // 业务参数 + params.put("id", "1"); + params.put("name", "葫芦娃"); +// bizContent.put("name", "葫芦娃1234567890葫芦娃1234567890"); // 超出长度 + + System.out.println("----------- 请求信息 -----------"); + System.out.println("请求参数:" + buildParamQuery(params)); + System.out.println("商户秘钥:" + secret); + String content = TaobaoSignature.getSignContent(params); + System.out.println("待签名内容:" + content); + String sign = TaobaoSignature.doSign(content, secret, "md5"); + System.out.println("签名(sign):" + sign); + + params.put("sign", sign); + + System.out.println("----------- 返回结果 -----------"); + String responseData = post(url, params);// 发送请求 + System.out.println(responseData); + } + + private String buildParamQuery(Map params) { + StringBuilder sb = new StringBuilder(); + for (Map.Entry entry : params.entrySet()) { + sb.append("&").append(entry.getKey()).append("=").append(entry.getValue()); + } + return sb.toString().substring(1); + } + +} diff --git a/sop-test/src/test/java/com/gitee/sop/TestBase.java b/sop-test/src/test/java/com/gitee/sop/TestBase.java new file mode 100644 index 00000000..f8606e4d --- /dev/null +++ b/sop-test/src/test/java/com/gitee/sop/TestBase.java @@ -0,0 +1,100 @@ +package com.gitee.sop; + +import junit.framework.TestCase; +import org.apache.commons.io.IOUtils; +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.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.message.BasicHeader; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.util.EntityUtils; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * @author tanghc + */ +public class TestBase extends TestCase { + public void post(String url, String postJson) throws IOException { + HttpClient httpClient = HttpClientBuilder.create().build(); + HttpPost post = new HttpPost(url); + // 构造消息头 + + post.setHeader("Content-Type", "application/x-www-form-urlencoded; charset=utf-8"); + // 构建消息实体 + StringEntity entity = new StringEntity(postJson, Charset.forName("UTF-8")); + entity.setContentEncoding("UTF-8"); + // 发送Json格式的数据请求 + entity.setContentType("application/json"); + post.setEntity(entity); + + HttpResponse response = httpClient.execute(post); + HttpEntity responseEntity = response.getEntity(); + String content = IOUtils.toString(responseEntity.getContent(), "UTF-8"); + System.out.println(content); + } + + /** + * 发送POST请求 + * @param url + * @return JSON或者字符串 + * @throws Exception + */ + public static String post(String url, Map params) throws Exception{ + CloseableHttpClient client = null; + CloseableHttpResponse response = null; + try{ + /** + * 创建一个httpclient对象 + */ + client = HttpClients.createDefault(); + /** + * 创建一个post对象 + */ + HttpPost post = new HttpPost(url); + List nameValuePairs = params.entrySet().stream().map(entry -> { + return new BasicNameValuePair(entry.getKey(), String.valueOf(entry.getValue())); + }).collect(Collectors.toList()); + /** + * 包装成一个Entity对象 + */ + UrlEncodedFormEntity entity = new UrlEncodedFormEntity(nameValuePairs, "UTF-8"); + /** + * 设置请求的内容 + */ + post.setEntity(entity); + /** + * 设置请求的报文头部的编码 + */ + post.setHeader(new BasicHeader("Content-Type", "application/x-www-form-urlencoded; charset=utf-8")); + /** + * 执行post请求 + */ + response = client.execute(post); + /** + * 通过EntityUitls获取返回内容 + */ + return EntityUtils.toString(response.getEntity(),"UTF-8"); + }catch (Exception e){ + e.printStackTrace(); + }finally { + IOUtils.closeQuietly(client); + IOUtils.closeQuietly(response); + } + return null; + } + + +}