一、写在前面

1.1 起源

最近有一点忙,更新博客出现了一些延迟。由于在工作中使用了Apple Wallet,所以在结束后准备以此记录一下。希望后面有要做此功能的同学,能够有所启发,觉得有帮助的,记得点赞收藏关注哦~

1.2 Apple Wallet 扫盲:

1.2.1 什么是Apple Wallet?

  • 总体来说:苹果钱包是Apple推出的软件,可以用于集中保管和使用的信用卡、借记卡、交通卡、登机牌、活动门票、学生证等等。全都收纳在您 iPhone 或 Apple Watch 的“钱包”中。
  • 简单来说:我们日常会办理非常多的卡片,比如银行卡、礼品卡、购物卡、会员卡等等,日常出行中这些卡片不易携带,但是又不可或缺。使用Apple Wallet能够将这些卡片装入Apple Wallet 这个软件中,更加方便快捷;

1.2.2 使用Apple Wallet 的优点

  • 从用户的角度:

    • 方便,使用apple wallet 卡片能够不用带实体卡
    • 使用简单,只需要扫描即可添加到设备中
    • 不易丢失,卡片能够通过icloud 同步,随时保存和分享
    • 新颖,与传统的实体卡更进一步
    • 管理方便,一个app就可以管理所有的卡片;
    • 能够查看更多的信息,卡片还能够随时更新和获取推送;
  • 从企业的角度:
    • 扩展业务,丰富使用场景,比竞品更多竞争力。
    • 增强用户体验,随时随地能够使用apple wallet 卡片来进行交易、认证、提醒等;
    • 独立使用,即便用户不下载企业应用、登录企业网站,依然可以在apple wallet中获取到企业生产的礼品卡,并随时获取最新内容;
    • 成本低,通过直接与Apple Wallet 集成,无缝衔接。Apple Wallet 的后期生成、更新不会收费。
    • 提高产品能力,比如机票卡片能够让用户及时收到飞机起飞前的通知提醒,会员卡可以让用户不需要带着实体卡就可以进店结账消费;

1.2.2 我们接入了Apple Wallet 能够做什么?

Apple Wallet 为我们提供了丰富的卡片场景,主要的卡片类型有以下几种:



简单来说,提供的这几种通行证(Apple Wallet 卡片)区分的类型只是为我们描述了具体的使用场景。我们可以根据实际需要来选择合适的卡片。

1.2.3 什么是通行证?

  • 上面提到了,通行证就是Apple Wallet 的卡片。同时也是我们发给用户、用户使用的卡片的统称;它的具体形式可以是礼品卡、活动门票等;

1.2.4 如何选择合适的通行证?

  • 对于通行证来说,不同的通行证类型卡片上展示的字段存在差异,同时排列的位置也会有一些区别。所以我们只需要根据实际要展示的元素,选择合适的通行证即可;
  • 如果是要做某一个通行证,选择对应类型的通行证无法满足要求的话,我们可以选择通用通行证,它往往卡片的字段更多,能容易满足要求;

1.2.5 如何生成和使用通行证?

  • 如何生成通行证:

    • 注册成为Apple Developer
    • 申请Apple Wallet 安全证书(一般有效期为一年)
    • 设计礼品卡样式,可点击查看官方参考文档
    • 将安全证书放入你的项目工程中,并引用jpasskit 工程创建礼品卡文件;
    • 将此文件传给调用方即可使用。这个接口我们可以使用 Add to Apple Wallet 图标来做触发,可点击查看官方参考文档
  • 如何传递通行证:
    • 在手机端上来说,用户点击了Add Apple Wallet图标后触发调用后端接口,拿到了 xxx.pkpass 的通行证,使用Safari 浏览器会自动识别,并在用户允许后添加至用户苹果设备的Apple Wallet中;
    • 在Web端上来说,我们可以使用二维码来做展示;当用户点击了Add Apple Wallet图标后会显示一个二维码,用户通过使用原生相机扫描后可以跳转至Safari 浏览器触发后端接口,然后拿到通行证后添加至Apple Wallet;
  • 如何添加通行证:
    • 对于用户来说,拿到通行证后会出现如下的预览界面,用户点击添加之后即可加入到自己的Apple Wallet 中:

    • 对于开发人员来说,需要使用jpasskit 来生成通行证,然后再发送给页面即可;后面会详细介绍这部分内容;
  • 如何使用通行证:
    • 主要看通行证对于企业来说,用于什么目的,做对应的查看、使用、提醒等等即可;
    • 比如登记牌卡片,对于航空公司来说可以设置地理位置,当客户到达机场能够自动识别并对应处理,也可以在飞机起飞前40分钟对用户提醒,提醒用户及时登机等;(礼品卡支持地理位置、通知、甚至NFC功能)
  • 如何更新通行证
    • 对于用户来说,只需要下拉刷新即可更新。或者用户在企业系统中对应礼品卡数据发生了变更会自动推送更新;对于后者,比如用户使用礼品卡进行了消费,那么礼品卡的余额会发生扣减,服务器会同步信息到Apple Wallet 中;
    • 对于开发人员来说,我们关注的也只有手动更新和自动更新两种形式;一种是用户手动下拉刷新,一种是服务器推送至手机;实现细节后面会进行详细介绍;
  • 如何删除通行证:
    • 对于用户来说,只需要简单的在Apple Wallet删除对应通行证即可;

