Files
fullstack-starter/docs/工具箱端-授权对接指南/工具箱端-设备授权二维码生成指南.md

21 KiB
Raw Blame History

工具箱端 - 设备授权二维码生成指南

概述

本文档说明工具箱端如何生成设备授权二维码用于设备首次授权和绑定。App 扫描二维码后,会将加密的设备信息提交到平台完成授权校验和绑定。

UX 集成模式补充(当前项目实现)

调用前提:工具箱先调用 config.setLicence 写入本地 licencefingerprint 由 UX 本机计算并持久化)。

在当前集成模式中,工具箱调用 UX 的 crypto.encryptDeviceInfo,直接传入 platformPublicKey 获取加密后的 Base64 密文。 UX 不保存业务设备实体仅保存本机身份材料licence/fingerprint

一、业务流程

工具箱 → 生成设备信息 → RSA-OAEP加密 → Base64编码 → 生成二维码
                                                          ↓
App扫描二维码 → 提取加密数据 → 调用平台接口 → 平台解密验证 → 授权成功

二、设备信息准备

2.1 设备信息字段

工具箱需要准备以下设备信息:

字段名 类型 说明 示例
licence String 设备授权码(工具箱唯一标识) "LIC-8F2A-XXXX"
fingerprint String 设备硬件指纹(设备唯一标识) "FP-2c91e9f3"

2.2 生成设备信息 JSON

将设备信息组装成 JSON 格式:

{
  "licence": "LIC-8F2A-XXXX",
  "fingerprint": "FP-2c91e9f3"
}

重要说明

  • licencefingerprint 必须是字符串类型
  • JSON 格式必须正确,不能有多余的逗号或格式错误
  • 建议使用标准的 JSON 库生成,避免手动拼接

伪代码示例

import json

device_info = {
    "licence": "LIC-8F2A-XXXX",      # 工具箱授权码
    "fingerprint": "FP-2c91e9f3"     # 设备硬件指纹
}

# 转换为 JSON 字符串
device_info_json = json.dumps(device_info, ensure_ascii=False)
# 结果: {"licence":"LIC-8F2A-XXXX","fingerprint":"FP-2c91e9f3"}

三、RSA-OAEP 加密

3.1 加密算法

使用 RSA-OAEP 非对称加密算法:

  • 算法名称RSA/ECB/OAEPWithSHA-256AndMGF1Padding
  • 密钥长度2048 位(推荐)
  • 填充方式OAEP with SHA-256 and MGF1
  • 加密方向:使用平台公钥加密,平台使用私钥解密

3.2 获取平台公钥

平台公钥需要从平台获取,通常以 Base64 编码的字符串形式提供。

公钥格式

  • 格式X.509 标准格式DER 编码)
  • 存储Base64 编码的字符串
  • 示例:MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzDlZvMDVaL+fjl05Hi182JOAUAaN4gh9rOF+1NhKfO4J6e0HLy8lBuylp3A4xoTiyUejNm22h0dqAgDSPnY/xZR76POFTD1soHr2LaFCN8JAbQ96P8gE7wC9qpoTssVvIVRH7QbVd260J6eD0Szwcx9cg591RSN69pMpe5IVRi8T99Hhql6/wnZHORPr18eESLOY93jRskLzc0q18r68RRoTJiQf+9YC8ub5iKp7rCjVnPi1UbIYmXmL08tk5mksYA0NqWQAa1ofKxx/9tQtB9uTjhTxuTu94XU9jlGU87qaHZs+kpqa8CAbYYJFbSP1xHwoZzpU2jpw2aF22HBYxwIDAQAB

3.3 加密步骤

  1. 加载平台公钥:从 Base64 字符串加载公钥对象
  2. 初始化加密器:使用 RSA/ECB/OAEPWithSHA-256AndMGF1Padding 算法
  3. 加密数据:使用公钥加密设备信息 JSON 字符串UTF-8 编码)
  4. Base64 编码:将加密后的字节数组进行 Base64 编码

3.4 Python 实现示例

import base64
import json
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend

