Use TrueLicense in Spring Boot project to generate and verify License (Server License)

Posted by parboy on Tue, 23 Jun 2020 09:38:53 +0200

Article catalog

1, Introduction

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

  • The application is deployed on the developer's own cloud server. In this case, users can access remotely through account login, so they only need to verify the validity period, access rights and other information of the target account at the time of account login.
  • The application is deployed in the customer's Intranet environment. In this case, the developer cannot control the client's network environment or guarantee that the server where the application is located can access the Internet. Therefore, the common practice is to use the server license file, load the certificate when the application starts, and then verify the validity of the certificate at the place of login or other key operations.

2, Use TrueLicense to generate License

1, in pom.xml Add key dependencies to

<dependency>
    <groupId>de.schlichtherle.truelicense</groupId>
    <artifactId>truelicense-core</artifactId>
    <version>1.33</version>
    <scope>provided</scope>
</dependency>

2. Verify the user-defined License parameters

TrueLicense's de.schlichtherle.license The verify method of the. Licensemanager class only verifies the validity and expiration time of the license file issued later by us. However, in the actual project, we may need to additionally verify the IP address, MAC address, CPU serial number, motherboard serial number and other information of the server deployed by the application, so we need to copy some methods of the framework to realize the verification customization The purpose of the argument.

First of all, you need to add a user-defined entity class that can allow the hardware information of the server (you can supplement it if you check other parameters):

package cn.zifangsky.license;

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

/**
 * User defined License parameters to be verified
 *
 * @author zifangsky
 * @date 2018/4/23
 * @since 1.0.0
 */
public class LicenseCheckModel implements Serializable{

    private static final long serialVersionUID = 8600137500316662317L;
    /**
     * IP addresses that can be allowed
     */
    private List<String> ipAddress;

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

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

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

    //Omit setter and getter methods

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

Second, add a parameter required by the License generation class:

package cn.zifangsky.license;

import com.fasterxml.jackson.annotation.JsonFormat;

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

/**
 * License Parameters required to generate a class
 *
 * @author zifangsky
 * @date 2018/4/19
 * @since 1.0.0
 */
public class LicenseCreatorParam implements Serializable {

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

    /**
     * Key alias
     */
    private String privateAlias;

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

    /**
     * Password to access the secret key library
     */
    private String storePass;

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

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

    /**
     * Certificate effective time
     */
    @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;

    //Omit setter and getter methods

    @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 +
                '}';
    }
}

Add abstract class AbstractServerInfos. The user obtains the hardware information of the server:

package cn.zifangsky.license;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

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

/**
 * It is used to obtain the basic information of client server, such as IP, Mac address, CPU serial number, motherboard serial number, etc
 *
 * @author zifangsky
 * @date 2018/4/23
 * @since 1.0.0
 */
public abstract class AbstractServerInfos {
    private static Logger logger = LogManager.getLogger(AbstractServerInfos.class);

    /**
     * Assembly of License parameters requiring additional verification
     * @author zifangsky
     * @date 2018/4/23 14:23
     * @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){
            logger.error("Failed to get server hardware information",e);
        }

        return result;
    }

    /**
     * Get IP address
     * @author zifangsky
     * @date 2018/4/23 11:32
     * @since 1.0.0
     * @return java.util.List<java.lang.String>
     */
    protected abstract List<String> getIpAddress() throws Exception;

    /**
     * Get Mac address
     * @author zifangsky
     * @date 2018/4/23 11:32
     * @since 1.0.0
     * @return java.util.List<java.lang.String>
     */
    protected abstract List<String> getMacAddress() throws Exception;

    /**
     * Get CPU serial number
     * @author zifangsky
     * @date 2018/4/23 11:35
     * @since 1.0.0
     * @return java.lang.String
     */
    protected abstract String getCPUSerial() throws Exception;

    /**
     * bios id 
     * @author zifangsky
     * @date 2018/4/23 11:35
     * @since 1.0.0
     * @return java.lang.String
     */
    protected abstract String getMainBoardSerial() throws Exception;

    /**
     * Get all the qualified InetAddress of the current server
     * @author zifangsky
     * @date 2018/4/23 17:38
     * @since 1.0.0
     * @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 again under all interfaces
            for (Enumeration inetAddresses = iface.getInetAddresses(); inetAddresses.hasMoreElements(); ) {
                InetAddress inetAddr = (InetAddress) inetAddresses.nextElement();

                //Exclude IP addresses of type 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
     * @author zifangsky
     * @date 2018/4/23 18:08
     * @since 1.0.0
     * @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 hex 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();
        }

        return null;
    }

}

Get the basic information of the client's Linux server:

package cn.zifangsky.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;

/**
 * Basic information for getting the client's Linux server
 *
 * @author zifangsky
 * @date 2018/4/23
 * @since 1.0.0
 */
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 = "";

