由于客户需要对公司销售的软件进行试用,公司考虑只允许客户在指定服务器上进去短期试用,所以在系统中集成了License授权模块。
一、设计思路
首先,我们需要获取要部署的服务器的基本信息,然后根据服务器信息生成对应的许可证,许可证采用DSA加密算法进行加密,再通过网关过滤器,对所有权限进行校验。
二、获取服务器基本信息
创建获取服务器基本信息模块,打成jar包,在要授权的机器上运行,生成 device.json 文件
Maven依赖
<dependency>
<groupId>net.sourceforge.nekohtml</groupId>
<artifactId>nekohtml</artifactId>
<version>1.9.18</version>
</dependency>
<dependency>
<groupId>de.schlichtherle.truelicense</groupId>
<artifactId>truelicense-core</artifactId>
<version>1.33</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
主类:
通过 DeviceUtil 获取对应的设备信息并生成 device.json 文件到 ./license 目录下,可自行定义目录地址。
public class LicenseDeviceApplication {
public static void main(String[] args) {
try {
DeviceInfo deviceInfo = DeviceUtil.getDeviceInfo();
String json = new Gson().toJson(deviceInfo);
Files.createDirectories(Paths.get("license"));
Files.writeString(Paths.get("license", "device.json"), json);
System.out.println("success");
} catch (Exception e) {
e.printStackTrace();
}
}
}
DeviceInfo 类:
@Data
@Accessors(chain = true)
public class DeviceInfo {
private String cpu;
private String mainBoard;
private List<String> ipList;
private List<String> macList;
}
DeviceUtil 类:
DeviceUtil 根据不同的操作系统采用不同的方式获取服务器数据
public class DeviceUtil {
private static final String WIN_PRE = "win";
public static DeviceInfo getDeviceInfo() throws Exception {
String os = System.getProperty("os.name").toLowerCase();
Device device;
if (os.startsWith(WIN_PRE)) {
device = new WinDevice();
} else {
device = new LinuxDevice();
}
return device.getDeviceInfo();
}
}
DeviceUtil 依赖的几个类:
获取设备信息的抽象类,将不同操作系统的通用信息获取方法抽象出来
public abstract class Device {
public DeviceInfo getDeviceInfo() throws Exception {
return new DeviceInfo()
.setIpList(this.getIpAddress())
.setMacList(this.getMacAddress())
.setCpu(this.getCpuSerial())
.setMainBoard(this.getMainBoardSerial());
}
List<String> getIpAddress() throws Exception {
List<InetAddress> inetAddresses = getLocalAllInetAddress();
return Optional.ofNullable(inetAddresses).orElse(new ArrayList<>(0))
.stream()
.map(e -> e.getHostAddress().toLowerCase())
.distinct()
.collect(Collectors.toList());
}
List<String> getMacAddress() throws Exception {
List<InetAddress> inetAddresses = getLocalAllInetAddress();
return Optional.ofNullable(inetAddresses).orElse(new ArrayList<>(0))
.stream()
.map(this::getMacByInetAddress)
.distinct()
.collect(Collectors.toList());
}
/**
* 获取 CpuSerial
*
* @return CpuSerial
* @throws Exception Exception
*/
abstract String getCpuSerial() throws Exception;
/**
* 获取 MainBoardSerial
*
* @return MainBoardSerial
* @throws Exception Exception
*/
abstract String getMainBoardSerial() throws Exception;
List<InetAddress> getLocalAllInetAddress() throws Exception {
List<InetAddress> inetAddressList = new ArrayList<>();
for (Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
networkInterfaces.hasMoreElements(); ) {
NetworkInterface networkInterface = networkInterfaces.nextElement();
for (Enumeration<InetAddress> inetAddresses = networkInterface.getInetAddresses();
inetAddresses.hasMoreElements(); ) {
InetAddress inetAddr = inetAddresses.nextElement();
if (!inetAddr.isLoopbackAddress()
&& !inetAddr.isLinkLocalAddress()
&& !inetAddr.isMulticastAddress()) {
inetAddressList.add(inetAddr);
}
}
}
return inetAddressList;
}
String getMacByInetAddress(InetAddress inetAddr) {
try {
byte[] mac = NetworkInterface.getByInetAddress(inetAddr).getHardwareAddress();
StringBuilder buf = new StringBuilder();
for (int i = 0; i < mac.length; i++) {
if (i != 0) {
buf.append("-");
}
String temp = Integer.toHexString(mac[i] & 0xff);
if (temp.length() == 1) {
buf.append("0").append(temp);
} else {
buf.append(temp);
}
}
return buf.toString().toUpperCase();
} catch (SocketException e) {
e.printStackTrace();
}
return null;
}
}
获取Linux操作系统服务器信息
public class LinuxDevice extends Device {
@Override
protected String getCpuSerial() throws Exception {
String serialNumber = "";
String[] shell = {"/bin/bash", "-c", "dmidecode -t processor | grep 'ID' | awk -F ':' '{print $2}' | head -n 1"};
Process process = Runtime.getRuntime().exec(shell);
process.getOutputStream().close();
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line = reader.readLine().trim();
if (isNotBlank(line)) {
serialNumber = line;
}
reader.close();
return serialNumber;
}
@Override
protected String getMainBoardSerial() throws Exception {
String serialNumber = "";
String[] shell = {"/bin/bash", "-c", "dmidecode | grep 'Serial Number' | awk -F ':' '{print $2}' | head -n 1"};
Process process = Runtime.getRuntime().exec(shell);
process.getOutputStream().close();
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line = reader.readLine().trim();
if (isNotBlank(line)) {
serialNumber = line;
}
reader.close();
return serialNumber;
}
private boolean isNotBlank(String str) {
return null != str && !"".equals(str.trim());
}
}
获取Windows操作系统的服务器信息
public class WinDevice extends Device {
@Override
protected String getCpuSerial() throws Exception {
String serialNumber = "";
Process process = Runtime.getRuntime().exec("wmic cpu get processorid");
process.getOutputStream().close();
Scanner scanner = new Scanner(process.getInputStream());
if (scanner.hasNext()) {
scanner.next();
}
if (scanner.hasNext()) {
serialNumber = scanner.next().trim();
}
scanner.close();
return serialNumber;
}
@Override
protected String getMainBoardSerial() throws Exception {
String serialNumber = "";
Process process = Runtime.getRuntime().exec("wmic baseboard get serialnumber");
process.getOutputStream().close();
Scanner scanner = new Scanner(process.getInputStream());
if (scanner.hasNext()) {
scanner.next();
}
if (scanner.hasNext()) {
serialNumber = scanner.next().trim();
}
scanner.close();
return serialNumber;
}
}
三、生成许可证
创建一个生成许可证模块,可以使用本地运行获取的方式,也可以采用接口的方式,通过上传 device.json 文件获取授权文件。这里演示下本地获取的方式:
Maven依赖
<dependency>
<groupId>net.sourceforge.nekohtml</groupId>
<artifactId>nekohtml</artifactId>
<version>1.9.18</version>
</dependency>
<dependency>
<groupId>de.schlichtherle.truelicense</groupId>
<artifactId>truelicense-core</artifactId>
<version>1.33</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
主类
public class LicenseGenApplication {
public static void main(String[] args) {
//生成公私钥对
//项目名称
String subject = "subject";
//公钥密码
String pubPwd = "a123456";
//私钥密码
String priPwd = "a123457";
LicenseGenUtils.licenseGen(subject, pubPwd, priPwd);
}
}
LicenseGenUtils 类
生成许可证,并输出到 ./license 目录下
public class LicenseGenUtils {
public static void licenseGen(String subject, String pubPwd, String priPwd) {
try {
KeyUtil.createKey(subject, pubPwd, priPwd);
//读取客户端设备信息
//需要将device.json文件放到./license目录下
File device = new File("./license/device.json");
String json = Files.readAllLines(device.toPath(), StandardCharsets.UTF_8).get(0);
//生成license
LicenseCreatorParam param = new LicenseCreatorParam()
.setSubject(subject)
.setPubPass(pubPwd)
.setPriPass(priPwd)
.setDeviceInfo(json);
new MyLicenseCreator()
.setCreatorParam(param)
.execute();
} catch (Exception e) {
e.printStackTrace();
}
}
}
LicenseGenUtils 类依赖的类
利用jdk中的 keytool 采用DSA加密算法,生成对应的公私钥。
@Slf4j
public class KeyUtil {
public static void createKey(String name, String pubPwd, String priPwd) {
Process process = null;
try {
//生成私匙库
String cmd = "keytool -genkeypair -keysize 1024 -validity 3650 -alias \"privateKey\" -keystore \"./license/private.key\" -keyalg \"DSA\" -storetype \"JKS\" -storepass \"{0}\" -keypass \"{1}\" -dname \"CN={2}, OU={3}, O={4}, L=SZ, ST=SZ, C=CN\"";
System.out.println("生成私匙库:" + MessageFormat.format(cmd, pubPwd, priPwd, name, name, name));
process = Runtime.getRuntime().exec(MessageFormat.format(cmd, pubPwd, priPwd, name, name, name));
handleBuffer(process);
process.waitFor();
process.destroy();
//导出证书
cmd = "keytool -exportcert -alias \"privateKey\" -keystore \"./license/private.key\" -storepass \"{0}\" -file \"./license/cert.cer\"";
System.out.println("导出证书:" + MessageFormat.format(cmd, pubPwd));
process = Runtime.getRuntime().exec(MessageFormat.format(cmd, pubPwd));
handleBuffer(process);
process.waitFor();
process.destroy();
//把这个证书文件导入到公匙库
//-noprompt 指定无交互,默认yes
cmd = "keytool -import -alias \"publicKey\" -file \"./license/cert.cer\" -keystore \"./license/public.key\" -storepass \"{0}\" -noprompt";
System.out.println("把这个证书文件导入到公匙库:" + MessageFormat.format(cmd, pubPwd));
process = Runtime.getRuntime().exec(MessageFormat.format(cmd, pubPwd));
handleBuffer(process);
process.waitFor();
process.destroy();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (null != process) {
process.destroy();
}
}
}
private static void handleBuffer(Process process) {
try (InputStream input = process.getInputStream();
BufferedReader inputReader = new BufferedReader(new InputStreamReader(input, Charset.forName("GBK")));
InputStream error = process.getErrorStream();
BufferedReader errorReader = new BufferedReader(new InputStreamReader(error, Charset.forName("GBK")))) {
String inputLog, errorLog;
while ((inputLog = inputReader.readLine()) != null) {
System.out.println(inputLog);
}
while ((errorLog = errorReader.readLine()) != null) {
System.out.println(errorLog);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
配置密钥库参数
public class MyKeyStoreParam extends AbstractKeyStoreParam {
private final String priKeyPath;
private final String alias;
private final String storePwd;
private final String keyPwd;
public MyKeyStoreParam(Class clazz, String resource, String alias, String storePwd, String keyPwd) {
super(clazz, resource);
this.priKeyPath = resource;
this.alias = alias;
this.storePwd = storePwd;
this.keyPwd = keyPwd;
}
@Override
public String getAlias() {
return alias;
}
@Override
public String getStorePwd() {
return storePwd;
}
@Override
public String getKeyPwd() {
return keyPwd;
}
@Override
public InputStream getStream() throws IOException {
return new FileInputStream(priKeyPath);
}
}
配置许可证创建的参数
@Data
@Accessors(chain = true)
public class LicenseCreatorParam {
private String subject;
private String priAlias = "privateKey";
/**
* 私钥文件路径
*/
private String priKeyPath = "./license/private.key";
/**
* 私钥密码
*/
private String priPass;
/**
* 公钥密码
*/
private String pubPass;
/**
* 证书地址
*/
private String licensePath = "./license/license.lic";
/**
* 证书生效时间
*/
private Date issuedTime = new Date();
/**
* 证书失效时间,180天后
*/
private Date expiryTime = new Date(System.currentTimeMillis() + 180L * 24L * 3600L * 1000L);
/**
* 额外的服务器硬件校验信息
*/
private String deviceInfo;
public LicenseParam toLicenseParam(Class<?> c) {
Preferences preferences = Preferences.userNodeForPackage(c);
//设置对证书内容加密的秘钥
CipherParam cipherParam = new DefaultCipherParam(getPubPass());
KeyStoreParam privateStoreParam = new MyKeyStoreParam(MyLicenseCreator.class, getPriKeyPath(), getPriAlias(),
getPubPass(), getPriPass());
return new DefaultLicenseParam(getSubject(), preferences, privateStoreParam, cipherParam);
}
}
许可证生成执行器
@Data
@Accessors(chain = true)
public class MyLicenseCreator {
private final static X500Principal DEFAULT_HOLDER_AND_ISSUER = new X500Principal("CN=YUBEST, OU=YUBEST, O=YUBEST, L=SZ, ST=SZ, C=CN");
private LicenseCreatorParam creatorParam;
public void execute() throws Exception {
LicenseParam param = creatorParam.toLicenseParam(MyLicenseCreator.class);
MyLicenseManager manager = new MyLicenseManager(param);
manager.setCreator(true);
LicenseContent content = wrapContent();
manager.store(content, new File(creatorParam.getLicensePath()));
}
private LicenseContent wrapContent() {
LicenseContent content = new LicenseContent();
content.setHolder(DEFAULT_HOLDER_AND_ISSUER);
content.setIssuer(DEFAULT_HOLDER_AND_ISSUER);
content.setSubject(creatorParam.getSubject());
content.setIssued(creatorParam.getIssuedTime());
content.setNotBefore(creatorParam.getIssuedTime());
content.setNotAfter(creatorParam.getExpiryTime());
content.setConsumerType("User");
content.setInfo("颁发给" + creatorParam.getSubject() + "的证书");
//扩展校验服务器硬件信息
content.setExtra(creatorParam.getDeviceInfo());
return content;
}
}
许可证管理器,负责证书的注册校验等操作
@Setter
public class MyLicenseManager extends LicenseManager {
private boolean creator;
public MyLicenseManager(LicenseParam param) {
super(param);
}
@Override
protected synchronized LicenseContent verify(final LicenseNotary notary)
throws Exception {
GenericCertificate certificate = getCertificate();
if (null != certificate) {
return (LicenseContent) certificate.getContent();
}
// Load license key from preferences,
final byte[] key = getLicenseKey();
if (null == key) {
throw new NoLicenseInstalledException(getLicenseParam().getSubject());
}
certificate = getPrivacyGuard().key2cert(key);
notary.verify(certificate);
//重写获取内容的方法
final LicenseContent content = this.readXml(certificate.getEncoded());
this.validate(content);
setCertificate(certificate);
return content;
}
private LicenseContent readXml(String encoded) throws Exception {
try (BufferedInputStream inputStream = new BufferedInputStream(new ByteArrayInputStream(encoded.getBytes(StandardCharsets.UTF_8)));
XMLDecoder decoder = new XMLDecoder(new BufferedInputStream(inputStream, 4096), null, null)) {
return (LicenseContent) decoder.readObject();
}
}
@Override
protected synchronized void validate(final LicenseContent content)
throws LicenseContentException {
super.validate(content);
if (creator) {
return;
}
String deviceJson = (String) content.getExtra();
if (isNull(deviceJson)) {
return;
}
DeviceInfo localDevice;
try {
localDevice = DeviceUtil.getDeviceInfo();
} catch (Exception e) {
throw new LicenseContentException("无法获取当前服务器设备信息");
}
if (null == localDevice) {
throw new LicenseContentException("无法获取当前服务器设备信息");
}
DeviceInfo licDevice = new Gson().fromJson(deviceJson, DeviceInfo.class);
boolean isCpuOk = true;
if (!isNull(licDevice.getCpu())) {
isCpuOk = licDevice.getCpu().equals(localDevice.getCpu());
}
boolean isMainBoardOk = true;
if (!isNull(licDevice.getMainBoard())) {
isMainBoardOk = licDevice.getMainBoard().equals(localDevice.getMainBoard());
}
boolean isIpOk = true;
if (isNotNull(licDevice.getIpList())) {
isIpOk = licDevice.getIpList().removeAll(localDevice.getIpList());
}
boolean isMacOk = true;
if (isNotNull(licDevice.getMacList())) {
isMacOk = licDevice.getMacList().removeAll(localDevice.getMacList());
}
if (!isCpuOk || !isMainBoardOk || !isIpOk || !isMacOk) {
throw new LicenseContentException("当前服务器不在授权范围内");
}
}
private boolean isNull(String str) {
return null == str || "".equals(str.trim());
}
private boolean isNotNull(List<?> list) {
return null != list && !list.isEmpty();
}
}
四、在网关集成license模块
在网关启动时校验许可证,并且通过过滤器的方式,对所有请求都进行许可证校验。网关采用的是gateway网关。
Maven依赖
<dependency>
<groupId>net.sourceforge.nekohtml</groupId>
<artifactId>nekohtml</artifactId>
<version>1.9.18</version>
</dependency>
<dependency>
<groupId>de.schlichtherle.truelicense</groupId>
<artifactId>truelicense-core</artifactId>
<version>1.33</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
LicenseConfig
注册 LicenseManager 类
@Data
@EqualsAndHashCode(callSuper = true)
@Component
@ConfigurationProperties(prefix = "license")
public class LicenseConfig extends LicenseVerifierParam {
private boolean enabled = true;
@Bean
public LicenseManager licenseManager() {
LicenseParam param = toLicenseParam(LicenseConfig.class);
return new MyLicenseManager(param);
}
}
许可证校验参数配置类
@Data
public class LicenseVerifierParam {
private String subject;
private String publicAlias = "publicKey";
private String storePass;
private String licPath;
private String pubKeyPath;
public LicenseParam toLicenseParam(Class<?> c) {
Preferences preferences = Preferences.userNodeForPackage(c);
CipherParam cipherParam = new DefaultCipherParam(getStorePass());
KeyStoreParam publicStoreParam = new MyKeyStoreParam(c, getPubKeyPath(),
getPublicAlias(), getStorePass(), null);
return new DefaultLicenseParam(getSubject(), preferences, publicStoreParam, cipherParam);
}
}
application.yml配置
license:
subject: subject
storePass: a123456
licPath: /etc/license/license.lic
pubKeyPath: /etc/license/public.key
LicenseFilter过滤器
过滤所有接口,对证书进行校验,校验做了缓存处理,半个小时校验一次。
@Slf4j
@Component
@RefreshScope
@RequiredArgsConstructor
public class LicenseFilter implements GlobalFilter, Ordered {
private final LicenseConfig licenseConfig;
private final LicenseManager licenseManager;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
try {
new MyLicenseVerifier().verify(licenseManager);
return chain.filter(exchange);
} catch (Exception e) {
log.error("证书校验失败", e);
}
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.OK);
response.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
//自定义响应数据
Object result = null;
DataBuffer dataBuffer = response.bufferFactory().wrap(JSON.toJSONString(result).getBytes());
return response.writeWith(Mono.just(dataBuffer));
}
@Override
public int getOrder() {
return -300;
}
}
LicenseRegister 注册许可证
启动时校验许可证并注册
@Component
@Slf4j
@RequiredArgsConstructor
public class LicenseRegister implements ApplicationRunner {
private final LicenseConfig licenseConfig;
private final LicenseManager licenseManager;
@Override
public void run(ApplicationArguments args) throws Exception {
if (licenseConfig.isEnabled()) {
log.info("------开始注册证书------");
new MyLicenseVerifier().install(licenseConfig.getLicPath(), licenseManager);
log.info("------结束注册证书------");
}
}
}