Springboot web project uses TrueLicense to realize software authentication and license - server side

Posted by PHPGuru_2700 on Tue, 12 Oct 2021 23:56:58 +0200

1, Introduction to License

License is the copyright license, which is generally used for the access license certificate provided by the charging software to the paying user. According to different application deployment locations, it can be divided into the following two situations:

  • The application is deployed on the developer's own ECs. In this case, users can access remotely through account login, so they only need to verify the validity period, access authority and other information of the target account when logging in.

  • The application is deployed in the customer's Intranet environment. In this case, developers cannot control the customer's network environment, nor can they ensure that the server where the application is located can access the external network. Therefore, the common practice is to use the server license file to load the certificate when the customer application starts, and then verify the validity of the certificate at the login or other key operations

2, License file generation server

SpringBoot2 uses TrueLicense to generate a License file for the client. The project name is cloud License server, version: 2.5.5

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!-- License -->
        <dependency>
            <groupId>de.schlichtherle.truelicense</groupId>
            <artifactId>truelicense-core</artifactId>
            <version>1.33</version>
        </dependency>

        <dependency>
            <groupId>net.sourceforge.nekohtml</groupId>
            <artifactId>nekohtml</artifactId>
            <version>1.9.18</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.7</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-resources-plugin</artifactId>
                <version>3.1.0</version>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

2.2 application.properties

server.port=8081

#Enable graceful shutdown
server.shutdown=graceful
#Buffer for 10 seconds
spring.lifecycle.timeout-per-shutdown-phase=10s

#License related configuration
license.licensePath=D:/workspace/gitee/spring-boot2-license/license/license.lic

2.3 LicenseCreatorParam

Parameters required for license generation class

package com.example.entity;

import com.fasterxml.jackson.annotation.JsonFormat;

import java.io.Serializable;
import java.util.Date;

/**
 * @program: cloud-license-serve
 * @description: License Generate the necessary parameters for the class
 * @author: xiaoyang
 * @create: 2021-10-11 09:00
 **/


public class LicenseCreatorParam implements Serializable {

    private static final long serialVersionUID = -7793154252684580872L;
    /**
     * Certificate subject
     */
    private String subject;

    /**
     * Key nickname
     */
    private String privateAlias;

    /**
     * Key password (it needs to be kept properly and cannot be known to the user)
     */
    private String keyPass;

    /**
     * Password to access the keystore
     */
    private String storePass;

    /**
     * Certificate generation path
     */
    private String licensePath;

    /**
     * Keystore storage path
     */
    private String privateKeysStorePath;

    /**
     * Effective time of certificate
     */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date issuedTime = new Date();

    /**
     * Certificate expiration time
     */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date expiryTime;

    /**
     * customer type
     */
    private String consumerType = "user";

    /**
     * Number of users
     */
    private Integer consumerAmount = 1;

    /**
     * Description information
     */
    private String description = "";

    /**
     * Additional server hardware verification information
     */
    private LicenseCheckModel licenseCheckModel;

    public String getSubject() {
        return subject;
    }

    public void setSubject(String subject) {
        this.subject = subject;
    }

    public String getPrivateAlias() {
        return privateAlias;
    }

    public void setPrivateAlias(String privateAlias) {
        this.privateAlias = privateAlias;
    }

    public String getKeyPass() {
        return keyPass;
    }

    public void setKeyPass(String keyPass) {
        this.keyPass = keyPass;
    }

    public String getStorePass() {
        return storePass;
    }

    public void setStorePass(String storePass) {
        this.storePass = storePass;
    }

    public String getLicensePath() {
        return licensePath;
    }

    public void setLicensePath(String licensePath) {
        this.licensePath = licensePath;
    }

    public String getPrivateKeysStorePath() {
        return privateKeysStorePath;
    }

    public void setPrivateKeysStorePath(String privateKeysStorePath) {
        this.privateKeysStorePath = privateKeysStorePath;
    }

    public Date getIssuedTime() {
        return issuedTime;
    }

