• C# Sign In With Apple苹果登陆后端验证


    苹果App授权登录

    苹果官方的授权文档:

    生成Token:https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens
    JWT:https://developer.apple.com/documentation/sign_in_with_apple/fetch_apple_s_public_key_for_verifying_token_signature

    苹果的授权登录

    APP内苹果授权登陆会提供如下几个参数:userID、email、fullName、authorizationCode、identityToken
    userID:授权的用户唯一标识
    email、fullName:授权的用户资料
    authorizationCode:授权code
    identityToken:授权用户的JWT凭证

    针对后端验证苹果提供了两种验证方式,一种是基于JWT的算法验证,另外一种是基于授权码的验证

    JWT验证

    identityToken参考:

     1 // jwt 格式
     2 eyJraWQiOiJBSURPUEsxIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLnNreW1pbmcuZGV2aWNlbW9uaXRvciIsImV4cCI6MTU2NTY2ODA4NiwiaWF0IjoxNTY1NjY3NDg2LCJzdWIiOiIwMDEyNDcuOTNiM2E3OTlhN2M4NGMwY2I0NmNkMDhmMTAwNzk3ZjIuMDcwNCIsImNfaGFzaCI6Ik9oMmFtOWVNTldWWTNkcTVKbUNsYmciLCJhdXRoX3RpbWUiOjE1NjU2Njc0ODZ9.e-pdwK4iKWErr_Gcpkzo8JNi_MWh7OMnA15FvyOXQxTx0GsXzFT3qE3DmXqAar96nx3EqsHI1Qgquqt2ogyj-lLijK_46ifckdqPjncTEGzVWkNTX8uhY7M867B6aUnmR7u-cf2HsmhXrvgsJLGp2TzCI3oTp-kskBOeCPMyTxzNURuYe8zabBlUy6FDNIPeZwZXZqU0Fr3riv2k1NkGx5MqFdUq3z5mNfmWbIAuU64Z3yKhaqwGd2tey1Xxs4hHa786OeYFF3n7G5h-4kQ4lf163G6I5BU0etCRSYVKqjq-OL-8z8dHNqvTJtAYanB3OHNWCHevJFHJ2nWOTT3sbw
     3  
     4 // header 解码
     5 {"kid":"AIDOPK1","alg":"RS256"} 其中kid对应上文说的密钥id
     6  
     7 // claims 解码
     8 {
     9 "iss":"https://appleid.apple.com",
    10 "aud":"com.skyming.devicemonitor",
    11 "exp":1565668086,"iat":1565667486,
    12 "sub":"001247.93b3a799a7c84c0cb46cd08f100797f2.0704",
    13 "c_hash":"Oh2am9eMNWVY3dq5JmClbg",
    14 "auth_time":1565667486
    15 }
    16  
    17 ss标识是苹果签发的,aud是接收者的APP ID,sub就是用户的唯一标识
    View Code

    解析的sub和前端传过来的比较是否一致;

    基于授权码的后端验证

    创建Secret

    private string CreateSecret()
            {
    
                var handler = new JwtSecurityTokenHandler();
                var subject = new Claim("sub", Client_Id);//找IOS要 
                var tokenDescriptor = new SecurityTokenDescriptor()
                {
                    Audience = "https://appleid.apple.com",//固定值
                    Issuer = Team_Id,//team ID,找IOS要                                 
                    IssuedAt = DateTime.Now.AddDays(-1),
                    NotBefore = DateTime.Now.AddDays(-1),
                    Subject = new ClaimsIdentity(new[] { subject }),
                };
    
                var algorithm = new ECDsaCng(GetPrivateKey());
                {
                    tokenDescriptor.SigningCredentials = CreateSigningCredentials(Key_Id, algorithm);//p8私钥文件得Key,找IOS要
                    var clientSecret = handler.CreateEncodedJwt(tokenDescriptor);
                    return clientSecret;
                }
    
            }
    View Code
            /// <summary>
            /// 获取P8
            /// </summary>
            /// <returns></returns>
            private CngKey GetPrivateKey()
            {
                using (var reader = new StringReader("p8文件内容"))
                {
                    var ecPrivateKeyParameters = (ECPrivateKeyParameters)new PemReader(reader).ReadObject();
                    var x = ecPrivateKeyParameters.Parameters.G.AffineXCoord.GetEncoded();
                    var y = ecPrivateKeyParameters.Parameters.G.AffineYCoord.GetEncoded();
                    var d = ecPrivateKeyParameters.D.ToByteArrayUnsigned();
                    return EccKey.New(x, y, d);
                }
            }
    View Code

    参考文献:https://stackoverflow.com/questions/42514289/how-to-use-apns-auth-key-p8-file-in-c

    网上还有另外一种P8文件的内容

         private static ECDsa GetPrivateKey()
            {
                CngKey privateKey = null;
    
                //p8文件内容
                string content = @"-----BEGIN PRIVATE KEY-----
                                  ****
                                  -----END PRIVATE KEY-----";
    
                // 这里直接用去头去尾的方法:
                var lines = content.Split('
    ');
                var trimmed = string.Join("", lines.Skip(1).Take(lines.Length - 2).Select(l => l.Trim()));
                var keyBlob = Convert.FromBase64String(trimmed);
    
                _Log.Info("test1");
    
                privateKey = CngKey.Import(keyBlob, CngKeyBlobFormat.Pkcs8PrivateBlob, CngProvider.MicrosoftSoftwareKeyStorageProvider);
    
                _Log.Info("test2");
    
    
                return new ECDsaCng(privateKey);
            }
    View Code

    这是一个坑深坑:

    本地调试没问题,发布到服务器上就报错:System.Security.Cryptography.CryptographicException: 系统找不到指定的文件。

    度娘个的解决方案就是:在服务器上的IIS修改一些配置。(ex:服务能随便改配置吗?万一把其他接口搞挂了呢);

    根据生成的Secret和授权码AuthorizationCode来验证是否正确

    var newToken = CreateSecret();
                    var datas = new Dictionary<string, string>()
                    {
                        { "client_id", Client_Id },
                        { "grant_type", "authorization_code"},//固定authorization_code
                        { "code", authLoginModel.AuthorizationCode },//授权码,前端验证登录给予 
                        { "client_secret", newToken } //client_secret,后面方法生成
                    };
    
                    var formdata = new FormUrlEncodedContent(datas);
                    using (var httpclient = new HttpClient())
                    {
                        httpclient.Timeout = TimeSpan.FromSeconds(30);
                        var result = await httpclient.PostAsync(Apple_Token_Url, formdata);
                        var re = await result.Content.ReadAsStringAsync();
                        if (result.IsSuccessStatusCode)
                        {
                            var deserializeObject = JsonConvert.DeserializeObject<AppleTokenResult>(re);
                            var jwtPlayload = DecodeJwtPlayload(deserializeObject.IdToken);
                            if (jwtPlayload.Aud.Equals(Client_Id) && !string.IsNullOrEmpty(jwtPlayload.Sub))
                            {
                                appleUserId = jwtPlayload.Sub;
                            }
                        }
                        else
                        {
                            return OperationResult.FromError<AuthLoginBindResponse>($"{(int)BizCodeEnum.SING_FAIL}", re);
                        }
                    }
    View Code

    验证成功后会返回

    {
    "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"
    }
    View Code

    其中id_token就是JWT文件,然后对JWT文件进行解析

        /// <summary>
            /// 解析jwt第二部分
            /// </summary>
            /// <param name="jwtString"></param>
            /// <returns></returns>
            private JwtPlayloadModel JwtPlayload(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<JwtPlayloadModel>(decode);
                }
                catch (Exception e)
                {
                    throw new Exception(e.Message);
                }
            }
    View Code
  • 相关阅读:
    JS・TextArea 字符串长度限制
    JavaScript面试题目集锦
    IE内存泄露分析
    JS动态添加样式和脚本
    取消锚(<a/>)点击后页面跳转的几种方法
    JS代码片段整理
    IE缓存问题的解决方法
    字符串与数字 转换
    如何安装和配置Cassandra
    字类和超类的转化问题
  • 原文地址:https://www.cnblogs.com/amoshu/p/12915787.html
Copyright © 2020-2023  润新知