Shopify App 公共应用审核要求

1,587 阅读3分钟

Shopify App应用的审核流程非常严格,本人在提交审核的过程中遇到了一些问题,在经过反复阅读开发文档,反复提交审核后终于把审核遭拒绝的开发层面的问题清零了。

本文主要针对如何添加防止网络攻击,和在应用程序中添加webhooks,分享解决办法,下图是本人在提交审核后被拒绝反馈的为满足要求的几个问题。 image.png

关于更多的Shopify 公共应用审核要求请移步阅读官方文档 ---> Requirements for public apps on Shopify

一、web内容安全策略CSP设置

1、什么是CSP?

CSP即Content-Security-Policy。 引用MDN Web Docs原文科普就: 内容安全策略   (CSP) 是一个额外的安全层,用于检测并削弱某些特定类型的攻击,包括跨站脚本 (XSS (en-US)) 和数据注入攻击等。无论是数据盗取、网站内容污染还是散发恶意软件,这些攻击都是主要的手段。

2、 CSP用什么作用?

web应用中,CSP启用后,不符合CSP的外部资源就会被阻止加载,可以有效的防止数据被盗取、网站内容被污染等网络攻击。

3、CSP可以设置哪些类型的外部资源?

这里简单罗列一下:

  • script-src
  • style-scr
  • media-src
  • font-src
  • object-src
  • child-src
  • frame-ancestors 此类型就是本文的重点之一,指的是设置嵌入web网页的外部资源(比如:frame、iframe、embed、applet等)
  • connect-scr
  • worker-scr
  • manifest-src 注:想了解更多详情,请移步至 ---> MDN Web Docs

3、Shopify公共应用如何设置Content-Security-Policy?

由于Shopify App应用需要嵌入至Shopify商家管理后台,为了防止网络攻击,于是Shopify要求所有公共APP应用都必须设置fram-ancestors白名单。那应该再哪里添加白名单设置呢?

在请求头部添加设置
 ctx.set(
  "Content-Security-Policy",
  `frame-ancestors https://${shop} https://admin.shopify.com;`
);

以下是直接在Shopify-cli脚手架serve.js添加设置

import "@babel/polyfill";
import dotenv from "dotenv";
import "isomorphic-fetch";
import createShopifyAuth, { verifyRequest } from "@shopify/koa-shopify-auth";
import Shopify, { ApiVersion } from "@shopify/shopify-api";
import Koa from "koa";
import next from "next";
import Router from "koa-router";
const crypto = require("crypto");
dotenv.config();

const port = parseInt(process.env.PORT, 10) || 8081;
const dev = process.env.NODE_ENV !== "production";
const app = next({
  dev,
});
const handle = app.getRequestHandler();
Shopify.Context.initialize({
  API_KEY: process.env.SHOPIFY_API_KEY,
  API_SECRET_KEY: process.env.SHOPIFY_API_SECRET,
  SCOPES: process.env.SCOPES.split(","),
  HOST_NAME: process.env.HOST.replace(/https:\/\/|\/$/g, ""),
  API_VERSION: ApiVersion.October20,
  IS_EMBEDDED_APP: true,
  SESSION_STORAGE: new Shopify.Session.MemorySessionStorage(),
});

const ACTIVE_SHOPIFY_SHOPS = {};