        //Using 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 = "";

        //Using the dmidecode command to get the motherboard serial number
        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 the basic information of the client's Windows Server:

package cn.zifangsky.license;

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

/**
 * Get basic information of client Windows Server
 *
 * @author zifangsky
 * @date 2018/4/23
 * @since 1.0.0
 */
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 = "";

        //Using WMIC to get CPU serial number
        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 = "";

        //Using WMIC to obtain the serial number of the main board
        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;
    }
}

Custom license manager to add additional server hardware information verification:

package cn.zifangsky.license;

import de.schlichtherle.license.LicenseContent;
import de.schlichtherle.license.LicenseContentException;
import de.schlichtherle.license.LicenseManager;
import de.schlichtherle.license.LicenseNotary;
import de.schlichtherle.license.LicenseParam;
import de.schlichtherle.license.NoLicenseInstalledException;
import de.schlichtherle.xml.GenericCertificate;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

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

/**
 * Custom license manager for additional verification of server hardware information
 *
 * @author zifangsky
 * @date 2018/4/23
 * @since 1.0.0
 */
public class CustomLicenseManager extends LicenseManager{
    private static Logger logger = LogManager.getLogger(CustomLicenseManager.class);

    //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);
    }

    /**
     * Replication create method
     * @author zifangsky
     * @date 2018/4/23 10:36
     * @since 1.0.0
     * @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
     * @author zifangsky
     * @date 2018/4/23 10:40
     * @since 1.0.0
     * @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, call the validate method in this class, and verify the IP address, Mac address and other information
     * @author zifangsky
     * @date 2018/4/23 10:40
     * @since 1.0.0
     * @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
     * @author zifangsky
     * @date 2018/5/2 15:43
     * @since 1.0.0
     * @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("Certificate expiration time cannot be earlier than current time");
        }
        if (null != notBefore && null != notAfter && notAfter.before(notBefore)){
            throw new LicenseContentException("Certificate effective time cannot be later than certificate expiration time");
        }
        final String consumerType = content.getConsumerType();
        if (null == consumerType){
            throw new LicenseContentException("User type cannot be empty");
        }
    }


    /**
     * Copy the validate method, add the IP address, Mac address and other information verification
     * @author zifangsky
     * @date 2018/4/23 10:40
     * @since 1.0.0
     * @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 user-defined License parameters
        //Allowed parameter information in License
        LicenseCheckModel expectedCheckModel = (LicenseCheckModel) content.getExtra();
        //Real parameter information of current server
        LicenseCheckModel serverCheckModel = getServerInfos();

        if(expectedCheckModel != null && serverCheckModel != null){
            //Verify IP address
            if(!checkIpAddress(expectedCheckModel.getIpAddress(),serverCheckModel.getIpAddress())){
                throw new LicenseContentException("Of the current server IP Not authorized");
            }

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

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

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


    /**
     * Rewrite XMLDecoder to parse XML
     * @author zifangsky
     * @date 2018/4/25 14:02
     * @since 1.0.0
     * @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) {
                logger.error("XMLDecoder analysis XML fail",e);
            }
        }

        return null;
    }

    /**
     * Obtain the License parameters that need additional verification for the current server
     * @author zifangsky
     * @date 2018/4/23 14:33
     * @since 1.0.0
     * @return demo.LicenseCheckModel
     */
    private LicenseCheckModel getServerInfos(){
        //Operating system type
        String osName = System.getProperty("os.name").toLowerCase();
        AbstractServerInfos abstractServerInfos = null;

        //Choose 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 that the IP/Mac address of the current server is within the allowed IP range < br / >
     * Returns true if there is an IP within the allowed IP/Mac address range
     * @author zifangsky
     * @date 2018/4/24 11:44
     * @since 1.0.0
     * @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 that the serial number of the current server hardware (motherboard, CPU, etc.) is within the allowable range
     * @author zifangsky
     * @date 2018/4/24 14:38
     * @since 1.0.0
     * @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;
        }
    }

}

Finally, the License generation class is used to generate the License certificate:

package cn.zifangsky.license;

import de.schlichtherle.license.CipherParam;
import de.schlichtherle.license.DefaultCipherParam;
import de.schlichtherle.license.DefaultLicenseParam;
import de.schlichtherle.license.KeyStoreParam;
import de.schlichtherle.license.LicenseContent;
import de.schlichtherle.license.LicenseManager;
import de.schlichtherle.license.LicenseParam;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

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