1.3 前置准备

  • Apple Developer Account: 对于前置准备,我们最需要的就是Apple 开发者账号,这个可以请公司对应人员帮忙注册;
  • 安全证书:注册账号之后,需要在Apple Developer 中生成安全证书,它用于与Apple Wallet 推送通知、生成通行证能够被Apple 设备信任的要素;
  • 证书形式如下,有效期一般为一年,需要定时替换最新:

此步骤可以请公司内相关人员帮忙注册和生成,同时网上相关的资料应该很多,此文不加赘述;

二、创建通行证卡片

2.1 理论知识

  1. 通行证的创建从原理上来说很简单

    • 前端页面需要使用官方的Add to Apple Wallet图标来支持Apple Walllet功能
    • 后端提供一个接口,当用户点击Add to Apple Wallet图标后会调用这个接口来获取一个通行证文件;
    • 这个通行证文件是一个压缩文件,像这样:
      • 我们将其pkpass 后缀修改为zip,可以将其解压,能够看到里面的结构;
    • 如何创建这样的一个通行证呢?
      1. 首先设计好通行证的样式和字段,设计好之后就知道了通行证的预期样式是什么样的了;
      2. 然后引入jpasskit 依赖到项目中;
      3. 再然后将安全证书导入到resource 目录中;
      4. 最后就可以使用jpasskit 的工具类PKFileBasedSigningUtil调用createSignedAndZippedPkPassArchive方法创建通行证字节码数组;
      5. 将字节码数组写入流,并发送给前端;
  2. 接下来我将用实际代码演示这个过程;

2.2 代码演示

  1. 设计通行证,点击查看官方参考文档
  2. 引入jpasskit 依赖到项目中:
<dependency><groupId>de.brendamour</groupId><artifactId>jpasskit</artifactId><version>0.1.2</version>
</dependency>

如果出现找不到类等情况,就看一下是否缺失某些依赖,因为jpasskit里面还关联了其他的一些依赖,但是目前项目中没有的;

  1. 将证书导入项目中,如图所示:
  2. 加载安全证书
    1. 使用本地真实路径加载证书(可以本地测试使用, 其中PROJECT_PATH可以换成真实的全路径):

      String privateKeyPassword = "xxxx"; // the password you used to exportprivateKeyPath
      String appleWWDRCA = PROJECT_PATH + "AppleWWDRCA.cer"; // this is apple's developer relation cert
      String privateKeyPath = PROJECT_PATH + "passtype_key_dev.p12"; // the private key you exported from keychain
      try {PKSigningInformation pkSigningInformation = PKSigningUtil.loadSigningInformationFromPKCS12FileAndIntermediateCertificateFile(privateKeyPath, privateKeyPassword, appleWWDRCA);
      } catch (Exception e){}
      
    2. 使用流的形式加载证书(可以用于服务器环境)
         // 这里密码是你自己的证书密码public static final String PRIVATE_KEY_PASSWORD = "xxxx";public static final String APPLE_WWDRCA = "AppleWWDRCA.cer";public static final String PASSTYPE_KEY_DEV = "passtype_key_dev.p12";@PostConstructpublic void init() {try {appleWWDRCA = ClassUtils.getDefaultClassLoader().getResourceAsStream(APPLE_WWDRCA);privateKeyPath = ClassUtils.getDefaultClassLoader().getResourceAsStream(PASSTYPE_KEY_DEV);pkSigningInformation = new PKSigningInformationUtil().loadSigningInformationFromPKCS12AndIntermediateCertificate(privateKeyPath, PRIVATE_KEY_PASSWORD,appleWWDRCA);} catch (Exception e) {}}
      