app.prepare().then(async () => {
  const server = new Koa();
  const router = new Router();
  
  server.keys = [Shopify.Context.API_SECRET_KEY];
  
  server.use(
    createShopifyAuth({
      async afterAuth(ctx) {
        const { shop, accessToken, scope } = ctx.state.shopify;

        ctx.set(
          "Content-Security-Policy",
          `frame-ancestors frame-ancestors https://${shop} https://admin.shopify.com;`
        );

        const host = ctx.query.host;
        ACTIVE_SHOPIFY_SHOPS[shop] = scope

        // Redirect to app with shop parameter upon auth
        ctx.redirect(`/?shop=${shop}&host=${host}`);
      },
    })
  );

  const handleRequest = async (ctx) => {
    const refererUrl = ctx.request.header.referer;
    const params = getUrlParamList(refererUrl);
    const shop = params["shop"] || "*.myshopify.com";
    
    // 上面代码那么多,而重点其实就只有这里
    // 上面代码那么多,而重点其实就只有这里
    // 上面代码那么多,而重点其实就只有这里
    ctx.set(
      "Content-Security-Policy",
      `frame-ancestors https://${shop} https://admin.shopify.com;`
    );
    await handle(ctx.req, ctx.res);
    ctx.respond = false;
    ctx.res.statusCode = 200;
  };

  router.post(
    "/graphql",
    verifyRequest({ returnHeader: true }),
    async (ctx, next) => {
      await Shopify.Utils.graphqlProxy(ctx.req, ctx.res);
    }
  );

  router.get("(/_next/static/.*)", handleRequest); 
  router.get("/_next/webpack-hmr", handleRequest);
  router.get("(.*)", async (ctx) => {
    const shop = ctx.query.shop;
    if (ACTIVE_SHOPIFY_SHOPS[shop] === undefined) {
      ctx.redirect(`/auth?shop=${shop}`);
    } else {
      await handleRequest(ctx);
    }
  });
  
  server.use(router.allowedMethods());
  server.use(router.routes());
  server.listen(port, () => {
    console.log(`> Ready on http://localhost:${port}`);
  });
});

  /**
 * 获取 url 参数列表
 * @param {*} url 选填, 不填则默认为当前页地址
 * @returns {Array}  返回的参数列表
 */
function getUrlParamList(url = "") {
  url = url.split(/\?|\&/);
  var arr = [];
  // 没有参数返回空数组
  if (url.length < 2) {
    return arr;
  }
  // url带参数,循环遍历,构建参数列表
  for (var i = 1; i < url.length; i++) {
    try {
      var kv = url[i];
      if (kv.indexOf("#")) {
        kv = kv.split("#")[0];
      }
      var tmp = kv.split("=");
      if (tmp) {
        arr[decodeURIComponent(tmp[0])] = decodeURIComponent(tmp[1]);
      }
    } catch (err) {
      continue;
    }
  }
  return arr;
}

二、webhooks请求验签

本文使用的是nodeJs开发,引入了以及几个库

import Koa from "koa";
const bodyParser = require("koa-bodyparser");

koa-bodyparser引入之后使用中间件注入

server.use(bodyParser());

1、注册webhooks

在shopify授权验证(createShopifyAuth)之后进行webhooks注册

const shopUpdate = await Shopify.Webhooks.Registry.register({
  shop,
  accessToken,
  path: "/webhooks/shop_update",
  topic: "SHOP_UPDATE",
  scope: "shop_update",
  webhookHandler: async (topic, shop, body) => {
    console.log("> SHOP_UPDATE body");
    console.log(body);
  },
});
if (shopUpdate.success) {
  console.log("Successfully registered shopUpdate webhook!");
} else {
  console.log(
    "Failed to register shopUpdate webhook",
    shopUpdate.result
  );
}

2、shopify审核要求应用订阅webhooks时需要对请求数据进行hmac签名验证

封装验签方法

function verifyWebhook(payload, hmac) {
    const genHash = crypto
      .createHmac("sha256", process.env.SHOPIFY_API_SECRET)
      .update(payload, "utf8", "hex")
      .digest("base64");
    return genHash === hmac;
}

shopify要求验证请求数据与请求头"x-shopify-hmac-sha256"不一致,则需返回401,表示验签失败

router.post("/webhooks/shop_update", async (ctx) => {
    ctx.set(
      "Content-Security-Policy",
      `frame-ancestors https://*.myshopify.com https://admin.shopify.com;`
    );

    const verified = verifyWebhook(
      ctx.request.rawBody,
      ctx.request.header["x-shopify-hmac-sha256"]
    );

    if (!verified) {
      ctx.status = 401;
      ctx.body = {
        status: 401,
        message: "forbidden",
      };
      return;
    } else {
      ctx.status = 200;
      ctx.body = {
        status: 200,
        message: "Successfully",
      };
    }
});

三、Shopify App部署

Shopify App部署的web服务器要求是带有SSL证书的https的域名服务器,而Shopify目前唯一支持部署的web服务器是Heroku平台提供的云服务器,关于如何部署至Heroku云服务器,可参考这篇文章-->Shopify应用上线部署 - 掘金 (juejin.cn)