    public void setIssuedTime(Date issuedTime) {
        this.issuedTime = issuedTime;
    }

    public Date getExpiryTime() {
        return expiryTime;
    }

    public void setExpiryTime(Date expiryTime) {
        this.expiryTime = expiryTime;
    }

    public String getConsumerType() {
        return consumerType;
    }

    public void setConsumerType(String consumerType) {
        this.consumerType = consumerType;
    }

    public Integer getConsumerAmount() {
        return consumerAmount;
    }

    public void setConsumerAmount(Integer consumerAmount) {
        this.consumerAmount = consumerAmount;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public LicenseCheckModel getLicenseCheckModel() {
        return licenseCheckModel;
    }

    public void setLicenseCheckModel(LicenseCheckModel licenseCheckModel) {
        this.licenseCheckModel = licenseCheckModel;
    }

    @Override
    public String toString() {
        return "LicenseCreatorParam{" +
                "subject='" + subject + '\'' +
                ", privateAlias='" + privateAlias + '\'' +
                ", keyPass='" + keyPass + '\'' +
                ", storePass='" + storePass + '\'' +
                ", licensePath='" + licensePath + '\'' +
                ", privateKeysStorePath='" + privateKeysStorePath + '\'' +
                ", issuedTime=" + issuedTime +
                ", expiryTime=" + expiryTime +
                ", consumerType='" + consumerType + '\'' +
                ", consumerAmount=" + consumerAmount +
                ", description='" + description + '\'' +
                ", licenseCheckModel=" + licenseCheckModel +
                '}';
    }

}

Customize the license parameters to be verified

package com.example.entity;

import java.io.Serializable;
import java.util.List;

/**
 * @program: cloud-license-serve
 * @description: Customize the License parameters to be verified
 * @author: xiaoyang
 * @create: 2021-10-11 09:02
 **/

public class LicenseCheckModel implements Serializable {

    private static final long serialVersionUID = 8600137500316662317L;
    /**
     * Allowed IP addresses
     */
    private List<String> ipAddress;

    /**
     * Allowable MAC address
     */
    private List<String> macAddress;

    /**
     * Allowable CPU serial number
     */
    private String cpuSerial;

    /**
     * Allowable motherboard serial number
     */
    private String mainBoardSerial;

    public List<String> getIpAddress() {
        return ipAddress;
    }

    public void setIpAddress(List<String> ipAddress) {
        this.ipAddress = ipAddress;
    }

    public List<String> getMacAddress() {
        return macAddress;
    }

    public void setMacAddress(List<String> macAddress) {
        this.macAddress = macAddress;
    }

    public String getCpuSerial() {
        return cpuSerial;
    }

    public void setCpuSerial(String cpuSerial) {
        this.cpuSerial = cpuSerial;
    }

    public String getMainBoardSerial() {
        return mainBoardSerial;
    }

    public void setMainBoardSerial(String mainBoardSerial) {
        this.mainBoardSerial = mainBoardSerial;
    }

    @Override
    public String toString() {
        return "LicenseCheckModel{" +
                "ipAddress=" + ipAddress +
                ", macAddress=" + macAddress +
                ", cpuSerial='" + cpuSerial + '\'' +
                ", mainBoardSerial='" + mainBoardSerial + '\'' +
                '}';
    }
}

2.4 add the abstract class AbstractServerInfos to obtain the hardware information of the server

package com.example.license;

import com.example.entity.LicenseCheckModel;
import lombok.extern.slf4j.Slf4j;

import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;

/**
 * @program: cloud-license-serve
 * @description: It is used to obtain the basic information of the client server, such as IP, MAC address, CPU serial number and motherboard serial number
 * @author: xiaoyang
 * @create: 2021-10-11 09:04
 **/
@Slf4j
public abstract class AbstractServerInfos {