如果是本地测试,直接用方法1,要发布到测试环境了可以用方法二来创建证书;

  1. 创建通行证卡片,以下为礼品卡的示例代码
 // 这里PKPass 是通行证对象,我们需要填充里面的内容;PKPass pass = new PKPass();// 固定设置1pass.setFormatVersion(1);// 这个字段用户apple wallet 更新,如果不需要Apple Wallet 更新可以去掉,目前只做创建也可以先删除调// pass.setWebServiceURL(new URL("https://xxxx:xxx/"));// 这个信息从Apple Developer中获取,对应替换成自己的pass.setPassTypeIdentifier("pass.com.xxx.xxx.xxx");// 这个信息从Apple Developer中获取, 对应替换成自己的// pass.setAuthenticationToken("xxxxxxxxxxxxxxxxx");String serialNumber = UUID.randomUUID().toString();// 这个是通行证唯一id,如果每次生成都是一个新的,那么可以每次重新New UUID。如果要去重,就需要保存这个ID,将它与业务id数据形成一对一关系;pass.setSerialNumber(serialNumber);// 来源于Apple Developerpass.setTeamIdentifier("xxxxxx"); // replace this with your team ID// 以下的信息都是通行证中的通用基本元素pass.setOrganizationName("Org name");pass.setBackgroundColor("#0F0242");pass.setForegroundColor("#FFFFFF");pass.setLabelColor("#FFFFFF");pass.setDescription("some description");pass.setLogoText("");pass.setExpirationDate(new Date());// 通行证中的二维码PKBarcode barcode = new PKBarcode();barcode.setFormat(PKBarcodeFormat.PKBarcodeFormatQR);barcode.setMessage("123456789");barcode.setMessageEncoding(Charset.forName("UTF-8"));pass.setBarcodes(Arrays.asList(barcode));// 这里的是通行证中的一些指定通行证的对应元素;如果不是使用PKStoreCard,替换成对应的就可以,然后根据自己预先设计的礼品卡样式来填充即可;PKStoreCard pkStoreCard = new PKStoreCard();        PKField pkField = new PKField();pkField.setKey("CardName");pkField.setValue("JD E-card");pkStoreCard.setPrimaryFields(ImmutableList.of(pkField));pass.setStoreCard(pkStoreCard);// 地理位置,可以不设置;PKLocation location = new PKLocation();location.setLatitude(xxxx); // replace with some latlocation.setLongitude(xxxx); // replace with some longList<PKLocation> locations = new ArrayList<PKLocation>();locations.add(location);pass.setLocations(locations);// 这里 isValid可以判断前面的元素是否都满足了,如果通过了就继续填充logo、图标等图形;if (pass.isValid()) {PKPassTemplateInMemory pkPassTemplateInMemory = new PKPassTemplateInMemory();// 这里PROJECT_PATH 替换为自己的项目文件路径pkPassTemplateInMemory.addFile(PKPassTemplateInMemory.PK_ICON, PROJECT_PATH + "images" + FILE_SEPARATOR + "icon.png");pkPassTemplateInMemory.addFile(PKPassTemplateInMemory.PK_ICON_RETINA, PROJECT_PATH + "images" + FILE_SEPARATOR + "icon@2x.png");pkPassTemplateInMemory.addFile(PKPassTemplateInMemory.PK_ICON_RETINAHD, PROJECT_PATH + "images" + FILE_SEPARATOR + "icon@3x.png");PKFileBasedSigningUtil pkSigningUtil = new PKFileBasedSigningUtil();// 传入通行证内容、通行证图片模板、和通行证安全证书,生成通行证文件byte[] 数据;byte[] signedAndZippedPkPassArchive = pkSigningUtil.createSignedAndZippedPkPassArchive(pass, pkPassTemplateInMemory, pkSigningInformation);
}
  1. 将文件发送给前端
private void sendHttpResponse(HttpServletResponse httpServletResponse, ServletOutputStream out, byte[] signedAndZippedPkPassArchive) throws IOException {httpServletResponse.setHeader("Cache-Control", "no-cache,no-store");httpServletResponse.setHeader("Pragma", "no-cache");httpServletResponse.setHeader("Expires", "0");httpServletResponse.setHeader("Content-Disposition", "attachment; filename=\"pass.pkpass\"");httpServletResponse.setContentType("application/vnd.apple.pkpass");httpServletResponse.setContentLength(signedAndZippedPkPassArchive.length);httpServletResponse.setHeader("last-modified", String.valueOf(System.currentTimeMillis()));out.write(signedAndZippedPkPassArchive);out.flush();
}

2.3 注意事项

  1. 卡片标题会自动变为大写;
  2. 设置了过期日期的卡片如果过期了则不支持更新
  3. 卡片本身可以设置多语言等丰富形式,建议可以找一些第三方设计平台设计好了,然后解压通行证文件解析有哪些字段;
  4. IPAD 设备不支持礼品卡

三、更新通行证

