C#实现基于MD5的软件注册码生成与验证全流程指南
2026/7/26 5:49:52 网站建设 项目流程

1. 项目概述与核心价值

做软件开发的,尤其是做桌面应用或者需要授权管理的工具,注册码(License Key)机制几乎是绕不开的一环。你可能已经尝试过一些简单的方案,比如硬编码一个字符串,或者用一些容易被逆向的算法。但很快就会发现,这些方法在稍微懂点技术的用户面前形同虚设。今天要聊的,就是基于C#和MD5来构建一个相对健壮、且易于实现的注册码生成与验证流程。这不仅仅是调用一个MD5.ComputeHash那么简单,里面涉及到盐值(Salt)的运用、信息编码、防篡改设计以及一系列新手极易踩坑的细节。

这个流程的核心价值在于,它提供了一种“低成本、中等级别”的防护。它无法对抗专业的、有组织的破解团队,但对于防止脚本小子和普通用户的随意共享,效果显著。整个方案完全基于C#原生类库,无需引入第三方加密组件,从生成到验证逻辑透明可控。接下来,我会拆解每一个步骤,告诉你为什么这么做,以及如何避开那些让整个机制失效的“坑”。

2. 核心设计思路与方案选型

在动手写代码之前,我们先得把设计思路理清楚。一个有效的注册码系统,目标不仅仅是生成一串看起来随机的字符,而是要达成几个关键目的:唯一性(每个用户或设备对应一个码)、防伪造(用户不能自己随便编一个有效的码)、防篡改(注册码中的关键信息不能被修改)以及可验证性(我们的程序能快速准确地判断码是否合法)。

2.1 为什么选择MD5?

首先,必须正视一个事实:MD5早已不是安全的哈希算法。在密码学领域,它因为碰撞漏洞(即不同的输入可能产生相同的哈希值)而被认为不安全,不应用于密码存储等安全敏感场景。但是,在注册码生成这个特定场景下,我们依然可以有限度地使用它,原因如下:

  1. 速度与资源消耗:MD5计算速度非常快,对CPU和内存的消耗极低。对于需要在客户端即时验证的注册码场景,这是一个重要优点。
  2. 固定长度输出:无论输入多长,MD5总是生成一个128位(16字节)的哈希值,这为我们生成固定长度格式的注册码提供了便利。
  3. 单向性与混淆:虽然能碰撞,但给定一个MD5结果,反向推导出原始输入在计算上仍然是困难的(需要彩虹表或暴力破解)。我们的目的不是防止学术上的碰撞,而是增加普通用户伪造和理解的难度。
  4. 场景匹配:注册码验证是“已知正确结果,比对用户输入”的过程。我们对比的是整个字符串的完整性,而不是破解哈希值。攻击者更可能去破解验证逻辑本身(如Patch掉跳转指令),而不是去碰撞一个由“机器码+盐值”生成的MD5。

所以,我们的定位是:利用MD5的哈希特性来生成一个校验和(Checksum),用于保护我们编码在注册码中的核心信息(如过期时间、版本号),而不是依赖MD5本身来保密。保密性由“盐值”和“核心信息的编码方式”共同提供。

2.2 核心流程设计

整个流程分为两个独立的部分:生成器(Generator)验证器(Verifier)

  • 生成器:通常是一个离线工具,由软件开发者自己持有。它接收用户提供的“机器码”或“授权信息”,结合私有的“盐值”,生成最终的注册码。
  • 验证器:内嵌在发布的软件客户端中。它读取用户输入的注册码,使用同样的逻辑进行解码和验证。

一个健壮的流程设计如下:

  1. 组装原始信息:将需要授权的信息(如用户邮箱、版本号、过期时间戳)按照预定格式拼接成一个字符串。
  2. 加盐:将一个只有开发者知道的、足够长且复杂的“盐值”字符串,与原始信息拼接。
  3. MD5哈希:对加盐后的字符串进行MD5计算,得到16字节的哈希值。
  4. 组合与编码:将原始信息(或它的某种摘要)与MD5哈希值的一部分组合起来,然后通过Base64或自定义的字母表进行编码,生成最终的用户友好字符串(通常分组,如XXXX-XXXX-XXXX-XXXX)。
  5. 验证:客户端收到注册码后,反向解码得到原始信息和哈希片段。然后,它使用同样的盐值对解码出的原始信息重新进行MD5计算,并比对计算出的哈希片段与解码出的哈希片段是否一致。同时,还会校验原始信息中的内容(如过期时间是否有效)。

