一、前言
本文旨在总结sign in with apple 遇到的一些问题。
二、方案
- 目的是验证前端传来的授权码和apple user是否匹配。方案采用使用私钥生成token的方式。需要公钥验证的,可以查看下方参考文献1。
三、过程
1.接口以及对Required参数的具体分析
https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens
-
client_id
是苹果控台设置的包名,前后端一致的,找产品要即可; -
code
前端传的授权码(ps:只能使用一次,用过一次后过期,过期返回值{"error":"invalid_grant"}
); -
grant_type
此处传authorization_code
字符串即可; -
client_secret
需要处理。
2.生成client_secret
官方文档最下方有具体生成步骤,觉得我下面讲的不清楚的可以自行查看。
(1)需要参数client_id
,kid
,teamId
和private key
,client_id
就是上面提到的包名字符串,重点说一下后面几个参数。
-
kid
:把https://help.apple.com/developer-account/#/dev77c875b7e
这个链接丢给产品经理(本人实在没图,更具体点可以参考下方参考文献2) -
teamId
也是在上一步中生成的,private App Id
格式是xxxxxxxxxx.com.abcd.lalala
,第一个‘.’前面的10位就是需要的teamId
; -
private key
也是按照上面文档生成的,是一个.p8
文件,我们把它后缀改成.txt
取其中的字符串即可;
(2)生成client_secret
- 经测试auth0-jwt和jjwt都可以,demo给的是auth0的sign
- 依赖:
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.3</version>
</dependency>
- 代码:
public static final String APPLE_JWT_AUD_URL = "https://appleid.apple.com";
/**
* 生成clientSecret
*
* @param kid
* @param teamId
* @param clientId
* @param primaryKey(写完发现,命名有误,privateKey)
* @return
*/
public String generateClientSecret(String kid, String teamId,
String clientId, String primaryKey) {
Map<String, Object> header = new HashMap<>();
header.put("kid", kid);
long second = System.currentTimeMillis() / 1000;
//将private key字符串转换成PrivateKey 对象
PrivateKey privateKey = null;
try {
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(
readPrimaryKey(primaryKey));
KeyFactory keyFactory = KeyFactory.getInstance("EC");
privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
} catch (Exception e) {
e.printStackTrace();
}
// 此处只需PrimaryKey
Algorithm algorithm = Algorithm.ECDSA256(null,
(ECPrivateKey) privateKey);
// 生成JWT格式的client_secret
String secret = JWT.create().withHeader(header).withClaim("iss", teamId)
.withClaim("iat", second).withClaim("exp", 86400 * 180 + second)
.withClaim("aud", APPLE_JWT_AUD_URL).withClaim("sub", clientId)
.sign(algorithm);
return secret;
}
private byte[] readPrimaryKey(String primaryKey) {
StringBuilder pkcs8Lines = new StringBuilder();
BufferedReader rdr = new BufferedReader(new StringReader(primaryKey));
String line = "";
try {
while ((line = rdr.readLine()) != null) {
pkcs8Lines.append(line);
}
} catch (IOException e) {
e.printStackTrace();
}
// 需要注意删除 "BEGIN" and "END" 行, 以及空格
String pkcs8Pem = pkcs8Lines.toString();
pkcs8Pem = pkcs8Pem.replace("-----BEGIN PRIVATE KEY-----", "");
pkcs8Pem = pkcs8Pem.replace("-----END PRIVATE KEY-----", "");
pkcs8Pem = pkcs8Pem.replaceAll("\\s+", "");
// Base64 转码
return Base64.decodeBase64(pkcs8Pem);
}
3.验证并登录
- 最后一块拼图
client_secret
获取到之后,就可以去请求获取token了。
public static final String APPLE_AUTH_TOKEN_URL = "https://appleid.apple.com/auth/token";
// POST 请求
Map<String, String> header = new HashMap<>();
header.put("content-type", " application/x-www-form-urlencoded");
// authorizationCode仅能使用一次
Map<String, String> form = new HashMap<>();
form.put("client_id", clientId);
form.put("client_secret", clientSecret);
form.put("code", authorizationCode);
form.put("grant_type", "authorization_code");
JSONObject result = HttpClientUtil.sendHttpPostByFormMap(
APPLE_AUTH_TOKEN_URL, form, header);
-
获取到结果:
-
只需要
id_token
即可,id_token
是JWT格式的,可以直接放入jwt.io里面解析,注意到箭头标记的三段,分别对应JWT的header
,payload
,verify signature
。截取其中的payload。
base64解码截取的payload,是一个json,取
sub
的值就是我们要的apple user。与前端传过来的user进行比对,如果一致,就代表验证成功,然后执行登录/注册逻辑。
四、总结
- 重点在于client_secret的generate以及JWT的sign&decode。