/**
 * License Generating class
 *
 * @author zifangsky
 * @date 2018/4/19
 * @since 1.0.0
 */
public class LicenseCreator {
    private static Logger logger = LogManager.getLogger(LicenseCreator.class);
    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
     * @author zifangsky
     * @date 2018/4/20 10:58
     * @since 1.0.0
     * @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){
            logger.error(MessageFormat.format("Certificate generation failed:{0}",param),e);
            return false;
        }
    }

    /**
     * Initialize certificate generation parameters
     * @author zifangsky
     * @date 2018/4/20 10:56
     * @since 1.0.0
     * @return de.schlichtherle.license.LicenseParam
     */
    private LicenseParam initLicenseParam(){
        Preferences preferences = Preferences.userNodeForPackage(LicenseCreator.class);

        //Set the secret key to encrypt the certificate content
        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
     * @author zifangsky
     * @date 2018/4/20 10:57
     * @since 1.0.0
     * @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;
    }

}

3. Add a Controller to generate certificate:

This Controller provides two external RESTful interfaces, namely "get server hardware information" and "generate certificate". The example code is as follows:

package cn.zifangsky.controller;

import cn.zifangsky.license.AbstractServerInfos;
import cn.zifangsky.license.LicenseCheckModel;
import cn.zifangsky.license.LicenseCreator;
import cn.zifangsky.license.LicenseCreatorParam;
import cn.zifangsky.license.LinuxServerInfos;
import cn.zifangsky.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.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

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

/**
 *
 * It is used to generate certificate files and cannot be placed in the code deployed to customers
 * @author zifangsky
 * @date 2018/4/26
 * @since 1.0.0
 */
@RestController
@RequestMapping("/license")
public class LicenseCreatorController {

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

    /**
     * Get server hardware information
     * @author zifangsky
     * @date 2018/4/26 13:13
     * @since 1.0.0
     * @param osName Operating system type. If it is empty, it will be judged automatically
     * @return com.ccx.models.license.LicenseCheckModel
     */
    @RequestMapping(value = "/getServerInfos",produces = {MediaType.APPLICATION_JSON_UTF8_VALUE})
    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;

        //Choose 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
     * @author zifangsky
     * @date 2018/4/26 13:13
     * @since 1.0.0
     * @param param Parameters required for certificate generation, such as: {"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", }}
     * @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(required = true) 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;
    }
}

3, Using the keytool tool provided with JDK to generate public private key certificate Library

If we set the public key library password as: public_password1234, the password of private key library is private_password1234, the generated command is as follows:

#Build 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 under the current path, respectively: privateKeys.keystore , publicCerts.keystore , certfile.cer . Among them, documents certfile.cer No longer need to be able to delete, file privateKeys.keystore Used in the current ServerDemo project to generate the license file for the customer, while the file publicCerts.keystore Then the application code is deployed to the client server, and the user decrypts the license file and verifies its license information.

4, Generate license files for customers

Deploy the ServerDemo project to the client server, and obtain the hardware information of the server through the following interface (delete the project after the license file is generated). Of course, you can manually obtain the hardware information of the client server through the command, and then generate the license file on the developer's own computer):

Then generate the license file:

When requesting, you need to add a content type to the Header, whose value is: application/json;charset=UTF-8. Examples of parameters are as follows:

{
	"subject": "license_demo",
	"privateAlias": "privateKey",
	"keyPass": "private_password1234",
	"storePass": "public_password1234",
	"licensePath": "C:/Users/zifangsky/Desktop/license_demo/license.lic",
	"privateKeysStorePath": "C:/Users/zifangsky/Desktop/license_demo/privateKeys.keystore",
	"issuedTime": "2018-07-10 00:00:01",
	"expiryTime": "2019-12-31 23:59:59",
	"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"
	}
}

If the request is successful, a license.lic This file is the server license file for deploying code to customers.

5, Add License verification to the application deployed by the customer

1. Add the parameters required by the License verification class

package cn.zifangsky.license;

/**
 * License Parameters required for verification class
 *
 * @author zifangsky
 * @date 2018/4/20
 * @since 1.0.0
 */
public class LicenseVerifyParam {

    /**
     * Certificate subject
     */
    private String subject;

    /**
     * Public key nickname
     */
    private String publicAlias;

    /**
     * Password to access public key library
     */
    private String storePass;

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

    /**
     * Keystore store path
     */
    private String publicKeysStorePath;

    public LicenseVerifyParam() {

    }

