[.NET Core] C#版微信小程序加密数据解密算法示例

微信官方文档 加密数据解密算法 中只提供了 c++NodePHPPython 四种语言的示例代码。

本文中的示例是基于 .NET Core 2.2C# 语言版本。

1. 安装所需包

用到了 Json 的反序列化,需要额外安装 Newtonsoft.Json 包。

Install-Package Newtonsoft.Json -Version 12.0.2

2. 创建解密后数据所需的模型

这里以获取用户手机号码的返回值为例。

WXGetPhoneNumberResult.cs

namespace WXBizDataCryptSample
{
    /// <summary>
    /// 微信获取手机号结果模型
    /// </summary>
    public class WXGetPhoneNumberResult : WXBaseWatermarkResult
    {
        /// <summary>
        /// 用户绑定的手机号(国外手机号会有区号)
        /// </summary>
        public string PhoneNumber { get; set; }

        /// <summary>
        /// 没有区号的手机号
        /// </summary>
        public string PurePhoneNumber { get; set; }

        /// <summary>
        /// 区号
        /// </summary>
        public string CountryCode { get; set; }
    }
}

WXBaseWatermarkResult.cs

namespace WXBizDataCryptSample
{
    /// <summary>
    /// 微信结果基类
    /// </summary>
    public class WXBaseWatermarkResult
    {
        /// <summary>
        /// 水印
        /// </summary>
        public WXWatermark Watermark { get; set; }
    }
}

WXWatermark.cs

其中 Appid 因为不想被前端获知,所以添加了 JsonIgnore 特性。
但是反序列化解密后的结果时还是需要该字段的,到时还要加一些额外的处理以指定其不被忽略。

using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using System;

namespace WXBizDataCryptSample
{
    /// <summary>
    /// 微信水印
    /// </summary>
    public class WXWatermark
    {
        /// <summary>
        /// AppID
        /// </summary>
        [JsonIgnore]
        public string Appid { get; set; }

        /// <summary>
        /// 时间戳(Unix时间)
        /// </summary>
        [JsonConverter(typeof(UnixDateTimeConverter))]
        public DateTime Timestamp { get; set; }
    }
}

3. 创建解密类

WXBizDataCrypt.cs

参考官方文档中其它语言版本的代码逻辑写的示例代码,其中使用 Newtonsoft.Json 将解密后的 Json 字符串反序列化为实体。

using Newtonsoft.Json;
using System;
using System.Security.Cryptography;
using System.Text;

namespace WXBizDataCryptSample
{
    /// <summary>
    /// 微信小程序用户加密数据的解密
    /// </summary>
    /// <typeparam name="T">解密后的实体类型(需继承自 WXBaseResult 类)</typeparam>
    public class WXBizDataCrypt<T> where T : WXBaseWatermarkResult
    {
        private string _appId;
        private string _sessionKey;

        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="appId">小程序的appid</param>
        /// <param name="sessionKey">用户在小程序登录后获取的会话密钥</param>
        public WXBizDataCrypt(string appId, string sessionKey)
        {
            _appId = appId;
            _sessionKey = sessionKey;
        }

        /// <summary>
        /// 检验数据的真实性,获取解密后的明文并反序列化.
        /// </summary>
        /// <param name="encryptedData">加密的用户数据</param>
        /// <param name="iv">与用户数据一同返回的初始向量</param>
        /// <returns>解密并反序列化后的实体(解密失败时返回实体类型的默认值)</returns>
        public T DecryptData(string encryptedData, string iv)
        {
            // 解密数据
            var result = Decrypt(encryptedData, iv);
            if (string.IsNullOrEmpty(result))
            {
                return default(T);
            }

            // 反序列化解密结果
            var jsonResolver = new PropertyIncludeSerializerContractResolver();
            jsonResolver.IncludeProperty(typeof(WXWatermark), "Appid");
            var serializerSettings = new JsonSerializerSettings();
            serializerSettings.ContractResolver = jsonResolver;
            var dataObj = JsonConvert.DeserializeObject<T>(result, serializerSettings);

            // 验证AppID
            if (dataObj.Watermark?.Appid != _appId)
            {
                return default(T);
            }

            return dataObj;
        }

