如何在C#中实现基于苹果令牌的推送通知(使用p8文件)?

J. *_*ati 10 c# apple-push-notifications

对于具有某种基于聊天功能的应用,我想添加推送通知支持以接收新消息.我想要做的是使用Apple的基于新令牌的身份验证(.p8文件),但我找不到有关服务器部分的更多信息.

我遇到了以下帖子: 如何在C#中使用APNs Auth Key(.p8文件)?

然而,答案并不令人满意,因为没有太多关于如何:

  • 与APN建立连接
  • 使用p8文件(某种编码除外)
  • 将数据发送到Apple推送通知服务

yaa*_*kov 10

目前,您无法在原始.NET Framework上实现此目的.新的基于JWT的APNS服务器仅使用HTTP/2,.NET Framework尚不支持.

System.Net.Http但是,只要满足以下先决条件,.NET Core的版本就可以了:

  • 在Windows上,您必须运行Windows 10 Anniversary Edition(v1607)或更高版本,或Windows Server 2016的等效版本(我认为).
  • 在Linux上,您必须具有libcurl支持HTTP/2 的版本.
  • 在macOS上,您必须编译libcurl支持HTTP/2,然后使用DYLD_INSERT_LIBRARIES环境变量来加载您的自定义构建libcurl.

System.Net.Http如果你真的想要,你应该可以在.NET Framework中使用.NET Core的版本.

我不知道在Mono,Xamarin或UWP上会发生什么.

那么你需要做三件事:

  1. 解析您已获得的私钥.这是当前的ECDSA密钥,您可以将其加载到System.Security.Cryptography.ECDsa对象中.
    • 在Windows上,您可以使用CNG API.在解析密钥文件的base64编码的DER部分之后,您可以使用创建密钥new ECDsaCng(CngKey.Import(data, CngKeyBlobFormat.Pkcs8PrivateBlob)).
    • 在macOS或Linux上,没有受支持的API,您必须自己解析DER结构,或使用第三方库.
  2. 创建JSON Web令牌/承载令牌.如果你使用System.IdentityModel.Tokens.JwtNuGet 的软件包,这很简单.您将需要Apple的密钥ID和团队ID.
public static string CreateToken(ECDsa key, string keyID, string teamID)
{
    var securityKey = new ECDsaSecurityKey(key) { KeyId = keyID };
    var credentials = new SigningCredentials(securityKey, "ES256");

    var descriptor = new SecurityTokenDescriptor
    {
          IssuedAt = DateTime.Now,
          Issuer = teamID,
          SigningCredentials = credentials
    };

    var handler = new JwtSecurityTokenHandler();
    var encodedToken = handler.CreateEncodedJwt(descriptor);
    return encodedToken;
}
Run Code Online (Sandbox Code Playgroud)
  1. 发送HTTP/2请求.这是正常的,但你需要做两件额外的事情:

    1. 设置yourRequestMessage.Versionnew Version(2, 0)使用HTTP/2发出请求.
    2. 设置yourRequestMessage.Headers.Authorizationnew AuthenticationHeaderValue("bearer", token)为您的请求提供承载认证令牌/ JWT.

    然后将您的JSON放入HTTP请求并将其发布到正确的URL.


小智 6

private string GetToken()
    {
        var dsa = GetECDsa();
        return CreateJwt(dsa, "keyId", "teamId");
    }
    
    private ECDsa GetECDsa()
    {
        using (TextReader reader = System.IO.File.OpenText("AuthKey_xxxxxxx.p8"))
        {
        var ecPrivateKeyParameters =
            (ECPrivateKeyParameters)new Org.BouncyCastle.OpenSsl.PemReader(reader).ReadObject();

        var q = ecPrivateKeyParameters.Parameters.G.Multiply(ecPrivateKeyParameters.D).Normalize();
        var qx = q.AffineXCoord.GetEncoded();
        var qy = q.AffineYCoord.GetEncoded();
        var d = ecPrivateKeyParameters.D.ToByteArrayUnsigned();

        // Convert the BouncyCastle key to a Native Key.
        var msEcp = new ECParameters {Curve = ECCurve.NamedCurves.nistP256, Q = {X = qx, Y = qy}, D = d};
        return ECDsa.Create(msEcp);
        }
    }
    
    private string CreateJwt(ECDsa key, string keyId, string teamId)
    {
        var securityKey = new ECDsaSecurityKey(key) { KeyId = keyId };
        var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.EcdsaSha256);

        var descriptor = new SecurityTokenDescriptor
        {
            IssuedAt = DateTime.Now,
            Issuer = teamId,
            SigningCredentials = credentials,
            
        };

        var handler = new JwtSecurityTokenHandler();
        var encodedToken = handler.CreateEncodedJwt(descriptor);
        return encodedToken;
    }
Run Code Online (Sandbox Code Playgroud)


tea*_*eng 6

由于 Token (.p8) APN 仅适用于 HTTP/2,因此大多数解决方案仅适用于 .net Core。由于我的项目使用.net Framework,因此需要进行一些调整。如果您像我一样使用 .net Framework,请继续阅读。

我到处搜索并遇到了几个问题,我设法解决了这些问题并将它们拼凑在一起。

下面是实际有效的 APNs 类。我为其创建了一个新的类库,并将 .P8 文件放置在类库的 AuthKeys 文件夹中。请记住右键单击 .P8 文件并将其设置为“始终复制”。请参阅获取 Web 项目引用的类库项目中的相对文件路径

