欧美成人精品手机在线观看_69视频国产_动漫精品第一页_日韩中文字幕网 - 日本欧美一区二区

LOGO OA教程 ERP教程 模切知識交流 PMS教程 CRM教程 開發文檔 其他文檔  
 
網站管理員

軟件離線授權碼設計思路

admin
2025年3月24日 22:33 本文熱度 274

離線授權碼設計

對于自己的軟件產品,希望別人付費、或者在我們授權的情況下才允許使用。那么我們應該如何去這個授權碼 / 許可證的機制呢?

前言

離線授權的方案無非就是兩種,一種是軟件層面的授權,一種是硬件層面的授權。

軟件層面:我們向用戶提供一串特定的字符串,用戶在軟件中輸入我們提供的字符串,在使用端校驗用戶輸入的授權碼,即可完成軟件的授權操作。

硬件層面:我們向用戶提供一個 USB Key(加密狗),用戶在運行軟件時我們可以通過硬件加密狗來判斷授權信息,這種加密狗在網上 幾塊錢到十幾塊錢就能買一個,有的加密狗還內置獨立時鐘,可以杜絕用戶進行時鐘回撥等。這種加密狗可以用在對授權安全級別更高的場景中,但需要承受額外的成本:用戶的使用成本,授權分發成本(加密狗+快遞費+快遞時間)。

軟件層面的加密方案使用起來更加靈活,硬件層面加密狗安全級別更高,本篇文章主要是探討軟件層面的授權方式。

授權碼要點

那么設計一個相對安全的離線授權碼,需要滿足那些哪些特征呢?

  • ? 綁定性:每個授權碼對應一臺設備的授權,客戶無法通過一個授權碼激活多臺設備。
  • ? 安全性:授權碼應難以被偽造或篡改
  • ? 可驗證:授權碼應能被系統快速驗證其合法性
  • ? 可復現:當客戶授權碼遺失時,我們可以根據客戶的機器碼重新生成授權碼,避免用戶無法使用軟件的情況。
  • ? 時效性(可選):授權碼可包含授權有效期,限定授權使用時長。
  • ? 可擴展(可選):可根據自己需求來靈活擴展授權碼內容,以方便攜帶更多信息。

你可以以任何形式生成一個滿足上方條件的授權碼,無需糾結生成的思路是否與我的思路一致

唯一標識

在離線授權場景中,我們需要確定授權設備的唯一標識,這個標識需要具有不可更改性質,在正常情況下該唯一標識都不會發生變化(例如重啟軟件、重啟系統不會發生變化,但重裝系統、更換硬件等行為會發生變化是可接受的)。

在這種場景中,我們唯一標識一般采用 CPUID、主板 BIOS UUID、網卡 MAC 地址、或自定義算法來生成唯一ID,提醒一下 如果產品是服務器應用,不應使用 硬盤序列號作為唯一標識的組成部分,因為服務器硬盤都有明確的生命周期會定期更換。當更換后就會造成授權失效的情況。

這個唯一標識沒有明確的授權規定,大家可以根據自己的需求來選擇,也可以通過現成的開源庫來獲取。

例如我開發的主語言是 Golang,我一般會使用 github.com/denisbrodbeck/machineid 這個庫來獲取硬件ID,并且可以在各個系統下運轉良好。

授權碼方案

對稱方案

對稱方案是指授權碼的生成邏輯、校驗邏輯完全一樣。所謂校驗就是客戶端內部生成正確的授權碼,然后和用戶輸入的授權碼做對比,對比成功,則說明授權有效。

以我個人習慣為例,授權碼的字符串一般包含“機器碼”、“項目名稱”、“到期時間”、“鹽值”,其中鹽值用于增加授權碼的復雜性,即使客戶知道了授權碼的生成方式,在不知道鹽值的情況下,無法自行偽造授權碼。

生成過程

我們先生成一個明文的字符串,然后通過特定分隔符拼接出明文的授權碼,格式可以是: 機器碼:項目名稱:到期時間:鹽值,例如:

123456:tpamis:281231:abcdef

  • 123456:客戶提供的機器碼,這里以 123456 代替
  • tpamis:授權項目名稱,同一個機器上可能存在多個軟件產品,可以加一個項目名稱用于區分。
  • 281231:到期時間,這里取 2028-12-31 的后6位作為到期時間。這里也可以寫成授權天數,先選定一個時間作為起始日期,然后在這個時間上疊加偏移時間,得到的最終時間作為授權到期時間。例如:我出生的時間是:1998-11-11,我以此作為起始時間。偏移時間:9628 天,得到授權到期時間為 2028-12-31
  • abcdef:鹽值,可以防止彩虹表攻擊,即使攻擊者知道生成算法,也無法通過預計算破解授權碼。

