C# Sign in with Apple后台接口

一、生成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>
    /// 获取苹果p8文件内容
    /// </summary>
    /// <returns></returns>
    private string GetApplePrivateKeyText() 
    {
        string filePath = "iOSFiles/AuthKey_976XVM7U2S.p8"; //iOS提供的p8文件,可以不用文件,直接复制文件里边的内容
        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;
    }

二、发送请求

1.请求地址为 https://appleid.apple.com/auth/token
2.method为POST;
3.ContentType为application/x-www-form-urlencoded;
4.所需参数为
①client_id : "" //iOS App的Bundle Identifier
②grant_type : "authorization_code" //固定值
③code : 苹果验证返回authorizationCode
④client_secret : CreateAppleClientSecret() //上面步骤一生成的token

示例代码

    /// <summary>
    /// 苹果登录
    /// </summary>
    /// <param name="bundleIdentifier">iOS App的Bundle Identifier</param>
    /// <param name="authorizationCode">苹果验证返回authorizationCode</param>
    /// <param name="appleUserId">苹果验证返回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 },//授权码,前端验证登录给予 
                        { "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))
            {
                // "信息不正确,请重试";
            }
            else //你的服务器处理逻辑,服务器数据库添加苹果账号ID验证appleUserId
            {

            }

        }
        catch (Exception ex)
        {

        }
    }

    /// <summary>
    /// json转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>
    /// 递归转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
        {
            //如果是数组
            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 + "]")));
                    }
                }

            }
            //如果是对象
            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 + "]")));
                }
            }
            //字符串、数字等
            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>
/// 苹果接口返回数据
/// </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>
/// 苹果接口返回的id_token解析
/// </summary>
public class HlllAppleJwtPlayloadModel
{
    /// <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; }
}

常见问题

  • POST请求返回400
    1.可能参数不正确;
    2.每次授权信息只能请求一次,成功之后,第二次之后请求均返回400。

  • 本地调试没问题,发布到服务器报错,错误为:
    系统找不到指定的文件。\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.在服务器以管理员身份运行 C:\Windows\System32\cmd.exe ;
2.输入以下命令,注意修改name='hlll'部分,单引号部分为你的IIS上面的网站名称。

 c:\windows\system32\inetsrv\appcmd.exe set config -section:applicationPools "/[name='你的网站名称'].processModel.loadUserProfile:true"

  • 本地调试没问题,发布到服务器报错,错误为:
    用于 ECDsaCng 算法的密钥必须具有 ECDsa 算法组(英文报错为 Keys used with the ECDsaCng algorithm must have an algorithm group of ECDsa)

解决方案
服务器.NET framework升级到4.6.2即可解决问题。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,287评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,346评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,277评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,132评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,147评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,106评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,019评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,862评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,301评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,521评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,682评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,405评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,996评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,651评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,803评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,674评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,563评论 2 352