def encrypt_device_info(licence: str, fingerprint: str, platform_public_key_base64: str) -> str:
    """
    使用平台公钥加密设备信息
    
    Args:
        licence: 设备授权码
        fingerprint: 设备硬件指纹
        platform_public_key_base64: 平台公钥Base64编码
    
    Returns:
        Base64编码的加密数据
    """
    # 1. 组装设备信息 JSON
    device_info = {
        "licence": licence,
        "fingerprint": fingerprint
    }
    device_info_json = json.dumps(device_info, ensure_ascii=False)
    
    # 2. 加载平台公钥
    public_key_bytes = base64.b64decode(platform_public_key_base64)
    public_key = serialization.load_der_public_key(
        public_key_bytes,
        backend=default_backend()
    )
    
    # 3. 使用 RSA-OAEP 加密
    # OAEP padding with SHA-256 and MGF1
    encrypted_bytes = public_key.encrypt(
        device_info_json.encode('utf-8'),
        padding.OAEP(
            mgf=padding.MGF1(algorithm=hashes.SHA256()),
            algorithm=hashes.SHA256(),
            label=None
        )
    )
    
    # 4. Base64 编码
    encrypted_base64 = base64.b64encode(encrypted_bytes).decode('utf-8')
    
    return encrypted_base64

3.5 Java/Kotlin 实现示例

import java.security.KeyFactory
import java.security.PublicKey
import java.security.spec.X509EncodedKeySpec
import java.util.Base64
import javax.crypto.Cipher
import java.nio.charset.StandardCharsets

object DeviceAuthorizationUtil {
    
    private const val CIPHER_ALGORITHM = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding"
    
    /**
     * 使用平台公钥加密设备信息
     * 
     * @param licence 设备授权码
     * @param fingerprint 设备硬件指纹
     * @param platformPublicKeyBase64 平台公钥Base64编码
     * @return Base64编码的加密数据
     */
    fun encryptDeviceInfo(
        licence: String,
        fingerprint: String,
        platformPublicKeyBase64: String
    ): String {
        // 1. 组装设备信息 JSON
        val deviceInfo = mapOf(
            "licence" to licence,
            "fingerprint" to fingerprint
        )
        val deviceInfoJson = objectMapper.writeValueAsString(deviceInfo)
        
        // 2. 加载平台公钥
        val publicKeyBytes = Base64.getDecoder().decode(platformPublicKeyBase64)
        val keySpec = X509EncodedKeySpec(publicKeyBytes)
        val keyFactory = KeyFactory.getInstance("RSA")
        val publicKey = keyFactory.generatePublic(keySpec)
        
        // 3. 使用 RSA-OAEP 加密
        val cipher = Cipher.getInstance(CIPHER_ALGORITHM)
        cipher.init(Cipher.ENCRYPT_MODE, publicKey)
        val encryptedBytes = cipher.doFinal(deviceInfoJson.toByteArray(StandardCharsets.UTF_8))
        
        // 4. Base64 编码
        return Base64.getEncoder().encodeToString(encryptedBytes)
    }
}

3.6 C# 实现示例

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

public class DeviceAuthorizationUtil
{
    private const string CipherAlgorithm = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding";
    
    /// <summary>
    /// 使用平台公钥加密设备信息
    /// </summary>
    public static string EncryptDeviceInfo(
        string licence, 
        string fingerprint, 
        string platformPublicKeyBase64)
    {
        // 1. 组装设备信息 JSON
        var deviceInfo = new
        {
            licence = licence,
            fingerprint = fingerprint
        };
        var deviceInfoJson = JsonSerializer.Serialize(deviceInfo);
        
        // 2. 加载平台公钥
        var publicKeyBytes = Convert.FromBase64String(platformPublicKeyBase64);
        using var rsa = RSA.Create();
        rsa.ImportSubjectPublicKeyInfo(publicKeyBytes, out _);
        
        // 3. 使用 RSA-OAEP 加密
        var encryptedBytes = rsa.Encrypt(
            Encoding.UTF8.GetBytes(deviceInfoJson),
            RSAEncryptionPadding.OaepSHA256
        );
        
        // 4. Base64 编码
        return Convert.ToBase64String(encryptedBytes);
    }
}

四、生成二维码

4.1 二维码内容

二维码内容就是加密后的 Base64 编码字符串(不是 JSON 格式)。

示例

MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzDlZvMDVaL+fjl05Hi182JOAUAaN4gh9rOF+1NhKfO4J6e0HLy8lBuylp3A4xoTiyUejNm22h0dqAgDSPnY/xZR76POFTD1soHr2LaFCN8JAbQ96P8gE7wC9qpoTssVvIVRH7QbVd260J6eD0Szwcx9cg591RSN69pMpe5IVRi8T99Hhql6/wnZHORPr18eESLOY93jRskLzc0q18r68RRoTJiQf+9YC8ub5iKp7rCjVnPi1UbIYmXmL08tk5mksYA0NqWQAa1ofKxx/9tQtB9uTjhTxuTu94XU9jlGU87qaHZs+kpqa8CAbYYJFbSP1xHwoZzpU2jpw2aF22HBYxwIDAQAB...