接著,我們通過一個 MD5 算法(也可使用其他 HASH 摘要算法)得到一個簽名 "cfb9fc32230fa1a19423ef8b6af63a61",此簽名就是一個授權碼。

但這時的授權碼有一個問題,簽名是不可逆的,所以我們無法在驗證簽名時通過簽名得到授權到期時間,所以我們需要人為的拼接一下:

  • ? 直接拼接: cfb9fc32230fa1a19423ef8b6af63a61281231
  • ? 使用分隔字符: cfb9fc32230fa1a19423ef8b6af63a61-281231
  • ? 舍去后六位,以 UUID 的方式呈現: cfb9fc32-230f-a1a1-9423-ef8b6a281231

掩藏到期時間

這個時你會發現 后六位明晃晃的擺在那里,太容易被人猜到是到期時間了,雖然不會被篡改,但仍希望隱藏在授權碼中,我們可以通過 十進制轉十六進制來實現:281231 轉十六進制為:44a8f,這時授權碼變成:cfb9fc32-230f-a1a1-9423-ef8b6a44a8f。

同時你可以將到期時間藏在授權碼中間,替換掉對應位置的簽名值,得到這樣的授權碼:cf44a8f2-230f-a1a1-9423ef8b6af63a61。

驗證授權碼

到這一步,已經完成了授權碼的生成過程,客戶端程序需要提前內置鹽值和同樣的生成算法,在用戶輸入授權碼激活時,先從授權碼中截取出到期時間。再用客戶端生成的授權碼進行對比驗證是否正確,以及是否過期。

用戶體驗優化

簽名生成的授權碼以及可以滿足正常的離線授權的功能了,但有一些不允許插入U盤或聯網的場景中,客戶可能需要手動輸入授權碼,我們應該盡可能縮短授權碼長度,方便用戶輸入。

我們可以在前面授權碼的基礎上,參考 Windows 的授權碼長度 (AAAAA-BBBBB-CCCCC-DDDD-EEEEE),每組5個字符,5 組共25個字符,通過橫杠分割方便用戶輸入。

前面 MD5 生成的簽名是 128 位的二進制,轉換為16進制后,長度為 32 個字符。其實我們可以 MD5 轉為 36 進制得到: CAQ3DFUC0YPEHDZLA7ZKHHYLD。

我們再拼接上前面得到的十六進制到期時間,并轉為全大寫,就得到了方便輸入的授權碼格式:CAQ3D-FUC0Y-PEHDZ-LA7ZK-HHYLD-44A8F。

如果需要長度和 Windows 一模一樣,則需要在 36 進制轉換后,再截取掉 5 位,拼接時間戳。

這里提供一個 Go 語言 十六進制轉36進制的方法示例:

package main

import (
    "crypto/md5"
    "encoding/hex"
    "fmt"
    "math/big"
)

// 將 MD5 哈希值轉換為 36 進制字符串
func md5ToBase36(md5Hash string) string {
    // 將 MD5 哈希值(十六進制)轉換為大整數
    bigInt := new(big.Int)
    bigInt.SetString(md5Hash, 16)

    // 定義 36 進制的字符集
    const charset = "0123456789abcdefghijklmnopqrstuvwxyz"

    // 將大整數轉換為 36 進制
    base36 := ""
    base := big.NewInt(36)
    zero := big.NewInt(0)
    remainder := new(big.Int)

    for bigInt.Cmp(zero) > 0 {
        bigInt.DivMod(bigInt, base, remainder)
        base36 = string(charset[remainder.Int64()]) + base36
    }

    return base36
}

func main() {
    // 計算字符串的 MD5 哈希值
    data := "123456:tpamis:281231:abcdef"
    hash := md5.Sum([]byte(data))
    md5Hash := hex.EncodeToString(hash[:]) // 轉換為十六進制字符串

    fmt.Println("MD5 哈希值:", md5Hash) // 輸出: cfb9fc32230fa1a19423ef8b6af63a61

    // 將 MD5 哈希值轉換為 36 進制
    base36Code := md5ToBase36(md5Hash)
    fmt.Println("36 進制編碼:", base36Code) // 輸出: caq3dfuc0ypehdzla7zkhhyld
}