3.1 理论知识

  • 通行证的更新要比通行证的创建要复杂一点。

    • 更新逻辑是:我们在创建通行证时设置我们服务器的回调地址:pass.setWebServiceURL(new URL("https://xxxx:xxx/"));
    • 用户下拉刷新会直接调用我们获取最新通行证接口
    • 如果是服务器推送的话我们会先发送设备token给苹果APNS,然后APNS会通知对应的设备来查询我们是否存在需要更新的通行证接口,如果有的话,就会继续调用获取最新通行证接口达到更新目的;
    • 当用户删除通行证时,我们也需要将它的设备token和绑定信息删除掉,以便于下次重新添加时注册;
  • 如果增加用户在Apple Wallet 下拉刷新的功能,那么只需要提供一个下拉刷新的接口即可,它的全路径是: 回调服务器地址/v1/passes/{passTypeIdentifier}/{serialNumber},在这个接口中Apple 会传入要更新的通行证id,我们则根据通行证id查询到该通行证绑定的业务最新数据,并重新生成通行证(重复之前创建通行证的部分步骤)发送给Apple 设备
  • 如果增加服务器推送,让对应用户的对应设备自动调用获取最新通行证接口(也就是步骤2的接口),我们需要增加五个接口来完成这一个完整的功能;它们分别是:
    • 注册设备接口,当添加时只要设置了回调地址,客户手机会自动访问这个接口将他的设备token发送给我们进行保存;
    • 检查是否存在需要更新的通行证接口,服务器推送更新到APNS时,用户手机会访问这个接口来检查是否存在需要被更新的通行证,如果存在的话,就会调用获取最新通行证接口;
    • 获取最新通行证接口,这个接口会像创建通行证一样,获取最新的信息并返回;它跟手动下拉刷新访问的接口是同一个
    • 删除设备接口,在这个里面我们需要删除对应的设备信息,以便于下次的注册使用;
  • 这里描述可能有一些宽泛,同学们可以查看官方文档以获取详细内容:
    • 接口设计规则
    • 通行证设计、创建、更新指南

3.2 代码演示