    /**
     * Assemble the License parameters that require additional verification
     * @since 1.0.0
     * @return demo.LicenseCheckModel
     */
    public LicenseCheckModel getServerInfos(){
        LicenseCheckModel result = new LicenseCheckModel();

        try {
            result.setIpAddress(this.getIpAddress());
            result.setMacAddress(this.getMacAddress());
            result.setCpuSerial(this.getCPUSerial());
            result.setMainBoardSerial(this.getMainBoardSerial());
        }catch (Exception e){
            log.error("Failed to get server hardware information",e);
        }

        return result;
    }

    /**
     * Get IP address
     * @return java.util.List<java.lang.String>
     */
    protected abstract List<String> getIpAddress() throws Exception;

    /**
     * Get Mac address
     * @return java.util.List<java.lang.String>
     */
    protected abstract List<String> getMacAddress() throws Exception;

    /**
     * Get CPU serial number
     * @return java.lang.String
     */
    protected abstract String getCPUSerial() throws Exception;

    /**
     * bios id 
     * @return java.lang.String
     */
    protected abstract String getMainBoardSerial() throws Exception;

    /**
     * Get all qualified InetAddress of the current server
     * @return java.util.List<java.net.InetAddress>
     */
    protected List<InetAddress> getLocalAllInetAddress() throws Exception {
        List<InetAddress> result = new ArrayList<>(4);

        // Traverse all network interfaces
        for (Enumeration networkInterfaces = NetworkInterface.getNetworkInterfaces(); networkInterfaces.hasMoreElements(); ) {
            NetworkInterface iface = (NetworkInterface) networkInterfaces.nextElement();
            // Traverse IP under all interfaces
            for (Enumeration inetAddresses = iface.getInetAddresses(); inetAddresses.hasMoreElements(); ) {
                InetAddress inetAddr = (InetAddress) inetAddresses.nextElement();

                //Exclude IP addresses of types LoopbackAddress, SiteLocalAddress, LinkLocalAddress, MulticastAddress
                if(!inetAddr.isLoopbackAddress() /*&& !inetAddr.isSiteLocalAddress()*/
                        && !inetAddr.isLinkLocalAddress() && !inetAddr.isMulticastAddress()){
                    result.add(inetAddr);
                }
            }
        }
        return result;
    }

    /**
     * Get the Mac address of a network interface
     * @param
     * @return void
     */
    protected String getMacByInetAddress(InetAddress inetAddr){
        try {
            byte[] mac = NetworkInterface.getByInetAddress(inetAddr).getHardwareAddress();
            StringBuffer stringBuffer = new StringBuffer();
            for(int i=0;i<mac.length;i++){
                if(i != 0) {
                    stringBuffer.append("-");
                }
                //Convert hexadecimal byte to string
                String temp = Integer.toHexString(mac[i] & 0xff);
                if(temp.length() == 1){
                    stringBuffer.append("0" + temp);
                }else{
                    stringBuffer.append(temp);
                }
            }
            return stringBuffer.toString().toUpperCase();
        } catch (SocketException e) {
            e.printStackTrace();
            log.error("Gets the name of a network interface Mac Address exception", e.getMessage());
        }
        return null;
    }
}

Get basic information about the client Linux server

package com.example.license;

import org.apache.commons.lang3.StringUtils;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.InetAddress;
import java.util.List;
import java.util.stream.Collectors;

/**
 * @program: cloud-license-serve
 * @description: Get basic information about Linux server
 * @author: xiaoyang
 * @create: 2021-10-11 09:06
 **/
public class LinuxServerInfos extends AbstractServerInfos {

    @Override
    protected List<String> getIpAddress() throws Exception {
        List<String> result = null;

        //Get all network interfaces
        List<InetAddress> inetAddresses = getLocalAllInetAddress();

        if(inetAddresses != null && inetAddresses.size() > 0){
            result = inetAddresses.stream().map(InetAddress::getHostAddress).distinct().map(String::toLowerCase).collect(Collectors.toList());
        }

        return result;
    }

    @Override
    protected List<String> getMacAddress() throws Exception {
        List<String> result = null;

        //1. Obtain all network interfaces
        List<InetAddress> inetAddresses = getLocalAllInetAddress();

        if(inetAddresses != null && inetAddresses.size() > 0){
            //2. Get Mac addresses of all network interfaces
            result = inetAddresses.stream().map(this::getMacByInetAddress).distinct().collect(Collectors.toList());
        }

        return result;
    }