幾個問題補充

為什么要轉 36進制而不是直接截取 MD5?

因為截取后過短的 MD5 增加了碰撞的概率,轉為 36進制,會包含 0-9、A-Z 36 個字符。36進制相比 16進制更為緊湊,在縮短長度同時保留較高的信息密度。且 36 進制在客戶輸入時,不需要考慮大小寫問題,用戶體驗更好。

摘要算法選擇

MD5 算法已被證實存在碰撞漏洞,但在安全性要求相對不是很高的場景中,還是可以接受的。如果需要安全性更高的摘要算法,可以換成例如 SHA-256、國密 SM3 等更為安全算法,在得到簽名后截取 32 位 當作 MD5 使用,實現安全性、用戶體驗的兼顧。

36進制字符集

因為 36 進制是自行實現的,所以可以自定義字符集,大家可以在這一步對字符集排布順序進行打亂,實現混淆效果。

非對稱方案

前面的授權碼方案可以提供一個方便輸入,長度較短的授權碼字符串,但攜帶授權信息較少、密鑰(鹽值)需要內置在客戶端中,遇到逆行破解場景,有鹽值泄露的可能性。

授權碼設計

非對稱加密我們可以使用 RSA 算法來實現。當我們使用 RSA 算法時,我們可以通過授權碼給客戶端攜帶更多信息,所以我們就以 JSON 為許可證內容的載體。我們可以這樣設計:

{
    "iss":"tpamis",
    "sub":"pord",
    "aud": "123456",
    "exp": "1861804800",
    "nbf": "1743436800",
    "iat":"1742728673",
    "rge":["功能a","功能b","功能b"]
}

接參考了 JWT 官方規定的 Payload 字段,設計的許可證 JSON 內容,且這些字段可以根據需求隨意添加調整。

  • ? iss (issuer):授權項目
  • ? sub (subject):授權方式,prod 正式授權、test 測試授權
  • ? aud (audience):授權對象,客戶的機器碼
  • ? exp (expiration time):過期時間
  • ? nbf (Not Before):生效時間
  • ? iat (Issued At):簽發時間
  • ? rge:授權功能列表

生成授權碼

我們生成一對 RSA 公私鑰,公鑰保存在客戶端,私鑰在我們手里,我們根據用戶的機器碼,生成一個授權碼,以上面為例,我額外擴展了授權模塊功能,可以精確控制客戶允許使用的功能范圍。

  1. 1. 生成包含許可證信息的JSON 字符串
{"iss":"tpamis","sub":"pord","aud":"123456","exp":"1861804800","nbf":"1743436800","iat":"1742728673","rge":["功能a","功能b","功能b"]}
  1. 2. 然后我們使用 RSA 私鑰加密生成密文,這個密文就是我們需要提供給客戶的授權碼。
I0UQvjrw3achexYK/D2ciNbsN+d28meH56aQPvosR9ZAKX2xp+kFNMfOjgBH+ZCL5+ir0h+pibfyva5weFBEEy1WgPMSqSiFGL5jfNIpzRY+Ct8hqsrjZm20TONvEjE7gwhFHW0m0NvdpFmwvbOjQPLk5ipZkNWW2l/DvEkYyogVMxCAfcNmczv1x9c1MeyXp0ru7GQifF1q1wGn4SBljc61zfUbtsv5aHk7zibOrNu4DsXnGjnYmwRCqYogAhB7g4Wzxfx0chMED9ulakTC8G5rBwT2w+LNgxKP+Si/nsOL0PeBzwrLTYulJIQEoqNsjMkDJ4JbXa/uoWrRoIuMTg==

當許可證內容過多,導致密文過長時,可以考慮輸出一個密鑰文件給客戶,客戶在產品中上傳、選擇 密鑰文件,我們通過讀取密鑰文件來獲取授權碼。這樣體驗更好。

授權碼驗證

用戶將我們的授權碼輸入到軟件中,客戶端使用公鑰解密,解密成功則讀取進入進行進一步的授權校驗,解密失敗直接提示授權碼錯誤。

說明:將公鑰內置到客戶端,在極端情況下,存在反編譯的可能性。所以我們可以假設公鑰已經泄露、不安全的狀態。不過只要私鑰不泄露,別人拿到公鑰也只能解密我們的許可證密文,而不能偽造一個許可證,相對來說也是可以接受的。

