前言:公司C端APP刚刚做了微信授权登录,第一版也已经成功上线。第二版APP被拒绝是因为苹果规定只要APP连接第三方社交平台的授权登录必须接受他的苹果账号授权登录,否则审核可能会被拒绝...接受它,你能放下它吗! (我们考虑过加一个服务器和一个交换机,提交审核前应该隐藏第三方授权登录功能,审核通过后开启。听IOS同事说有一个冒着被封号的风险,我放弃了登陆苹果id怎么取消验证码,然后开始看苹果的官方文档……)
一、对接过程;苹果的官方文档很难描述。也许外国人的思维方式和我们不一样?可能是我的英语水平太低了。看了中文文档,还是觉得还是不知道整个过程。后来找了一个帖子,供搜索引擎参考。
1、先看时序图。苹果的授权登录与微信有些不同。 APP调用接口获取类似微信的信息,然后传输到服务器。服务器验证授权信息。如果有效(校验或),如果有效则可以进行后续的逻辑处理(一般是注册用户或者缓存授权信息以备下次操作):
服务器验证授权登录的有效性有两种方式。上图为验证。本文只讲这种实现方式(实现比较简单,也有最简单的一种登陆苹果id怎么取消验证码,就是服务端不Check,哈哈_)。
2、登录开发者网站,开启应用授权登录功能:
3、APP前端连接苹果对应接口获取授权信息。字段说明请参考以下链接:
使用 Apple 登录详细信息登录
登录方式
App 登录授权(Java 后端认证)
值得一提的是,他是一个签名的 Json Web Token (JWT)
主要是三件事:
:加密算法是通过这个对象获取的,具体使用的公钥是从这里获取的。有两个苹果公钥,随机的
:传输的数据,这里主要是一些授权信息
:前两部分的签名(,)
二、java服务器实现代码:
1、需要导入的jar:
<!-- 苹果账号授权登录依赖的jar -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.1</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.1</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.1</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.9.8</version>
</dependency>
2、令牌解析工具类:
package com.lieni.apple.utils;
import java.math.BigInteger;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.RSAPublicKeySpec;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.lieni.apple.bean.AppleJwtPayloadBean;
import com.lieni.core.util.JsonUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
public class IdentityTokenUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(IdentityTokenUtil.class);
public final static String APP_PUBLIC_KEYS_URL = "https://appleid.apple.com/auth/keys";
private final static String APPLE_ISSUER_URL = "https://appleid.apple.com";
private final static String APPLE_AUD = "你的APP ID";
public final static String APPLE_PRIVATE_EMAIL_ADDRESS = "@privaterelay.appleid.com";
/**
* 通过下面这个方法验证identityToken的有效性:授权用户的JWT凭证
*
* @param identityToken
* @return
*/
public static boolean verify(String identityToken, PublicKey publicKey) {
LOGGER.info("identityToken=[{}]", identityToken);
try {
if (publicKey == null) {
LOGGER.error("publicKey can not be null.");
return false;
}
// 3 验证 https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens
JwtParser jwtParser = Jwts.parserBuilder().requireIssuer(APPLE_ISSUER_URL).requireAudience(APPLE_AUD)
.setSigningKey(publicKey).build();
Jws<Claims> claim = jwtParser.parseClaimsJws(identityToken);
if (claim != null) {
LOGGER.debug("claim=[{}]", claim);
String sub = claim.getBody().get("sub").toString();// sub,即用户的Apple的openId
String iss = claim.getBody().get("iss").toString();
String aud = claim.getBody().get("aud").toString();
long exp = Long.valueOf(claim.getBody().get("exp").toString()) * 1000;// exp is second
return APPLE_ISSUER_URL.equals(iss) && APPLE_AUD.equals(aud) && System.currentTimeMillis() < exp;
}
return false;
} catch (ExpiredJwtException e) {
LOGGER.error("apple identityToken expired", e);
return false;
} catch (Exception e) {
LOGGER.error("apple identityToken illegal", e);
return false;
}
}
public static String getKidByIdentityToken(String identityToken) {
if (StringUtils.isBlank(identityToken)) {
return null;
}
Map<String, JSONObject> map = parserIdentityToken(identityToken);
if (MapUtils.isEmpty(map)) {
return null;
}
JSONObject header = map.get("header");
return header != null ? header.getString("kid") : null;
}
/**
* 对前端传来的JWT字符串identityToken的第二部分进行解码 主要获取其中的aud和sub,aud对应ios前端的包名,sub对应当前用户的授权openID
*
* @param identityToken
* @return
*/
private static Map<String, JSONObject> parserIdentityToken(String identityToken) {
Map<String, JSONObject> map = new HashMap<>();
String[] arr = identityToken.split("\.");
String deHeader = new String(Base64.decodeBase64(arr[0]));
LOGGER.info("header=[{}]", deHeader);
JSONObject header = JSON.parseObject(deHeader);
map.put("header", header);
String dePayload = new String(Base64.decodeBase64(arr[1]));
LOGGER.info("dePayload=[{}]", dePayload);
JSONObject payload = JSON.parseObject(dePayload);
map.put("payload", payload);
return map;
}
public static AppleJwtPayloadBean getPayloadByIdentityToken(String identityToken) {
String[] arr = identityToken.split("\.");
if (ArrayUtils.isEmpty(arr)) {
return new AppleJwtPayloadBean();
}
String dePayload = new String(Base64.decodeBase64(arr[1]));
if (StringUtils.isBlank(dePayload)) {
return new AppleJwtPayloadBean();
}
return JsonUtil.toObject(dePayload, AppleJwtPayloadBean.class);
}
public static PublicKey getPublicKey(Map<String, String> keyMap) {
if (MapUtils.isEmpty(keyMap)) {
LOGGER.error("keyMap ca not be empty.");
return null;
}
// 首先通过identityToken中的header中的kid,然后结合苹果获取公钥的接口,拿到相应的n和e的值,然后通过下面这个方法构建RSA公钥
BigInteger modulus = new BigInteger(1, Base64.decodeBase64(keyMap.get("n")));
BigInteger publicExponent = new BigInteger(1, Base64.decodeBase64(keyMap.get("e")));
RSAPublicKeySpec keySpec = new RSAPublicKeySpec(modulus, publicExponent);
try {
return KeyFactory.getInstance("RSA").generatePublic(keySpec);
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
LOGGER.error("get publicKey error.", e);
}
return null;
}
public static void main(String[] args) {
String identityToken = "eyJraWQiOiI4NkQ4OEtmIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiTGllbmkuaU9TLUMiLCJleHAiOjE1OTUzMjA5MzksImlhdCI6MTU5NTMyMDMzOSwic3ViIjoiMDAwNjUyLmQ4MDNjZDNlNmEzZjRkZmJiNjhlOTNjMTU5OTNkZTUzLjA3MTQiLCJjX2hhc2giOiJBbUY2QzFJZWFfMXJhQUdtQTUtMmFRIiwiZW1haWwiOiJrbXdpZWF3YzNzQHByaXZhdGVyZWxheS5hcHBsZWlkLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjoidHJ1ZSIsImlzX3ByaXZhdGVfZW1haWwiOiJ0cnVlIiwiYXV0aF90aW1lIjoxNTk1MzIwMzM5LCJub25jZV9zdXBwb3J0ZWQiOnRydWV9.POGQEq7Ccy9WqRfs92-wccxiz4q7jhZdKUnhaQiSbYy4nSRU-UEQq1kyoCdjR9Knyt1_sAUbQ5T6UBWQ6q4eOYpBNk37ZGcsnEK4_iBSO5H2cPkytT_ZXqIrUykJ1Iti9QIuYA0Y53pg-ZNBsFlRUIHG_b5OFMQmLoyCojIUwNDLSBV0KXR24rwMnDNMNCcuv1xPk26IvhxAvKCKQfOyNgdxx7ByhiW_qX7vlfVT1YqTPXjFEl24CaUwHBCOcYywSoQEprwP2-ZbCaIvEs0MkGGNTdtnUKAq2_O8Z-RShaqZ2gOtkKcFhfzZeFV8DouIJccXoEx9_2s7aFBAc5tLVw";
// boolean verify = IdentityTokenUtil.verify(identityToken);
System.out.println(JsonUtil.toJson(getPayloadByIdentityToken(identityToken)));
}
}
3、提供认证服务的本地服务(缓存苹果的公钥):
@Service
public class AppleLoginService {
private static final Logger LOGGER = LoggerFactory.getLogger(AppleLoginService.class);
@Resource(name = "jdkRedisTemplate")
private RedisTemplate<String, Map<String, String>> jdkRedisTemplate;
public boolean verify(String identityToken) {
return IdentityTokenUtil.verify(identityToken,
getPublicKey(IdentityTokenUtil.getKidByIdentityToken(identityToken)));
}
/**
* 获取验证所需的PublicKey
*
* @param kid
* @return
*/
private PublicKey getPublicKey(String kid) {
return IdentityTokenUtil.getPublicKey(mapApplePublicKeyByKid(kid));
}
@SuppressWarnings("unchecked")
private Map<String, String> mapApplePublicKeyByKid(String kid) {
if (jdkRedisTemplate.hasKey(AppleServerRedisKeyGenerator.genAppleAuthLoginPublicKey())) {
Object values = jdkRedisTemplate.opsForHash().get(AppleServerRedisKeyGenerator.genAppleAuthLoginPublicKey(),
kid);
if (!(values instanceof Map)) {
return Collections.emptyMap();
}
return (Map<String, String>) values;
}
Map<String, Map<String, String>> map = new HashMap<>();
try {
String responseBody = HttpCaller.get(HttpClients.createDefault(), IdentityTokenUtil.APP_PUBLIC_KEYS_URL,
null, "UTF-8");
if (StringUtils.isBlank(responseBody)) {
return Collections.emptyMap();
}
LOGGER.debug("responseBody=[{}]", responseBody);
JSONObject jsonObject = JSONObject.parseObject(responseBody);
if (null == jsonObject) {
return Collections.emptyMap();
}
JSONArray keys = jsonObject.getJSONArray("keys");
for (int i = 0; i < keys.size(); i++) {
Map<String, String> m = new HashMap<>();
m.put("n", keys.getJSONObject(i).getString("n"));
m.put("e", keys.getJSONObject(i).getString("e"));
map.put(keys.getJSONObject(i).getString("kid"), m);
}
// 加缓存
jdkRedisTemplate.opsForHash().putAll(AppleServerRedisKeyGenerator.genAppleAuthLoginPublicKey(), map);
jdkRedisTemplate.expire(AppleServerRedisKeyGenerator.genAppleAuthLoginPublicKey(), 24, TimeUnit.HOURS);
return map.get(kid);
} catch (IOException e) {
LOGGER.error("mapApplePublicKeyByKid error.", e);
return Collections.emptyMap();
}
}
}
因为赶着上线,上面的代码还是比较粗糙的。码友来看看吧。将在后续项目中进行优化。以上代码已经通过亲测验证,大家可以放心使用了!
节点/小火箭/美区ID/国外苹果ID/美区小火箭购买/美区小火箭兑换码/shadowrocket兑换码/苹果商店下载shadowrocket网址
原文链接:C端APP微信授权登录,审核被拒了,怎么办?,转载请注明来源!