    @Override
    protected String getCPUSerial() throws Exception {
        //serial number
        String serialNumber = "";

        //Use the dmidecode command to get the CPU serial number
        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(StringUtils.isNotBlank(line)){
            serialNumber = line;
        }

        reader.close();
        return serialNumber;
    }

    @Override
    protected String getMainBoardSerial() throws Exception {
        //serial number
        String serialNumber = "";

        //Use the dmidecode command to obtain the serial number of the motherboard
        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(StringUtils.isNotBlank(line)){
            serialNumber = line;
        }

        reader.close();
        return serialNumber;
    }
}

Get basic information of client Windows Server

package com.example.license;

import java.net.InetAddress;
import java.util.List;
import java.util.Scanner;
import java.util.stream.Collectors;

/**
 * @program: cloud-license-serve
 * @description: Get basic information about Windows Server
 * @author: xiaoyang
 * @create: 2021-10-11 09:10
 **/
public class WindowsServerInfos extends AbstractServerInfos {

    @Override
    protected List<String> getIpAddress() throws Exception {
        List<String> result = null;

        //Get all network interfaces
        List<InetAddress> inetAddresses = getLocalAllInetAddress();

        if(inetAddresses != null && inetAddresses.size() > 0){
            result = inetAddresses.stream().map(InetAddress::getHostAddress).distinct().map(String::toLowerCase).collect(Collectors.toList());
        }

        return result;
    }

    @Override
    protected List<String> getMacAddress() throws Exception {
        List<String> result = null;

        //1. Obtain all network interfaces
        List<InetAddress> inetAddresses = getLocalAllInetAddress();

        if(inetAddresses != null && inetAddresses.size() > 0){
            //2. Get Mac addresses of all network interfaces
            result = inetAddresses.stream().map(this::getMacByInetAddress).distinct().collect(Collectors.toList());
        }

        return result;
    }

    @Override
    protected String getCPUSerial() throws Exception {
        //serial number
        String serialNumber = "";

        //Get CPU serial number using WMIC
        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 {
        //serial number
        String serialNumber = "";

        //Use WMIC to obtain the serial number of the motherboard
        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;
    }
}

Note: the template method pattern is used here to encapsulate the algorithm of the invariant part into an abstract class, while the specific implementation of the basic method is implemented by subclasses

2.5 custom LicenseManager (CustomLicenseManager) is used to add additional server hardware information verification

package com.example.license;

/**
 * @program: cloud-license-serve
 * @description: Customize the LicenseManager to add additional server hardware information verification
 * @author: xiaoyang
 * @create: 2021-10-11 09:11
 **/


import com.example.entity.LicenseCheckModel;
import de.schlichtherle.license.*;
import de.schlichtherle.xml.GenericCertificate;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;

import java.beans.XMLDecoder;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.UnsupportedEncodingException;
import java.util.Date;
import java.util.List;

/**
 * Customize the LicenseManager to add additional server hardware information verification
 *
 */
@Slf4j
public class CustomLicenseManager extends LicenseManager{

    //XML encoding
    private static final String XML_CHARSET = "UTF-8";
    //Default BUFSIZE
    private static final int DEFAULT_BUFSIZE = 8 * 1024;

    public CustomLicenseManager() {

    }

    public CustomLicenseManager(LicenseParam param) {
        super(param);
    }

    /**
     * Duplicate create method
     * @param
     * @return byte[]
     */
    @Override
    protected synchronized byte[] create(
            LicenseContent content,
            LicenseNotary notary)
            throws Exception {
        initialize(content);
        this.validateCreate(content);
        final GenericCertificate certificate = notary.sign(content);
        return getPrivacyGuard().cert2key(certificate);
    }