代碼示例

以 Go 語言為例 加密解密過程如下

package main

import (
    "encoding/json"
    "fmt"
    "github.com/dromara/dongle"
)

func main() {
    // 聲明 map[string]any 許可證結構
    licenseData := map[string]any{
        "iss""tpamis",
        "sub""pord",
        "aud""123456",
        "exp""1861804800",
        "nbf""1743436800",
        "iat""1742728673",
        "rge": []string{"功能a""功能b""功能b"},
    }

    // 將 map 轉換為 JSON 字符串
    jsonData, _ := json.Marshal(licenseData)
    // 使用RSA 私鑰 授權Json
    cipherText := dongle.Encrypt.FromBytes(jsonData).ByRsa(pkcs1PrivateKey).ToBase64String()
    fmt.Println("RSA密文", cipherText)
    
    // 客戶端 使用 公鑰進行解密
    licenseJson := dongle.Decrypt.FromBase64String(cipherText).ByRsa(pkcs1PublicKey).ToString()
    fmt.Println("許可證Json", licenseJson)
}

var pkcs1PublicKey = []byte(`-----BEGIN RSA PUBLIC KEY-----
MIGJAoGBAK12MTd84qkCZzp4iLUj8YSUglaFMsFlv9KlIL4+Xts40PK3+wbsXPEw
cujGeUmdgMeZiK7SLLSz8QeE0v7Vs+cGK4Bs4qLtMGCiO6wEuyt10KsafTyBktFn
dk/+gBLr7B/b+9+HaMIIoJUdsFksdAg3cxTSpwVApe98loFNRfqDAgMBAAE=
-----END RSA PUBLIC KEY-----`
)

