蘋果在2019年 9 月12 號更新了審核指南,加入 4.8 Sign in with Apple 一條,要求所有使用第三方登錄 的 App,都必須接入 Sign in with Apple。已經上架的 App 需在 2020 年 4 月 前完成接入工作,新上架 App(如果支持三方登錄)必須接入Sign in with Apple,否則將被拒。
App登錄成功后,需要將獲取到的 identityToken、code等信息發送給后台,然后由后台調用 Apple 的后台API,來驗證用戶的真實性,從而完成驗證。
本文講述C#基於授權碼的Sign In With Apple后端驗證:
client_secret的構建方法
先在后台生成授權應用APP ID的密鑰KEY文件,然后下載密鑰文件,此文件只能下載一次,請妥善保存,格式樣例:
#密鑰KEY格式樣例
-----BEGIN PRIVATE KEY-----
BASE64編碼后的密鑰
-----END PRIVATE KEY-----
秘鑰讀取
/// <summary>
/// 獲取P8
/// </summary>
/// <returns></returns>
private CngKey GetPrivateKey()
{
const string privateKey =
@"BASE64編碼后的密鑰"; // contents of .p8 file
var cngKey = CngKey.Import(
Convert.FromBase64String(privateKey),
CngKeyBlobFormat.Pkcs8PrivateBlob);
return cngKey;
}
private static SigningCredentials CreateSigningCredentials(string keyId,
ECDsa algorithm)
{
var key = new ECDsaSecurityKey(algorithm) { KeyId = keyId };
return new SigningCredentials(key,
SecurityAlgorithms.EcdsaSha256Signature);
}
驗證
/// <summary>
/// 檢驗生成的授權碼是正確的,需要給出正確的授權碼
/// </summary>
/// <param name="authorizationCode">授權碼</param>
/// <param name="appUserId">apple用戶ID</param>
/// <returns></returns>
public async Task<string> TestAppleSign(string authorizationCode, string
appUserId)
{
var httpClientHandler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (message, certificate2,
arg3, arg4) => true
};
var httpClient = new HttpClient(httpClientHandler, true)
{
//超時時間設置長一點點,有時候響應超過3秒,根據情況設置
//我這里是單元測試,防止異常所以寫30秒
Timeout = TimeSpan.FromSeconds(30)
};
var newToken = CreateSecret();
var clientId = "";//需要IOS提供
var datas = new Dictionary<string, string>()
{
{"client_id", clientId},
{"grant_type", "authorization_code"},//固定authorization_code
{"code",authorizationCode },//authorizationCode },//授權碼,前端驗證登錄給予
{"client_secret",newToken} //client_secret,后面方法生成
};
//x-www-form-urlencoded 使用FormUrlEncodedContent
var formdata = new FormUrlEncodedContent(datas);
var result = await
httpClient.PostAsync("https://appleid.apple.com/auth/token", formdata);
var re = await result.Content.ReadAsStringAsync();
if (result.IsSuccessStatusCode)
{
var deserializeObject =
JsonConvert.DeserializeObject<TokenResult>(re);
var jwtPlayload = DecodeJwtPlayload(deserializeObject.IdToken);
if (!jwtPlayload.Aud.Equals(appUserId))//appUserId,前端驗證登錄給予
{
return await Task<string>.FromResult(jwtPlayload.Aud);
}
else
{
//請根據re的返回值,查看上面的錯誤表格
return await Task<string>.FromResult(re);
}
}
else
{
//請根據re的返回值,查看上面的錯誤表格
return await Task<string>.FromResult(re);
}
}
/// <summary>
/// 生成CreateSecret
/// </summary>
private string CreateSecret()
{
var handler = new JwtSecurityTokenHandler();
var subject = new Claim("sub", "");//需要IOS提供
var tokenDescriptor = new SecurityTokenDescriptor()
{
Audience = "https://appleid.apple.com",//固定值
Issuer = "",//team ID,需要IOS提供
IssuedAt = DateTime.UtcNow.AddDays(-1),
NotBefore = DateTime.UtcNow.AddDays(-1),
Subject = new ClaimsIdentity(new[] { subject }),
};
var algorithm = new ECDsaCng(GetPrivateKey());
{
tokenDescriptor.SigningCredentials =
CreateSigningCredentials("", algorithm);//p8私鑰文件得Key,需要IOS提供
var clientSecret = handler.CreateEncodedJwt(tokenDescriptor);
return clientSecret;
}
}
返回值樣例
{"access_token":"a0996b16cfb674c0eb0d29194c880455b.0.nsww.5fi5MVC-i3AVNhddrNg7Qw",
"token_type":"Bearer",
"expires_in":3600,
"refresh_token":"r9ee922f1c8b048208037f78cd7dfc91a.0.nsww.KlV2TeFlTr7YDdZ0KtvEQQ","id_token":"eyJraWQiOiJBSURPUEsxIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLnNreW1pbmcuYXBwbGVsb2dpbmRlbW8iLCJleHAiOjE1NjU2NjU1OTQsImlhdCI6MTU2NTY2NDk5NCwic3ViIjoiMDAwMjY2LmRiZTg2NWIwYWE3MjRlMWM4ODM5MDIwOWI5YzdkNjk1LjAyNTYiLCJhdF9oYXNoIjoiR0ZmODhlX1ptc0pqQ2VkZzJXem85ZyIsImF1dGhfdGltZSI6MTU2NTY2NDk2M30.J6XFWmbr0a1hkJszAKM2wevJF57yZt-MoyZNI9QF76dHfJvAmFO9_RP9-tz4pN4ua3BuSJpUbwzT2xFD_rBjsNWkU-ZhuSAONdAnCtK2Vbc2AYEH9n7lB2PnOE1mX5HwY-dI9dqS9AdU4S_CjzTGnvFqC9H5pt6LVoCF4N9dFfQnh2w7jQrjTic_JvbgJT5m7vLzRx-eRnlxQIifEsHDbudzi3yg7XC9OL9QBiTyHdCQvRdsyRLrewJT6QZmi6kEWrV9E21WPC6qJMsaIfGik44UgPOnNnjdxKPzxUAa-Lo1HAzvHcAX5i047T01ltqvHbtsJEZxAB6okmwco78JQA"}
解析jwt第二部分
/// <summary>
/// 解析jwt第二部分
/// </summary>
/// <param name="jwtString"></param>
/// <returns></returns>
private JwtPlayload DecodeJwtPlayload(string jwtString)
{
try
{
var code = jwtString.Split('.')[1];
code = code.Replace('-', '+').Replace('_', '/').PadRight(4 *
((code.Length + 3) / 4), '=');
var bytes = Convert.FromBase64String(code);
var decode = Encoding.UTF8.GetString(bytes);
return JsonConvert.DeserializeObject<JwtPlayload>(decode);
}
catch (Exception e)
{
throw new Exception(e.Message);
}
}
所用實體類
/// <summary>
/// 接口返回值
/// </summary>
public class TokenResult
{
/// <summary>
/// 一個token
/// </summary>
[JsonProperty("access_token")]
public string AccessToken { get; set; }
/// <summary>
/// Bearer
/// </summary>
[JsonProperty("token_type")]
public string TokenType { get; set; }
/// <summary>
///
/// </summary>
[JsonProperty("expires_in")]
public long ExpiresIn { get; set; }
/// <summary>
/// 一個token
/// </summary>
[JsonProperty("refresh_token")]
public string RefreshToken { get; set; }
/// <summary>
/// "結果是JWT,字符串形式,identityToken 解析后和客戶端端做比對
/// </summary>
[JsonProperty("id_token")]
public string IdToken { get; set; }
}
/// <summary>
/// jwt第二部分
/// </summary>
private class JwtPlayload
{
/// <summary>
/// "https://appleid.apple.com"
/// </summary>
[JsonProperty("iss")]
public string Iss { get; set; }
/// <summary>
/// 這個是你的app的bundle identifier
/// </summary>
[JsonProperty("aud")]
public string Aud { get; set; }
/// <summary>
///
/// </summary>
[JsonProperty("exp")]
public long Exp { get; set; }
/// <summary>
///
/// </summary>
[JsonProperty("iat")]
public long Iat { get; set; }
/// <summary>
/// 用戶ID
/// </summary>
[JsonProperty("sub")]
public string Sub { get; set; }
/// <summary>
///
/// </summary>
[JsonProperty("at_hash")]
public string AtHash { get; set; }
/// <summary>
///
/// </summary>
[JsonProperty("email")]
public string Email { get; set; }
/// <summary>
///
/// </summary>
[JsonProperty("email_verified")]
public bool EmailVerified { get; set; }
/// <summary>
///
/// </summary>
[JsonProperty("is_private_email")]
public bool IsPrivateEmail { get; set; }
/// <summary>
///
/// </summary>
[JsonProperty("auth_time")]
public long AuthTime { get; set; }
/// <summary>
///
/// </summary>
[JsonProperty("nonce_supported")]
public bool NonceSupported { get; set; }
}
