在iOS开发中如果涉及到虚拟物品购买,就需要使用iap服务。iap支付确实很蛋疼,商品分类多:消耗品、非消耗品、自动订阅商品,非续订商品。像非消耗品需要支持访客模式购买,还需要支持恢复的功能,就是用户换了手机在没有登录(app自己的账号)的情况下也可以恢复之前购买的商品(相同icloud ID),确实给开发带来不少的额外工作,你还必须得支持,不然审核不给过。这还是正向流程,像逆向流程对接更是一言难尽。这里分享下在对接苹果通知(退款、订阅状态变更等消息)过程中一些经验,苹果后台可以配置接收通知的url 分沙盒和线上两个url。
苹果支付通知v2跟v1区别还是很大的。首先是流程上 v1在收到通知后需要再次调用苹果的http接口校验票据的合法性,而v2版本是通过jws(JSON Web Signature)验证其合法性,不需要再次调用苹果服务,直接公钥验签就行。再次是数据结构上的不同,v1通知 http body里直接给了明文,v2由于是jws ,所以内容本身是不可读的。
通知内容:
signedPayload 就是jws,通过 . 号拼接的,分为三部分,分别是 header,payload,signature。分别 通过new String(Base64.getUrlDecoder().decode("***"))就可以解码看到可读的文本。
header解码:
payload解码:
里面的signedTransactionInfo也是个jws,做同样的操作就可以了。
获取通知内容不难,关键是怎么验签。因为苹果后台提供下载私钥,开始以为就用这个私钥导出的公钥验签就可以了。为啥会有这个想法,主要还是思维定势。像支付宝、微信的通知都是这么弄的,会给我们提供公钥来验签。但隐约的又觉得不太靠谱,签名的私钥不应该给到外人。后来在 https://jwt.io/ 网站 测试验签的时候,发现把整个jws贴进来就能验签通过,但是我并没有告诉它公钥啊。经过仔细发现,原来它直接使用header里的证书验签的。至此问题得到解决,苹果后台提供下载的私钥其实是在我们请求苹果接口时签名用的。这也有点不合理的地方,私钥是除了自己随都不可以知道的,但是苹果霸道的给我们直接提供私钥下载,而不是给我们提供一个配置公钥的入口。
结论:苹果支付通知的验签 是通过jwt里面的证书验签的,x5c就是 header解码后的内容,这里使用的是第一个证书。
public Jws<Claims> verifyJWT(List<String> x5c, String jws){try {X509Certificate cert = getCert(x5c.get(0));if (!cert.getSubjectDN().getName().contains("Apple Inc")){logger.info("not apple cert . name = {}", cert.getIssuerX500Principal().getName());return null;}return Jwts.parserBuilder().setSigningKey(cert.getPublicKey()).build().parseClaimsJws(jws);}catch (JwtException exc){logger.info("jws verify failure.", exc);return null;} catch (Exception exc){logger.info("jws verify error.", exc);return null;}}public static X509Certificate getCert(String x5c) throws CertificateException {String stripped = x5c.replaceAll("-----BEGIN (.*)-----", "");stripped = stripped.replaceAll("-----END (.*)----", "");stripped = stripped.replaceAll("\r\n", "");stripped = stripped.replaceAll("\n", "");stripped.trim();byte[] keyBytes = Base64.getDecoder().decode(stripped);CertificateFactory fact = CertificateFactory.getInstance("X.509");return (X509Certificate) fact.generateCertificate(new ByteArrayInputStream(keyBytes));}
jw验签的工具类 maven依赖:
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>0.11.2</version></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><version>0.11.2</version><scope>runtime</scope></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId> <version>0.11.2</version><scope>runtime</scope></dependency>