import com.xxx.service.DemoService;
import com.xxx.service.RequestBodyUtils;
import com.alibaba.fastjson.JSON;
import com.google.common.collect.ImmutableList;
import com.turo.pushy.apns.PushNotificationResponse;
import com.turo.pushy.apns.util.SimpleApnsPushNotification;
import com.turo.pushy.apns.util.concurrent.PushNotificationFuture;
import de.brendamour.jpasskit.apns.PKSendPushNotificationUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.context.support.HttpRequestHandlerServlet;
import springfox.documentation.annotations.ApiIgnore;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Serializable;
import java.util.Date;
import java.util.List;@RestController
@RequestMapping
@Api(tags = {"ceshi"})
public class DemoController {@Autowiredprivate DemoService demoService;@GetMapping("addWallet")@ResponseBody@ApiOperation("添加到苹果钱包中")public void addWallet(@ApiIgnore HttpRequestHandlerServlet httpRequest, @ApiIgnore HttpServletResponse httpResponse) {// 根据之前的创建通行证生成完善这个接口就可以;}/*** 注册设备以接收通行证的推送通知** @param deviceLibraryIdentifier 用于在未来请求中识别和验证此设备的唯一标识符。* @param passTypeIdentifier      通行证的类型,在通行证中指定。* @param serialNumber            通行证的序列号,在通行证中指定。* @param version                 协议版本——当前为 v1。*                                说明:此处可能用于将设备编号等信息记录;*                                header : 提供了 Authorization 标头;它的值是单词ApplePass,后跟一个空格,然后是通行证中指定的通行证的授权令牌。*                                这里授权令牌看要不要进行保存;*                                如果此设备的序列号已注册,则返回 HTTP 状态 200。*                                如果注册成功,则返回 HTTP 状态 201。*                                如果请求未授权,则返回 HTTP 状态 401。*                                否则,返回适当的标准 HTTP 状态。*                                这里可能要创建一个设备表,用于记录用户是否已经注册,如果注册成功则返回success*///Registering a Device to Receive Push Notifications for a Pass@PostMapping("v1/devices/{deviceLibraryIdentifier}/registrations/{passTypeIdentifier}/{serialNumber}")@ApiOperation("注册设备")public ResponseEntity registrationWallet(@PathVariable("deviceLibraryIdentifier") String deviceLibraryIdentifier,@PathVariable("passTypeIdentifier") String passTypeIdentifier,@PathVariable("serialNumber") String serialNumber,@RequestBody PushToken pushToken) {// 这里的pushToken需要保存起来,然后推送更新时,将这个pushToken带上即可;System.err.println("time:"+ new Date());System.err.println("[暗余]. 注册设备");System.err.println("deviceLibraryIdentifier:"+deviceLibraryIdentifier);System.err.println("passTypeIdentifier:"+passTypeIdentifier);System.err.println("serialNumber:"+serialNumber);System.err.println("pushToken:"+ JSON.toJSONString(pushToken));return new ResponseEntity(HttpStatus.OK);}/*** 获取与设备关联的通行证的序列号** @param version                 协议版本——当前为 v1。* @param deviceLibraryIdentifier 用于识别和验证设备的唯一标识符。* @param passTypeIdentifier      通行证的类型,在通行证中指定。* @param tag                     来自先前请求的标签。(可选的)*                                如果passesUpdatedSince参数存在,则仅返回自 指示的时间以来已更新的通行证tag。否则,返回所有通行证。* @return 如果有匹配的通行证,则返回 HTTP 状态 200 以及带有以下键和值的 JSON 字典:* lastUpdated(细绳)* 当前的修改标签。* <p>* serialNumbers(字符串数组)* 匹配通行证的序列号。* <p>* 如果没有匹配的通行证,则返回 HTTP 状态 204。* 否则,返回适当的标准 HTTP 状态*/// Getting the Serial Numbers for Passes Associated with a Device@GetMapping("/v1/devices/{deviceLibraryIdentifier}/registrations/{passTypeIdentifier}")@ResponseBody@ApiOperation("获取与设备关联的通行证的序列号")public ResponseEntity getSerialNumber(@PathVariable("deviceLibraryIdentifier") String deviceLibraryIdentifier,@PathVariable("passTypeIdentifier") String passTypeIdentifier,@RequestParam(value = "passesUpdatedSince",required = false) String passesUpdatedSince) {// 这里的pushToken需要保存起来,然后推送更新时,将这个pushToken带上即可;System.err.println("time:"+ new Date());System.err.println("[暗余]. 获取与设备关联的通行证的序列号");System.err.println("passTypeIdentifier:"+passTypeIdentifier);SerialNumberBo serialNumberBo = new SerialNumberBo();serialNumberBo.setLastUpdateTime("123455");serialNumberBo.setSerialNumbers(ImmutableList.of("aed1a60b-20fa-4346-8d26-43d8fe14f29d","123","456"));
//        serialNumberBo.setSerialNumbers(ImmutableList.of("123"));return new ResponseEntity(serialNumberBo, HttpStatus.OK);}/**** passTypeId 凭证的类型,从服务器中指定* serial 服务器分配给凭证模板的序列号。您可以通过调用 xxx 来获取它;getPassbookTemplate*//*** 获取最新版本的通行证** @param version            协议版本——当前为 v1。* @param passTypeIdentifier 通行证的类型,在通行证中指定。* @param serialNumber       通行证中指定的唯一通行证标识符。*                           header: 提供了 Authorization 标头;它的值是单词ApplePass,后跟一个空格,然后是通行证中指定的通行证的授权令牌。*                           如果请求被授权,则返回 HTTP 状态 200 以及传递数据的有效负载。*                           如果请求未授权,则返回 HTTP 状态 401。*                           否则,返回适当的标准 HTTP 状态。*                           支持此端点上的标准 HTTP 缓存:检查If-Modified-Since标头,如果通道未更改,则返回 HTTP 状态代码 304。*/@GetMapping("/v1/passes/{passTypeIdentifier}/{serialNumber}")@ResponseBody@ApiOperation("获取最新版本的通行证")public void pushWallet(@PathVariable("passTypeIdentifier") String passTypeIdentifier,@PathVariable("serialNumber") String serialNumber,@ApiIgnore HttpServletResponse httpResponse) {// 这里的pushToken需要保存起来,然后推送更新时,将这个pushToken带上即可;System.err.println("time:"+ new Date());System.err.println("[暗余]. 获取最新版本的通行证");System.err.println("passTypeIdentifier:"+passTypeIdentifier);System.err.println("serialNumber:"+serialNumber);demoService.pushWallet(passTypeIdentifier, serialNumber, httpResponse);}/*** @param version                 协议版本——当前为 v1。* @param deviceLibraryIdentifier 设备库标识符  用于识别和验证设备的唯一标识符。* @param passTypeIdentifier      通行证的类型,在通行证中指定。* @param serialNumber            通行证中指定的唯一通行证标识符。*                                header: 提供了 Authorization 标头;它的值是单词ApplePass,后跟一个空格,然后是通行证中指定的通行证的授权令牌。*                                如果解除关联成功,则返回 HTTP 状态 200。*                                如果请求未授权,则返回 HTTP 状态 401。*                                否则,返回适当的标准 HTTP 状态。*                                <p>*                                服务器解除指定设备与通行证的关联,当通行证发生变化时,不再向该设备发送推送通知。*/
//    注销设备@ResponseBody@DeleteMapping("/v1/devices/{deviceLibraryIdentifier}/registrations/{passTypeIdentifier}/{serialNumber}")@ApiOperation("注销设备")public ResponseEntity unregisteringDevice(@PathVariable("version") String version,@PathVariable("deviceLibraryIdentifier") String deviceLibraryIdentifier,@PathVariable("passTypeIdentifier") String passTypeIdentifier,@PathVariable("serialNumber") String serialNumber) {// 这里的pushToken需要保存起来,然后推送更新时,将这个pushToken带上即可;System.err.println("time:"+ new Date());System.err.println("[暗余]. 注销设备");System.err.println("version:"+version);System.err.println("deviceLibraryIdentifier:"+deviceLibraryIdentifier);System.err.println("passTypeIdentifier:"+passTypeIdentifier);System.err.println("serialNumber:"+serialNumber);return new ResponseEntity(HttpStatus.OK);}/*** 记录错误日志* @param version 版本号* @param request 日志信息* @return 默认成功*/nseBody@RequestMapping(value = "/v1/log", method = RequestMethod.GET)public ResponseEntity<Void> log(@RequestBody LogContentRequest request) {if (Objects.nonNull(request)) {System.out.println(JSON.toJSONString(request));}return new ResponseEntity<>(HttpStatus.OK);}public static String getRequestBody() throws IOException {RequestAttributes attributes = RequestContextHolder.getRequestAttributes();ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) attributes;HttpServletRequest request = servletRequestAttributes.getRequest();BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream()));//读取输入流的内容转换为String类型IOUtils必须引入org.apache.dubbo.common.utils.IOUtils;包String body = RequestBodyUtils.read(reader);return body;}@Data@NoArgsConstructor@AllArgsConstructorstatic class SerialNumberBo implements Serializable{private String lastUpdateTime;private List<String> serialNumbers;}@Data@NoArgsConstructor@AllArgsConstructorstatic class LogContentRequest implements Serializable {private List<String> logs;}@Data@NoArgsConstructor@AllArgsConstructorstatic class PushToken implements Serializable {private String pushToken;}// 主动推送通知public static void main(String[] args) throws IOException {String privateKeyPath = "D:\\xxx\\passtype_key_dev.p12"; // the private key you exported from keychainString privateKeyPassword = "xxxx "; // the password you used to exportprivateKeyPathString pushToken = "xxx";PKSendPushNotificationUtil pkSendPushNotificationUtil = new PKSendPushNotificationUtil(privateKeyPath, privateKeyPassword);PushNotificationFuture<SimpleApnsPushNotification, PushNotificationResponse<SimpleApnsPushNotification>> simpleApnsPushNotificationPushNotificationResponsePushNotificationFuture =pkSendPushNotificationUtil.sendPushNotificationAsync(pushToken);SimpleApnsPushNotification pushNotification = simpleApnsPushNotificationPushNotificationResponsePushNotificationFuture.getPushNotification();System.err.println(JSON.toJSONString(pushNotification));}
}

3.3 注意事项