这个设计的精髓在于:注册码的有效性,取决于用户是否知道用于生成哈希的“盐值”和“组合编码规则”。只要盐值不泄露,攻击者就无法为任意信息生成合法的校验码。

3. 关键实现细节与避坑指南

理解了设计思路,我们进入具体的C#实现环节。这里每一步都有需要注意的细节。

3.1 信息格式化与盐值管理

原始信息不能随便拼接。例如,我们要授权给用户user@example.com,使用专业版,有效期至2024-12-31。一个简单的拼接是:“user@example.com|PRO|20241231”。这里使用竖线|作为分隔符是个好习惯,因为它不太可能出现在邮箱或版本号中。

盐值(Salt)的选择与管理是安全的核心

  • 绝对不要硬编码在客户端:这是最常见的错误。如果你的盐值字符串直接以const string salt = “mySecretSalt”;的形式写在验证代码里,破解者用反编译工具(如dnSpy)几分钟就能找到它。
  • 建议做法
    1. 动态获取:盐值可以从服务器端API动态获取(首次验证时),或者由安装程序在安装时写入一个隐蔽的配置文件或注册表项。
    2. 分段混淆:将盐值拆分成多个部分,分散在代码的不同位置,运行时再组合。
    3. 与环境绑定:盐值的一部分可以来源于机器特征(如硬盘序列号、主板ID),但要注意这可能导致用户更换硬件后注册码失效,需要配套的授权转移机制。

避坑指南1:盐值硬编码我曾在一个早期项目里把盐值直接写在字符串常量里。结果软件发布后不到一周,注册机就满天飞了。破解者直接搜索这个常量字符串,然后自己写了个生成工具。教训就是:盐值必须被当作最高机密来保护,其存储和加载方式需要设计一定的反逆向手段。

3.2 MD5计算与字节处理

在C#中,使用System.Security.Cryptography.MD5类进行计算。这里要注意字符编码问题。

