diff --git a/pom.xml b/pom.xml index 166d20fcd..d85aa06be 100644 --- a/pom.xml +++ b/pom.xml @@ -237,6 +237,12 @@ spring-boot-starter-webflux + + commons-codec + commons-codec + 1.12 + + diff --git a/src/main/java/au/com/royalpay/payment/manage/shopify/auth/domain/ShopifyRequestVerifyException.java b/src/main/java/au/com/royalpay/payment/manage/shopify/auth/domain/ShopifyRequestVerifyException.java new file mode 100644 index 000000000..c72e75060 --- /dev/null +++ b/src/main/java/au/com/royalpay/payment/manage/shopify/auth/domain/ShopifyRequestVerifyException.java @@ -0,0 +1,7 @@ +package au.com.royalpay.payment.manage.shopify.auth.domain; + +public class ShopifyRequestVerifyException extends RuntimeException{ + public ShopifyRequestVerifyException(String message) { + super(message); + } +} diff --git a/src/main/java/au/com/royalpay/payment/manage/shopify/auth/domain/entity/ShopifyCommonParameter.java b/src/main/java/au/com/royalpay/payment/manage/shopify/auth/domain/entity/ShopifyCommonParameter.java new file mode 100644 index 000000000..c5f56f2d0 --- /dev/null +++ b/src/main/java/au/com/royalpay/payment/manage/shopify/auth/domain/entity/ShopifyCommonParameter.java @@ -0,0 +1,20 @@ +package au.com.royalpay.payment.manage.shopify.auth.domain.entity; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ShopifyCommonParameter { + private String shop; + private String code; + private String state; + private String hmac; + private String host; + private String timestamp; + +} diff --git a/src/main/java/au/com/royalpay/payment/manage/shopify/auth/domain/service/ShopifyAuthService.java b/src/main/java/au/com/royalpay/payment/manage/shopify/auth/domain/service/ShopifyAuthService.java index 111839af2..751f33fed 100644 --- a/src/main/java/au/com/royalpay/payment/manage/shopify/auth/domain/service/ShopifyAuthService.java +++ b/src/main/java/au/com/royalpay/payment/manage/shopify/auth/domain/service/ShopifyAuthService.java @@ -39,7 +39,7 @@ public class ShopifyAuthService { private RestTemplate restTemplate; public ShopifyPermissionURL shopifyPermission(String shopifyStoreHost, String hmac, String timestamp) { - String redirectUri = PlatformEnvironment.getEnv().concatUrl("/auth.html#/shopify/login"); + String redirectUri = PlatformEnvironment.getEnv().concatUrl("/auth.html"); String permissionUrl = String.format(PERMISSION_URL, shopifyStoreHost, clientId, scope, redirectUri, String.valueOf(new Date().getTime()).substring(0,10)); return ShopifyPermissionURL.builder().url(permissionUrl).build(); } diff --git a/src/main/java/au/com/royalpay/payment/manage/shopify/auth/domain/service/ShopifyRequestValidator.java b/src/main/java/au/com/royalpay/payment/manage/shopify/auth/domain/service/ShopifyRequestValidator.java new file mode 100644 index 000000000..b3b6ec49f --- /dev/null +++ b/src/main/java/au/com/royalpay/payment/manage/shopify/auth/domain/service/ShopifyRequestValidator.java @@ -0,0 +1,30 @@ +package au.com.royalpay.payment.manage.shopify.auth.domain.service; + +import au.com.royalpay.payment.manage.shopify.auth.domain.entity.ShopifyCommonParameter; +import au.com.royalpay.payment.manage.shopify.support.HmacVerificationUtil; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class ShopifyRequestValidator { + + @Value("${shopify.auth.apiSecretKey}") + private String clientSecret; + + public Boolean valid(ShopifyCommonParameter parameter) { + StringBuilder message =new StringBuilder(); + message.append("code=").append(parameter.getCode()) + .append("&host=").append(parameter.getHost()) + .append("&shop=").append(parameter.getShop()) + .append("&state=").append(parameter.getState()) + .append("×tamp=").append(parameter.getTimestamp()); + return HmacVerificationUtil.hmacSHA256(message.toString(),clientSecret,parameter.getHmac()); + } + + public boolean verifyPermission(String shopifyStoreHost, String hmac, String timestamp) { + StringBuilder message =new StringBuilder(); + message.append("shop=").append(shopifyStoreHost) + .append("×tamp=").append(timestamp); + return HmacVerificationUtil.hmacSHA256(message.toString(),clientSecret,hmac); + } +} diff --git a/src/main/java/au/com/royalpay/payment/manage/shopify/auth/web/ShopifyAuthController.java b/src/main/java/au/com/royalpay/payment/manage/shopify/auth/web/ShopifyAuthController.java index 3e4183838..8c4a4e117 100644 --- a/src/main/java/au/com/royalpay/payment/manage/shopify/auth/web/ShopifyAuthController.java +++ b/src/main/java/au/com/royalpay/payment/manage/shopify/auth/web/ShopifyAuthController.java @@ -1,8 +1,12 @@ package au.com.royalpay.payment.manage.shopify.auth.web; +import au.com.royalpay.payment.manage.shopify.auth.domain.ShopifyRequestVerifyException; import au.com.royalpay.payment.manage.shopify.auth.domain.application.ShopifyMerchantAuthApplication; import au.com.royalpay.payment.manage.shopify.auth.domain.entity.ShopifyAccessToken; +import au.com.royalpay.payment.manage.shopify.auth.domain.service.ShopifyRequestValidator; import au.com.royalpay.payment.manage.shopify.auth.web.command.ShopifyPermissionRequest; +import au.com.royalpay.payment.manage.shopify.auth.web.command.ShopifyVerifyRequest; +import com.alibaba.fastjson.JSONObject; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -21,6 +25,23 @@ public class ShopifyAuthController { @Autowired private ShopifyMerchantAuthApplication shopifyMerchantAuthApplication; + @Autowired + private ShopifyRequestValidator shopifyRequestValidator; + + /** + * 校验shopify请求 + * + * @param request + * @return + */ + @PostMapping("/verify") + public JSONObject verifyRequest(@RequestBody @Valid ShopifyVerifyRequest request) { + if (!shopifyRequestValidator.valid(request.build())) { + throw new ShopifyRequestVerifyException("This request parameters is invalid"); + } + return new JSONObject(); + } + /** * 获取shopify店铺授权URL * @@ -29,6 +50,9 @@ public class ShopifyAuthController { */ @PostMapping("/install") public ShopifyAccessToken shopifyPermission(@RequestBody @Valid ShopifyPermissionRequest request) { + if (!shopifyRequestValidator.valid(request.build())) { + throw new ShopifyRequestVerifyException("This request parameters is invalid"); + } ShopifyAccessToken shopifyAccessToken = shopifyMerchantAuthApplication.install(request); return shopifyAccessToken; } diff --git a/src/main/java/au/com/royalpay/payment/manage/shopify/auth/web/ShopifyAuthTemplateController.java b/src/main/java/au/com/royalpay/payment/manage/shopify/auth/web/ShopifyAuthTemplateController.java index 11a36c17e..b94a92bd9 100644 --- a/src/main/java/au/com/royalpay/payment/manage/shopify/auth/web/ShopifyAuthTemplateController.java +++ b/src/main/java/au/com/royalpay/payment/manage/shopify/auth/web/ShopifyAuthTemplateController.java @@ -1,7 +1,9 @@ package au.com.royalpay.payment.manage.shopify.auth.web; +import au.com.royalpay.payment.manage.shopify.auth.domain.ShopifyRequestVerifyException; import au.com.royalpay.payment.manage.shopify.auth.domain.application.ShopifyMerchantAuthApplication; import au.com.royalpay.payment.manage.shopify.auth.domain.entity.ShopifyPermissionURL; +import au.com.royalpay.payment.manage.shopify.auth.domain.service.ShopifyRequestValidator; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; @@ -16,6 +18,9 @@ public class ShopifyAuthTemplateController { @Autowired private ShopifyMerchantAuthApplication shopifyMerchantAuthApplication; + @Autowired + private ShopifyRequestValidator shopifyRequestValidator; + /** * shopify店铺安装入口 * @@ -25,9 +30,12 @@ public class ShopifyAuthTemplateController { * @return */ @GetMapping("/auth") - public RedirectView shopifyStoreInstall(@RequestParam("shop") String shopifyStoreHost, + public RedirectView shopifyStorePermission(@RequestParam("shop") String shopifyStoreHost, @RequestParam("hmac") String hmac, @RequestParam("timestamp") String timestamp) { + if (!shopifyRequestValidator.verifyPermission(shopifyStoreHost, hmac, timestamp)) { + throw new ShopifyRequestVerifyException("This request parameters is invalid"); + } ShopifyPermissionURL shopifyPermissionURL = shopifyMerchantAuthApplication.getShopifyPermissionUrl(shopifyStoreHost, hmac, timestamp); return new RedirectView(shopifyPermissionURL.getUrl()); } diff --git a/src/main/java/au/com/royalpay/payment/manage/shopify/auth/web/command/ShopifyPermissionRequest.java b/src/main/java/au/com/royalpay/payment/manage/shopify/auth/web/command/ShopifyPermissionRequest.java index fc5cab5f6..d2190a921 100644 --- a/src/main/java/au/com/royalpay/payment/manage/shopify/auth/web/command/ShopifyPermissionRequest.java +++ b/src/main/java/au/com/royalpay/payment/manage/shopify/auth/web/command/ShopifyPermissionRequest.java @@ -1,5 +1,6 @@ package au.com.royalpay.payment.manage.shopify.auth.web.command; +import au.com.royalpay.payment.manage.shopify.auth.domain.entity.ShopifyCommonParameter; import au.com.royalpay.payment.manage.shopify.store.web.command.CreateShopifyMerchantCommand; import lombok.AllArgsConstructor; import lombok.Builder; @@ -7,6 +8,7 @@ import lombok.Data; import lombok.NoArgsConstructor; import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; @Data @Builder @@ -14,9 +16,6 @@ import javax.validation.constraints.NotBlank; @AllArgsConstructor public class ShopifyPermissionRequest { - @NotBlank(message = "Shop can not blank") - private String shop; - @NotBlank(message = "Login Id can not blank") private String loginId; @@ -26,12 +25,43 @@ public class ShopifyPermissionRequest { @NotBlank(message = "Code can not blank") private String code; + @NotBlank(message = "hmac can not blank") + private String hmac; + + @NotBlank(message = "host can not blank") + private String host; + + @NotBlank(message = "Shop can not blank") + @Pattern(regexp = "^[a-zA-Z0-9][a-zA-Z0-9\\-]*\\.myshopify\\.com",message = "Shop hostname is invalid") + private String shop; + + @NotBlank(message = "state can not blank") + private String state; + + @NotBlank(message = "timestamp can not blank") + private String timestamp; + public static ShopifyPermissionRequest instanceOf(CreateShopifyMerchantCommand command) { return ShopifyPermissionRequest.builder() .loginId(command.getPaymentAccount().getLoginId()) .password(command.getPaymentAccount().getPassword()) - .shop(command.getShopifyShop()) .code(command.getCode()) + .hmac(command.getHmac()) + .host(command.getHost()) + .shop(command.getShopifyShop()) + .state(command.getState()) + .timestamp(command.getTimestamp()) + .build(); + } + + public ShopifyCommonParameter build() { + return ShopifyCommonParameter.builder() + .code(code) + .hmac(hmac) + .host(host) + .shop(shop) + .state(state) + .timestamp(timestamp) .build(); } } diff --git a/src/main/java/au/com/royalpay/payment/manage/shopify/auth/web/command/ShopifyVerifyRequest.java b/src/main/java/au/com/royalpay/payment/manage/shopify/auth/web/command/ShopifyVerifyRequest.java new file mode 100644 index 000000000..76a89f34d --- /dev/null +++ b/src/main/java/au/com/royalpay/payment/manage/shopify/auth/web/command/ShopifyVerifyRequest.java @@ -0,0 +1,41 @@ +package au.com.royalpay.payment.manage.shopify.auth.web.command; + +import au.com.royalpay.payment.manage.shopify.auth.domain.entity.ShopifyCommonParameter; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; + +@Data +public class ShopifyVerifyRequest { + + @NotBlank(message = "Code can not blank") + private String code; + + @NotBlank(message = "hmac can not blank") + private String hmac; + + @NotBlank(message = "host can not blank") + private String host; + + @NotBlank(message = "Shop can not blank") + @Pattern(regexp = "^[a-zA-Z0-9][a-zA-Z0-9\\-]*\\.myshopify\\.com", message = "Shop hostname is invalid") + private String shop; + + @NotBlank(message = "state can not blank") + private String state; + + @NotBlank(message = "timestamp can not blank") + private String timestamp; + + public ShopifyCommonParameter build() { + return ShopifyCommonParameter.builder() + .code(code) + .hmac(hmac) + .host(host) + .shop(shop) + .state(state) + .timestamp(timestamp) + .build(); + } +} diff --git a/src/main/java/au/com/royalpay/payment/manage/shopify/store/web/command/CreateShopifyMerchantCommand.java b/src/main/java/au/com/royalpay/payment/manage/shopify/store/web/command/CreateShopifyMerchantCommand.java index 35337757c..eb29b9431 100644 --- a/src/main/java/au/com/royalpay/payment/manage/shopify/store/web/command/CreateShopifyMerchantCommand.java +++ b/src/main/java/au/com/royalpay/payment/manage/shopify/store/web/command/CreateShopifyMerchantCommand.java @@ -4,6 +4,7 @@ import lombok.Data; import lombok.experimental.Accessors; import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; @Data @Accessors(chain = true) @@ -13,9 +14,22 @@ public class CreateShopifyMerchantCommand { private PaymentAccountCommand paymentAccount; + @NotBlank(message = "Auth code can not blank") + private String code; + + @NotBlank(message = "hmac can not blank") + private String hmac; + + @NotBlank(message = "host can not blank") + private String host; + @NotBlank(message = "Shop can not blank") + @Pattern(regexp = "^[a-zA-Z0-9][a-zA-Z0-9\\-]*\\.myshopify\\.com",message = "Shop hostname is invalid") private String shopifyShop; - @NotBlank(message = "Auth code can not blank") - private String code; + @NotBlank(message = "state can not blank") + private String state; + + @NotBlank(message = "timestamp can not blank") + private String timestamp; } diff --git a/src/main/java/au/com/royalpay/payment/manage/shopify/support/HmacVerificationUtil.java b/src/main/java/au/com/royalpay/payment/manage/shopify/support/HmacVerificationUtil.java new file mode 100644 index 000000000..5ba85aac0 --- /dev/null +++ b/src/main/java/au/com/royalpay/payment/manage/shopify/support/HmacVerificationUtil.java @@ -0,0 +1,58 @@ +package au.com.royalpay.payment.manage.shopify.support; + +import org.apache.commons.codec.digest.HmacAlgorithms; +import org.apache.commons.codec.digest.HmacUtils; +import org.apache.commons.lang3.StringUtils; +import org.bouncycastle.crypto.RuntimeCryptoException; +import org.bouncycastle.jce.provider.BouncyCastleProvider; + +import javax.crypto.Mac; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import javax.xml.bind.annotation.adapters.HexBinaryAdapter; +import java.nio.charset.StandardCharsets; +import java.security.Security; +import java.util.Locale; + +public class HmacVerificationUtil { + + public static boolean checkParameters(String message, String secret, String hmac) { + try { + Security.addProvider(new BouncyCastleProvider()); + SecretKey secretKey = new SecretKeySpec(secret.getBytes("UTF8"), "HmacSHA256"); + Mac mac = Mac.getInstance(secretKey.getAlgorithm()); + mac.init(secretKey); + byte[] digest = mac.doFinal(message.getBytes("UTF-8")); + String marshal = new HexBinaryAdapter().marshal(digest).toLowerCase(Locale.ROOT); + return StringUtils.equals(marshal, hmac); + } catch (Exception e) { + throw new RuntimeCryptoException("加密异常"); + } + } + + public static boolean hmacSHA256(String input, String key, String hmac) { + String encode = encode(input, key, HmacAlgorithms.HMAC_SHA_256); + return StringUtils.equals(encode, hmac); + } + + private static String encode(String input, String key, HmacAlgorithms algorithm) { + Mac mac = HmacUtils.getInitializedMac(algorithm, key.getBytes(StandardCharsets.UTF_8)); + byte[] content = input.getBytes(StandardCharsets.UTF_8); + byte[] signResult = mac.doFinal(content); + return bytesToHex(signResult); + } + + private static String bytesToHex(byte[] hash) { + StringBuilder hexString = new StringBuilder(); + for (byte b : hash) { + String hex = Integer.toHexString(0xff & b); + if (hex.length() == 1) { + hexString.append('0'); + } + hexString.append(hex); + } + return hexString.toString(); + } + + +} diff --git a/src/main/ui/static/shopify/auth/shopify.auth.js b/src/main/ui/static/shopify/auth/shopify.auth.js index d344747c1..a2e317838 100644 --- a/src/main/ui/static/shopify/auth/shopify.auth.js +++ b/src/main/ui/static/shopify/auth/shopify.auth.js @@ -35,7 +35,7 @@ define(['angular', 'uiRouter', 'uiBootstrap'], function (angular) { }).state('shopify.register', { url: '/register', templateUrl: '/static/shopify/auth/templates/shopify_register.html', - params: {'shop': null, 'code': null}, + params: {'code': null, 'hmac':null, 'host': null,'shop': null, 'state': null,'timestamp':null}, controller: 'ShopifyRegisterController' }); }]); @@ -69,14 +69,35 @@ define(['angular', 'uiRouter', 'uiBootstrap'], function (angular) { module.controller('ShopifyLoginController', ['$scope', '$http', '$state', '$stateParams', '$location', function ($scope, $http, $state, $stateParams, $location) { var that = $scope; + + var code = getQueryVariable("code") + var hmac = getQueryVariable("hmac") + var host = getQueryVariable("host") + var shop = getQueryVariable("shop") + var state = getQueryVariable("state") + var timestamp = getQueryVariable("timestamp") + that.model = { - shop: getQueryVariable("shop"), loginId: '', password: '', - code: getQueryVariable("code") + code: code, + hmac: hmac, + host:host, + shop: shop, + state: state, + timestamp: timestamp } - that.loginDisable = false + + that.verifyRequest = function () { + $http.post("/shopify/auth/verify", that.model).then(function (res) { + }, function (error) { + that.resError = error.data.message; + that.loginDisable = false + }) + } + that.verifyRequest() + that.activeShopifyMerchant = function () { that.loginDisable = true $http.post("/shopify/auth/install", that.model).then(function (res) { @@ -89,7 +110,14 @@ define(['angular', 'uiRouter', 'uiBootstrap'], function (angular) { } that.registerMerchant = function () { - $state.go('shopify.register', {shop: getQueryVariable("shop"), code: getQueryVariable("code")}); + $state.go('shopify.register', { + code: code, + hmac: hmac, + host: host, + shop: shop, + state: state, + timestamp: timestamp + }); } }]); @@ -241,8 +269,12 @@ define(['angular', 'uiRouter', 'uiBootstrap'], function (angular) { const param = { paymentMerchant, paymentAccount, + code: $stateParams.code, + hmac: $stateParams.hmac, + host: $stateParams.host, shopifyShop: $stateParams.shop, - code: $stateParams.code + state: $stateParams.state, + timestamp: $stateParams.timestamp } $http.post('shopify/store/register', param).then(function (resp) { location.href = resp.data.redirectUrl diff --git a/src/test/java/au/com/royalpay/payment/manage/shopify/auth/domain/service/ShopifyRequestValidatorTest.java b/src/test/java/au/com/royalpay/payment/manage/shopify/auth/domain/service/ShopifyRequestValidatorTest.java new file mode 100644 index 000000000..ceb8c17f0 --- /dev/null +++ b/src/test/java/au/com/royalpay/payment/manage/shopify/auth/domain/service/ShopifyRequestValidatorTest.java @@ -0,0 +1,46 @@ +package au.com.royalpay.payment.manage.shopify.auth.domain.service; + +import au.com.royalpay.payment.manage.shopify.auth.domain.entity.ShopifyCommonParameter; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; + +import java.util.regex.Pattern; + +@Slf4j +@RunWith(SpringRunner.class) +@SpringBootTest +@ActiveProfiles({"dev", "alipay", "bestpay", "jd", "wechat", "rpay", "yeepay", "rppaysvc", "common", "alipayplusaps"}) +class ShopifyRequestValidatorTest { + + @Autowired + private ShopifyRequestValidator shopifyRequestValidator; + + @Test + public void shopifyRequestValidatorTest() { + ShopifyCommonParameter parameter = ShopifyCommonParameter.builder() + .code("4618ddc9da54cee7be06b35f49c72349") + .host("Z2Vlay10ZXN0LXNob3AubXlzaG9waWZ5LmNvbS9hZG1pbg") + .shop("geek-test-shop.myshopify.com") + .timestamp("1643097047") + .state("1643097021") + .hmac("e7884f623057afd700b27ba8a5b7529a3f2a2943d2931d73fb82c57f2cf0baaa") + .build(); + Boolean valid = shopifyRequestValidator.valid(parameter); + log.warn(String.format("---------------------result: [%s]-------------",valid)); + } + + @Test + public void testShopifyDomain() { + String shop = "exampleshop.myshopify.com"; + + boolean matches = Pattern.matches("^[a-zA-Z0-9][a-zA-Z0-9\\-]*\\.myshopify\\.com", shop); + + log.warn(String.format("---------------------matches: [%s]-------------",matches)); + } +} \ No newline at end of file