commit
95ef1d2ff2
@ -1,17 +0,0 @@
|
||||
package au.com.royalpay.payment.manage.shopify.auth.domain.entity;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@NoArgsConstructor
|
||||
public class ShopifyAccessToken {
|
||||
|
||||
private String access_token;
|
||||
|
||||
private String scope;
|
||||
|
||||
private String redirectUrl;
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
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;
|
||||
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
package au.com.royalpay.payment.manage.shopify.auth.domain.entity;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
|
||||
@Data
|
||||
public class ShopifyPermissionBackParam {
|
||||
|
||||
@NotBlank(message = "code not blank")
|
||||
private String code;
|
||||
private String hmac;
|
||||
private String host;
|
||||
@NotBlank(message = "shop not blank")
|
||||
private String shop;
|
||||
private String state;
|
||||
private String timestamp;
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
package au.com.royalpay.payment.manage.shopify.auth.domain.entity;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class ShopifyPermissionURL {
|
||||
|
||||
private String url;
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
package au.com.royalpay.payment.manage.shopify.auth.domain.manage;
|
||||
|
||||
import au.com.royalpay.payment.manage.mappers.shopify.ShopifyStoreMapper;
|
||||
import au.com.royalpay.payment.manage.shopify.store.domain.entity.ShopifyStore;
|
||||
import au.com.royalpay.shopify.config.ShopifyAuthProvider;
|
||||
import au.com.royalpay.shopify.entity.ShopifyAccessToken;
|
||||
import au.com.royalpay.shopify.service.ShopifyAuthService;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class ShopifyManageService {
|
||||
|
||||
private final ShopifyAuthProvider shopifyAuthProvider;
|
||||
private final ShopifyAuthService shopifyAuthService;
|
||||
private final ShopifyStoreMapper shopifyStoreMapper;
|
||||
|
||||
public ShopifyManageService(ShopifyAuthProvider shopifyAuthProvider, ShopifyAuthService shopifyAuthService, ShopifyStoreMapper shopifyStoreMapper) {
|
||||
this.shopifyAuthProvider = shopifyAuthProvider;
|
||||
this.shopifyAuthService = shopifyAuthService;
|
||||
this.shopifyStoreMapper = shopifyStoreMapper;
|
||||
}
|
||||
|
||||
public List<JSONObject> rotateAccessToken(String newSecret, String refreshToken) {
|
||||
shopifyAuthProvider.addSecret(newSecret);
|
||||
List<JSONObject> failedStore = shopifyStoreMapper.listAvailableStores().parallelStream().filter(store -> !store.getTokenSecret().equalsIgnoreCase(newSecret))
|
||||
.map(store -> rotateStoreAccessToken(store, refreshToken))
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
if (failedStore.isEmpty()) {
|
||||
shopifyAuthProvider.activateSecret(newSecret);
|
||||
}
|
||||
return failedStore;
|
||||
}
|
||||
|
||||
private JSONObject rotateStoreAccessToken(ShopifyStore store, String refreshToken) {
|
||||
try {
|
||||
ShopifyAccessToken newToken = shopifyAuthService.rotateAccessToken(store.storeName(), refreshToken, store.getAccessToken());
|
||||
shopifyStoreMapper.update(ShopifyStore.builder().id(store.getId()).accessToken(newToken.getAccess_token()).tokenSecret(newToken.getSecret()).build());
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
return new JSONObject(Map.of("shop", store.getShopifyShop(), "client_moniker", store.getClientMoniker(), "message", e.getMessage()));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -1,70 +0,0 @@
|
||||
package au.com.royalpay.payment.manage.shopify.auth.domain.service;
|
||||
|
||||
import au.com.royalpay.payment.manage.shopify.auth.domain.entity.ShopifyAccessToken;
|
||||
import au.com.royalpay.payment.manage.shopify.auth.web.command.ShopifyAccessTokenRequest;
|
||||
import au.com.royalpay.payment.manage.shopify.auth.domain.entity.ShopifyPermissionURL;
|
||||
import au.com.royalpay.payment.manage.shopify.auth.web.command.ShopifyPermissionRequest;
|
||||
import au.com.royalpay.payment.tools.env.PlatformEnvironment;
|
||||
import au.com.royalpay.payment.tools.exceptions.ServerErrorException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestClientException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class ShopifyAuthService {
|
||||
|
||||
@Value("${shopify.auth.apiKey}")
|
||||
private String clientId;
|
||||
|
||||
@Value("${shopify.auth.apiSecretKey}")
|
||||
private String clientSecret;
|
||||
|
||||
@Value("${shopify.auth.scope}")
|
||||
private String scope;
|
||||
|
||||
private static final String PERMISSION_URL = "https://%s/admin/oauth/authorize?client_id=%s&scope=%s&redirect_uri=%s&state=%s&grant_options[]=per-user";
|
||||
|
||||
private static final String ACCESS_TOKEN_URL = "https://%s/admin/oauth/access_token";
|
||||
|
||||
@Autowired
|
||||
@Qualifier("shopifyRestTemplate")
|
||||
private RestTemplate restTemplate;
|
||||
|
||||
@Autowired
|
||||
private StringRedisTemplate stringRedisTemplate;
|
||||
|
||||
public ShopifyPermissionURL shopifyPermission(String shopifyStoreHost) {
|
||||
String redirectUri = PlatformEnvironment.getEnv().concatUrl("/shopify/auth/back");
|
||||
|
||||
String state = String.valueOf(new Date().getTime()).substring(0,10);
|
||||
|
||||
stringRedisTemplate.boundValueOps("shopifyAuthState:"+shopifyStoreHost).set(state);
|
||||
|
||||
String permissionUrl = String.format(PERMISSION_URL, shopifyStoreHost, clientId, scope, redirectUri, state);
|
||||
return ShopifyPermissionURL.builder().url(permissionUrl).build();
|
||||
}
|
||||
|
||||
public ShopifyAccessToken getAccessToken(String shop, String code) {
|
||||
ShopifyAccessTokenRequest request = new ShopifyAccessTokenRequest(clientId, clientSecret, code);
|
||||
ResponseEntity<ShopifyAccessToken> responseEntity;
|
||||
try {
|
||||
responseEntity = restTemplate.postForEntity(String.format(ACCESS_TOKEN_URL, shop), request, ShopifyAccessToken.class);
|
||||
} catch (RestClientException e) {
|
||||
log.error(String.format("Shopify merchant [%s] get access token error: %s", shop, e.getMessage()));
|
||||
throw new ServerErrorException(e.getMessage());
|
||||
}
|
||||
ShopifyAccessToken shopifyAccessToken = responseEntity.getBody();
|
||||
log.info(String.format("Shopify merchant [%s] access token: %s", shop, shopifyAccessToken));
|
||||
return shopifyAccessToken;
|
||||
}
|
||||
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
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 queryStrWithoutHmac, String hmac) {
|
||||
return HmacVerificationUtil.hmacSHA256(queryStrWithoutHmac, clientSecret, hmac);
|
||||
}
|
||||
|
||||
public boolean verify(String message, String hmac) {
|
||||
return HmacVerificationUtil.hmacSHA256(message, clientSecret, hmac);
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
package au.com.royalpay.payment.manage.shopify.auth.web.command;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ShopifyAccessTokenRequest {
|
||||
|
||||
private String client_id;
|
||||
private String client_secret;
|
||||
private String code;
|
||||
|
||||
public ShopifyAccessTokenRequest(String clientId, String clientSecret, String code) {
|
||||
this.client_id = clientId;
|
||||
this.client_secret = clientSecret;
|
||||
this.code = code;
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
package au.com.royalpay.payment.manage.shopify.config;
|
||||
|
||||
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.boot.web.client.RestTemplateBuilder;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
|
||||
import org.springframework.http.converter.StringHttpMessageConverter;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
@Configuration
|
||||
public class ShopifyApplicationConfig {
|
||||
|
||||
@Bean
|
||||
@Qualifier("shopifyRestTemplate")
|
||||
public RestTemplate restTemplate() {
|
||||
FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter();
|
||||
fastJsonHttpMessageConverter.setSupportedMediaTypes(ImmutableList.of(MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN));
|
||||
return new RestTemplateBuilder()
|
||||
.messageConverters(new ByteArrayHttpMessageConverter(),
|
||||
new StringHttpMessageConverter(StandardCharsets.UTF_8),
|
||||
fastJsonHttpMessageConverter)
|
||||
.build();
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
package au.com.royalpay.payment.manage.shopify.support;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class GraphqlSchemaReaderUtil {
|
||||
|
||||
public static String getSchemaFromFileName(final String filename) throws IOException {
|
||||
return new String(
|
||||
GraphqlSchemaReaderUtil.class.getClassLoader().getResourceAsStream("graphql/" + filename + ".graphql").readAllBytes());
|
||||
}
|
||||
}
|
@ -1,75 +0,0 @@
|
||||
package au.com.royalpay.payment.manage.shopify.support;
|
||||
|
||||
import org.apache.commons.codec.DecoderException;
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
import org.apache.commons.codec.binary.Hex;
|
||||
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 org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
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.Arrays;
|
||||
import java.util.Locale;
|
||||
|
||||
public class HmacVerificationUtil {
|
||||
private static final Logger logger = LoggerFactory.getLogger(HmacVerificationUtil.class);
|
||||
|
||||
private HmacVerificationUtil() {
|
||||
}
|
||||
|
||||
public static boolean checkParameters(String message, String secret, String hmac) {
|
||||
try {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
SecretKey secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
|
||||
Mac mac = Mac.getInstance(secretKey.getAlgorithm());
|
||||
mac.init(secretKey);
|
||||
byte[] digest = mac.doFinal(message.getBytes(StandardCharsets.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) {
|
||||
if (isHex(hmac)) {
|
||||
try {
|
||||
byte[] requestHmac = Hex.decodeHex(hmac);
|
||||
byte[] hmacRes = hmac(input, key, HmacAlgorithms.HMAC_SHA_256);
|
||||
String hmacHex = Hex.encodeHexString(hmacRes);
|
||||
logger.debug("hex-mode: input={}; key={}; encoded={}; request-hmac: {}", input, key, hmacHex, hmac);
|
||||
return Arrays.equals(requestHmac, hmacRes);
|
||||
} catch (DecoderException ignore) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
//base64
|
||||
byte[] hmacRes = hmac(input, key, HmacAlgorithms.HMAC_SHA_256);
|
||||
String hmacB64 = Base64.encodeBase64String(hmacRes);
|
||||
logger.debug("b64-mode: input={}; key={}; encoded={}; request-hmac: {}", input, key, hmacB64, hmac);
|
||||
byte[] requestHmac = Base64.decodeBase64(hmac);
|
||||
return Arrays.equals(requestHmac, hmacRes);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isHex(String str) {
|
||||
return str != null && str.toUpperCase(Locale.ROOT).matches("^[0-9A-F]+$");
|
||||
}
|
||||
|
||||
private static byte[] hmac(String input, String key, HmacAlgorithms algorithm) {
|
||||
Mac mac = HmacUtils.getInitializedMac(algorithm, key.getBytes(StandardCharsets.UTF_8));
|
||||
byte[] content = input.getBytes(StandardCharsets.UTF_8);
|
||||
return mac.doFinal(content);
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
package au.com.royalpay.payment.manage.shopify.support;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ElementType.METHOD, ElementType.TYPE})
|
||||
public @interface ShopifyEndpoint {
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
package au.com.royalpay.payment.manage.shopify.support;
|
||||
|
||||
import au.com.royalpay.payment.tools.exceptions.ServerErrorException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.util.StreamUtils;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class ShopifyHttpUtils {
|
||||
private ShopifyHttpUtils() {
|
||||
}
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ShopifyHttpUtils.class);
|
||||
|
||||
public static String getRequestBody(HttpServletRequest request) {
|
||||
try (InputStream in = request.getInputStream()) {
|
||||
String body = StreamUtils.copyToString(in, StandardCharsets.UTF_8);
|
||||
logger.debug("Shopify read body |-{}", body);
|
||||
return body;
|
||||
} catch (IOException e) {
|
||||
throw new ServerErrorException("Failed to read request content");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
package au.com.royalpay.payment.manage.shopify.support;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Optional;
|
||||
|
||||
public class ShopifyRequestInfoInterceptor extends HandlerInterceptorAdapter {
|
||||
private Logger logger = LoggerFactory.getLogger(getClass());
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
||||
if (!(handler instanceof HandlerMethod)){
|
||||
return super.preHandle(request, response, handler);
|
||||
}
|
||||
Method method = ((HandlerMethod) handler).getMethod();
|
||||
|
||||
if(HttpMethod.GET.matches(request.getMethod())) {
|
||||
if (AnnotatedElementUtils.isAnnotated(method, ShopifyEndpoint.class)) {
|
||||
String shop = request.getParameter("shop");
|
||||
if (StringUtils.isNotBlank(shop)) {
|
||||
response.addHeader("Content-Security-Policy", "frame-ancestors 'none'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (HttpMethod.POST.matches(request.getMethod())) {
|
||||
if (AnnotatedElementUtils.isAnnotated(method, ShopifyEndpoint.class)) {
|
||||
|
||||
String requestBody = ShopifyHttpUtils.getRequestBody(request);
|
||||
logger.debug("shopify request body:[POST]{} -->{}", request.getRequestURI(), requestBody);
|
||||
String shop = Optional.ofNullable(requestBody).map(JSON::parseObject).map(body->body.getString("shop_domain")).orElse(null);
|
||||
if (StringUtils.isNotBlank(shop)) {
|
||||
response.addHeader("Content-Security-Policy", "frame-ancestors 'none'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return super.preHandle(request, response, handler);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package au.com.royalpay.payment.manage.shopify.store.domain.entity;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class ShopifyStoreTest {
|
||||
|
||||
@Test
|
||||
void storeName() {
|
||||
ShopifyStore store = ShopifyStore.builder()
|
||||
.shopifyShop("test-store-nicks-4.myshopify.com")
|
||||
.build();
|
||||
Assertions.assertEquals(store.storeName(), "test-store-nicks-4", "storename match");
|
||||
}
|
||||
}
|
Loading…
Reference in new issue