4.2 二维码生成

使用标准的二维码生成库生成二维码图片。

Python 示例(使用 qrcode 库)

import qrcode
from PIL import Image

def generate_qr_code(encrypted_data: str, output_path: str = "device_qr.png"):
    """
    生成设备授权二维码
    
    Args:
        encrypted_data: Base64编码的加密数据
        output_path: 二维码图片保存路径
    """
    qr = qrcode.QRCode(
        version=1,                      # 控制二维码大小1-40
        error_correction=qrcode.constants.ERROR_CORRECT_M,  # 错误纠正级别
        box_size=10,                    # 每个小方块的像素数
        border=4,                       # 边框的厚度
    )
    qr.add_data(encrypted_data)
    qr.make(fit=True)
    
    # 创建二维码图片
    img = qr.make_image(fill_color="black", back_color="white")
    img.save(output_path)
    
    print(f"二维码已生成: {output_path}")

Java/Kotlin 示例(使用 ZXing 库)

import com.google.zxing.BarcodeFormat
import com.google.zxing.EncodeHintType
import com.google.zxing.qrcode.QRCodeWriter
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
import java.awt.image.BufferedImage
import javax.imageio.ImageIO
import java.io.File

fun generateQRCode(encryptedData: String, outputPath: String = "device_qr.png") {
    val hints = hashMapOf<EncodeHintType, Any>().apply {
        put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M)
        put(EncodeHintType.CHARACTER_SET, "UTF-8")
        put(EncodeHintType.MARGIN, 1)
    }
    
    val writer = QRCodeWriter()
    val bitMatrix = writer.encode(encryptedData, BarcodeFormat.QR_CODE, 300, 300, hints)
    
    val width = bitMatrix.width
    val height = bitMatrix.height
    val image = BufferedImage(width, height, BufferedImage.TYPE_INT_RGB)
    
    for (x in 0 until width) {
        for (y in 0 until height) {
            image.setRGB(x, y, if (bitMatrix[x, y]) 0x000000 else 0xFFFFFF)
        }
    }
    
    ImageIO.write(image, "PNG", File(outputPath))
    println("二维码已生成: $outputPath")
}

五、完整流程示例

5.1 Python 完整示例

import json
import base64
import qrcode
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend

def generate_device_authorization_qr(
    licence: str,
    fingerprint: str,
    platform_public_key_base64: str,
    qr_output_path: str = "device_qr.png"
) -> str:
    """
    生成设备授权二维码
    
    Args:
        licence: 设备授权码
        fingerprint: 设备硬件指纹
        platform_public_key_base64: 平台公钥Base64编码
        qr_output_path: 二维码图片保存路径
    
    Returns:
        加密后的Base64字符串二维码内容
    """
    # 1. 组装设备信息 JSON
    device_info = {
        "licence": licence,
        "fingerprint": fingerprint
    }
    device_info_json = json.dumps(device_info, ensure_ascii=False)
    print(f"设备信息 JSON: {device_info_json}")
    
    # 2. 加载平台公钥
    public_key_bytes = base64.b64decode(platform_public_key_base64)
    public_key = serialization.load_der_public_key(
        public_key_bytes,
        backend=default_backend()
    )
    
    # 3. 使用 RSA-OAEP 加密
    encrypted_bytes = public_key.encrypt(
        device_info_json.encode('utf-8'),
        padding.OAEP(
            mgf=padding.MGF1(algorithm=hashes.SHA256()),
            algorithm=hashes.SHA256(),
            label=None
        )
    )
    
    # 4. Base64 编码
    encrypted_base64 = base64.b64encode(encrypted_bytes).decode('utf-8')
    print(f"加密后的 Base64: {encrypted_base64[:100]}...")  # 只显示前100个字符
    
    # 5. 生成二维码
    qr = qrcode.QRCode(
        version=1,
        error_correction=qrcode.constants.ERROR_CORRECT_M,
        box_size=10,
        border=4,
    )
    qr.add_data(encrypted_base64)
    qr.make(fit=True)
    
    img = qr.make_image(fill_color="black", back_color="white")
    img.save(qr_output_path)
    print(f"二维码已生成: {qr_output_path}")
    
    return encrypted_base64