  • 通行证更新,需要使用https接口,并且证书认证应该是可信机构认证的;如果本地想进行测试的话,可以使用http,但是Iphone手机需要连接电脑开启开发者权限,在手机的开发者选项中允许HTTP请求即可;
  • 苹果定义的这几个接口,实际里面的内容需要我们根据我们自己的业务去完善。也就是说,从负责的内容来说,企业需要从生成、更新全流程把控自己的礼品卡,而Apple只提供用户的主动访问接口、APNS的通知,以及保证带了正确安全证书的通行证是可信的。
  • 更新存在延迟甚至丢包,所以不能保证每次都能成功更新,所以每个接口要保证其健壮性;

四、写在最后

4.1 结尾

这篇文章是博主实战得出的一些经验总结,如果有纰漏欢迎指出。写文不易,觉得不错的话麻烦点点关注鼓励一下哦,后续会继续更新原创精品内容!后面会继续讲Google Wallet 的部分,如果小伙伴谁遇到了问题可以留在评论区,我能解答的话会帮忙看一下;笔芯~

4.2 补充说明

以下是我对Apple Wallet 的封装代码,有需要可供参考

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import de.brendamour.jpasskit.PKBarcode;
import de.brendamour.jpasskit.PKField;
import de.brendamour.jpasskit.PKPass;
import de.brendamour.jpasskit.enums.PKBarcodeFormat;
import de.brendamour.jpasskit.enums.PKDateStyle;
import de.brendamour.jpasskit.enums.PKTextAlignment;
import de.brendamour.jpasskit.passes.PKGenericPass;
import de.brendamour.jpasskit.signing.PKFileBasedSigningUtil;
import de.brendamour.jpasskit.signing.PKPassTemplateInMemory;
import de.brendamour.jpasskit.signing.PKSigningException;
import de.brendamour.jpasskit.signing.PKSigningInformation;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpStatus;import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.InvalidParameterException;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;/*** @author csdn 暗余*/
public  class AppleWallet {private final PKPass pkPass;private final String serialNumber;private final PKGenericPass pkGenericPass;private final List<PKField> headerFields;private final List<PKField> primaryFields;private final List<PKField> secondaryFields;private final List<PKField> auxiliaryFields;private final List<PKField> backFields;private PKSigningInformation pkSigningInformation;private final PKPassTemplateInMemory pkPassTemplateInMemory;private static final String VALID_ERROR = "The apple pass is NOT Valid!";public AppleWallet(String serialNumber) {pkPass = new PKPass();this.serialNumber = serialNumber;pkPass.setSerialNumber(serialNumber);pkGenericPass = new PKGenericPass();headerFields = Lists.newArrayList();primaryFields = Lists.newArrayList();secondaryFields = Lists.newArrayList();auxiliaryFields = Lists.newArrayList();backFields = Lists.newArrayList();pkPassTemplateInMemory = new PKPassTemplateInMemory();}public AppleWallet buildWhole(String backgroundColor, String foregroundColor, String labelColor, String description, String logoText) {pkPass.setBackgroundColor(backgroundColor);pkPass.setForegroundColor(foregroundColor);pkPass.setLabelColor(labelColor);pkPass.setDescription(description);pkPass.setLogoText(logoText);return this;}public AppleWallet buildQrCode(String message) {PKBarcode pkBarcode = new PKBarcode();pkBarcode.setFormat(PKBarcodeFormat.PKBarcodeFormatQR);pkBarcode.setMessage(message);pkBarcode.setMessageEncoding(StandardCharsets.UTF_8);pkPass.setBarcodes(ImmutableList.of(pkBarcode));return this;}public AppleWallet buildField(GenericPassEnum genericPassType, String keyField, String label, String value, PKDateStyle timeStyle, PKDateStyle dateStyle, PKTextAlignment pkTextAlignment) {if (Objects.nonNull(value) && value.length() > 0) {PKField pkField = new PKField();pkField.setKey(Optional.ofNullable(keyField).orElse(StringUtils.EMPTY));pkField.setLabel(Optional.ofNullable(label).orElse(StringUtils.EMPTY));pkField.setValue(value);pkField.setTimeStyle(timeStyle);pkField.setDateStyle(dateStyle);pkField.setTextAlignment(pkTextAlignment);addGenericList(genericPassType, pkField);}return this;}private void addGenericList(GenericPassEnum genericPassType, PKField pkField) {switch (genericPassType) {case HEADER:headerFields.add(pkField);break;case PRIMARY:primaryFields.add(pkField);break;case SECONDARY:secondaryFields.add(pkField);break;case AUXILIARY:auxiliaryFields.add(pkField);break;case BACK:backFields.add(pkField);break;default:throw new InvalidParameterException("Undefined type!");}}public AppleWallet buildField(GenericPassEnum genericPassType, String keyField, String label, Serializable value, PKTextAlignment pkTextAlignment) {PKField pkField = new PKField();pkField.setKey(Optional.ofNullable(keyField).orElse(StringUtils.EMPTY));pkField.setLabel(Optional.ofNullable(label).orElse(StringUtils.EMPTY));pkField.setValue(Optional.ofNullable(value).orElse(StringUtils.EMPTY));pkField.setTextAlignment(pkTextAlignment);addGenericList(genericPassType, pkField);return this;}public AppleWallet startGenericPass() {pkGenericPass.setHeaderFields(headerFields);pkGenericPass.setPrimaryFields(primaryFields);pkGenericPass.setSecondaryFields(secondaryFields);pkGenericPass.setAuxiliaryFields(auxiliaryFields);pkGenericPass.setBackFields(backFields);pkPass.setGeneric(pkGenericPass);return this;}public AppleWallet addFile(String pkPassTemplateInMemoryType, String url) throws IOException {int responseCode = 0;URL fileUrl = null;if (StringUtils.isNotEmpty(url)) {try {fileUrl = new URL(url);HttpURLConnection con = (HttpURLConnection) fileUrl.openConnection();responseCode = con.getResponseCode();} catch (Exception e) {responseCode = HttpStatus.NOT_FOUND.value();}}if (responseCode < HttpStatus.MULTIPLE_CHOICES.value() && responseCode >= HttpStatus.OK.value()) {pkPassTemplateInMemory.addFile(pkPassTemplateInMemoryType, Objects.requireNonNull(fileUrl));}return this;}public AppleWallet addFile(String pathInTemplate, InputStream inputStream) throws IOException {pkPassTemplateInMemory.addFile(pathInTemplate, inputStream);return this;}public PKPassTemplateInMemory getPKPassTemplateInMemory() {return pkPassTemplateInMemory;}public String getSerialNumber() {return serialNumber;}public byte[] create2Byte() throws PKSigningException {PKFileBasedSigningUtil pkSigningUtil = new PKFileBasedSigningUtil();return pkSigningUtil.createSignedAndZippedPkPassArchive(pkPass, pkPassTemplateInMemory, pkSigningInformation);}public AppleWallet buildWholeParent(String passTypeIdentifier, String authenticationToken, String teamIdentifier, String organizationName, PKSigningInformation pkSigningInformation) {this.pkSigningInformation = pkSigningInformation;pkPass.setPassTypeIdentifier(passTypeIdentifier);pkPass.setAuthenticationToken(authenticationToken);pkPass.setTeamIdentifier(teamIdentifier);pkPass.setOrganizationName(Optional.ofNullable(organizationName).orElse(StringUtils.EMPTY));return this;}public PKPass getPkPass() {return pkPass;}public AppleWallet isValid() {if (!pkPass.isValid()) {List<String> validationErrors = this.pkPass.getValidationErrors();String error = CollectionUtils.isNotEmpty(validationErrors) ? validationErrors.get(0) : VALID_ERROR;throw new RuntimeException(error);} else {return this;}}public AppleWallet setWebServiceURL(String url) {try {pkPass.setWebServiceURL(new URL(url));pkPass.setFormatVersion(1);} catch (MalformedURLException e) {}return this;}public enum GenericPassEnum {HEADER, PRIMARY, SECONDARY, AUXILIARY, BACK, OTHER}
}

苹果钱包(AppleWallet)接入操作手册,超详细相关推荐