        /// <summary>
        /// 使用AES解密用户数据
        /// </summary>
        /// <param name="encryptedData">加密的用户数据</param>
        /// <param name="iv">与用户数据一同返回的初始向量</param>
        /// <returns>解密后的字符串</returns>
        private string Decrypt(string encryptedData, string iv)
        {
            // 验证参数及密钥
            if (string.IsNullOrEmpty(_sessionKey) || _sessionKey.Length != 24)
            {
                return string.Empty;
            }
            if (string.IsNullOrEmpty(iv) || iv.Length != 24)
            {
                return string.Empty;
            }

            AesManaged aes = new AesManaged();
            aes.KeySize = 256;
            aes.BlockSize = 128;
            aes.Mode = CipherMode.CBC;
            aes.IV = Convert.FromBase64String(iv);
            aes.Key = Convert.FromBase64String(_sessionKey);
            aes.Padding = PaddingMode.PKCS7;

            var cipher = Convert.FromBase64String(encryptedData);
            byte[] decryptText = aes.CreateDecryptor().TransformFinalBlock(cipher, 0, cipher.Length);

            return Encoding.UTF8.GetString(decryptText);
        }
    }
}

PropertyIncludeSerializerContractResolver.cs

由于 WXWatermark.Appid 属性指定了 [JsonIgnore] 特性,反序列化时会忽略该字段。
但处理中需要用该字段做判断,需要在反序列化时不要忽略该属性。
PropertyIncludeSerializerContractResolver 类就是用来实现这个功能的。其可以在反序列化时指定某个类的某个字段始终不被忽略,不管是否指定了 [JsonIgnore] 特性。

using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using System;
using System.Collections.Generic;
using System.Reflection;

namespace WXBizDataCryptSample
{
    /// <summary>
    /// 属性包含序列化分解器
    /// </summary>
    public class PropertyIncludeSerializerContractResolver : DefaultContractResolver
    {
        /// <summary>
        /// 需要序列化的类型属性的字典
        /// </summary>
        private readonly Dictionary<Type, HashSet<string>> _includes;

        /// <summary>
        /// 构造函数
        /// </summary>
        public PropertyIncludeSerializerContractResolver()
        {
            _includes = new Dictionary<Type, HashSet<string>>();
        }

        /// <summary>
        /// 添加需要序列化的类型属性
        /// </summary>
        /// <param name="type"></param>
        /// <param name="jsonPropertyNames"></param>
        public void IncludeProperty(Type type, params string[] jsonPropertyNames)
        {
            if (!_includes.ContainsKey(type))
                _includes[type] = new HashSet<string>();

            foreach (var prop in jsonPropertyNames)
                _includes[type].Add(prop);
        }

        /// <summary>
        /// 重载 DefaultContractResolver.CreateProperty 方法
        /// </summary>
        /// <param name="member"></param>
        /// <param name="memberSerialization"></param>
        /// <returns></returns>
        protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
        {
            var property = base.CreateProperty(member, memberSerialization);

            if (IsInclude(property.DeclaringType, property.PropertyName))
            {
                property.ShouldSerialize = i => true;
                property.Ignored = false;
            }

            return property;
        }

        /// <summary>
        /// 判断属性是否在序列化属性字典中
        /// </summary>
        /// <param name="type"></param>
        /// <param name="jsonPropertyName"></param>
        /// <returns></returns>
        private bool IsInclude(Type type, string jsonPropertyName)
        {
            if (!_includes.ContainsKey(type))
                return false;

            return _includes[type].Contains(jsonPropertyName);
        }
    }
}

4. 测试代码

using Newtonsoft.Json;
using System;

namespace WXBizDataCryptSample
{
    class Program
    {
        static void Main(string[] args)
        {
            var crypt = new WXBizDataCrypt<WXGetPhoneNumberResult>(
                appId: TestData.APP_ID,
                sessionKey: TestData.SESSION_KEY);

            var result = crypt.DecryptData(
                encryptedData: TestData.ENCRYPTED_DATA,
                iv: TestData.IV);

            Console.WriteLine(JsonConvert.SerializeObject(result));

            Console.WriteLine("press <enter> to exit.");
            Console.ReadLine();
        }
    }
}

参考文档

  1. 加密数据解密算法
  2. C#でAES暗号化をやってみる
  3. Serializing Dates in JSON
  4. NewtonSoft add JSONIGNORE at runTime

© 2019, 佳佳. 版权所有. 未经作者同意,严禁转载。

发表评论

电子邮件地址不会被公开。 必填项已用*标注