    /**
     * Copy the install method, where the validate method calls the validate method in this class to verify the IP address, Mac address and other information
     * @param
     * @return de.schlichtherle.license.LicenseContent
     */
    @Override
    protected synchronized LicenseContent install(
            final byte[] key,
            final LicenseNotary notary)
            throws Exception {
        final GenericCertificate certificate = getPrivacyGuard().key2cert(key);

        notary.verify(certificate);
        final LicenseContent content = (LicenseContent)this.load(certificate.getEncoded());
        this.validate(content);
        setLicenseKey(key);
        setCertificate(certificate);

        return content;
    }

    /**
     * Copy the verify method and call the validate method in this class to verify the IP address, Mac address and other information
     * @param
     * @return de.schlichtherle.license.LicenseContent
     */
    @Override
    protected synchronized LicenseContent verify(final LicenseNotary notary)
            throws Exception {
        GenericCertificate certificate = getCertificate();

        // 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 = (LicenseContent)this.load(certificate.getEncoded());
        this.validate(content);
        setCertificate(certificate);

        return content;
    }

    /**
     * Verify the parameter information of the generated certificate
     * @param content Certificate body
     */
    protected synchronized void validateCreate(final LicenseContent content)
            throws LicenseContentException {
        final LicenseParam param = getLicenseParam();

        final Date now = new Date();
        final Date notBefore = content.getNotBefore();
        final Date notAfter = content.getNotAfter();
        if (null != notAfter && now.after(notAfter)){
            throw new LicenseContentException("The certificate expiration time cannot be earlier than the current time");
        }
        if (null != notBefore && null != notAfter && notAfter.before(notBefore)){
            throw new LicenseContentException("The certificate effective time cannot be later than the certificate expiration time");
        }
        final String consumerType = content.getConsumerType();
        if (null == consumerType){
            throw new LicenseContentException("User type cannot be empty");
        }
    }


    /**
     * Copy the validate method to add verification of other information such as IP address and Mac address
     * @param content LicenseContent
     */
    @Override
    protected synchronized void validate(final LicenseContent content)
            throws LicenseContentException {
        //1. First call the validate method of the parent class
        super.validate(content);

        //2. Then verify the customized License parameters
        //Allowable parameter information in License
        LicenseCheckModel expectedCheckModel = (LicenseCheckModel) content.getExtra();
        //Real parameter information of the current server
        LicenseCheckModel serverCheckModel = getServerInfos();

        if(expectedCheckModel != null && serverCheckModel != null){
            //Verify IP address
            if(!checkIpAddress(expectedCheckModel.getIpAddress(),serverCheckModel.getIpAddress())){
                throw new LicenseContentException("Current server IP Not within the scope of authorization");
            }

            //Verify Mac address
            if(!checkIpAddress(expectedCheckModel.getMacAddress(),serverCheckModel.getMacAddress())){
                throw new LicenseContentException("Current server Mac The address is not within the scope of authorization");
            }

            //Verify motherboard serial number
            if(!checkSerial(expectedCheckModel.getMainBoardSerial(),serverCheckModel.getMainBoardSerial())){
                throw new LicenseContentException("The serial number of the main board of the current server is not within the authorization range");
            }

            //Verify CPU serial number
            if(!checkSerial(expectedCheckModel.getCpuSerial(),serverCheckModel.getCpuSerial())){
                throw new LicenseContentException("Current server CPU The serial number is not within the scope of authorization");
            }
        }else{
            throw new LicenseContentException("Unable to get server hardware information");
        }
    }


    /**
     * Rewrite xmlcoder to parse XML
     * @param encoded XML Type string
     * @return java.lang.Object
     */
    private Object load(String encoded){
        BufferedInputStream inputStream = null;
        XMLDecoder decoder = null;
        try {
            inputStream = new BufferedInputStream(new ByteArrayInputStream(encoded.getBytes(XML_CHARSET)));

            decoder = new XMLDecoder(new BufferedInputStream(inputStream, DEFAULT_BUFSIZE),null,null);

            return decoder.readObject();
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        } finally {
            try {
                if(decoder != null){
                    decoder.close();
                }
                if(inputStream != null){
                    inputStream.close();
                }
            } catch (Exception e) {
                log.error("XMLDecoder analysis XML fail",e.getMessage());
            }
        }

        return null;
    }

