文章目录

  • 1. 项目背景
  • 2. xdr设备实例信息
  • 1. xdr实例信息-XdrInstanceDto
  • 2. xdr特定的配置信息-XdrConfig
  • 3. xdr接入方式-XdrConfigTypeEnum
  • 2. 解密联动码获取AkSk信息
  • 1. 生成联动码code的加密和解密密钥
  • 2. AES加密算法: 对联动码code加密
  • 3. AES解密算法: 对联动码code解密


1. 项目背景

① 概念:

  • openApi:所有对外对接提供的API接口定义为openApi,openApi走AkSk认证,无需 access_token。
  • AK(Access Key):设备的唯一标识;
  • SK(Secret Key): 秘钥,和AK成对出现;

② 原理:

  • 首先通信双方持有相同的 AKSK 信息;
  • 调用接口前,调用方通过AK找到SK信息,用SK对请求的接口的请求方式、地址、参数、请求头进行签名,得到签名串;
  • 调用方在请求头放入AK和签名串;
  • 被调用方从请求头取出AK,并通过AK找到SK信息,用SK以同样的方式进行签名;
  • 被调用方用签名结果和请求头中的签名串进行比对,签名一致,认证通过;

③ AkSk信息需要从xdr设备联动码code中解密获得:

  • 获取xdr设备实例中的设备名称(用户名username)和设备密码(密码password)
  • 生成对称加密算法AES-256中加密和解密的密钥;
  • 使用对称加密算法的密钥对xdr设备实例中的联动码code进行解密获取AkSk信息;

2. xdr设备实例信息

1. xdr实例信息-XdrInstanceDto
@Data
public class XdrInstanceDto {

    @ApiModelProperty("xdr实例名称")
    private String name;

    @ApiModelProperty("xdr实例id")
    private String instId;

    @ApiModelProperty("是否启用")
    private Boolean enable;

    @ApiModelProperty("xdr特定的配置信息")
    private XdrConfig config;
}
2. xdr特定的配置信息-XdrConfig
@Data
public class XdrConfig {

    @ApiModelProperty("接入方式")
    @Enum(clazz = XdrConfigTypeEnum.class, method = "getType", message = "app.instance.config.xdr.type.not.valid")
    private Integer type;

    @Size(min = 1, max = 1024, message = "app.instance.config.code.size.limit")
    @ApiModelProperty("用于获取设备唯一标识的联通码")
    private String code;

    @ApiModelProperty("解密AK所需的用户名")
    @Pattern(regexp = "^\\w{1,70}$", message = "app.instance.config.username.not.valid")
    private String username;

    @ApiModelProperty("解密AK所需的密码")
    @Size(min = 1, max = 90, message = "app.instance.config.password.not.valid")
    private String password;

    @ApiModelProperty("企业id")
    @Pattern(regexp = "^\\d{1,16}$", message = "app.instance.config.company.id.not.valid")
    private String companyId;

    @ApiModelProperty("云图地址")
    @Pattern(regexp = "^[A-Za-z\\d.]*$", message = "app.instance.config.cloudAddress.not.valid")
    private String cloudAddress;

    @ApiModelProperty("联动总线地址")
    @Pattern(regexp = "^(http://|https://)[A-Za-z\\d.:]*$", message = "app.instance.config.auth.address.not.valid")
    private String linkageBusAddress;

}
3. xdr接入方式-XdrConfigTypeEnum
@Getter
@AllArgsConstructor
public enum XdrConfigTypeEnum {

    /**
     * 分布式
     */
    LOCAL(1),

    /**
     * SAAS
     */
    SAAS(0);

    /**
     * 接入类型
     */
    final Integer type;
}

2. 解密联动码获取AkSk信息

  • 获取xdr设备实例中的设备名称(用户名username)和设备密码(密码password)
  • 生成对称加密算法AES-256中加密和解密的密钥;
  • 使用对称加密算法的密钥对xdr设备实例中的联动码code进行解密获取AkSk信息;
/**
 * 根据实例信息生成AkSk信息:解密联动码获取AKSK信息
 *
 * @param xdrInstanceDto 连接实例
 * @return AkSk信息
 */
@Nullable
private AkSkInfo generateAkSkInfo(XdrInstanceDto xdrInstanceDto) {
    try {
        // 1、生成对称加密和解密密钥
        String key = AesGcmEncrypt.generateLinkageCodeKey(
                xdrInstanceDto.getConfig().getUsername(),
                xdrInstanceDto.getConfig().getPassword()
        );
        // 2、使用密钥对联动码code解密获的AkSk信息
        String akskJsonStr = AesEcbEncrypt.decryptAes(
                xdrInstanceDto.getConfig().getCode(),
                key
        );
        // 3、将akskJsonStr反序列化为AkSkInfo对象
        @Nullable
        AkSkInfo akskInfo = JsonTranscodeUtil.transcode(
                akskJsonStr,
                AkSkInfo.class
        );
        return akskInfo;
    } catch (Exception e) {
        log.error("decrypt linkage code failed, code: {}", xdrInstanceDto.getConfig().getCode(), e);
        throw new DeviceConnectException("exception.xdr.device.connect.failed");
    }
}
1. 生成联动码code的加密和解密密钥
/**
 * AES 加解密联动码code
 */
