C# Sign in with Apple后臺(tái)接口

一、生成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)題。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容