之后,要获取 P8 文件的位置,请用于AppDomain.CurrentDomain.RelativeSearchPathWeb 项目或AppDomain.CurrentDomain.BaseDirectorywin 应用程序。请参阅为什么 AppDomain.CurrentDomain.BaseDirectory 在 asp.net 应用程序中不包含“bin”?

要从 P8 获取令牌,您需要使用BouncyCastle类,请从 Nuget 下载它。

using Jose;
using Newtonsoft.Json;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.OpenSsl;
using Security.Cryptography;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;

    namespace PushLibrary
    {
        public class ApplePushNotificationPush
        {
            //private const string WEB_ADDRESS = "https://api.sandbox.push.apple.com:443/3/device/{0}";
            private const string WEB_ADDRESS = "https://api.push.apple.com:443/3/device/{0}";
    
            private string P8_PATH = AppDomain.CurrentDomain.RelativeSearchPath + @"\AuthKeys\APNs_AuthKey.p8";
    
            public ApplePushNotificationPush()
            {
    
            }
    
            public async Task<bool> SendNotification(string deviceToken, string title, string content, int badge = 0, List<Tuple<string, string>> parameters = null)
            {
                bool success = true;
    
                try
                {
                    string data = System.IO.File.ReadAllText(P8_PATH);
                    List<string> list = data.Split('\n').ToList();
    
                    parameters = parameters ?? new List<Tuple<string, string>>();
    
                    string prk = list.Where((s, i) => i != 0 && i != list.Count - 1).Aggregate((agg, s) => agg + s);
                    ECDsaCng key = new ECDsaCng(CngKey.Import(Convert.FromBase64String(prk), CngKeyBlobFormat.Pkcs8PrivateBlob));
    
                    string token = GetProviderToken();
    
                    string url = string.Format(WEB_ADDRESS, deviceToken);
                    HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, url);
    
                    httpRequestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
    
                    httpRequestMessage.Headers.TryAddWithoutValidation("apns-push-type", "alert"); // or background
                    httpRequestMessage.Headers.TryAddWithoutValidation("apns-id", Guid.NewGuid().ToString("D"));
                    //Expiry
                    //
                    httpRequestMessage.Headers.TryAddWithoutValidation("apns-expiration", Convert.ToString(0));
                    //Send imediately
                    httpRequestMessage.Headers.TryAddWithoutValidation("apns-priority", Convert.ToString(10));
                    //App Bundle
                    httpRequestMessage.Headers.TryAddWithoutValidation("apns-topic", "com.xxx.yyy");
                    //Category
                    httpRequestMessage.Headers.TryAddWithoutValidation("apns-collapse-id", "test");
    
                    //
                    var body = JsonConvert.SerializeObject(new
                    {
                        aps = new
                        {
                            alert = new
                            {
                                title = title,
                                body = content,
                                time = DateTime.Now.ToString()
                            },
                            badge = 1,
                            sound = "default"
                        },
                        acme2 = new string[] { "bang", "whiz" }
                    });
    
                    httpRequestMessage.Version = new Version(2, 0);
    
                    using (var stringContent = new StringContent(body, Encoding.UTF8, "application/json"))
                    {
                        //Set Body
                        httpRequestMessage.Content = stringContent;
    
                        Http2Handler.Http2CustomHandler handler = new Http2Handler.Http2CustomHandler();
    
                        handler.SslProtocols = System.Security.Authentication.SslProtocols.Tls12 | System.Security.Authentication.SslProtocols.Tls11 | System.Security.Authentication.SslProtocols.Tls;
    
                        //handler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true;
    
                        //Continue
                        using (HttpClient client = new HttpClient(handler))
                        {
                            HttpResponseMessage resp = await client.SendAsync(httpRequestMessage).ContinueWith(responseTask =>
                            {
                                return responseTask.Result;
                            });
    
                            if (resp != null)
                            {
                                string apnsResponseString = await resp.Content.ReadAsStringAsync();
    
                                handler.Dispose();
                            }
    
                            handler.Dispose();
                        }
                    }
                }
                catch (Exception ex)
                {
                    success = false;
                }
    
                return success;
            }
    
            private string GetProviderToken()
            {
                double epochNow = (int)DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1)).TotalSeconds;
                Dictionary<string, object> payload = new Dictionary<string, object>()
                {
                    { "iss", "YOUR APPLE TEAM ID" },
                    { "iat", epochNow }
                };
                var extraHeaders = new Dictionary<string, object>()
                {
                    { "kid", "YOUR AUTH KEY ID" },
                    { "alg", "ES256" }
                };
    
                CngKey privateKey = GetPrivateKey();
    
                return JWT.Encode(payload, privateKey, JwsAlgorithm.ES256, extraHeaders);
            }
    
            private CngKey GetPrivateKey()
            {
                using (var reader = File.OpenText(P8_PATH))
                {
                    ECPrivateKeyParameters 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);
                }
            }
        }
    }
Run Code Online (Sandbox Code Playgroud)

其次,如果您注意到的话,我正在使用自定义 WinHTTPHandler 来使代码基于如何使 .net HttpClient 使用 http 2.0? 来支持 HTTP/2?。我正在使用另一个类库创建它,请记住从 Nuget 下载 WinHTTPHandler。

    public class Http2CustomHandler : WinHttpHandler
    {
        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
        {
            request.Version = new Version("2.0");

            return base.SendAsync(request, cancellationToken);
        }
    }
Run Code Online (Sandbox Code Playgroud)

之后,只需调用ApplePushNotificationPush类上的“ SendNotification ” ,您就应该在 iPhone 上收到消息。