public class AesGcmEncrypt {

    private static final int CUT_POINT = 2;

    /**
     * 生成联动码解密密钥
     * 
     * @param deviceName 设备名称
     * @param devicePwd  设备密码
     * @return 加密密钥
     */
    public static String generateLinkageCodeKey(String deviceName, String devicePwd) {
        if (StringUtils.isEmpty(deviceName) || deviceName.length() < CUT_POINT ||
                StringUtils.isEmpty(devicePwd) || devicePwd.length() < CUT_POINT) {
            return "";
        }
        // 根据deviceName和devicePwd生成一个key
        String key = deviceName.substring(0, CUT_POINT)
                .concat(devicePwd.substring(0, CUT_POINT))
                .concat(deviceName.substring(CUT_POINT))
                .concat(devicePwd.substring(CUT_POINT));
        // 生成密钥
        return DigestUtils.sha256Hex(key).substring(0, 32);
    }
}
2. AES加密算法: 对联动码code加密
/**
 * AES 加解密 解密联动码
 */
public class AesGcmEncrypt {

    static {
        Security.addProvider(new BouncyCastleProvider());
    }

    private static final String ALGORITHM = "AES";

    private static final String PADDING_TYPE = "AES/GCM/PKCS5Padding";

    private static final int GCM_IV_SIZE = 16;

    private static final int GCM_TAG_LENGTH = 16;

    private static final String CHARSET_NAME = "UTF-8";

    private static final int CUT_POINT = 2;

    /**
     * AES加密
     *
     * @param content 将要加密的内容
     * @param key     密钥
     * @return 已经加密的字节数组内容转为Base64之后的字符串
     */
    public static String encryptAes(String content, String key)
            throws UnsupportedEncodingException, NoSuchPaddingException, NoSuchAlgorithmException,
            IllegalBlockSizeException, BadPaddingException, InvalidKeyException, InvalidAlgorithmParameterException {
        byte[] keyByte = key.getBytes(CHARSET_NAME);
        SecretKeySpec keySpec = new SecretKeySpec(keyByte, ALGORITHM);
        Cipher cipher = Cipher.getInstance(PADDING_TYPE);
        byte[] iv = new byte[GCM_IV_SIZE];
        SecureRandom secureRandom = new SecureRandom();
        secureRandom.nextBytes(iv);
        GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH * Byte.SIZE, iv);
        cipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmParameterSpec);
        byte[] resultByte = cipher.doFinal(content.getBytes(CHARSET_NAME));
        byte[] encrypted = new byte[iv.length + resultByte.length];
        System.arraycopy(iv, 0, encrypted, 0, iv.length);
        System.arraycopy(resultByte, 0, encrypted, iv.length, resultByte.length);
        return Base64Utils.encodeToString(encrypted);
    }
}
3. AES解密算法: 对联动码code解密
/**
 * AES 加解密 解密联动码
 */
public class AesGcmEncrypt {

    static {
        Security.addProvider(new BouncyCastleProvider());
    }

    private static final String ALGORITHM = "AES";

    private static final String PADDING_TYPE = "AES/GCM/PKCS5Padding";

    private static final int GCM_IV_SIZE = 16;

    private static final int GCM_TAG_LENGTH = 16;

    private static final String CHARSET_NAME = "UTF-8";

    private static final int CUT_POINT = 2;

    /**
     * AES解密
     *
     * @param content 将要解密的字节数组内容的base64字符串
     * @param key     密钥
     * @return 已经解密的内容
     */
    public static String decryptAes(String content, String key)
            throws UnsupportedEncodingException, NoSuchPaddingException, NoSuchAlgorithmException,
            InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException {
        byte[] data = Base64Utils.decodeFromString(content);
        byte[] keyByte = key.getBytes(CHARSET_NAME);
        SecretKeySpec keySpec = new SecretKeySpec(keyByte, ALGORITHM);
        byte[] iv = Arrays.copyOfRange(data, 0, GCM_IV_SIZE);
        GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH * Byte.SIZE, iv);
        Cipher cipher = Cipher.getInstance(PADDING_TYPE);
        cipher.init(Cipher.DECRYPT_MODE, keySpec, gcmParameterSpec);
        byte[] decryptData = cipher.doFinal(data, GCM_IV_SIZE, data.length - GCM_IV_SIZE);
        String resultStr = new String(decryptData);
        return resultStr.trim();
    }
}