    public LicenseVerifyParam(String subject, String publicAlias, String storePass, String licensePath, String publicKeysStorePath) {
        this.subject = subject;
        this.publicAlias = publicAlias;
        this.storePass = storePass;
        this.licensePath = licensePath;
        this.publicKeysStorePath = publicKeysStorePath;
    }

    //Omit setter and getter methods

    @Override
    public String toString() {
        return "LicenseVerifyParam{" +
                "subject='" + subject + '\'' +
                ", publicAlias='" + publicAlias + '\'' +
                ", storePass='" + storePass + '\'' +
                ", licensePath='" + licensePath + '\'' +
                ", publicKeysStorePath='" + publicKeysStorePath + '\'' +
                '}';
    }
}

Then add the License verification class:

package cn.zifangsky.license;

import de.schlichtherle.license.*;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.io.File;
import java.text.DateFormat;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.prefs.Preferences;

/**
 * License Verification class
 *
 * @author zifangsky
 * @date 2018/4/20
 * @since 1.0.0
 */
public class LicenseVerify {
    private static Logger logger = LogManager.getLogger(LicenseVerify.class);

    /**
     * Install License certificate
     * @author zifangsky
     * @date 2018/4/20 16:26
     * @since 1.0.0
     */
    public synchronized LicenseContent install(LicenseVerifyParam param){
        LicenseContent result = null;
        DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

        //1. Installation certificate
        try{
            LicenseManager licenseManager = LicenseManagerHolder.getInstance(initLicenseParam(param));
            licenseManager.uninstall();

            result = licenseManager.install(new File(param.getLicensePath()));
            logger.info(MessageFormat.format("Certificate installation succeeded, certificate validity:{0} - {1}",format.format(result.getNotBefore()),format.format(result.getNotAfter())));
        }catch (Exception e){
            logger.error("Certificate installation failed!",e);
        }

        return result;
    }

    /**
     * Verify License certificate
     * @author zifangsky
     * @date 2018/4/20 16:26
     * @since 1.0.0
     * @return boolean
     */
    public boolean verify(){
        LicenseManager licenseManager = LicenseManagerHolder.getInstance(null);
        DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

        //2. Verification certificate
        try {
            LicenseContent licenseContent = licenseManager.verify();
//            System.out.println(licenseContent.getSubject());

            logger.info(MessageFormat.format("The certificate has passed the verification, and the validity period of the certificate is:{0} - {1}",format.format(licenseContent.getNotBefore()),format.format(licenseContent.getNotAfter())));
            return true;
        }catch (Exception e){
            logger.error("Certificate verification failed!",e);
            return false;
        }
    }

    /**
     * Initialize certificate generation parameters
     * @author zifangsky
     * @date 2018/4/20 10:56
     * @since 1.0.0
     * @param param License Parameters required for verification class
     * @return de.schlichtherle.license.LicenseParam
     */
    private LicenseParam initLicenseParam(LicenseVerifyParam param){
        Preferences preferences = Preferences.userNodeForPackage(LicenseVerify.class);

        CipherParam cipherParam = new DefaultCipherParam(param.getStorePass());

        KeyStoreParam publicStoreParam = new CustomKeyStoreParam(LicenseVerify.class
                ,param.getPublicKeysStorePath()
                ,param.getPublicAlias()
                ,param.getStorePass()
                ,null);

        return new DefaultLicenseParam(param.getSubject()
                ,preferences
                ,publicStoreParam
                ,cipherParam);
    }

}

2. Add a Listener to install the License certificate when the project starts

package cn.zifangsky.license;

import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;

/**
 * Install certificate on project startup
 *
 * @author zifangsky
 * @date 2018/4/24
 * @since 1.0.0
 */
@Component
public class LicenseCheckListener implements ApplicationListener<ContextRefreshedEvent> {
    private static Logger logger = LogManager.getLogger(LicenseCheckListener.class);

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

    /**
     * Public key nickname
     */
    @Value("${license.publicAlias}")
    private String publicAlias;

    /**
     * Password to access public key library
     */
    @Value("${license.storePass}")
    private String storePass;

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

    /**
     * Keystore store path
     */
    @Value("${license.publicKeysStorePath}")
    private String publicKeysStorePath;

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        //root application context has no parent
        ApplicationContext context = event.getApplicationContext().getParent();
        if(context == null){
            if(StringUtils.isNotBlank(licensePath)){
                logger.info("++++++++ Start installing certificate ++++++++");

                LicenseVerifyParam param = new LicenseVerifyParam();
                param.setSubject(subject);
                param.setPublicAlias(publicAlias);
                param.setStorePass(storePass);
                param.setLicensePath(licensePath);
                param.setPublicKeysStorePath(publicKeysStorePath);

                LicenseVerify licenseVerify = new LicenseVerify();
                //Installation certificate
                licenseVerify.install(param);

                logger.info("++++++++ End of certificate installation ++++++++");
            }
        }
    }
}

