前言
shopify 平台感兴趣的自己搜一下了
我这儿的业务场景是:
我方作为应用提供方,让商户接入、安装我方应用,我方通过商户返回的code拿到商户授权的 accessToken 进行接口访问
这里对应用方和商户的首次交互做个案例
shopify OAuth accessToken 生成介绍
OAuth首次交互流程
下图在官方的API文档中可以找到,这里贴了出来
scope列表
权限名称 | 描述 |
---|---|
read_analytics | View store metrics |
read_assigned_fulfillment_orders, write_assigned_fulfillment_orders | View or manage fulfillment orders |
read_customers, write_customers | View or manage customers, customer addresses, order history, and customer groups |
read_discounts, write_discounts | View or manage automatic discounts and discount codes |
read_draft_orders, write_draft_orders | View or manage orders created by merchants on behalf of customers |
read_files, write_files | View or manage files |
read_fulfillments, write_fulfillments | View or manage fulfillment services |
read_gdpr_data_request | View GDPR data requests |
read_gift_cards, write_gift_cards | View or manage gift cards (Available to Plus merchants only) |
read_inventory, write_inventory | View or manage inventory across multiple locations |
read_legal_policies, write_legal_policies | View or manage a shop’s legal policies |
read_locations | View the geographic location of stores, headquarters, and warehouses |
read_marketing_events, write_marketing_events | View or manage marketing events and engagement data |
read_merchant_managed_fulfillment_orders, write_merchant_managed_fulfillment_orders | View or manage fulfilment orders assigned to merchant-managed locations |
read_online_store_navigation | View menus for display on the storefront |
read_online_store_pages, write_online_store_pages | View or manage Online Store pages |
read_order_edits, write_order_edits | View or manage edits to orders |
read_orders, write_orders, read_all_orders | View or manage orders, transactions, fulfillments, and abandoned checkouts from the last 60 days, or View all past and future orders |
read_price_rules, write_price_rules | View or manage conditional discounts |
read_products, write_products | View or manage products, variants, and collections |
read_product_listings, write_product_listings | View or manage product or collection listings |
read_reports, write_reports | View or manage reports on the Reports page in the Shopify admin |
read_resource_feedbacks, write_resource_feedbacks | View or manage the status of shops and resources |
read_script_tags, write_script_tags | View or manage the JavaScript code in storefront or orders status pages |
read_shipping, write_shipping | View or manage shipping carriers, countries, and provinces |
read_shopify_payments_accounts | View Shopify Payments accounts |
read_shopify_payments_bank_accounts | View bank accounts that can receive Shopify Payment payouts |
read_shopify_payments_disputes | View Shopify Payment disputes raised by buyers |
read_shopify_payments_payouts | View Shopify Payments payouts and the account’s current balance |
read_content, write_content | View or manage articles, blogs, comments, pages, and redirects |
read_themes, write_themes | View or manage theme templates and assets |
read_third_party_fulfillment_orders, write_third_party_fulfillment_orders | View or manage fulfillment orders assigned to a location managed by any fulfillment service |
read_translations, write_translations | View or manage content that can be translated |
java代码示例
接口
import cn.hutool.crypto.digest.HMac;
import cn.hutool.crypto.digest.HmacAlgorithm;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson2.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.servlet.ModelAndView;
import javax.annotation.Resource;
import java.nio.charset.StandardCharsets;
import java.util.Properties;
/**
* shopify 接口
* 参考文档:https://www.shopify.com/partners/blog/17056443-how-to-generate-a-shopify-api-token
* @author Heng.Wei
* @date 2022-10-11
**/
@Slf4j
@Controller
@RequestMapping("/shopify")
public class ShopifyController {
/** API key of your app. */
private final static String CLIENT_KEY = "[注册的应用的API KEY]";
/** 加密的密钥 */
private final static String CLIENT_SECRET = "[API SECRET]";
/** 摘要算法 */
private final static HMac HMAC = new HMac(HmacAlgorithm.HmacSHA256, CLIENT_SECRET.getBytes(StandardCharsets.UTF_8));
/** 向商户申请授权码 */
private final static String AUTHORIZE_URL = "https://%s/admin/oauth/authorize?client_id=%s&grant_options[]=%s&redirect_uri=%s&scope=%s";
/** 生成 accessToken 地址, 带三个参数 client_id、client_secret、code */
private final static String ACCESS_TOKEN_URL = "https://%s/admin/oauth/access_token";
/** 权限范围 */
private final static String SCOPE = "write_product_listings,read_orders,write_products";
/** 商户安装完成后的重定向地址 */
private final static String REDIRECT_URI = "http://localhost:20008/shopify/generate_token";
private final static String GRANT_OPTIONS = "per-user";
@Resource
private RestTemplate restTemplate;
/**
* <pre>
* 商户发起安装请求
* </pre>
*
* @param hmac GET参数生成的摘要
* @param host 这个参数官网没有描述
* @param shop 商店域名
* @param timestamp 时间戳
* @author weiheng
**/
@GetMapping("/install")
public ModelAndView index(@RequestParam("hmac") String hmac,
@RequestParam("host") String host,
@RequestParam("shop") String shop,
@RequestParam("timestamp") String timestamp) {
log.info("shop[{}], hmac[{}], host[{}], timestamp[{}]", shop, hmac, host, timestamp);
String params = "host=" + host + "&shop=" + shop + "×tamp=" + timestamp;
boolean verify = verify(params, hmac, shop);
if (verify) {
// 校验通过
String path = String.format(AUTHORIZE_URL, shop, CLIENT_KEY, GRANT_OPTIONS, REDIRECT_URI, SCOPE);
return new ModelAndView("redirect:" + path);
}
return new ModelAndView("redirect:404");
}
/**
* <pre>
* 通过商户给的 code,生成 token 凭据
* </pre>
*
* @param hmac HMAC SHA256 数字签名
* @param code 商户给的授权码
* @param host 官网未给出该字段定义
* @param shop 商店域名
* @param timestamp 时间戳
* @author weiheng
**/
@GetMapping("/generate_token")
public void generateToken(@RequestParam("hmac") String hmac,
@RequestParam("code") String code,
@RequestParam("host") String host,
@RequestParam("shop") String shop,
@RequestParam("timestamp") String timestamp) {
log.info("shop[{}], hmac[{}], code[{}], host[{}], timestamp[{}]", shop, hmac, code, host, timestamp);
String params = "code=" + code + "&host=" + host + "&shop=" + shop + "×tamp=" + timestamp;
boolean verify = verify(params, hmac, shop);
if (!verify) {
log.error("shop[{}], 校验未通过", shop);
return;
}
// 校验通过 - Exchange access code for the shop token
Properties request = new Properties();
request.put("client_id", CLIENT_KEY);
request.put("client_secret", CLIENT_SECRET);
request.put("code", code);
String path = String.format(ACCESS_TOKEN_URL, shop);
JSONObject obj = restTemplate.postForObject(path, request, JSONObject.class);
log.info("resultObj[{}]", obj);
if (obj == null) {
log.error("获取token异常, Properties[{}]", request);
return;
}
ShopifyTokenDTO shopifyTokenDto = JSON.parseObject(obj.toJSONString(), ShopifyTokenDTO.class);
log.info("shopifyTokenDto[{}]", shopifyTokenDto);
// TODO 缓存 商户 的 token 凭据
}
/**
* <pre>
* 请求有效性校验
* </pre>
*
* @param params ASCII排序后的参数 URL格式的参数,如 a=111&b=222&c=333
* @param hmac 调用方传过来的摘要信息
* @param shop 商户域名
* @return 校验是否通过
* @author weiheng
**/
private boolean verify(String params, String hmac, String shop) {
String digest = HMAC.digestHex(params.getBytes(StandardCharsets.UTF_8));
boolean verify = HMAC.verify(digest.getBytes(StandardCharsets.UTF_8), hmac.getBytes(StandardCharsets.UTF_8));
log.info("shop[{}], digest[{}], verify[{}]", shop, digest, verify);
return verify;
}
}
自定义 token 实体
import com.alibaba.fastjson.annotation.JSONField;
import lombok.Data;
import java.io.Serializable;
/**
* <pre>
* shopify 商户返回的token信息
* </pre>
* @author Heng.Wei
* @date 2022-10-11
**/
@Data
public class ShopifyTokenDTO implements Serializable {
@JSONField(name = "access_token")
private String accessToken;
private String scope;
@JSONField(name = "expires_in")
private Integer expiresIn;
@JSONField(name = "associated_user_scope")
private String associatedUserScope;
private String session;
@JSONField(name = "account_number")
private String accountNumber;
@JSONField(name = "associated_user")
private ShopifyUserDTO associatedUser;
}
自定义实体类 ShopifyUserDTO
import com.alibaba.fastjson.annotation.JSONField;
import lombok.Data;
import java.io.Serializable;
/**
* <pre>
* shopify associated user
* </pre>
* @author Heng.Wei
* @date 2022-10-11
**/
@Data
public class ShopifyUserDTO implements Serializable {
private Long id;
@JSONField(name = "first_name")
private String firstName;
@JSONField(name = "last_name")
private String lastName;
private String email;
@JSONField(name = "account_owner")
private Boolean accountOwner;
private String locale;
private Boolean collaborator;
@JSONField(name = "email_verified")
private Boolean emailVerified;
}
亲测OK,上图
这里拿到 token 就算完事儿了