一、背景
公司上架APP到Apple Store,审核被打回,原因是APP中接入了其他第三方登录,所以必须要求接入苹果登录,这项新规定,逼不得已只要接入苹果登录。
二、使用JWT方式接入登录
参考网上很多资料,我看到的都是使用JWT方式接入的,之所以要自己再写一篇同样接入方式的文章,是因为在参考过程中,踩了坑,现在把自己测试成功的方式贴出来,避免后者再踩坑。
1.引入JWT相关工具包
<dependency>
<groupId>com.auth0</groupId>
<artifactId>jwks-rsa</artifactId>
<version>0.12.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.80</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.7.0</version>
</dependency>
以上内容是测试demo中引入的内容,如果项目已存在相同的包可以不用重复引入,核心包jjwt、jwks-rsa。
2.请求参数说明
- IdentityToken:identityToken 是App端请求苹果授权中心拿到的JWT,它由点号“.”分割为三部分,分别是头信息,消息体,签名,前两部分可以通过Base64解码为如下信息。
消息头
{
"alg": "ES256",
"kid": "ABC123DEFG"
}
消息体(仅罗列主要字段)
{
"iss": "https://appleid.apple.com",
"iat": 1437179036,
"exp": 1493298100,
"aud": "com.mytest.app",
"sub": "DEF123GHIJ "
}
字段名称 | 说明 |
---|---|
iss | 签发机构地址 |
iat | 授权时间戳 |
exp | 过期时间戳 |
aud | bundle id |
sub | 用户唯一标识 |
3.Java代码实现
核心工具类
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.auth0.jwk.Jwk;
import io.jsonwebtoken.*;
import org.springframework.web.client.RestTemplate;
import java.nio.charset.StandardCharsets;
import java.security.PublicKey;
import java.util.Base64;
import java.util.Map;
/**
* @Classname IOSToeknUtils
* @Description IOS token操作工具
* @Date 2023/03/23
* @Created by Goden
*/
public class IOSToeknUtils {
private final static String authUrl = "https://appleid.apple.com/auth/keys";
private final static String authIss = "https://appleid.apple.com";
/**
* 解码identityToken
* @param identityToken
* @return
*/
public static JSONObject parserIdentityToken(String identityToken) {
String[] arr = identityToken.split("\\.");
String firstDate = new String(Base64.getDecoder().decode(arr[0]), StandardCharsets.UTF_8);
String decode = new String(Base64.getDecoder().decode(arr[1]), StandardCharsets.UTF_8);
JSONObject claimObj = JSON.parseObject(decode);
// 将第一部分获取到的kid放入消息体中,方便后续匹配对应的公钥使用
claimObj.put("kid", JSONObject.parseObject(firstDate).get("kid"));
return claimObj;
}
/**
* 根据kid获取对应的苹果公钥
* @param kid
* @return
*/
public static PublicKey getPublicKey(String kid) {
try {
RestTemplate restTemplate = new RestTemplate();
JSONObject data = restTemplate.getForObject(authUrl, JSONObject.class);
assert data != null;
JSONArray jsonArray = data.getJSONArray("keys");
for (Object obj : jsonArray) {
Map json = ((Map) obj);
// 获取kid对应的公钥
if (json.get("kid").equals(kid)) {
Jwk jwa = Jwk.fromValues(json);
return jwa.getPublicKey();
}
}
} catch (Exception e) {
}
return null;
}
/**
* 对前端传来的identityToken进行验证
*
* @param identityToken
* @param jsonObject
* @return
* @throws Exception
*/
public static Boolean verifyExc(String identityToken, JSONObject jsonObject) throws Exception {
String kid = (String) jsonObject.get("kid");
PublicKey publicKey = getPublicKey(kid);
JwtParser jwtParser = Jwts.parser().setSigningKey(publicKey);
jwtParser.requireIssuer(authIss);
jwtParser.requireAudience((String) jsonObject.get("aud"));
jwtParser.requireSubject((String) jsonObject.get("sub"));
try {
Jws<Claims> claim = jwtParser.parseClaimsJws(identityToken);
if (claim != null && claim.getBody().containsKey("auth_time")) {
return true;
}
return false;
} catch (ExpiredJwtException e) {
return false;
} catch (Exception e) {
return false;
}
}
}
测试类
import com.alibaba.fastjson.JSONObject;
/**
* @Classname TestIOSLogin
* @Description 测试IOS登录
* @Date 2023/3/23
* @Created by Goden
*/
public class TestIOSLogin {
public static void main(String[] args) {
// 请求的JWT
String identityToken = "eyJraWQiOiJlWGF1bm1MIiwiYWxnIjoiUlMyNTYifQ." +
"eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLmRpeWl5aW4ub25saW5lNTMiLCJl" +
"eHAiOjE1OTc2NTAxNzQsImlhdCI6MTU5NzY0OTU3NCwic3ViIjoiMDAxMzc3LmQ0ZDVmMTAwODQ0ZTQzZjdiMWM1O" +
"WRiMzUyZWZkZmI4LjAyNTkiLCJjX2hhc2giOiJkTDVRdld2VTNjVHBxczNSazlUTnRBIiwiZW1haWwiOiI0OTk4O" +
"TY1MDdAcXEuY29tIiwiZW1haWxfdmVyaWZpZWQiOiJ0cnVlIiwiYXV0aF90aW1lIjoxNTk3NjQ5NTc0LCJub25jZV9" +
"zdXBwb3J0ZWQiOnRydWV9." +
"hM9HjNsMJW2PjYP7SfbzF-GqOt0VnMjYGq4BoU68rkQ-K2lPp_ae5ziX6Bbr3WHg" +
"6cc3Z8OzGO63OfExvSj9gQTR596CZLvNGXhbI3piTK6597-cYsPCTbY7xHxgdHLuL8XhD-9dXPn9rouVYu4QA1" +
"8JBQG1Q4sGsRzLEJ5DjOM9x1bkBz4Vu_5LEOefHFHkWN_RPCh_AOJGviDzm81kTkCTWn8jpm0tGdevMR93MOf44" +
"f7bjP2T8yezl0Vbv09TrnkdAqG0BsihCD0VN9JV7X2eagyumoxTdFfoRiOflFKAaQqohVzcqy9tHOGm_6w5h8bsR" +
"CmtBC4PnqIFqNy_AQ";
// 解码后的消息体
JSONObject playloadObj = IOSToeknUtils.parserIdentityToken(identityToken);
Boolean success;
try {
success = IOSToeknUtils.verifyExc(identityToken, playloadObj);
} catch (Exception e) {
throw new RuntimeException(e);
}
if (!success) {
// TODO 校验token失败具体操作
return;
}
// TODO 检验token成功具体业务操作。。。
}
}
注意
截止目前,调用接口获取苹果公钥,会返回3个公钥信息,每个公钥信息中都有“kid”这个字段,需要获取解码identityToken头信息中“kid”值匹配的公钥,而不是直接拿返回的公钥数组中第一个公钥来做校验,否者会报如下错误
io.jsonwebtoken.SignatureException: JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.