  1. SVN服务器安装与常用操作(超详细)

    SVN服务器安装与常用操作(超详细) 服务器 1.下载SVN服务器,地址:http://subversion.apache.org/packages.html 进入网址后,滚动到浏览器最底部看到如下截 ...

  2. 基于Python的Excel读写操作--内容超详细,值得排排坐

    今日份超详细的解读,Excel文件基础操作,让你以学就会. 写这个的原因是,一位群友问了很久,然后网上的教程写的又不是很详细,看的有些犯迷糊,所以我今日份写一份超详细的基本操作,让你打下良好的基础. ...

  3. 苹果屏幕录制怎么设置?超详细步骤首次分享!

    苹果屏幕录制怎么设置?在苹果手机里面有非常多的隐藏功能,如果不是长时间使用,可能你根本不会发现这些使用方法.最近很多人问小编苹果手机录制屏幕的方法,其实随着iOS不断的更新,我们早就可以通过苹果手机本 ...

  4. 最新VMware 安装Windows10——图文操作,超详细~~~

    VMware 安装win10系统 1.首先选择新建虚机 2.暂时不选择光盘映像文件 3.需要自己选择安装的版本,这里是win10 4. 选择安装位置 5.这里选择多个文件 6.点击自定义硬件 7.选择 ...

