一、生成client_secret
安裝jose-jwt,示例代碼如下
/// <summary>
/// 生成JWT
/// </summary>
/// <returns></returns>
private string CreateAppleClientSecret()
{
var iat = Math.Round((DateTime.UtcNow.AddMinutes(-1) - new DateTime(1970, 1, 1, 0, 0, 0)).TotalSeconds, 0);
var exp = Math.Round((DateTime.UtcNow.AddMinutes(30) - new DateTime(1970, 1, 1, 0, 0, 0)).TotalSeconds, 0);
string keyId = "8D7EEULXX4";
var extraHeader = new Dictionary<string, object>()
{
{ "alg", "ES256" },
{ "kid", keyId }, //key id
{ "typ", "JWT" }
};
var payload = new Dictionary<string, object>()
{
{ "sub" , bundleIdentifier }, //iOS App的Bundle Identifier
{ "exp", exp },
{ "iat", iat },
{ "iss", "2K26LU5HS2" }, //team ID
{ "aud", "https://appleid.apple.com" },
{ "origin", "https://appleid.apple.com" }
};
var keyString = GetApplePrivateKeyText();
CngKey privateKey = CngKey.Import(Convert.FromBase64String(keyString), CngKeyBlobFormat.Pkcs8PrivateBlob);
string token = JWT.Encode(payload, privateKey, JwsAlgorithm.ES256, extraHeader);
return token;
}
/// <summary>
/// 獲取蘋(píng)果p8文件內(nèi)容
/// </summary>
/// <returns></returns>
private string GetApplePrivateKeyText()
{
string filePath = "iOSFiles/AuthKey_976XVM7U2S.p8"; //iOS提供的p8文件,可以不用文件,直接復(fù)制文件里邊的內(nèi)容
string path = Server.MapPath(filePath);
string text = System.IO.File.ReadAllText(path);
//去頭去尾的方法:
var lines = text.Split('\n');
var privateKeyText = string.Join("", lines.Skip(1).Take(lines.Length - 2).Select(l => l.Trim()));
return privateKeyText;
}
二、發(fā)送請(qǐng)求
1.請(qǐng)求地址為 https://appleid.apple.com/auth/token;
2.method為POST;
3.ContentType為application/x-www-form-urlencoded;
4.所需參數(shù)為
①client_id : "" //iOS App的Bundle Identifier
②grant_type : "authorization_code" //固定值
③code : 蘋(píng)果驗(yàn)證返回authorizationCode
④client_secret : CreateAppleClientSecret() //上面步驟一生成的token
示例代碼
/// <summary>
/// 蘋(píng)果登錄
/// </summary>
/// <param name="bundleIdentifier">iOS App的Bundle Identifier</param>
/// <param name="authorizationCode">蘋(píng)果驗(yàn)證返回authorizationCode</param>
/// <param name="appleUserId">蘋(píng)果驗(yàn)證返回user</param>
/// <returns></returns>
public string SignInWithApple(string bundleIdentifier, string authorizationCode, string appleUserId)
{
try
{
ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback((object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors errors) =>
{
return true; //總是接受
});
ServicePointManager.SecurityProtocol = (SecurityProtocolType)3072;
HttpWebRequest request = (HttpWebRequest)WebRequest.Create("https://appleid.apple.com/auth/token");
request.Method = "POST";
request.ContentType = "application/x-www-form-urlencoded";
var datas = new Dictionary<string, string>()
{
{ "client_id", bundleIdentifier },
{ "grant_type", "authorization_code"},//固定authorization_code
{ "code", authorizationCode },//授權(quán)碼,前端驗(yàn)證登錄給予
{ "client_secret", CreateAppleClientSecret() } //client_secret,后面方法生成
};
string postData = JsonUrlEncode(JsonConvert.SerializeObject(datas));
byte[] buf = Encoding.Default.GetBytes(postData);
request.ContentLength = buf.Length;
Stream requestStream = request.GetRequestStream();
requestStream.Write(buf, 0, buf.Length);
requestStream.Close();
HttpWebResponse response = (HttpWebResponse)request.GetResponse();
Stream stream = response.GetResponseStream();
StreamReader streamReader = new StreamReader(stream, encoding: Encoding.UTF8);
responseString = streamReader.ReadToEnd();
HlllAppleAuthTokenResultModel tokenResultModel = JsonConvert.DeserializeObject<HlllAppleAuthTokenResultModel>(responseString);
HlllAppleJwtPlayloadModel playloadModel = DecodeAppleJwtPlayload(tokenResultModel.id_token);
if (!playloadModel.Aud.Equals(bundleIdentifier) || !playloadModel.Sub.Equals(appleUserId))
{
// "信息不正確,請(qǐng)重試";
}
else //你的服務(wù)器處理邏輯,服務(wù)器數(shù)據(jù)庫(kù)添加蘋(píng)果賬號(hào)ID驗(yàn)證appleUserId
{
}
}
catch (Exception ex)
{
}
}
/// <summary>
/// json轉(zhuǎn)urlencode
/// </summary>
/// <returns></returns>
public static string JsonUrlEncode(string json)
{
Dictionary<string, object> dic = JsonConvert.DeserializeObject<Dictionary<string, object>>(json);
StringBuilder builder = new StringBuilder();
foreach (KeyValuePair<string, object> item in dic)
{
builder.Append(GetFormDataContent(item, ""));
}
return builder.ToString().TrimEnd('&');
}
/// <summary>
/// 遞歸轉(zhuǎn)formdata
/// </summary>
/// <param name="item"></param>
/// <param name="preStr"></param>
/// <returns></returns>
private static string GetFormDataContent(KeyValuePair<string, object> item, string preStr)
{
StringBuilder builder = new StringBuilder();
if (string.IsNullOrEmpty(item.Value?.ToString()))
{
builder.AppendFormat("{0}={1}", string.IsNullOrEmpty(preStr) ? item.Key : (preStr + "[" + item.Key + "]"), System.Web.HttpUtility.UrlEncode((item.Value == null ? "" : item.Value.ToString()).ToString()));
builder.Append("&");
}
else
{
//如果是數(shù)組
if (item.Value.GetType().Name.Equals("JArray"))
{
var children = JsonConvert.DeserializeObject<List<object>>(item.Value.ToString());
for (int j = 0; j < children.Count; j++)
{
Dictionary<string, object> childrendic = JsonConvert.DeserializeObject<Dictionary<string, object>>(JsonConvert.SerializeObject(children[j]));
foreach (var row in childrendic)
{
builder.Append(GetFormDataContent(row, string.IsNullOrEmpty(preStr) ? (item.Key + "[" + j + "]") : (preStr + "[" + item.Key + "][" + j + "]")));
}
}
}
//如果是對(duì)象
else if (item.Value.GetType().Name.Equals("JObject"))
{
Dictionary<string, object> children = JsonConvert.DeserializeObject<Dictionary<string, object>>(item.Value.ToString());
foreach (var row in children)
{
builder.Append(GetFormDataContent(row, string.IsNullOrEmpty(preStr) ? item.Key : (preStr + "[" + item.Key + "]")));
}
}
//字符串、數(shù)字等
else
{
builder.AppendFormat("{0}={1}", string.IsNullOrEmpty(preStr) ? item.Key : (preStr + "[" + item.Key + "]"), System.Web.HttpUtility.UrlEncode((item.Value == null ? "" : item.Value.ToString()).ToString()));
builder.Append("&");
}
}
return builder.ToString();
}
private HlllAppleJwtPlayloadModel DecodeAppleJwtPlayload(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<HlllAppleJwtPlayloadModel>(decode);
}
catch (Exception e)
{
throw new Exception(e.Message);
}
}
/// <summary>
/// 蘋(píng)果接口返回?cái)?shù)據(jù)
/// </summary>
public class HlllAppleAuthTokenResultModel
{
/// <summary>
/// access_token
/// </summary>
public string access_token { set; get; } = "";
/// <summary>
/// token_type
/// </summary>
public string token_type { set; get; } = "";
/// <summary>
/// expires_in
/// </summary>
public long expires_in { set; get; } = 0;
/// <summary>
/// refresh_token
/// </summary>
public string refresh_token { set; get; } = "";
/// <summary>
/// id_token
/// </summary>
public string id_token { set; get; } = "";
}
/// <summary>
/// 蘋(píng)果接口返回的id_token解析
/// </summary>
public class HlllAppleJwtPlayloadModel
{
/// <summary>
/// "https://appleid.apple.com"
/// </summary>
[JsonProperty("iss")]
public string Iss { get; set; }
/// <summary>
/// 這個(gè)是你的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; }
}
常見(jiàn)問(wèn)題
- POST請(qǐng)求返回400
1.可能參數(shù)不正確;
2.每次授權(quán)信息只能請(qǐng)求一次,成功之后,第二次之后請(qǐng)求均返回400。
- 本地調(diào)試沒(méi)問(wèn)題,發(fā)布到服務(wù)器報(bào)錯(cuò),錯(cuò)誤為:
系統(tǒng)找不到指定的文件。\r\n【具體信息】 在 System.Security.Cryptography.NCryptNative.ImportKey(SafeNCryptProviderHandle provider, Byte[] keyBlob, String format)\r\n 在 System.Security.Cryptography.CngKey.Import(Byte[] keyBlob, String curveName, CngKeyBlobFormat format, CngProvider provider)
解決方案
1.在服務(wù)器以管理員身份運(yùn)行 C:\Windows\System32\cmd.exe ;
2.輸入以下命令,注意修改name='hlll'部分,單引號(hào)部分為你的IIS上面的網(wǎng)站名稱。
c:\windows\system32\inetsrv\appcmd.exe set config -section:applicationPools "/[name='你的網(wǎng)站名稱'].processModel.loadUserProfile:true"
- 本地調(diào)試沒(méi)問(wèn)題,發(fā)布到服務(wù)器報(bào)錯(cuò),錯(cuò)誤為:
用于 ECDsaCng 算法的密鑰必須具有 ECDsa 算法組(英文報(bào)錯(cuò)為 Keys used with the ECDsaCng algorithm must have an algorithm group of ECDsa)
解決方案
服務(wù)器.NET framework升級(jí)到4.6.2即可解決問(wèn)題。