# 使用示例
if __name__ == "__main__":
    # 平台公钥(示例,实际使用时需要从平台获取)
    platform_public_key = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzDlZvMDVaL+fjl05Hi182JOAUAaN4gh9rOF+1NhKfO4J6e0HLy8lBuylp3A4xoTiyUejNm22h0dqAgDSPnY/xZR76POFTD1soHr2LaFCN8JAbQ96P8gE7wC9qpoTssVvIVRH7QbVd260J6eD0Szwcx9cg591RSN69pMpe5IVRi8T99Hhql6/wnZHORPr18eESLOY93jRskLzc0q18r68RRoTJiQf+9YC8ub5iKp7rCjVnPi1UbIYmXmL08tk5mksYA0NqWQAa1ofKxx/9tQtB9uTjhTxuTu94XU9jlGU87qaHZs+kpqa8CAbYYJFbSP1xHwoZzpU2jpw2aF22HBYxwIDAQAB"
    
    # 设备信息
    licence = "LIC-8F2A-XXXX"
    fingerprint = "FP-2c91e9f3"
    
    # 生成二维码
    encrypted_data = generate_device_authorization_qr(
        licence=licence,
        fingerprint=fingerprint,
        platform_public_key_base64=platform_public_key,
        qr_output_path="device_authorization_qr.png"
    )
    
    print(f"\n二维码内容加密后的Base64:\n{encrypted_data}")

5.2 Java/Kotlin 完整示例

import com.fasterxml.jackson.databind.ObjectMapper
import com.google.zxing.BarcodeFormat
import com.google.zxing.EncodeHintType
import com.google.zxing.qrcode.QRCodeWriter
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
import java.awt.image.BufferedImage
import java.security.KeyFactory
import java.security.PublicKey
import java.security.spec.X509EncodedKeySpec
import java.util.Base64
import javax.crypto.Cipher
import javax.imageio.ImageIO
import java.io.File
import java.nio.charset.StandardCharsets

object DeviceAuthorizationQRGenerator {
    
    private const val CIPHER_ALGORITHM = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding"
    private val objectMapper = ObjectMapper()
    
    /**
     * 生成设备授权二维码
     */
    fun generateDeviceAuthorizationQR(
        licence: String,
        fingerprint: String,
        platformPublicKeyBase64: String,
        qrOutputPath: String = "device_qr.png"
    ): String {
        // 1. 组装设备信息 JSON
        val deviceInfo = mapOf(
            "licence" to licence,
            "fingerprint" to fingerprint
        )
        val deviceInfoJson = objectMapper.writeValueAsString(deviceInfo)
        println("设备信息 JSON: $deviceInfoJson")
        
        // 2. 加载平台公钥
        val publicKeyBytes = Base64.getDecoder().decode(platformPublicKeyBase64)
        val keySpec = X509EncodedKeySpec(publicKeyBytes)
        val keyFactory = KeyFactory.getInstance("RSA")
        val publicKey = keyFactory.generatePublic(keySpec)
        
        // 3. 使用 RSA-OAEP 加密
        val cipher = Cipher.getInstance(CIPHER_ALGORITHM)
        cipher.init(Cipher.ENCRYPT_MODE, publicKey)
        val encryptedBytes = cipher.doFinal(deviceInfoJson.toByteArray(StandardCharsets.UTF_8))
        
        // 4. Base64 编码
        val encryptedBase64 = Base64.getEncoder().encodeToString(encryptedBytes)
        println("加密后的 Base64: ${encryptedBase64.take(100)}...")
        
        // 5. 生成二维码
        val hints = hashMapOf<EncodeHintType, Any>().apply {
            put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M)
            put(EncodeHintType.CHARACTER_SET, "UTF-8")
            put(EncodeHintType.MARGIN, 1)
        }
        
        val writer = QRCodeWriter()
        val bitMatrix = writer.encode(encryptedBase64, BarcodeFormat.QR_CODE, 300, 300, hints)
        
        val width = bitMatrix.width
        val height = bitMatrix.height
        val image = BufferedImage(width, height, BufferedImage.TYPE_INT_RGB)
        
        for (x in 0 until width) {
            for (y in 0 until height) {
                image.setRGB(x, y, if (bitMatrix[x, y]) 0x000000 else 0xFFFFFF)
            }
        }
        
        ImageIO.write(image, "PNG", File(qrOutputPath))
        println("二维码已生成: $qrOutputPath")
        
        return encryptedBase64
    }
}