using System.Security.Cryptography; using System.Text; public static string CalculateMD5Hash(string input) { // 明确指定编码格式,UTF-8是通用选择 using (var md5 = MD5.Create()) { byte[] inputBytes = Encoding.UTF8.GetBytes(input); byte[] hashBytes = md5.ComputeHash(inputBytes); // 后续需要将字节数组转换为字符串,注意不要用ToString() // 通常我们会转换为16进制字符串或Base64 StringBuilder sb = new StringBuilder(); for (int i = 0; i < hashBytes.Length; i++) { // 格式化为两位十六进制,不足补零 sb.Append(hashBytes[i].ToString(“x2”)); } return sb.ToString(); } }

关键点

  • using语句:确保MD5实例被正确释放。
  • 编码一致:生成和验证时,必须使用相同的字符编码(如UTF-8)。如果生成时用UTF-8,验证时用ASCII,哈希结果会完全不同。
  • 哈希输出ComputeHash返回的是byte[]。我们通常将其转换为16进制字符串(如a1b2c3…)或Base64字符串。16进制更常见,因为长度固定(32字符),且易于分割处理。

3.3 注册码的组装与格式化

直接使用32位的MD5十六进制字符串作为注册码并不友好。我们通常会将原始信息(或它的一个简短表示)和部分哈希值组合,并进行格式化。

一种常见的组合方式是:

  1. 将原始信息字符串(如“user@example.com|PRO|20241231”)先进行一次MD5,取其前8位字符作为“信息摘要”。
  2. 将“信息摘要” + “盐值” + “原始信息” 拼接,再进行一次MD5,得到完整的“校验哈希”。
  3. 取“校验哈希”的前12位字符。
  4. 将“信息摘要”(8位)和“校验哈希片段”(12位)组合,得到20位字符。
  5. 将这20位字符每5位一组,用连字符连接,形成最终注册码,例如A1B2C-D3E4F-56789-GHJK0

为什么这么做?

  • 包含信息:“信息摘要”代表了授权的核心内容。在验证时,我们可以从中解析出用户邮箱或版本类型(如果设计得当)。
  • 双重校验:校验哈希是由“信息摘要+盐值+原始信息”生成的,任何一部分被篡改都会导致验证失败。
  • 用户友好:分组后的字符串更易于阅读和输入。

避坑指南2:信息泄露早期我试过把完整的过期时间20241231明文放在注册码的可解码部分。结果有用户发现,通过修改系统时间就能绕过过期验证。这是因为客户端只验证了MD5校验和,而校验和是基于这个明文时间生成的,用户只要同时修改时间和注册码中的明文部分即可。后来改为将时间戳也参与生成“信息摘要”,并且验证时直接使用解码出的时间与当前系统时间比对,解决了这个问题。核心是:不要信任客户端传来的、可用于逻辑判断的明文信息,必须有其防篡改的校验机制。

3.4 Base64与自定义编码

除了十六进制,Base64也是常用的编码方式,它更紧凑(将3字节编码为4字符)。但Base64可能包含+/等URL不友好字符,以及末尾的=填充符。

// 将MD5的字节数组直接转为Base64 string base64Hash = Convert.ToBase64String(hashBytes); // 输出可能类似 “qZk+NkcGgWq6PiVxeFDCbJzQ2J0=”

为了生成更干净的注册码,我们可以使用自定义的字母表进行“类Base64”编码,例如只使用大写字母和数字,去掉容易混淆的0O1I等。

private static readonly char[] CustomAlphabet = “ABCDEFGHJKLMNPQRSTUVWXYZ23456789”.ToCharArray(); // 32个字符,相当于Base32 public static string ToCustomBase32(byte[] data) { // … 实现Base32编码逻辑 … // 将5位一组转换为自定义字母表中的一个字符 }

使用自定义编码的好处是注册码看起来更规整,且完全由易于手动输入和识别的字符组成。

4. 完整代码实现与分步解析

下面,我将展示一个相对完整的、包含关键环节的示例。为了清晰,我将生成器和验证器的核心逻辑分开。

4.1 注册码生成器实现

假设我们的授权信息包括:用户名、版本、过期天数。盐值从外部配置文件读取。

using System; using System.Security.Cryptography; using System.Text; public class LicenseGenerator { private readonly string _secretSalt; public LicenseGenerator(string secretSalt) { _secretSalt = secretSalt ?? throw new ArgumentNullException(nameof(secretSalt)); } public string GenerateLicense(string userName, string edition, int validDays) { // 1. 组装原始信息 DateTime expiryDate = DateTime.UtcNow.AddDays(validDays); string rawInfo = $“{userName}|{edition}|{expiryDate:yyyyMMdd}”; // 2. 生成信息摘要 (MD5的前8位十六进制) string infoDigest = GetMD5Hex(rawInfo).Substring(0, 8); // 3. 生成完整校验哈希 (信息摘要 + 盐值 + 原始信息) string stringToHash = $“{infoDigest}{_secretSalt}{rawInfo}”; string fullHash = GetMD5Hex(stringToHash); // 4. 取校验哈希前12位 string checksumPart = fullHash.Substring(0, 12); // 5. 组合并格式化 string combined = $“{infoDigest}{checksumPart}”; // 共20位 return FormatLicenseKey(combined); } private string GetMD5Hex(string input) { using (var md5 = MD5.Create()) { byte[] bytes = Encoding.UTF8.GetBytes(input); byte[] hashBytes = md5.ComputeHash(bytes); return BitConverter.ToString(hashBytes).Replace(“-“, “”).ToLowerInvariant(); } } private string FormatLicenseKey(string input) { // 简单按5位一组分割 return string.Join(“-“, new[] { input.Substring(0, 5), input.Substring(5, 5), input.Substring(10, 5), input.Substring(15, 5) }); } } // 使用示例 var generator = new LicenseGenerator(“YourLongAndComplexSecretSalt@2024!”); string license = generator.GenerateLicense(“customer@mail.com”, “Professional”, 365); Console.WriteLine($“生成的注册码:{license}”);

4.2 注册码验证器实现

验证器需要实现反向操作:解格式化、分离部件、重新计算并比对。

public class LicenseValidator { private readonly string _secretSalt; public LicenseValidator(string secretSalt) { _secretSalt = secretSalt; } public ValidationResult Validate(string licenseKey) { // 1. 去除格式,还原组合字符串 string cleanKey = licenseKey?.Replace(“-“, “”).ToLowerInvariant(); if (string.IsNullOrEmpty(cleanKey) || cleanKey.Length != 20) { return ValidationResult.Invalid(“注册码格式错误”); } // 2. 分离信息摘要和校验部分 string infoDigest = cleanKey.Substring(0, 8); string checksumPart = cleanKey.Substring(8, 12); // 3. 这里有个关键点:我们无法直接从infoDigest反推出rawInfo。 // 我们需要尝试解码或从其他地方获取原始信息。 // 一种常见做法是将用户名或机器码作为输入参数传入。 // 这里假设我们从licenseKey中无法解析,需要外部提供userName和edition。 // 这是一个设计缺陷的体现。更好的设计是将关键信息编码在注册码中并可安全解码。 // 为了示例,我们假设通过一个解密函数能从infoDigest或整个key中还原出rawInfo。 // 让我们调整设计:在生成时,将rawInfo的Base64编码(或加密后)替换infoDigest。 // 由于篇幅,我们采用一个简化可逆的示例:将rawInfo做简单混淆后放入前8位并不安全。 // 更安全的做法是使用对称加密(如AES)加密rawInfo,将密文作为一部分放入注册码。 // 4. 假设我们通过其他方式获得了rawInfo(例如,软件要求用户输入邮箱,并与注册码绑定) // 验证逻辑变为:使用提供的userName, edition, expiryDate重新组装rawInfo,然后重复生成步骤,看得到的checksumPart是否匹配。 // 这要求验证器知道这些信息。 return ValidationResult.Invalid(“演示代码,需补充完整信息还原逻辑”); } // 一个更实用的验证方法,需要用户提供其标识信息 public ValidationResult ValidateWithUserInfo(string licenseKey, string expectedUserName, string expectedEdition) { string cleanKey = licenseKey?.Replace(“-“, “”).ToLowerInvariant(); if (string.IsNullOrEmpty(cleanKey) || cleanKey.Length != 20) return ValidationResult.Invalid(“格式错误”); string infoDigestPart = cleanKey.Substring(0, 8); string checksumPart = cleanKey.Substring(8, 12); // 尝试用已知信息构造rawInfo。我们需要知道过期时间。 // 但过期时间在rawInfo里。我们陷入了循环。 // 这说明我们的设计需要调整,让rawInfo或过期时间可以被安全地提取出来验证。 } } public class ValidationResult { public bool IsValid { get; } public string Message { get; } public DateTime? ExpiryDate { get; } private ValidationResult(bool isValid, string message, DateTime? expiryDate = null) { IsValid = isValid; Message = message; ExpiryDate = expiryDate; } public static ValidationResult Valid(DateTime expiryDate) => new ValidationResult(true, “有效”, expiryDate); public static ValidationResult Invalid(string reason) => new ValidationResult(false, reason); }

上面的验证器代码暴露了一个关键设计问题:验证方如何无损地获得生成注册码时使用的rawInfo?如果无法获得,就无法重新计算哈希进行比对。

4.3 改进方案:将信息编码进注册码

为了解决这个问题,我们需要修改生成逻辑,将必要的授权信息(如用户名、过期日)以一种可解码但防篡改的方式放入注册码。

改进后的生成思路

  1. 构造rawInfo字符串(如“user@example.com|PRO|20241231”)。
  2. rawInfo进行对称加密(例如使用AES),得到一个密文字节数组。加密密钥是另一个秘密(不同于MD5的盐值)。
  3. 将密文字节数组转换为十六进制字符串,作为注册码的“信息块”。
  4. 将“信息块” +_secretSalt进行MD5,取前12位作为“校验块”。
  5. 将“信息块”和“校验块”组合、格式化。

验证时

  1. 解格式化,分离“信息块”和“校验块”。
  2. 用同样的AES密钥解密“信息块”,得到rawInfo明文。如果解密失败,说明信息块被篡改或密钥错误。
  3. 从解密出的rawInfo中解析出用户名、过期时间等。
  4. 将“信息块” +_secretSalt进行MD5,计算前12位,与用户注册码中的“校验块”比对。一致则通过。
  5. 额外验证rawInfo中的内容,如过期时间是否晚于当前时间。

这样,验证方只需要持有AES密钥和MD5盐值,就能独立完成解密和校验,无需用户再提供额外信息。注册码本身是自包含的。

避坑指南3:时间验证与时钟篡改即使用户无法篡改注册码,他还可以篡改Windows系统时间。如果你的软件只检查“过期时间 > 当前系统时间”,那么把系统时间调回过去,软件就永远不过期。一个缓解方案是:在首次激活或定期(如每次启动)时,通过网络时间协议(NTP)获取一个可信的服务器时间进行比对。虽然不能完全杜绝(用户可以断网或拦截NTP请求),但提高了破解门槛。另一种方案是将首次激活的时间点加密后存储在本地,之后根据这个基准点和软件运行时长来计算是否过期。

5. 增强安全性的进阶策略

基础的MD5加盐验证可以挡住大部分普通用户,但对于稍有经验的破解者,他们可能会直接使用调试器(如OllyDbg, x64dbg)或.NET反编译工具(如dnSpy, ILSpy)来分析和修改你的验证逻辑。以下是一些进阶的加固思路:

5.1 代码混淆与反调试

  • 使用混淆工具:对编译后的.NET程序集进行混淆,重命名类、方法、变量名为无意义的字符,增加字符串加密,控制流扁平化等,使得反编译后的代码难以阅读。商业工具如DotfuscatorObfuscar,或开源工具如ConfuserEx
  • 集成反调试检测:在代码中插入检查是否被调试器附加的代码。如果检测到调试器,可以静默退出、执行错误逻辑或触发延迟。
    if (System.Diagnostics.Debugger.IsAttached) { // 触发一些无害但令人困惑的行为,或者直接退出 Environment.FailFast(“Anti-debug triggered”); }
    注意:有经验的破解者会绕过这些检查,但这增加了他们的工作量。

5.2 验证逻辑分散与动态化

  • 不要有一个集中的ValidateLicense()方法:将验证逻辑打散,分布到程序启动、各个功能模块调用前等多个地方。例如,在软件主窗体加载时验证一次,在点击某个高级功能按钮时再验证一次(验证的可能是注册码的不同部分)。
  • 动态计算关键值:不要将盐值或AES密钥以完整的静态字符串形式存在内存中。可以在运行时通过多个不相关的计算过程动态拼接出来。
  • 使用哈希链:注册码的验证可以依赖于之前某次验证的结果(存储在加密的配置文件中),形成一条链。单次破解验证点可能无法使整个软件解锁。

5.3 在线验证与激活机制

最高级别的保护是引入在线服务。即使采用上述所有本地保护措施,一个完全离线的软件最终也是可能被破解的(例如被做成“破解补丁”)。

  • 在线激活:用户输入注册码后,软件将注册码和本机特征码(如CPU ID、硬盘序列号哈希)发送到你的服务器。服务器验证注册码的有效性、绑定机器,并返回一个针对本机加密的“激活文件”或令牌。客户端软件依赖这个本地令牌运行。
  • 定期心跳:软件运行时定期(如每周)与服务器通信,验证令牌是否有效、授权是否被撤销。这可以应对注册码泄露后的批量使用。
  • 差异化服务:将核心功能放在云端,本地软件只是一个客户端。没有有效的在线账户/令牌,就无法使用核心功能。

当然,在线验证意味着你需要开发和维护一个后端服务,并且软件需要网络权限。这适用于商业软件,对于小型或个人工具可能过于繁重。

6. 常见问题排查与实战技巧

在实际开发和部署过程中,你肯定会遇到各种各样的问题。下面是一些典型场景和解决方法。

6.1 注册码验证不一致

问题描述:用生成器生成的码,在客户端验证总是失败。

排查步骤

  1. 检查盐值:99%的问题出在这里。确保生成器和验证器使用的是完全相同的盐值字符串,包括大小写、空格和特殊字符。最好将盐值保存在一个配置文件中,两边引用同一份文件。
  2. 检查编码:确保MD5计算前字符串的编码一致。都是UTF-8还是ASCII?在生成和验证的代码里打印出(或日志记录)待计算哈希的字符串的字节数组,进行比对。
  3. 检查信息格式:确保组装rawInfo的格式完全一致。分隔符是|还是,?日期格式是yyyyMMdd还是yyyy-MM-dd?是否包含不必要的空格或换行符?
  4. 检查大小写:十六进制字符串比较时是否区分大小写?你的ToLowerInvariantToUpperInvariant用对地方了吗?
  5. 检查步骤:逐步调试验证器,将每一步得到的中间结果(解格式后的字符串、分离出的信息块、重新计算出的哈希)与生成器的中间结果对比。

6.2 如何应对“注册机”

问题描述:即使采用了复杂逻辑,还是出现了针对你软件的注册机。

应对策略

  1. 升级算法:考虑使用更安全的哈希算法,如SHA256或SHA3,虽然计算稍慢,但对抗彩虹表更有效。但本质上,如果盐值泄露或逻辑被逆向,换算法作用有限。
  2. 变更方案:定期(如每个大版本)更换盐值、加密密钥甚至注册码的格式规则。让旧版本的注册机对新版本软件失效。但这会带来兼容性问题,需要妥善处理老用户的升级。
  3. 转向在线验证:这是最根本的解决方法。本地验证终究是“把锁放在用户家里”。
  4. 法律与技术结合:对于商业软件,在用户协议中明确禁止逆向工程和破解。虽然执行困难,但有一定的威慑作用。

6.3 用户环境问题

问题描述:用户反映注册码在A电脑有效,在B电脑无效;或者重装系统后失效。

解决方案

  1. 明确授权对象:你的注册码是绑定给“用户”还是“设备”?如果是设备,就需要在生成注册码时加入该设备的唯一标识(如硬盘序列号、网卡MAC地址的哈希值)。这就是常说的“机器码”。验证时,软件读取本机机器码进行比对。
    • 优点:防止一个码多处使用。
    • 缺点:用户更换硬件或重装系统可能导致失效,需要提供授权转移流程(如通过在线账户解绑/重新绑定)。
  2. 提供转移机制:设计一个授权管理后台,允许用户手动解绑旧设备,然后在新设备上激活。这需要在线功能支持。
  3. 清晰的错误提示:不要只是提示“注册码无效”。可以根据验证失败的不同阶段,给出更友好的提示,如“注册码格式错误”、“注册码与当前设备不匹配”、“注册码已过期”等。这能减少用户困惑和支持压力。

6.4 性能考量

问题描述:在软件启动时进行复杂的验证(如多次哈希、解密),导致启动变慢。

优化技巧

  1. 缓存验证结果:首次验证通过后,将一个加密的、带时间戳的验证结果缓存到本地文件或注册表。下次启动时,先检查缓存的有效性(如是否在24小时内),如果有效则跳过完整验证流程。
  2. 延迟验证:不要在启动时就验证所有功能。只在用户尝试使用受保护的核心功能时,才触发该功能的许可验证。
  3. 异步验证:将验证操作放在后台线程执行,避免阻塞UI线程导致界面卡顿。

7. 一个更完整的示例框架

结合上述所有讨论,这里给出一个更健壮、包含信息加密的示例框架概要。请注意,这仍然是简化版,用于展示完整流程。

核心类设计

  • LicenseData:包含用户名、邮箱、版本、生成时间、过期时间等授权信息的数据对象。
  • LicenseCryptoService:负责LicenseData对象的对称加密/解密(使用AES)。
  • LicenseGenerator:使用LicenseCryptoService加密信息,并利用MD5加盐生成校验和,最终格式化输出注册码。
  • LicenseValidator:解格式化注册码,利用MD5校验和验证完整性,使用LicenseCryptoService解密信息,并验证业务逻辑(如过期时间)。

关键流程

  1. 生成端
    • 创建LicenseData对象,填充信息。
    • 用AES加密LicenseData(序列化为JSON或二进制),得到encryptedData
    • 计算MD5(encryptedData + _md5Salt),取前N位作为checksum
    • 组合encryptedDataHex + checksum,进行Base32或自定义编码,并格式化分组。
  2. 验证端
    • 去除格式,解码得到encryptedDataHexchecksum
    • 重新计算MD5(encryptedDataHex + _md5Salt),得到calculatedChecksum,与checksum比对。不一致则立即失败。
    • 用AES解密encryptedDataHex,得到LicenseData对象。解密失败则失败。
    • 验证LicenseData中的信息:过期时间、版本是否匹配等。

这个框架将信息保密(AES)、完整性校验(MD5+Salt)和业务验证分离,结构清晰,安全性也相对更高。实现这个框架需要你具备AES加密和JSON序列化的知识,这将是构建一个真正可用授权系统的基础。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询