    /**
     * Get the License parameters that need additional verification for the current server
     * @return demo.LicenseCheckModel
     */
    private LicenseCheckModel getServerInfos(){
        //Operating system type
        String osName = System.getProperty("os.name").toLowerCase();
        AbstractServerInfos abstractServerInfos = null;

        //Select different data acquisition methods according to different operating system types
        if (osName.startsWith("windows")) {
            abstractServerInfos = new WindowsServerInfos();
        } else if (osName.startsWith("linux")) {
            abstractServerInfos = new LinuxServerInfos();
        }else{//Other server types
            abstractServerInfos = new LinuxServerInfos();
        }

        return abstractServerInfos.getServerInfos();
    }

    /**
     * Verify whether the IP/Mac address of the current server is within the allowable IP range < br / >
     * Returns true if the IP address is within the allowed IP/Mac address range
     * @return boolean
     */
    private boolean checkIpAddress(List<String> expectedList,List<String> serverList){
        if(expectedList != null && expectedList.size() > 0){
            if(serverList != null && serverList.size() > 0){
                for(String expected : expectedList){
                    if(serverList.contains(expected.trim())){
                        return true;
                    }
                }
            }

            return false;
        }else {
            return true;
        }
    }

    /**
     * Verify whether the serial number of the current server hardware (motherboard, CPU, etc.) is within the allowable range
     * @return boolean
     */
    private boolean checkSerial(String expectedSerial,String serverSerial){
        if(StringUtils.isNotBlank(expectedSerial)){
            if(StringUtils.isNotBlank(serverSerial)){
                if(expectedSerial.equals(serverSerial)){
                    return true;
                }
            }

            return false;
        }else{
            return true;
        }
    }

}

2.6 License generation class, which is used to generate License certificates

package com.example.license;

/**
 * @program: cloud-license-serve
 * @description: license Generation class, used to generate license certificate
 * @author: xiaoyang
 * @create: 2021-10-11 09:13
 **/


import com.example.entity.LicenseCreatorParam;
import de.schlichtherle.license.*;
import lombok.extern.slf4j.Slf4j;

import javax.security.auth.x500.X500Principal;
import java.io.File;
import java.text.MessageFormat;
import java.util.prefs.Preferences;

/**
 * License Generate class
 *
 */
@Slf4j
public class LicenseCreator {

    private final static X500Principal DEFAULT_HOLDER_AND_ISSUER = new X500Principal("CN=localhost, OU=localhost, O=localhost, L=SH, ST=SH, C=CN");
    private LicenseCreatorParam param;

    public LicenseCreator(LicenseCreatorParam param) {
        this.param = param;
    }

    /**
     * Generate License certificate
     * @return boolean
     */
    public boolean generateLicense(){
        try {
            LicenseManager licenseManager = new CustomLicenseManager(initLicenseParam());
            LicenseContent licenseContent = initLicenseContent();

            licenseManager.store(licenseContent,new File(param.getLicensePath()));

            return true;
        }catch (Exception e){
            log.error(MessageFormat.format("Certificate generation failed:{0}",param),e.getMessage());
            return false;
        }
    }

    /**
     * Initialize certificate generation parameters
     * @return de.schlichtherle.license.LicenseParam
     */
    private LicenseParam initLicenseParam(){
        Preferences preferences = Preferences.userNodeForPackage(LicenseCreator.class);

        //Set the secret key to encrypt the contents of the certificate
        CipherParam cipherParam = new DefaultCipherParam(param.getStorePass());

        KeyStoreParam privateStoreParam = new CustomKeyStoreParam(LicenseCreator.class
                ,param.getPrivateKeysStorePath()
                ,param.getPrivateAlias()
                ,param.getStorePass()
                ,param.getKeyPass());

        LicenseParam licenseParam = new DefaultLicenseParam(param.getSubject()
                ,preferences
                ,privateStoreParam
                ,cipherParam);

        return licenseParam;
    }

