苹果在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; }
}
|
请发表评论