我们开发的应用系统,卖给企事业使用,为了保证不被二次售卖使用,以及后期的持续收入,我们需要加入系统授权。(简单描述,其实好处挺多的)
这就要求每一份独立部署的材料都至少具备:
①客户端标识符:唯一标识部署机器,自动生成,不能被更改,传播无效。用于申请系统授权码。
②授权码使用期限:系统需要授权码才能使用,可一定程度保障系统安全。
以上至少具备的两点是基于一下思路:
用户使用 “客户端标识符”向“我们”申请“系统授权码文件”,并在系统中导入“授权码文件”,然后正常使用。当在使用的“授权码文件”即将过期或过期后,需要申请新的授权码文件并导入,继续正常使用。
当然,可以提前申请新授权码文件并导入,避免正在使用的系统出现业务暂停。(这都是用户体验方面,不展开)
综上,涉及两个系统,每个系统大致功能如下
应用系统:授权码文件上传、授权码文件解析、授权码文件存储、授权码信息查询、授权码校验
授权管理系统:授权码申请
考虑点:授权码生成算法最好是动态的,涉及的密钥最好也是动态的。当然,这是我的个人想法,目前我们做的不是。。。
=========================================================================
依赖:主要功能依赖是这个,涉及的其它工具依赖如hutool,guava等不多说了。
<!-- 获取系统信息 --> <dependency> <groupId>com.github.oshi</groupId> <artifactId>oshi-core</artifactId> <version>6.2.2</version> </dependency>
做法:
客户端标识符:保证唯一,使用客户端机器序列号和硬件UUID
public static String getClientUuid() { String serialNumber = OshiUtil.getSystem().getSerialNumber(); String hardwareUUID = OshiUtil.getSystem().getHardwareUUID(); return SecureUtil.sha1(serialNumber + hardwareUUID); }
授权码生成:算法分三步,涉及对称加密和非对称加密,均为国密算法:sm2, sm3, sm4
public static String genLicense(String clientUuid) { Date current = new Date(); AuthLicense authLicense = new AuthLicense(); authLicense.setSubject("授权许可"); authLicense.setIssuer("夏日温华"); authLicense.setIssuedAt(current.getTime()); authLicense.setNotBefore(current.getTime()); authLicense.setNotAfter(DateUtil.endOfDay(DateUtil.offsetMonth(current, TERM_MONTH)).getTime()); // 1. SM4加密clientUuid并得到base64编码字符串 String token1 = SmUtil.sm4(SecureUtil.md5().digest("这里是密钥,静态的,提前约定好的,最好做成动态的")).encryptBase64(clientUuid); // 2. SM4加密authLicenseInfo并得到base64编码字符串,密钥为"clientUuid" String licenseBaseStr = Base64.encode(JSON.toJSONString(authLicense)); String token2 = SmUtil.sm4(SecureUtil.md5().digest(clientUuid)).encryptBase64(licenseBaseStr); // 3. 数字签名 String token3 = Base64.encode(Sm2Util.encrypt(SmUtil.sm3(token1 + "." + token2), "公钥", "私钥")); // 授权许可证 return StrUtil.join(".", token1, token2, token3); }
授权码校验:就是生成算法的逆向解析,做成动态的也好,思想一样
public static AuthLicense checkLicense() { String license = readLicense(); // 读取授权码,自己实现 return checkLicense(license); } public static AuthLicense checkLicense(String license) { String clientUuid = getClientUuid(); if (StrUtil.isBlank(license)) { throw new ServiceException("不正确的授权许可文件"); } String[] tokens = license.split("\\."); if (tokens.length != 3) { throw new ServiceException("不正确的授权许可文件"); } // 1. 校验clientUuid String token1 = StrUtil.str(SmUtil.sm4(SecureUtil.md5().digest("这里是密钥,静态的,提前约定好的,最好做成动态的")).decrypt(tokens[0]), StandardCharsets.UTF_8); Assert.isTrue(clientUuid.equals(token1), "不正确的授权许可文件"); // 2. 验签 String sign = Sm2Util.decrypt(Base64.decodeStr(tokens[2]), "公钥", "私钥"); String reSign = SmUtil.sm3(tokens[0] + "." + tokens[1]); Assert.isTrue(ObjectUtil.equals(sign, reSign), "不正确的授权许可文件"); // 3. 验证授权信息 String token2 = StrUtil.str(SmUtil.sm4(SecureUtil.md5().digest(clientUuid)).decrypt(tokens[1]), StandardCharsets.UTF_8); AuthLicense authLicense = JSON.parseObject(Base64.decodeStr(token2), AuthLicense.class); checkAuthLicense(authLicense); authLicense.setClientUuid(clientUuid); return authLicense; } private static void checkAuthLicense(AuthLicense authLicense) { long current = DateUtil.current(); if (authLicense.getNotAfter() == null || !ISSUER.equals(authLicense.getIssuer())) { throw new ServiceException("无效的授权许可证"); } if (current > authLicense.getNotAfter()) { throw new ServiceException("您的授权许可证到期,请重新申请并上传新的授权许可证文件"); } }
系统授权码上传、信息查询等API因系统而异,不加了。
哈哈哈哈哈,简单吧,想说啥?忍住.
目前我只是把系统的一些做个简单介绍,不涉及个人优化操作,所以当前做法可能会有一些问题,如果您在参考,可以自己优化下,
问题:用docker容器部署时,如果基础镜像一样,在不同的机器上部署,也可能会导致有重复的客户端标识。