    /**
     * Set certificate generation body information
     * @return de.schlichtherle.license.LicenseContent
     */
    private LicenseContent initLicenseContent(){
        LicenseContent licenseContent = new LicenseContent();
        licenseContent.setHolder(DEFAULT_HOLDER_AND_ISSUER);
        licenseContent.setIssuer(DEFAULT_HOLDER_AND_ISSUER);

        licenseContent.setSubject(param.getSubject());
        licenseContent.setIssued(param.getIssuedTime());
        licenseContent.setNotBefore(param.getIssuedTime());
        licenseContent.setNotAfter(param.getExpiryTime());
        licenseContent.setConsumerType(param.getConsumerType());
        licenseContent.setConsumerAmount(param.getConsumerAmount());
        licenseContent.setInfo(param.getDescription());

        //Extended verification server hardware information
        licenseContent.setExtra(param.getLicenseCheckModel());

        return licenseContent;
    }

}

2.7 test, add a Controller that generates a certificate

package com.example.controller;

import com.example.entity.LicenseCheckModel;
import com.example.entity.LicenseCreatorParam;
import com.example.license.AbstractServerInfos;
import com.example.license.LicenseCreator;
import com.example.license.LinuxServerInfos;
import com.example.license.WindowsServerInfos;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;

/**
 * @program: cloud-license-serve
 * @description: It is used to generate license files and cannot be placed in the code deployed to customers
 * @author: xiaoyang
 * @create: 2021-10-11 09:23
 **/

@RestController
@RequestMapping("/license")
@ResponseBody
public class LicenseCreatorController {

    /**
     * Certificate generation path
     */
    @Value("${license.licensePath}")
    private String licensePath;

    /**
     * Get server hardware information
     *
     * @param osName Operating system type. If it is blank, it will be judged automatically
     * @return com.ccx.models.license.LicenseCheckModel
     */
    @RequestMapping(value = "/getServerInfos", produces = {MediaType.APPLICATION_JSON_UTF8_VALUE})
    @ResponseBody
    public LicenseCheckModel getServerInfos(@RequestParam(value = "osName", required = false) String osName) {
        //Operating system type
        if (StringUtils.isBlank(osName)) {
            osName = System.getProperty("os.name");
        }
        osName = osName.toLowerCase();

        AbstractServerInfos abstractServerInfos = null;

        //Select different data acquisition methods according to different operating system types
        if (osName.startsWith("windows")) {
            abstractServerInfos = new WindowsServerInfos();
        } else if (osName.startsWith("linux")) {
            abstractServerInfos = new LinuxServerInfos();
        } else {//Other server types
            abstractServerInfos = new LinuxServerInfos();
        }
        return abstractServerInfos.getServerInfos();
    }

