系列文章:
接上篇文章,在这篇文章中我将提供授权码生成与验证的示例代码,实现离线软件授权码的生成与验证机制。
这个代码包将涵盖以下关键技术点:
RSA 加密算法的使用;
获取硬件机器码信息;
获取当前网络时间;
授权码签名与验证。
咱们话不多说,直接进入主题。
1. 客户端提供机器码
使用下面的方法来获取机器码,并将机器码信息发送至服务器以生成授权码。
示例代码如下:
// GetHashedMachineID 获取哈希后的机器标识
func GetHashedMachineID() (string, error) {
// 根据操作系统类型选择合适的命令来获取机器标识
var cmd *exec.Cmd
switch runtime.GOOS {
case "windows":
cmd = exec.Command("wmic", "csproduct", "get", "UUID")
case "darwin":
cmd = exec.Command("system_profiler", "SPHardwareDataType")
default:
cmd = exec.Command("cat", "/etc/machine-id")
}
// 执行命令并捕获输出
output, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("无法获取机器标识: %v", err)
}
// 去除多余的空白字符
machineID := strings.TrimSpace(string(output))
// 针对 Windows 系统特殊处理,提取 UUID 部分
if runtime.GOOS == "windows" {
lines := strings.Split(machineID, "\n")
if len(lines) > 1 {
machineID = strings.TrimSpace(lines[1])
}
}
// 使用 SHA-256 哈希算法对机器标识进行哈希处理
hasher := sha256.New()
hasher.Write([]byte(machineID))
hashedID := hex.EncodeToString(hasher.Sum(nil))
return hashedID, nil
}
使用示例:
func main() {
hardwareID, err := softwareverify.GetHashedMachineID()
if err != nil {
fmt.Println("获取机器码失败:", err)
return
}
fmt.Println("机器码:", hardwareID)
}
输出:
机器码: f840c3221e54b90e305dc6a03ce3a1716a53c8f1ca98169dcff3917ed5600c79
2. 服务端生成授权码
软件验证包(softwareverify) 中的 server.go:
package softwareverify
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"fmt"
"time"
)
// GenerateRSAKeys 生成 RSA 密钥对
func GenerateRSAKeys() (*rsa.PrivateKey, *rsa.PublicKey, error) {
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, nil, err
}
return privateKey, &privateKey.PublicKey, nil
}
// GenerateLicense 生成授权码
func GenerateLicense(privateKey *rsa.PrivateKey, hardwareID string, validityDays int) (string, error) {
expiration := time.Now().AddDate(0, 0, validityDays).Unix()
licenseData := fmt.Sprintf("%s:%d", hardwareID, expiration)
// 使用私钥签名授权数据
hashed := sha256.Sum256([]byte(licenseData))
signature, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, hashed[:])
if err != nil {
return "", err
}
// 编码授权码(授权数据 + 签名)
license := fmt.Sprintf("%s:%s", licenseData, base64.StdEncoding.EncodeToString(signature))
return license, nil
}
// ExportPublicKey 导出公钥
func ExportPublicKey(pubKey *rsa.PublicKey) (string, error) {
pubASN1, err := x509.MarshalPKIXPublicKey(pubKey)
if err != nil {
return "", err
}
pubPEM := pem.EncodeToMemory(&pem.Block{
Type: "RSA PUBLIC KEY",
Bytes: pubASN1,
})
return string(pubPEM), nil
}
使用示例:
func main() {
// 硬件机器码示例(客户提供的硬件机器码)
hardwareID := "f840c3221e54b90e305dc6a03ce3a1716a53c8f1ca98169dcff3917ed5600c79"
// 授权天数
validityDays := 30
// 生成 RSA 密钥对
privateKey, publicKey, err := softwareverify.GenerateRSAKeys()
if err != nil {
fmt.Println("生成密钥失败:", err)
return
}
// 生成授权码
license, err := softwareverify.GenerateLicense(privateKey, hardwareID, validityDays)
if err != nil {
fmt.Println("生成授权码失败:", err)
return
}
fmt.Println("授权码:")
fmt.Println(license)
// 导出并保存公钥
pubKeyStr, _ := softwareverify.ExportPublicKey(publicKey)
fmt.Println("公钥:")
fmt.Println(pubKeyStr)
}
输出:
授权码:
f840c3221e54b90e305dc6a03ce3a1716a53c8f1ca98169dcff3917ed5600c79:1734582971:HvUBFbL8bacUhuVYYZ9N/BDXQ5ns15Q+t/MpwoRcZLaup7ltAxQ6NrhCBU1C5JfmmMb0MgYNk2NYogfLd7GdcFAx/eLjxx+SpAQ4MfG5rb3fHDWhHnKhyt37cn8RbvhqlOrmCwdv+28oaG3fpd2aGKt3BC/wx9kg/lZjiH1E8Fl4IhQramP/sS0qRxWWhy14uLvkFz6xBfSH8LYL7Lj6qT2erDSElnwpnG3VaLGIsJgZFzDe6q+oh/PfTGvY9NADQy/rffNbHmObdc0zOEsDgDpSf9fGS+IV2jhA1jbnoRfOD3R+qU9+7x+g4vY3bKulWi/BD9DPiWlA/uyzjB3eFQ==
公钥:
-----BEGIN RSA PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAngACnt+cr3Yijlgw1wTa
6LkM+7dXQu/9a+PckTSZtN2QZBuxvu6sLempuRR9BbB20VF4L86pg8KuvpwXliJf
mcdpVfn2aucFg0/JtvNdRoMembwoP2sFtYoCVbgHWdhlI8Z5k6ovbg06XGCy2ZbR
9epINx1IaFdqnrdGW2vJ4jBXdOlln5Tc9zojSTkSK59ONpdvpQCDLaAFDm9/Hu/E
OVenPWZ86THfiiRFpzu6qxQCOadNeSbK1d3PE6YSnUEyxk63Ny/F+6hXv2IT3JJw
c9/wkEikr21u9dCSO5PT80jCC8WFS7xz1Em4KczsSfAxoMtqxDX6pPuJzfcLoO7B
8wIDAQAB
-----END RSA PUBLIC KEY-----
3. 客户端验证授权码
在客户端,咱们使用服务器提供的公钥对授权码进行验证。
提取出授权数据和签名,然后通过公钥解密验证签名的有效性。
软件验证包(softwareverify) 中的 client.go:
package softwareverify
import (
"crypto"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/pem"
"errors"
"fmt"
"net/http"
"os/exec"
"runtime"
"strconv"
"strings"
"time"
)
// 定义自定义错误类型
var (
ErrInvalidPublicKey = errors.New("无效的公钥")
ErrInvalidLicenseFormat = errors.New("授权码格式错误")
ErrInvalidSignature = errors.New("授权签名验证失败")
ErrInvalidTimestamp = errors.New("授权时间验证失败")
ErrExpiredLicense = errors.New("授权已过期")
ErrHardwareIDMismatch = errors.New("硬件机器码不匹配")
)
// ImportPublicKey 导入公钥
func ImportPublicKey(pubPEM string) (*rsa.PublicKey, error) {
block, _ := pem.Decode([]byte(pubPEM))
if block == nil || block.Type != "RSA PUBLIC KEY" {
return nil, ErrInvalidPublicKey
}
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return nil, err
}
return pub.(*rsa.PublicKey), nil
}
// VerifyLicense 验证授权码
func VerifyLicense(pubKey *rsa.PublicKey, license, hardwareID string) (bool, error) {
parts := strings.Split(license, ":")
if len(parts) != 3 {
return false, ErrInvalidLicenseFormat
}
// 提取授权数据和签名
licenseData := parts[0] + ":" + parts[1]
// 解码签名
signature, err := base64.StdEncoding.DecodeString(parts[2])
if err != nil {
return false, fmt.Errorf("base64 decode failed: %v", err)
}
// 获取网络时间
currentTimeUnix, err := GetNetworkTime()
if err != nil {
return false, ErrInvalidTimestamp
}
// 校验硬件 ID 和有效期
expiration, err := strconv.ParseInt(parts[1], 10, 64)
if err != nil || currentTimeUnix > expiration {
return false, ErrExpiredLicense
}
if parts[0] != hardwareID {
return false, ErrHardwareIDMismatch
}
// 验证签名
hashed := sha256.Sum256([]byte(licenseData))
err = rsa.VerifyPKCS1v15(pubKey, crypto.SHA256, hashed[:], signature)
if err != nil {
return false, ErrInvalidSignature
}
return true, nil
}
// GetNetworkTime 获取网络时间
func GetNetworkTime() (int64, error) {
maxRetries := 3
retryDelay := 500 * time.Millisecond
client := &http.Client{Timeout: 5 * time.Second}
var dateHeader string
var resp *http.Response
var err error
// 尝试从网络获取时间
for i := 0; i < 3; i++ {
req, err := http.NewRequest("GET", "http://www.baidu.com", nil)
if err != nil {
return 0, errors.New("创建请求失败: " + err.Error())
}
resp, err = client.Do(req)
if err != nil {
if i == maxRetries-1 {
return 0, errors.New("请求失败(重试 " + fmt.Sprintf("%d/%d", i+1, maxRetries) + "): " + err.Error())
}
time.Sleep(retryDelay) // 等待一段时间后重试
continue
}
// 获取响应头中的日期
dateHeader = resp.Header.Get("Date")
if dateHeader != "" {
break
}
if i == maxRetries-1 {
return 0, errors.New("未找到 Date 头部信息,尝试使用本地时间")
}
}
// 确保在函数返回前关闭响应体
defer func() {
if resp != nil && resp.Body != nil {
resp.Body.Close()
}
}()
// 如果网络时间获取失败,则返回本地时间
if dateHeader == "" {
return 0, errors.New("网络时间不可用")
}
// 解析日期为时间戳
t, err := time.Parse(time.RFC1123, dateHeader)
if err != nil {
return 0, errors.New("日期解析失败: " + err.Error())
}
// 返回东八区(北京时间)时间戳
beijingTime := t.In(time.FixedZone("CST", 8*3600))
return beijingTime.Unix(), nil
}
// GetHashedMachineID 获取哈希后的机器标识
func GetHashedMachineID() (string, error) {
// 根据操作系统类型选择合适的命令来获取机器标识
var cmd *exec.Cmd
switch runtime.GOOS {
case "windows":
cmd = exec.Command("wmic", "csproduct", "get", "UUID")
case "darwin":
cmd = exec.Command("system_profiler", "SPHardwareDataType")
default:
cmd = exec.Command("cat", "/etc/machine-id")
}
// 执行命令并捕获输出
output, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("无法获取机器标识: %v", err)
}
// 去除多余的空白字符
machineID := strings.TrimSpace(string(output))
// 针对 Windows 系统特殊处理,提取 UUID 部分
if runtime.GOOS == "windows" {
lines := strings.Split(machineID, "\n")
if len(lines) > 1 {
machineID = strings.TrimSpace(lines[1])
}
}
// 使用 SHA-256 哈希算法对机器标识进行哈希处理
hasher := sha256.New()
hasher.Write([]byte(machineID))
hashedID := hex.EncodeToString(hasher.Sum(nil))
return hashedID, nil
}
使用示例:
func main() {
// 示例公钥(应从服务端获得)
pubPEM := `
-----BEGIN RSA PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAngACnt+cr3Yijlgw1wTa
6LkM+7dXQu/9a+PckTSZtN2QZBuxvu6sLempuRR9BbB20VF4L86pg8KuvpwXliJf
mcdpVfn2aucFg0/JtvNdRoMembwoP2sFtYoCVbgHWdhlI8Z5k6ovbg06XGCy2ZbR
9epINx1IaFdqnrdGW2vJ4jBXdOlln5Tc9zojSTkSK59ONpdvpQCDLaAFDm9/Hu/E
OVenPWZ86THfiiRFpzu6qxQCOadNeSbK1d3PE6YSnUEyxk63Ny/F+6hXv2IT3JJw
c9/wkEikr21u9dCSO5PT80jCC8WFS7xz1Em4KczsSfAxoMtqxDX6pPuJzfcLoO7B
8wIDAQAB
-----END RSA PUBLIC KEY-----
`
license := "f840c3221e54b90e305dc6a03ce3a1716a53c8f1ca98169dcff3917ed5600c79:1734582971:HvUBFbL8bacUhuVYYZ9N/BDXQ5ns15Q+t/MpwoRcZLaup7ltAxQ6NrhCBU1C5JfmmMb0MgYNk2NYogfLd7GdcFAx/eLjxx+SpAQ4MfG5rb3fHDWhHnKhyt37cn8RbvhqlOrmCwdv+28oaG3fpd2aGKt3BC/wx9kg/lZjiH1E8Fl4IhQramP/sS0qRxWWhy14uLvkFz6xBfSH8LYL7Lj6qT2erDSElnwpnG3VaLGIsJgZFzDe6q+oh/PfTGvY9NADQy/rffNbHmObdc0zOEsDgDpSf9fGS+IV2jhA1jbnoRfOD3R+qU9+7x+g4vY3bKulWi/BD9DPiWlA/uyzjB3eFQ=="
// 导入公钥
publicKey, err := softwareverify.ImportPublicKey(pubPEM)
if err != nil {
fmt.Println("导入公钥失败:", err)
return
}
hardwareID, err := softwareverify.GetHashedMachineID()
if err != nil {
fmt.Println("获取机器码失败:", err)
return
}
// 验证授权码
valid, err := softwareverify.VerifyLicense(publicKey, license, hardwareID)
if err != nil {
fmt.Println("授权验证失败:", err)
} else if valid {
fmt.Println("授权验证成功,授权有效")
}
}
输出:
授权验证成功,授权有效
到这儿,客户端就验证成功了。
小结
通过代码示例详细的阐述了授权码的生成和验证流程。
具体步骤如下:
客户端首先获取机器码。
然后将该机器码发送至服务器。
服务器接收到机器码后,生成相应的授权码并返回给客户端。
客户端随后对收到的授权码进行验证。
如果验证通过,则输出授权码验证成功。
好了,这篇就到这,在下一篇文章中,我将介绍如何使用 Fyne 库编写一个生成授权码的软件,欢迎关注。
可点击下方👇 关注公众号
添加作者微信 👇 技术沟通交流