// 使用示例
fun main() {
    // 平台公钥(示例,实际使用时需要从平台获取)
    val platformPublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzDlZvMDVaL+fjl05Hi182JOAUAaN4gh9rOF+1NhKfO4J6e0HLy8lBuylp3A4xoTiyUejNm22h0dqAgDSPnY/xZR76POFTD1soHr2LaFCN8JAbQ96P8gE7wC9qpoTssVvIVRH7QbVd260J6eD0Szwcx9cg591RSN69pMpe5IVRi8T99Hhql6/wnZHORPr18eESLOY93jRskLzc0q18r68RRoTJiQf+9YC8ub5iKp7rCjVnPi1UbIYmXmL08tk5mksYA0NqWQAa1ofKxx/9tQtB9uTjhTxuTu94XU9jlGU87qaHZs+kpqa8CAbYYJFbSP1xHwoZzpU2jpw2aF22HBYxwIDAQAB"
    
    // 设备信息
    val licence = "LIC-8F2A-XXXX"
    val fingerprint = "FP-2c91e9f3"
    
    // 生成二维码
    val encryptedData = DeviceAuthorizationQRGenerator.generateDeviceAuthorizationQR(
        licence = licence,
        fingerprint = fingerprint,
        platformPublicKeyBase64 = platformPublicKey,
        qrOutputPath = "device_authorization_qr.png"
    )
    
    println("\n二维码内容加密后的Base64:\n$encryptedData")
}

六、平台端验证流程

平台端会按以下流程验证:

  1. 接收请求App 扫描二维码后,将 encryptedDeviceInfoappid 提交到平台
  2. RSA-OAEP 解密:使用平台私钥解密 encryptedDeviceInfo
  3. 提取设备信息:从解密后的 JSON 中提取 licencefingerprint
  4. 设备验证
    • 检查 filing_device_licence 表中是否存在该 licence
    • 如果存在,验证 fingerprint 是否匹配
    • 如果 fingerprint 不匹配,记录非法授权日志并返回错误
  5. App 绑定:检查 filing_app_licence 表中是否存在绑定关系
    • 如果不存在,创建新的绑定记录
    • 如果已存在,返回已绑定信息
  6. 返回响应:返回 deviceLicenceIdlicence

七、常见错误和注意事项

7.1 加密失败

可能原因

  1. 公钥格式错误:确保使用正确的 Base64 编码的公钥
  2. 算法不匹配:必须使用 RSA/ECB/OAEPWithSHA-256AndMGF1Padding
  3. 数据长度超限RSA-2048 最多加密 245 字节(设备信息 JSON 通常不会超过)
  4. 字符编码错误:确保使用 UTF-8 编码

7.2 二维码扫描失败

可能原因

  1. 二维码内容过长如果加密后的数据过长可能需要使用更高版本的二维码version
  2. 错误纠正级别过低:建议使用 ERROR_CORRECT_M 或更高
  3. 二维码图片质量差:确保二维码图片清晰,有足够的对比度

7.3 平台验证失败

可能原因

  1. licence 已存在但 fingerprint 不匹配:设备被替换或授权码被复用
  2. JSON 格式错误:确保 JSON 格式正确,字段名和类型匹配
  3. 加密数据损坏:确保 Base64 编码和解码正确

八、安全设计说明

8.1 为什么使用 RSA-OAEP

  1. 非对称加密:只有平台拥有私钥,可以解密数据
  2. OAEP 填充:提供更好的安全性,防止某些攻击
  3. SHA-256:使用强哈希算法,提供更好的安全性

8.2 为什么第三方无法伪造

  1. 只有平台能解密:第三方无法获取平台私钥,无法解密数据
  2. fingerprint 验证:平台会验证硬件指纹,防止授权码被复用
  3. 非法授权日志:平台会记录所有非法授权尝试

九、测试建议

  1. 单元测试

    • 测试 JSON 生成是否正确
    • 测试加密和解密是否匹配
    • 测试 Base64 编码和解码是否正确
  2. 集成测试

    • 使用真实平台公钥生成二维码
    • App 扫描二维码并提交到平台
    • 验证平台是否能正确解密和验证
  3. 边界测试

    • 测试超长的 licence 或 fingerprint
    • 测试特殊字符的处理
    • 测试错误的公钥格式

十、参考实现

  • PythoncryptographyRSA 加密)、qrcode 库(二维码生成)
  • Java/KotlinJDK javax.cryptoRSA 加密、ZXing 库(二维码生成)
  • C#System.Security.CryptographyRSA 加密、ZXing.Net 库(二维码生成)

十一、联系支持

如有问题,请联系平台技术支持团队获取:

  • 平台公钥Base64 编码)
  • 测试环境地址
  • 技术支持