  5. 主生产计划 操作教程 用友u8_用友财务软件不会操作?超详细操作流程及技巧,收藏...

    我常常在想,身为普通会计的我到底做错了什么? 如果什么都没有做错,那为什么要日复一日地被用友折磨? 试错再试错,太辛苦了!总算对了,感谢我们办公室的李姐,她是我们办公室的大神,经常在工作之余,总结一些 ...

  6. Git分支合并操作教程(超详细配图说明)

    测试内容: A.首先建立master,提交文件a.b.c B.新建分支branch1,包含master文件a.b.c,修改a提交branch1 (此时两个分支a文件不同,b.c相同) C.在maste ...

  7. 从零学ELK系列(九):Nginx接入ELK(超详细图文教程)

    [前言] 在前几篇博文中将ELK+Filebeat收集SpringBoot项目日志搭建完毕,本次我们将展示如何将Nginx接入我们搭建的日志系统,把步骤记录下来,一是方便自己以后安装,二是可以为大家做 ...

  8. C语言函数操作大全----(超详细)

    fopen(打开文件) 相关函数 open,fclose 表头文件 #include<stdio.h> 定义函数 FILE * fopen(const char * path,const ...

  9. 三、jQuery 中的 DOM 操作(超详细)

    文章目录 前言 一. DOM 介绍 二. 查找节点 2.1查找元素节点 2.2查找属性节点 三. 创建节点 3.1.创建元素节点 3.2.创建文本节点 3.3.创建属性节点 四.插入节点 4.1 DO ...

最新文章

  1. iOS 11开发教程(八)定制iOS11应用程序图标
  2. 安装好android的adt以后重启eclipse,但是没有创建AVD的图标
  3. 公众号新上线微信小游戏(疯狂猜图)
  4. 蚂蚁金服4轮面经(Java研发):G1收集器+连接池+分布式架构
  5. php中msubstr,PHP学习:thinkphp中字符截取函数msubstr()用法分析
  6. 单基因gsea_这篇3+分核心基因筛选,点个在看,我们复现这篇文章!
  7. 如何创建linux 脚本,如何创建和执行shell脚本
  8. 对边缘计算与云原生的理解与思考
  9. 新闻发布系统(java实现)+论文
  10. Linux命令之dhclient,dhclient命令 – 动态获取或释放IP地址
  11. 谷歌或Edge浏览器在开始菜单页面不显示图标
  12. Pandas数据分析与处理补充习题
  13. MTK平台系统稳定性分析
  14. 运营Tumblr的几个建议-教你成为tumblr达人
  15. #微信公众号互联登录-01#
  16. Java Initialization Order
  17. 网易云音乐导出歌单-速食版
  18. 前端 几个好看的button
  19. 你应该了解的 MySQL 细节
  20. 教你路由器端口映射设置方法

热门文章

  1. dfs深度优先搜索问题
  2. 农村土地确权之调查公示 —— ArcGIS中地块分布图标注设置说明[地块分布图制作]
  3. xynu 2139: 德莱联盟(判断线段是否相交 )
  4. C语言中数据的表现形式及其运算
  5. 云上全流程透明性备品备件协同管理
  6. Day 20 IOl流
  7. 级数_2:常数项级数的审敛法
  8. cropper(裁剪图片)插件使用(案例)
  9. 一些好听的纯音乐及下载
  10. 克罗内克函数Kronecker Delta【OI Pharos 6.2.1】