Note: the parameter information of the above code is as follows:

#License related configuration
license.subject=license_demo
license.publicAlias=publicCert
license.storePass=public_password1234
license.licensePath=C:/Users/zifangsky/Desktop/license_demo/license.lic
license.publicKeysStorePath=C:/Users/zifangsky/Desktop/license_demo/publicCerts.keystore

3. Add an interceptor to verify the License certificate when logging in

package cn.zifangsky.license;

import com.alibaba.fastjson.JSON;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;

/**
 * LicenseCheckInterceptor
 *
 * @author zifangsky
 * @date 2018/4/25
 * @since 1.0.0
 */
public class LicenseCheckInterceptor extends HandlerInterceptorAdapter{
    private static Logger logger = LogManager.getLogger(LicenseCheckInterceptor.class);

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        LicenseVerify licenseVerify = new LicenseVerify();

        //Check whether the certificate is valid
        boolean verifyResult = licenseVerify.verify();

        if(verifyResult){
            return true;
        }else{
            response.setCharacterEncoding("utf-8");
            Map<String,String> result = new HashMap<>(1);
            result.put("result","Your certificate is invalid. Please check whether the server is authorized or reapply for the certificate!");

            response.getWriter().write(JSON.toJSONString(result));

            return false;
        }
    }

}

4. Add login page and test

<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta content="text/html;charset=UTF-8"/>
    <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
    <meta name="viewport" content="width=device-width, initial-scale=1"/>
    <title>Login page</title>
    <script src="https://cdn.bootcss.com/jquery/2.2.4/jquery.min.js"></script>
    <link href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
    <link href="https://cdn.bootcss.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
    <script src="https://cdn.bootcss.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
    <link rel="stylesheet" th:href="@{/css/style.css}"/>

    <script>
        //Enter to log in
        function enterlogin(e) {
            var key = window.event ? e.keyCode : e.which;
            if (key === 13) {
                userLogin();
            }
        }

        //User password login
        function userLogin() {
            //Get user name and password
            var username = $("#username").val();
            var password = $("#password").val();

            if (username == null || username === "") {
                $("#errMsg").text("Please enter login user name!");
                $("#errMsg").attr("style", "display:block");
                return;
            }
            if (password == null || password === "") {
                $("#errMsg").text("Please enter the login password!");
                $("#errMsg").attr("style", "display:block");
                return;
            }

            $.ajax({
                url: "/check",
                type: "POST",
                dataType: "json",
                async: false,
                data: {
                    "username": username,
                    "password": password
                },
                success: function (data) {
                    if (data.code == "200") {
                        $("#errMsg").attr("style", "display:none");
                        window.location.href = '/userIndex';
                    } else if (data.result != null) {
                        $("#errMsg").text(data.result);
                        $("#errMsg").attr("style", "display:block");
                    } else {
                        $("#errMsg").text(data.msg);
                        $("#errMsg").attr("style", "display:block");
                    }
                }
            });
        }
    </script>
</head>
<body onkeydown="enterlogin(event);">
<div class="container">
    <div class="form row">
        <div class="form-horizontal col-md-offset-3" id="login_form">
            <h3 class="form-title">LOGIN</h3>
            <div class="col-md-9">
                <div class="form-group">
                    <i class="fa fa-user fa-lg"></i>
                    <input class="form-control required" type="text" placeholder="Username" id="username"
                           name="username" autofocus="autofocus" maxlength="20"/>
                </div>
                <div class="form-group">
                    <i class="fa fa-lock fa-lg"></i>
                    <input class="form-control required" type="password" placeholder="Password" id="password"
                           name="password" maxlength="8"/>
                </div>
                <div class="form-group">
                    <span class="errMsg" id="errMsg" style="display: none">Error prompt</span>
                </div>
                <div class="form-group col-md-offset-9">
                    <button type="submit" class="btn btn-success pull-right" name="submit" onclick="userLogin()">Sign in
                    </button>
                </div>
            </div>
        </div>
    </div>
</div>
</body>
</html>

Starting the project, you can find that the license certificate generated before can be used normally:

Now visit http://127.0.0.1:7080/login, you can log in normally:

Regenerate the license certificate and set a short expiration date.
Restart ClientDemo and log in again. You can find the following prompt information:


So far, it's over about building and verifying licenses with TrueLicense
Turn from big guy: Use TrueLicense in Spring Boot project to generate and verify License (Server License)

Topics: Java Mac Apache log4j