    /**
     * Generate certificate
     *
     * @param param Parameters required to generate the certificate,
     * For example: {"subject": "CCX models", "privatealias": "privatekey", "KEYPASS": "5t7zz5y0djfcqtxvzkh5ldgjjsgmzq",
     *              "storePass":"3538cef8e7","licensePath":"C:/Users/zifangsky/Desktop/license.lic",
     *              "privateKeysStorePath":"C:/Users/zifangsky/Desktop/privateKeys.keystore",
     *              "issuedTime":"2018-04-26 14:48:12","expiryTime":"2018-12-31 00:00:00",
     *              "consumerType":"User","consumerAmount":1,"description":"This is the certificate description ",
     *              "licenseCheckModel":{"ipAddress":["192.168.245.1","10.0.5.22"],
     *              "macAddress":["00-50-56-C0-00-01","50-7B-9D-F9-18-41"],"cpuSerial":"BFEBFBFF000406E3","mainBoardSerial":"L1HF65E00X9"}}
     * @return java.util.Map<java.lang.String, java.lang.Object>
     */
    @RequestMapping(value = "/generateLicense", produces = {MediaType.APPLICATION_JSON_UTF8_VALUE})
    public Map<String, Object> generateLicense(@RequestBody LicenseCreatorParam param) {
        Map<String, Object> resultMap = new HashMap<>(2);

        if (StringUtils.isBlank(param.getLicensePath())) {
            param.setLicensePath(licensePath);
        }

        LicenseCreator licenseCreator = new LicenseCreator(param);
        boolean result = licenseCreator.generateLicense();

        if (result) {
            resultMap.put("result", "ok");
            resultMap.put("msg", param);
        } else {
            resultMap.put("result", "error");
            resultMap.put("msg", "Certificate file generation failed!");
        }
        return resultMap;
    }

}

2.8 use the Keytool tool provided with JDK to generate the public-private key certificate library

If we set the password of the public key library as public_password1234 and the password of the private key library as private_password1234, create a directory, enter cmd in the directory address, and the file generation command is as follows:

#Generate command
keytool -genkeypair -keysize 1024 -validity 3650 -alias "privateKey" -keystore "privateKeys.keystore" -storepass "public_password1234" -keypass "private_password1234" -dname "CN=localhost, OU=localhost, O=localhost, L=SH, ST=SH, C=CN"
 
#Export command
keytool -exportcert -alias "privateKey" -keystore "privateKeys.keystore" -storepass "public_password1234" -file "certfile.cer"
 
#Import command
keytool -import -alias "publicCert" -file "certfile.cer" -keystore "publicCerts.keystore" -storepass "public_password1234"

After the above command is executed, three files will be generated in the current path: privateKeys.keystore, publicCerts.keystore and certfile.cer. The file certfile.cer is no longer needed and can be deleted. The file privateKeys.keystore is used for the current cloud license serve project to generate a license file for the customer, and the file publicCerts.keystore is used with the application code department Deploy to the client server, and the user decrypts the license file and verifies its license information.
Note: remember to copy the files generated here to the directory where the generated license file configured in the project is located


2.9 generate license files for customers

Deploy the cloud license serve project to the client server, and obtain the hardware information of the server through the following interface (this project needs to be deleted after the license file is generated. Of course, you can also manually obtain the hardware information of the client server through the command, and then generate the license file on the developer's own computer), and use postman to test.
Get server hardware information http://localhost:8081/license/getServerInfos


Generate simple certificate http://localhost:8081/license/generateLicense

{
	"subject": "license_demo",
	"privateAlias": "privateKey",
	"keyPass": "private_password1234",
	"storePass": "public_password1234",
	"licensePath": "D:/workspace/gitee/spring-boot2-license/license/license.lic",
	"privateKeysStorePath": "D:/workspace/gitee/spring-boot2-license/license/privateKeys.keystore",
	"issuedTime": "2021-05-14 00:00:01",
	"expiryTime": "2021-05-14 11:21:00",
	"consumerType": "User",
	"consumerAmount": 1,
	"description": "This is the certificate description information"
}


Generate complex certificate http://localhost:8081/license/generateLicense

{
	"subject": "license_demo",
	"privateAlias": "privateKey",
	"keyPass": "private_password1234",
	"storePass": "public_password1234",
	"licensePath": "D:/workspace/gitee/spring-boot2-license/license/license.lic",
	"privateKeysStorePath": "D:/workspace/gitee/spring-boot2-license/license/privateKeys.keystore",
	"issuedTime": "2021-10-12 00:00:01",
	"expiryTime": "2021-10-13 23:59:59",
	"consumerType": "User",
	"consumerAmount": 1,
	"description": "This is the certificate description information",
	  "ipAddress": [
        "172.16.1.126",
        "192.168.56.1"
    ],
    "macAddress": [
        "00-E1-4C-68-07-B5",
        "0A-00-27-00-00-10"
    ],
    "cpuSerial": "178BFBFF00860F01",
    "mainBoardSerial": "BBDW0820AG001635"
}

So far, the server has realized the function of generating license files. My project structure is attached below:

The text is borrowed from a blogger (thanks for sharing!!!)

Topics: Java Spring Boot Back-end Web Security