Skip to content

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

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

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

1. 安装所需包

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

powershell
Install-Package Newtonsoft.Json -Version 12.0.2

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

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

WXGetPhoneNumberResult.cs

csharp
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

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

WXWatermark.cs

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

csharp
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 字符串反序列化为实体。

csharp
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] 特性。

csharp
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. 测试代码

csharp
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

Page Layout Max Width

Adjust the exact value of the page width of VitePress layout to adapt to different reading needs and screens.

Adjust the maximum width of the page layout
A ranged slider for user to choose and customize their desired width of the maximum width of the page layout can go.

Content Layout Max Width

Adjust the exact value of the document content width of VitePress layout to adapt to different reading needs and screens.

Adjust the maximum width of the content layout
A ranged slider for user to choose and customize their desired width of the maximum width of the content layout can go.