苹果开发者账号迁移
苹果开发者账号迁移,会对 App、服务器哪些影响?
影响
看自身业务与苹果有产生哪些数据。
- 苹果推送(Apns 推送);
- 苹果快捷登录;
- 苹果订阅。
推送
App 重新登录,代表 App 重新获取苹果推送的 token,并上传给服务器。
证书,指的是 P8 证书。
迁移完成后,App 不重新登录,旧的证书是否能推送成功?
无法推送成功,报错:InvalidProviderToken
迁移完成后,App 重新登录,旧的证书是否能推送成功?
无法推送成功,报错:InvalidProviderToken
迁移完成后,App 不重新登录,新的证书是否能推送成功?
可以推送成功。
迁移完成后,App 重新登录,新的证书是否能推送成功?
可以推送成功。
结论:迁移完成后,需马上更换推送证书。
苹果快捷登录
App 调用原生方法进行安全验证后,获得 code。服务器通过 code 和 clientId、clientSecret 去获取用户唯一标识 sub,服务器统称为 openId,苹果的则为 appleOpenId。
appleOpenId 如果存在,则代表用户已存在,直接登录。
appleOpenId 如果不存在,则代表用户不存在,进行注册并登录。
- 迁移完成后,旧的 clientId 是否可以完成快捷登录?
可以完成登录,旧用户会得到新的 openId,导致用户信息无法进行对应。新用户则无影响。
- 迁移完成后,新的 clientId,已存在的用户登录时,获取到的 openId 是否与之前的一致?
也不一致,需要自己根据 transfer_sub 去对应 openId。
结论:不更新 clientId、clientSecret 也可以成功登录,但是产生了新的 openId。
方案
如果仅使用了苹果推送,完成迁移后,仅需更新证书即可。
如果使用了苹果快捷登录,则需要提前完成账号迁移。使用 transfer_sub 去进行账号关联,避免旧用户登录时,当成了新用户。
核心逻辑,分为两个大的步骤:
第一步,不要先进行开发者账号迁移,先做好已有数据的准备,先根据已有的 openId 获取到 transfer_sub。Transferring your apps and users to another team
第二步,进行开发者账号迁移,完成迁移后,在苹果快捷时就会返回 transfer_sub 数据,依靠transfer_sub才能对应上旧用户。Bringing new apps and users into your team
详细操作步骤
第一步:数据准备
新建表 apple_migration。
create table if not exists apple_migration
(
member_id bigint not null primary key,
open_id varchar(64) not null,
transfer_sub varchar(64),
new_open_id varchar(64),
status smallint default 0,
create_time bigint,
update_time bigint
);
comment on table apple_migration is '苹果账号迁移表';
comment on column apple_migration.transfer_sub is '迁移标识符';
comment on column apple_migration.status is '状态(0:未处理 1:已处理transferSub 2:已处理newOpenId)';
根据 member_account 表中的数据填充 apple_migration 表。记录填充的最大的 member_id 为 max_member_id。
INSERT INTO apple_migration (member_id, open_id, transfer_sub, create_time, update_time)
SELECT
ma.member_id,
ma.apple_openid,
NULL as transfer_sub, -- transfer_sub字段需要一个默认值,这里设为NULL
EXTRACT(EPOCH FROM NOW()) * 1000 as create_time, -- 使用当前时间戳(毫秒)
EXTRACT(EPOCH FROM NOW()) * 1000 as update_time -- 使用当前时间戳(毫秒)
FROM
member_account ma
WHERE
ma.app_id = ${app_id} -- 具体的app_id
AND ma.apple_openid IS NOT NULL
AND NOT EXISTS (
SELECT 1
FROM apple_migration am
WHERE am.member_id = ma.member_id
);
第二步:获取 transfer_sub
服务器更新,将定时 job 的代码进行更新。
并开启执行 executeGetTransferSub 任务,每 5 分钟执行 500 条的数据。
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.xxl.job.core.context.XxlJobHelper;
import com.xxl.job.core.handler.annotation.XxlJob;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@RequiredArgsConstructor
@Service
public class AppleMigrationJobService {
private final AppleMigrationManager appleMigrationManager;
@XxlJob("executeGetTransferSub")
public void executeGetTransferSub() {
String param = XxlJobHelper.getJobParam();
log.info("[Apple开发者账号迁移,检查transferSub job开始,传入参数<{}>]", param);
JSONObject jsonObject = StrUtil.isBlank(param) ? new JSONObject() : JSONUtil.parseObj(param);
String clientId = jsonObject.getStr("clientId");
String clientSecret = jsonObject.getStr("clientSecret");
String teamId = jsonObject.getStr("teamId");
Integer limit = jsonObject.getInt("limit", 1000);
appleMigrationManager.executeGetTransferSub(clientId, clientSecret, teamId, limit);
log.info("[Apple开发者账号迁移,检查transferSub job完成]");
XxlJobHelper.handleSuccess();
}
@XxlJob("executeGetNewOpenId")
public void executeGetNewOpenId() {
String param = XxlJobHelper.getJobParam();
log.info("[Apple开发者账号迁移,检查new openid job开始,传入参数<{}>]", param);
JSONObject jsonObject = StrUtil.isBlank(param) ? new JSONObject() : JSONUtil.parseObj(param);
String clientId = jsonObject.getStr("clientId");
String clientSecret = jsonObject.getStr("clientSecret");
Integer limit = jsonObject.getInt("limit", 1000);
appleMigrationManager.executeGetNewOpenId(clientId, clientSecret, limit);
log.info("[Apple开发者账号迁移,检查new openid job完成]");
XxlJobHelper.handleSuccess();
}
}
具体实现类。
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.maxcares.taas.one.ams.entity.MemberAccount;
import com.maxcares.taas.one.ams.service.MemberAccountService;
import com.maxcares.taas.one.ums.constant.enums.AppleMigrationStatusEnum;
import com.maxcares.taas.one.ums.entity.AppleMigration;
import com.maxcares.taas.one.ums.service.AppleMigrationService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;
@Slf4j
@RequiredArgsConstructor
@Component
public class AppleMigrationManager {
private final MemberAccountService memberAccountService;
private final AppleMigrationService appleMigrationService;
private final Cache<String, String> cache = CacheBuilder.newBuilder()
.expireAfterWrite(55, TimeUnit.MINUTES)
.build();
public void executeGetTransferSub(String clientId, String clientSecret, String teamId, Integer limit) {
// 1. 查询待处理数据
List<AppleMigration> appleMigrationList = appleMigrationService.list(Wrappers.lambdaQuery(AppleMigration.class)
.isNull(AppleMigration::getTransferSub)
.isNull(AppleMigration::getNewOpenId)
.eq(AppleMigration::getStatus, AppleMigrationStatusEnum.NO_PROCESS.getVal())
.orderByAsc(AppleMigration::getMemberId)
.last("limit " + limit));
if (appleMigrationList.isEmpty()) {
log.info("[没有需要GetTransferSub的数据列表 clientId<{}>]", clientId);
return;
}
// 2. 定义待更新列表,只存成功的记录
List<AppleMigration> updateList = new ArrayList<>();
for (AppleMigration appleMigration : appleMigrationList) {
String openId = appleMigration.getOpenId();
// 调用 API
String transferSub = getTransferSub(openId, teamId, clientId, clientSecret);
if (StrUtil.isBlank(transferSub)) {
log.warn("[获取TransferSub失败或为空,跳过 memberId<{}> openId<{}>]", appleMigration.getMemberId(), openId);
appleMigration.setStatus(AppleMigrationStatusEnum.TRANSFER_SUB.getVal());
updateList.add(appleMigration);
continue;
}
appleMigration.setTransferSub(transferSub);
appleMigration.setStatus(AppleMigrationStatusEnum.TRANSFER_SUB.getVal());
updateList.add(appleMigration);
}
// 3. 批量更新
if (!updateList.isEmpty()) {
appleMigrationService.updateBatchById(updateList);
}
}
public void executeGetNewOpenId(String clientId, String clientSecret, Integer limit) {
// 1. 查询待处理的迁移记录
List<AppleMigration> appleMigrationList = appleMigrationService.list(Wrappers.lambdaQuery(AppleMigration.class)
.isNotNull(AppleMigration::getTransferSub)
.isNull(AppleMigration::getNewOpenId)
.eq(AppleMigration::getStatus, AppleMigrationStatusEnum.TRANSFER_SUB.getVal())
.orderByAsc(AppleMigration::getMemberId)
.last("limit " + limit));
if (appleMigrationList.isEmpty()) {
log.info("[没有需要GetNewOpenId的数据列表 clientId<{}>]", clientId);
return;
}
// 2. 查询对应的 MemberAccount
Map<Integer, MemberAccount> memberAccountMap = memberAccountService.listByIds(appleMigrationList.stream()
.map(AppleMigration::getMemberId)
.collect(Collectors.toList()))
.stream()
.collect(Collectors.toMap(MemberAccount::getMemberId, Function.identity()));
// 3. 定义两个新列表,只存放处理成功、需要更新的数据
List<MemberAccount> updateMemberAccountList = new ArrayList<>();
List<AppleMigration> updateAppleMigrationList = new ArrayList<>();
for (AppleMigration appleMigration : appleMigrationList) {
// 调用 API 获取新 OpenId
String newOpenId = getNewOpenId(appleMigration.getTransferSub(), clientId, clientSecret);
if (StrUtil.isBlank(newOpenId)) {
log.warn("[获取NewOpenId失败或为空,跳过更新 memberId<{}>", appleMigration.getMemberId());
appleMigration.setStatus(AppleMigrationStatusEnum.NEW_OPENID.getVal());
updateAppleMigrationList.add(appleMigration);
continue;
}
// 更新 Migration 实体
appleMigration.setNewOpenId(newOpenId);
// 更新 MemberAccount 实体
MemberAccount memberAccount = memberAccountMap.get(appleMigration.getMemberId());
if (memberAccount != null) {
memberAccount.setAppleOpenid(newOpenId);
updateMemberAccountList.add(memberAccount);
}
appleMigration.setStatus(AppleMigrationStatusEnum.NEW_OPENID.getVal());
updateAppleMigrationList.add(appleMigration);
}
// 4. 批量更新(判空,防止列表为空报错)
if (!updateMemberAccountList.isEmpty()) {
memberAccountService.updateBatchById(updateMemberAccountList);
}
if (!updateAppleMigrationList.isEmpty()) {
appleMigrationService.updateBatchById(updateAppleMigrationList);
}
}
private String getTransferSub(String openId, String teamId, String clientId, String clientSecret) {
try {
String token = getAccessToken(clientId, clientSecret);
if (StrUtil.isBlank(token)) {
log.error("[apple迁移获取token失败 openId<{}>]", openId);
return null;
}
cache.put("token", token);
String transferSub = getTransferSub(openId, teamId, clientId, clientSecret, token);
if (StrUtil.isBlank(transferSub)) {
log.error("[apple迁移获取transferSub失败 openId<{}>]", openId);
return null;
}
return transferSub;
} catch (Exception e) {
cache.invalidate("token");
log.error("[apple迁移获取transferSub失败 <{}>]", e.getMessage(), e);
return null;
}
}
private String getNewOpenId(String transferSub, String clientId, String clientSecret) {
try {
String token = getAccessToken(clientId, clientSecret);
if (StrUtil.isBlank(token)) {
log.error("[apple迁移获取token失败 transferSub<{}>]", transferSub);
return null;
}
cache.put("token", token);
String newOpenId = getMemberNewOpenId(transferSub, clientId, clientSecret, token);
if (StrUtil.isBlank(newOpenId)) {
log.error("[apple迁移获取newOpenId失败 transferSub<{}>]", transferSub);
return null;
}
return newOpenId;
} catch (Exception e) {
cache.invalidate("token");
log.error("[apple迁移获取newOpenId失败 <{}>]", e.getMessage(), e);
return null;
}
}
public static String getMemberNewOpenId(String transferSub, String clientId, String clientSecret, String token) {
Map<String, Object> formDate = Map.of(
"transfer_sub", transferSub,
"client_id", clientId,
"client_secret", clientSecret
);
HttpResponse response = HttpUtil.createPost("https://appleid.apple.com/auth/usermigrationinfo")
.header("Authorization", "Bearer " + token)
.form(formDate)
.timeout(10000)
.execute();
log.debug("[获取new openid response<{}>]", response);
if (!response.isOk()) {
log.error("[apple迁移获取new openid失败 transferSub<{}>]", transferSub);
return null;
}
String body = response.body();
return JSONUtil.parseObj(body).getStr("sub");
}
private String getAccessToken(String clientId, String clientSecret) {
Map<String, Object> formDate = Map.of(
"grant_type", "client_credentials",
"scope", "user.migration",
"client_id", clientId,
"client_secret", clientSecret
);
String body = HttpUtil.createPost("https://appleid.apple.com/auth/token")
.form(formDate)
.timeout(10000)
.execute()
.body();
return JSONUtil.parseObj(body).getStr("access_token");
}
public static String getTransferSub(String openId, String teamId, String clientId, String clientSecret, String accessToken) {
Map<String, Object> formData = Map.of(
"sub", openId,
"target", teamId,
"client_id", clientId,
"client_secret", clientSecret);
HttpResponse response = HttpRequest.post("https://appleid.apple.com/auth/usermigrationinfo")
.header("Authorization", "Bearer " + accessToken)
.form(formData)
.timeout(10000)
.execute();
log.debug("[获取transferSub response<{}>]", response);
if (!response.isOk()) {
log.error("[apple迁移获取transferSub失败 openId<{}>]", openId);
return null;
}
String body = response.body();
return JSONUtil.parseObj(body).getStr("transfer_sub");
}
}
第三步:数据补充
等 apple_migration 表中的数据都跑完后,再根据 max_member_id 将这一段时间内产生的新用户也导入进来。
INSERT INTO apple_migration (member_id, open_id, transfer_sub, create_time, update_time)
SELECT
ma.member_id,
ma.apple_openid,
NULL as transfer_sub, -- transfer_sub字段需要一个默认值,这里设为NULL
EXTRACT(EPOCH FROM NOW()) * 1000 as create_time, -- 使用当前时间戳(毫秒)
EXTRACT(EPOCH FROM NOW()) * 1000 as update_time -- 使用当前时间戳(毫秒)
FROM
member_account ma
WHERE
ma.app_id = ${app_id} -- 具体的app_id
AND ma.apple_openid IS NOT NULL
AND ma.member_id > ${max_member_id} -- 填充max_member_id,缩小范围,提高效率
AND NOT EXISTS (
SELECT 1
FROM apple_migration am
WHERE am.member_id = ma.member_id
);
将 apple_migration 表中获取过一遍,但是未获取到有效的 transfer_sub 重新获取一次。
update apple_migration set status = 0 where transfer_sub is null and status = 1;
第四步:更新服务
更新认证服务器、更新苹果快捷登录的 clientId、clientSecret。
更新内容:如果苹果登录信息中发现有 transfer_sub 字段,则传递下去,根据 transfer_sub 在 apple_migration 表查询出 apple_openid,并更新 new_apple_openid。还需同步更新 member_account 表对应 member_id 的 apple_openid。
第五步:开发者账号迁移
将 App 转入到新的开发者账户。
转入成功后,App 登录测试,验证老用户的苹果快捷登录,登录后信息是否一致。
第六步:获取`new_open_id
开启执行 executeGetNewOpenId 任务,每 5 分钟执行 500 条的数据。
最后检查 new_open_id 是否都被填充上了新的值。
参考
苹果官方文档:
- 0
- 0
-
分享