ASP.NET Sign In With Apple 后端验证(C#)

苹果在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; }

        }


posted on 2020-09-05 15:05  沐小淘  阅读(903)  评论(1编辑  收藏  举报

导航