1、前言
2、客户端
在xcode工程中添加Sign in with Apple
2.1 在项目属性找到Signing & Capabilities
,选择添加Capability
2.2 在弹出的添加页面,输入sign找到Sign in with Apple
2.3 将Sign in with Apple
添加成功
2.4 添加AuthenticationServices.Framework
2.5 然后在登录按钮点击事件调用代码登录
-(void)LoginApple{
//
if (@available(iOS 13.0, *)) {
ASAuthorizationAppleIDProvider *appleIDProvider = [[ASAuthorizationAppleIDProvider alloc] init];
ASAuthorizationAppleIDRequest *appleIDRequest = [appleIDProvider createRequest];
appleIDRequest.requestedScopes = @[ASAuthorizationScopeFullName, ASAuthorizationScopeEmail];
ASAuthorizationController *authorizationController = [[ASAuthorizationController alloc] initWithAuthorizationRequests:@[appleIDRequest]];
authorizationController.delegate = appController;
authorizationController.presentationContextProvider = appController;
[authorizationController performRequests];
}else{
NSLog(@"error");
}
}
2.6 回调事件
在回调类中添加 <ASAuthorizationControllerDelegate,ASAuthorizationControllerPresentationContextProviding>
/// 授权成功回调
- (void)authorizationController:(ASAuthorizationController *)controller didCompleteWithAuthorization:(ASAuthorization *)authorization API_AVAILABLE(ios(13.0)){
if ([authorization.credential isKindOfClass:[ASAuthorizationAppleIDCredential class]]) {
ASAuthorizationAppleIDCredential *appleIDCredential = authorization.credential;
NSData *identityToken = appleIDCredential.identityToken;
// 注意:使用过授权的,可能获取不到以下三个参数,要做判空处理
NSString *nickname = appleIDCredential.fullName.nickname;
if(nickname == nullptr ){
if(appleIDCredential.fullName.familyName != nullptr && appleIDCredential.fullName.givenName != nullptr){
nickname = [NSString stringWithFormat:@"%@%@",appleIDCredential.fullName.familyName, appleIDCredential.fullName.givenName];
}else if(appleIDCredential.fullName.familyName != nullptr){
nickname = appleIDCredential.fullName.familyName;
}else if(appleIDCredential.fullName.givenName != nullptr){
nickname = appleIDCredential.fullName.givenName;
}
}
if(nickname == nullptr){
nickname = @"";
}
// 服务器验证需要使用的参数
NSString *identityTokenStr = [[NSString alloc] initWithData:identityToken encoding:NSUTF8StringEncoding];
// TODO, 将用户名和 token 传给服务器做验证。
// 注意: 非第一次授权,nickname可能为空。
}else if ([authorization.credential isKindOfClass:[ASPasswordCredential class]]){
// 这个获取的是iCloud记录的账号密码,需要输入框支持iOS 12 记录账号密码的新特性,如果不支持,可以忽略
ASPasswordCredential *passwordCredential = authorization.credential;
NSString *user = passwordCredential.user;
NSString *password = passwordCredential.password;
}else{
NSLog(@"授权信息不符");
}
}
/// 授权失败回调
- (void)authorizationController:(ASAuthorizationController *)controller didCompleteWithError:(NSError *)error API_AVAILABLE(ios(13.0)){
NSLog(@"Handle error:%@", error);
NSString *errorMsg = nil;
switch (error.code) {
case ASAuthorizationErrorCanceled:
errorMsg = @"用户取消了授权请求";
break;
case ASAuthorizationErrorFailed:
errorMsg = @"授权请求失败";
break;
case ASAuthorizationErrorInvalidResponse:
errorMsg = @"授权请求响应无效";
break;
case ASAuthorizationErrorNotHandled:
errorMsg = @"未能处理授权请求";
break;
case ASAuthorizationErrorUnknown:
errorMsg = @"授权请求失败未知原因";
break;
default:
break;
}
NSLog(@"errorMsg = %@", errorMsg);
}
/// 设置展示内容给用户的Window
- (ASPresentationAnchor)presentationAnchorForAuthorizationController:(ASAuthorizationController *)controller API_AVAILABLE(ios(13.0)){
return [UIApplication sharedApplication].windows.lastObject;
}
3、服务端
服务端使用客户端登录成功后获取的identityTokenStr
值来做验证。identityTokenStr
是基于JWT的算法验证。
eyJraWQiOiJZdXlYb1kiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLmh6ZGNtLnl5eWxjIiwiZXhwIjoxNjQ5ODg2NDc1LCJpYXQiOjE2NDk4MDAwNzUsInN1YiI6IjAwMTE5NC4zZjExMjE3OGFkYTI0NDU2YjllYTRjMDBhMTZlZmIyYS4yMTQ3IiwiY19oYXNoIjoiekQ0THpRelYxTjY3TmdlX3ZsTjhTQSIsImVtYWlsIjoibXFoNnF0a2s4eUBwcml2YXRlcmVsYXkuYXBwbGVpZC5jb20iLCJlbWFpbF92ZXJpZmllZCI6InRydWUiLCJpc19wcml2YXRlX2VtYWlsIjoidHJ1ZSIsImF1dGhfdGltZSI6MTY0OTgwMDA3NSwibm9uY2Vfc3VwcG9ydGVkIjp0cnVlfQ.zmnDnSxuo9m95ly3tT_ZqO4ZKoRnYBFdgMWGwclbWvSim7pDKbnabvBP8F2pZ_A2O1KbsQyvzSRge6mDAJbFfuyTF2rC1nI2Ghm4oAl1_hZ39UskaM30L0exFrSjltTdpidnlho1SZNFauZy3IOh84IhyKrvXp7QbzmDXZI8_eiXBu7L50S7uEqTUWv3k7V-jUK6etOWg-7uuOadLAyQIuKx-MQ-Pr44IlHRw76abmaZzwWewc0CDGtmZBSlL3VdEdHbetl8FVlDRCUp-kQz0f6K4KKfDOEQNCU0JhIgEemlMX0q-0HW81GBuyPylCvGPMkDeIwP19oSxpiJMHkb1A
JWT格式(以.点号分隔):
header: 包括了key id 与加密算法
payload:
1. iss: 签发机构,苹果
2. aud: 接收者,目标app
3. exp: 过期时间
4. iat: 签发时间
5. sub: 用户id
6. c_hash: 一个哈希数列
7. auth_time: 签名时间signature: 用于验证JWT的签名
header
{
"kid": "YuyXoY",
"alg": "RS256"
}
payload
{
"iss": "https://appleid.apple.com",
"aud": "com.hzdcm.yyylc",
"exp": 1649886475,
"iat": 1649800075,
"sub": "001194.3f112178ada24456b9ea4c00a16efb2a.2147",
"c_hash": "zD4LzQzV1N67Nge_vlN8SA",
"email": "mqh6qtkk8y@privaterelay.appleid.com",
"email_verified": "true",
"is_private_email": "true",
"auth_time": 1649800075,
"nonce_supported": true
}
token验证原理
因为identityToken使用非对称加密 RSASSA【RSA签名算法】 和 ECDSA【椭圆曲线数据签名算法】,当验证签名的时候,先从https://appleid.apple.com/auth/keys获取公钥,再利用公钥来解密Singature,当解密内容与base64UrlEncode(header) + “.” + base64UrlEncode(payload)的内容完全一样的时候,表示验证通过。
Java服务器验证代码
package com.game.data.platform;
import com.arch.common.JsonUtil;
import com.arch.common.StringUtil;
import com.arch.log.Log;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import io.jsonwebtoken.*;
import org.apache.commons.codec.binary.Base64;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
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.List;
import java.util.Map;
/**
* 苹果账号登录验证
*/
public class PlatformApple {
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Keys{
public String kid;
public String use;
public String alg;
public String n;
public String e;
}
// 缓存公钥
public static Map<String,Keys> keysMap = new HashMap<>();
// 验证Token
public static boolean checkToken(String identityToken) {
if (StringUtil.isNullOrEmpty(identityToken)) {
return false;
}
try {
String []jwt = identityToken.split("\\.");
if (jwt.length != 3){
return false;
}
String header = new String(Base64.decodeBase64(jwt[0]),"utf-8");
String token = new String(Base64.decodeBase64(jwt[1]),"utf-8");
JsonNode jsonNode = JsonUtil.readTree(header );
Keys keys = getKeys(jsonNode.get("kid").asText(""), jsonNode.get("alg").asText(""));
JsonNode wNode = JsonUtil.readTree(token );
String audience = wNode.get("aud").asText("");
String subject = wNode.get("sub").asText("");
PublicKey publicKeySpec = build(keys.n, keys.e);
int result = verify(publicKeySpec, token,audience, subject);
if (result == 0){
return true;
}
}catch (Exception e){
}
return false;
}
// 获取公钥
public static Keys getKeys(String kid, String alg){
String key = kid+"."+alg;
Keys keys = keysMap.get(key);
if (keys != null){
return keys;
}
final String url = "https://appleid.apple.com/auth/keys";
try{
CloseableHttpClient httpclient = HttpClients.createDefault();
HttpGet httpget = new HttpGet(url);
try (CloseableHttpResponse response = httpclient.execute(httpget)) {
if (response.getStatusLine().getStatusCode() == 200) {
JsonNode result = JsonUtil.readTree(response.getEntity().getContent());
JsonNode ks = result.get("keys");
if (ks != null){
String kstr = ks.toString();
List<Keys> list = JsonUtil.deserialize(new TypeReference<List<Keys>>() {}, kstr);
for (Keys k : list){
keysMap.put(k.kid+"."+k.alg, k);
}
return keysMap.get(key);
}
}
}
}catch (Exception e) {
Log.error(e);
}
return null;
}
// 构建key
public static PublicKey build(String n, String e) throws NoSuchAlgorithmException, InvalidKeySpecException {
BigInteger modulus = new BigInteger(1, Base64.decodeBase64(n));
BigInteger publicExponent = new BigInteger(1, Base64.decodeBase64(e));
RSAPublicKeySpec keySpec = new RSAPublicKeySpec(modulus, publicExponent);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicKey = keyFactory.generatePublic(keySpec);
return publicKey;
}
// 验证
public static int verify(PublicKey key, String jwt, String audience, String subject) {
JwtParser jwtParser = Jwts.parser().setSigningKey(key);
jwtParser.requireIssuer("https://appleid.apple.com");
jwtParser.requireAudience(audience);
jwtParser.requireSubject(subject);
try {
Jws<Claims> claim = jwtParser.parseClaimsJws(jwt);
if (claim != null && claim.getBody().containsKey("auth_time")) {
return 0;
}
return 1;
} catch (ExpiredJwtException e) {
Log.error("apple identityToken expired", e);
return 2;
} catch (Exception e) {
Log.error("apple identityToken illegal", e);
return 3;
}
}
}
然后,直接调用PlatformApple.checkToken(identityToken)
来验证