var pkcs1PrivateKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQCtdjE3fOKpAmc6eIi1I/GElIJWhTLBZb/SpSC+Pl7bONDyt/sG
7FzxMHLoxnlJnYDHmYiu0iy0s/EHhNL+1bPnBiuAbOKi7TBgojusBLsrddCrGn08
gZLRZ3ZP/oAS6+wf2/vfh2jCCKCVHbBZLHQIN3MU0qcFQKXvfJaBTUX6gwIDAQAB
AoGAFwAfEo56t5JcAcLNzccQVVYj2jkbO820G8hNiSxYA5WLD0QaAxcAU/Lqqbb3
ii1aUB0ppJS13NgnU6nnGGdZzUYBG1Hai6EkVyCGrI4amQ93AaVdKncL8gJ4RZAm
YzPPUwSMEESsu24pS1NF1G1Y8C+28b/Wr0oqOsCvL6PhsMECQQDwsPJJoWRx7ZJw
E1K5KLT0cXKyrIpyXY3I6tyA5imCzOzccf3d1vDgB0L9sdSO7bG3ceSwpAeiWEbg
5jGZemPzAkEAuH6U4pEI4AMbWnatpK55Rc235NDgmT3VyIuRaKC02YXAZ+jznFep
XMd4DTli4R9r3j2YVhUpyDVbdQpFH98DMQJAQpOvcU6DSkA80WOG7lCkPTlkUKgJ
Y7kdDwZoF/+SW+vzWMbvQf3CgzV/Ak2+TgrRrbyDVZkJw45HjM4fyiRgoQJBALH/
/qlxgPyQQs3O/s2KQBsm1auAE5IF5MLuVUZ69sF/mBko2hEXSqHnGV645TuKU0pC
Zz12ga9WO3z6gaK0SaECQQDah1pKt9ViBBy4USXK3OWXEloHuTwmyr9AbLqqI5tQ
2eNuH0NkuJYQmnXmHLbKOELoYocldEBXmkzPXSN+X9kV
-----END RSA PRIVATE KEY-----`
)

JWT 方案

JWT 方案則是直接使用標準的 JWT 庫來實現授權碼的簽發功能,客戶端和服務端共享 JWT 的密鑰來驗證授權簽名的有效性。

本方案的優點是可以充分利用現有的標準化 JWT 庫,只需定義一個密鑰,而無需進行過多的開發,但許可證內容會直接暴露給客戶,也可以考慮 JWE,需自行抉擇。

JWT Header
{
  "alg": "HS256",
  "typ": "JWT"
}

JWT Payload
{
  "iss": "tpamis",
  "sub": "pord",
  "aud": "123456",
  "exp": "1861804800",
  "nbf": "1743436800",
  "iat": "1742728673",
  "rge": [
    "功能a",
    "功能b",
    "功能b"
  ]
}

密鑰
dbkuaizi.com

JWT Token
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ0cGFtaXMiLCJzdWIiOiJwb3JkIiwiYXVkIjoiMTIzNDU2IiwiZXhwIjoiMTg2MTgwNDgwMCIsIm5iZiI6IjE3NDM0MzY4MDAiLCJpYXQiOiIxNzQyNzI4NjczIiwicmdlIjpbIuWKn-iDvWEiLCLlip_og71iIiwi5Yqf6IO9YiJdfQ.izIAwMyiLxPiHrWf-FDGu3fHMvfaC7bqEh40ha8YYAA

結尾

沒有絕對的安全

任何安全領域的手段都是相對安全,沒有絕對的安全。再復雜的授權碼的本質只能是 防君子不防小人,例如用戶可以通過逆向 得到你的鹽值、甚至直接修改你的程序邏輯跳過驗證的邏輯,更何況我們的場景中只有離線校驗的邏輯。

防止時鐘回撥

時鐘回撥是指,就是用戶直接通過修改系統時間到軟件授權到期之前,從而繼續使用軟件的目的。那么我們在離線場景中如何去避免這樣的情況發生呢?
首先我需要聲明的是,離線授權場景中,不能徹底避免這種情況(除了獨立時鐘的加密狗),我們能做的就是通過代碼邏輯盡可能規避這種情況的發生,一般有這幾種手段:

記錄上一次運行時間

眾所周知,時間是不會倒流的,也就是說軟件的第二次運行時間,不可能小于上一次運行時的系統時間。
所以,在軟件每次運行時我們可以先記錄當前時間,并與上一次系統時間做對比,若第二次運行時的系統時間小于之前記錄的時間,則認為出現了系統時鐘回撥的問題,直接提示用戶并終止軟件運行。

安全的存儲時間標記
那么如何保證我們存儲的時間沒有被篡改呢?我們可以通過 (時間戳.鹽)+md5 的方式實現:
例如:時間戳為 1717487962 鹽 是 abcdefg,
通過: md5('1717487962.abcdefg') 摘要算法,獲得時間戳簽名:a06de98fc28e45bf38a9a5f27630cd03。
然后將這樣的字符串進行存儲:1717487962.a06de98fc28e45bf38a9a5f27630cd03,啟動時再通過上面的邏輯進行校驗,即可保證 記錄的時間不被修改。

如何防止用戶刪除時間標記
可以在打包的時候,就記錄一個打包時間作為初始化時間戳,這樣即使用戶第一次運行也必須有時間戳。
若沒讀取不到時間戳標記,則說明用戶人為清理了時間標記,結束運行。

與業務數據強關聯

如果你產品運行過程中產生的業務數據很重要,也可以使用業務中的時間戳來做時鐘回撥校驗,例如使用最后一條訂單的創建時間。
若業務數據很重要,用戶總不可能為了繼續使用軟件而刪掉業務數據吧。

拋磚引玉

授權碼的設計方案有很多,這里只是整理了我用過的幾種方案,大家可以根據自己的需要對邏輯進行調整,如果你有更好的方案,歡迎留言交流。


閱讀原文:原文鏈接


該文章在 2025/3/25 10:29:44 編輯過
關鍵字查詢
相關文章
正在查詢...
點晴ERP是一款針對中小制造業的專業生產管理軟件系統,系統成熟度和易用性得到了國內大量中小企業的青睞。
點晴PMS碼頭管理系統主要針對港口碼頭集裝箱與散貨日常運作、調度、堆場、車隊、財務費用、相關報表等業務管理,結合碼頭的業務特點,圍繞調度、堆場作業而開發的。集技術的先進性、管理的有效性于一體,是物流碼頭及其他港口類企業的高效ERP管理信息系統。
點晴WMS倉儲管理系統提供了貨物產品管理,銷售管理,采購管理,倉儲管理,倉庫管理,保質期管理,貨位管理,庫位管理,生產管理,WMS管理系統,標簽打印,條形碼,二維碼管理,批號管理軟件。
點晴免費OA是一款軟件和通用服務都免費,不限功能、不限時間、不限用戶的免費OA協同辦公管理系統。
Copyright 2010-2025 ClickSun All Rights Reserved