An introduction to the

A License, or copyright License, is used to provide access permission certificate for paid software users. Based on different application deployment locations, the following two scenarios can be discussed:

  • The application is deployed on the developer’s own cloud server. In this case, users log in to the system using an account. Therefore, you only need to verify the validity period and access permission of the target account during the login.
  • The application is deployed on the customer’s Intranet. Because the developer has no control over the client’s network environment and cannot guarantee that the application server can access the Internet, it is common practice to use the server license file, load the certificate at application startup, and verify the validity of the certificate at login or other critical operations.

Note: Due to the length of this article, only license restrictions at the code level are discussed here, not reverse cracking and other issues. In addition, I will only explain the key code implementation below, the complete code can be referred to: gitee.com/zifangsky/L…

Use TrueLicense to generate a License

(1) Use Spring Boot to build the test project ServerDemo, which is used to generate License files for customers:

Note: The full Demo project can be found at: gitee.com/zifangsky/L…

I) Add key dependencies to POM.xml:

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

Ii) Verifying customized License parameters:

TrueLicense DE. Schlichtherle. License. LicenseManager class cabin verify check only behind us license issued by the effective and expiration time, However, in actual projects, we may need to verify the IP address, MAC address, CPU serial number, motherboard serial number and other information of the server where the application is deployed. Therefore, we need to copy part of the framework method to verify the user-defined parameters.

First, we need to add a custom entity class for the allowed server hardware information (if other parameters are verified, you can add it yourself) :

package cn.zifangsky.license;

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

/** * Customize License parameters to be verified **@author zifangsky
 * @date 2018/4/23
 * @since1.0.0 * /
public class LicenseCheckModel implements Serializable{

    private static final long serialVersionUID = 8600137500316662317L;
    /** * The allowed IP address */
    private List<String> ipAddress;

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

    /** * The allowed CPU serial number */
    private String cpuSerial;

    /** ** The allowed motherboard serial number */
    private String mainBoardSerial;

    // omit setter and getter methods

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

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;

/** * Parameters required for License generation **@author zifangsky
 * @date 2018/4/19
 * @since1.0.0 * /
public class LicenseCreatorParam implements Serializable {

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

    /** * The key is also called */
    private String privateAlias;

    /** * Key password (need to be kept safe, do not let users know) */
    private String keyPass;

    /** * The password to access the keystore */
    private String storePass;

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

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

    /** * Certificate validity 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;

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

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

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

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

    // omit setter and getter methods

    @Override
    public String toString(a) {
        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 +
                '} '; }}Copy the code

Add abstract class AbstractServerInfos to get server hardware information:

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;

/** * Used to obtain basic server information, such as THE IP address, Mac address, CPU serial number, and mainboard serial number **@author zifangsky
 * @date 2018/4/23
 * @since1.0.0 * /
public abstract class AbstractServerInfos {
    private static Logger logger = LogManager.getLogger(AbstractServerInfos.class);

    /** * Assemble License parameters that require additional verification *@author zifangsky
     * @date2018/4/23 in *@since 1.0.0
     * @return demo.LicenseCheckModel
     */
    public LicenseCheckModel getServerInfos(a){
        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 obtain server hardware information",e);
        }

        return result;
    }

    /** * Obtain the 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(a) throws Exception;

    /** * Obtain the 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(a) throws Exception;

    /** * Get CPU serial number *@author zifangsky
     * @date2018/4/23 everything *@since 1.0.0
     * @return java.lang.String
     */
    protected abstract String getCPUSerial(a) throws Exception;

    /** * Get the motherboard serial number *@author zifangsky
     * @date2018/4/23 everything *@since 1.0.0
     * @return java.lang.String
     */
    protected abstract String getMainBoardSerial(a) throws Exception;

    /** * Get all the valid inetAddresses * of the current server@author zifangsky
     * @date2018/4/23 covenant *@since 1.0.0
     * @return java.util.List<java.net.InetAddress>
     */
    protected List<InetAddress> getLocalAllInetAddress(a) throws Exception {
        List<InetAddress> result = new ArrayList<>(4);

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

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

    /** * Obtain 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 hexadecimal byte to a 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; }}Copy the code

Obtain basic information about the customer’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;

/** * Used to obtain basic information about the customer's Linux server **@author zifangsky
 * @date 2018/4/23
 * @since1.0.0 * /
public class LinuxServerInfos extends AbstractServerInfos {

    @Override
    protected List<String> getIpAddress(a) 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(a) throws Exception {
        List<String> result = null;

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

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

        return result;
    }

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

        // Run the dmidecode command to obtain 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(a) throws Exception {
        / / serial number
        String serialNumber = "";

        // Run the dmidecode command to obtain the serial number of the mainboard
        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();
        returnserialNumber; }}Copy the code

Obtain basic information about the customer’s Windows server:

package cn.zifangsky.license;

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

/** * Used to obtain basic information about the customer's Windows server **@author zifangsky
 * @date 2018/4/23
 * @since1.0.0 * /
public class WindowsServerInfos extends AbstractServerInfos {

    @Override
    protected List<String> getIpAddress(a) 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(a) throws Exception {
        List<String> result = null;

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

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

        return result;
    }

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

        // Use WMIC to get the 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(a) throws Exception {
        / / serial number
        String serialNumber = "";

        // Use WMIC to get the motherboard serial number
        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();
        returnserialNumber; }}Copy the code

Note: The template method pattern is used here to encapsulate the algorithm of the immutable part into an abstract class, while the concrete implementation of the basic method is implemented by subclasses. For more information, see the document I wrote earlier: Template Method Patterns

Customizing the LicenseManager, which is used 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 LicenseManager, which is used to add additional server hardware information verification **@author zifangsky
 * @date 2018/4/23
 * @since1.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";
    / / the default BUFSIZE
    private static final int DEFAULT_BUFSIZE = 8 * 1024;

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

    /** * duplicate create method *@author zifangsky
     * @date2018/4/23 ye that *@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);
    }

    /** * The install method is copied. The validate method calls the validate method of this class to verify the IP address, Mac address and other information *@author zifangsky
     * @date2018/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;
    }

    /** * Verify (); /** * verify (); /** * verify ()@author zifangsky
     * @date2018/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;
    }

    /** * Verifies the parameters of the generated certificate *@author zifangsky
     * @date 2018/5/2 15:43
     * @since 1.0.0
     * @paramContent 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("The effective date of the certificate shall not be later than the expiration date of the certificate.");
        }
        final String consumerType = content.getConsumerType();
        if (null == consumerType){
            throw new LicenseContentException("User type cannot be empty"); }}/** * Override the validate method to add the IP address, Mac address and other information *@author zifangsky
     * @date2018/4/23 10:40: *@since 1.0.0
     * @param content LicenseContent
     */
    @Override
    protected synchronized void validate(final LicenseContent content)
            throws LicenseContentException {
        //1. Call the parent class's validate method first
        super.validate(content);

        //2. Verify the customized License parameters
        // Parameters allowed in the License file
        LicenseCheckModel expectedCheckModel = (LicenseCheckModel) content.getExtra();
        // Actual server parameter information
        LicenseCheckModel serverCheckModel = getServerInfos();

        if(expectedCheckModel ! =null&& serverCheckModel ! =null) {// Verify the IP address
            if(! checkIpAddress(expectedCheckModel.getIpAddress(),serverCheckModel.getIpAddress())){throw new LicenseContentException("The IP address of the current server is not within the authorized range");
            }

            // Verify the Mac address
            if(! checkIpAddress(expectedCheckModel.getMacAddress(),serverCheckModel.getMacAddress())){throw new LicenseContentException("The Mac address of the current server is not in the authorized range");
            }

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

            // Verify CPU serial number
            if(! checkSerial(expectedCheckModel.getCpuSerial(),serverCheckModel.getCpuSerial())){throw new LicenseContentException("The CPU serial number of the current server is not within the authorized range"); }}else{
            throw new LicenseContentException("Cannot get server hardware information"); }}/** * Override XMLDecoder to parse XML *@author zifangsky
     * @date 2018/4/25 14:02
     * @since 1.0.0
     * @paramEncoded 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 fails to parse XML",e); }}return null;
    }

    /** * Obtain the License parameters that need to be verified *@author zifangsky
     * @dateBehold, 2018/4/23 *@since 1.0.0
     * @return demo.LicenseCheckModel
     */
    private LicenseCheckModel getServerInfos(a){
        // 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();
    }

    /** * Verifies whether the current server IP/Mac address is in the allowed IP/Mac address range. <br/> * Returns true * if any IP address is in 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; }}/** * Check whether the serial number of the current server hardware (mainboard, CPU, etc.) is within the allowed range *@author zifangsky
     * @date2018/4/24 out *@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; }}}Copy the code

Finally, the License generation class is used to generate License certificates.

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 generation class **@author zifangsky
 * @date 2018/4/19
 * @since1.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(a){
        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; }}/** * Initializes the certificate generation parameters *@author zifangsky
     * @date 2018/4/20 10:56
     * @since 1.0.0
     * @return de.schlichtherle.license.LicenseParam
     */
    private LicenseParam initLicenseParam(a){
        Preferences preferences = Preferences.userNodeForPackage(LicenseCreator.class);

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

        KeyStoreParam privateStoreParam = newCustomKeyStoreParam(LicenseCreator.class ,param.getPrivateKeysStorePath() ,param.getPrivateAlias() ,param.getStorePass()  ,param.getKeyPass()); LicenseParam licenseParam =new DefaultLicenseParam(param.getSubject()
                ,preferences
                ,privateStoreParam
                ,cipherParam);

        return licenseParam;
    }

    /** * Sets the certificate generation body *@author zifangsky
     * @date 2018/4/20 10:57
     * @since 1.0.0
     * @return de.schlichtherle.license.LicenseContent
     */
    private LicenseContent initLicenseContent(a){
        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());

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

        returnlicenseContent; }}Copy the code

Iii) Add a Controller that generates a certificate:

The Controller provides two RESTful interfaces, obtaining server hardware information and generating certificates. 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;

/** ** is used to generate certificate files, which cannot be placed in code for customer deployment *@author zifangsky
 * @date 2018/4/26
 * @since1.0.0 * /
@RestController
@RequestMapping("/license")
public class LicenseCreatorController {

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

    /** * Get server hardware information *@author zifangsky
     * @date2018/4/26 "*@since 1.0.0
     * @paramOsName Indicates the type of the operating system. If this parameter is empty, * is automatically detected@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;

        // 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 *@author zifangsky
     * @date2018/4/26 "*@since 1.0.0
     * @paramParam Specifies the parameters required for generating a 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.ke ystore","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.2" "], "45.1", "10.0.5.22 macAddress" : [" 00-50-56 - C0-00-01 ", "50-7-9 d b - the F9-18-41"], "cpuSerial BFEBFBFF000406E3" : ""," mainBoardSeria l":"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(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!");
        }

        returnresultMap; }}Copy the code

(2) Use the built-in KEYtool of the JDK to generate the public and private key certificate library:

If we set the password of the public key library to public_password1234 and the password of the private key library to private_password1234, the following command is generated:

# 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"
Copy the code

Keystore, publicCerts. Keystore, and certfile.cer files are generated in the current path. The certfile.cer file is no longer needed and can be deleted. The privatekeys. keystore file is used to generate a license file for the current ServerDemo project, and the publiccerts.keystore file is deployed to the client server with the application code. Users decrypt license files and verify license information.

(3) Generate license files for customers:

Deploy the ServerDemo project on the client server and obtain the hardware information of the server through the following interfaces. (Delete the project 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) :

Note: The figure above uses the RESTClient plug-in for Firefox

Then generate the license file:

You need to add a content-type to the Header with the value application/json; Charset = utf-8. The following are examples of parameters:

{
	"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": "The 2018-07-10 00:00:01"."expiryTime": "The 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"}}Copy the code

If the request is successful, a license.lic file is generated at the licensePath parameter setting path. This file is the server license file for the client to deploy the code.

Add License verification to the application deployed by the customer

(1) Use Spring Boot to build the test project ServerDemo, which is used to simulate the application deployed for customers:

Note: The full Demo project can be found at: gitee.com/zifangsky/L…

(2) Parameters required for adding the License verification class:

package cn.zifangsky.license;

/** * Parameters required for License verification **@author zifangsky
 * @date 2018/4/20
 * @since1.0.0 * /
public class LicenseVerifyParam {

    /**
     * 证书subject
     */
    private String subject;

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

    /** * The password to access the public key library */
    private String storePass;

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

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

    public LicenseVerifyParam(a) {}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(a) {
        return "LicenseVerifyParam{" +
                "subject='" + subject + '\' ' +
                ", publicAlias='" + publicAlias + '\' ' +
                ", storePass='" + storePass + '\' ' +
                ", licensePath='" + licensePath + '\' ' +
                ", publicKeysStorePath='" + publicKeysStorePath + '\' ' +
                '} '; }}Copy the code

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 **@author zifangsky
 * @date 2018/4/20
 * @since1.0.0 * /
public class LicenseVerify {
    private static Logger logger = LogManager.getLogger(LicenseVerify.class);

    /** * Install the License certificate *@author zifangsky
     * @date7 all 2018/4/20 *@since1.0.0 * /
    public synchronized LicenseContent install(LicenseVerifyParam param){
        LicenseContent result = null;
        DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

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

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

        return result;
    }

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

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

            logger.info(MessageFormat.format("Certificate verification passed, certificate validity: {0} - {1}",format.format(licenseContent.getNotBefore()),format.format(licenseContent.getNotAfter())));
            return true;
        }catch (Exception e){
            logger.error("Certificate verification failed!",e);
            return false; }}/** * Initializes the certificate generation parameters *@author zifangsky
     * @date 2018/4/20 10:56
     * @since 1.0.0
     * @paramParam Specifies the parameter required for License verification *@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 newDefaultLicenseParam(param.getSubject() ,preferences ,publicStoreParam ,cipherParam); }}Copy the code

(3) Add a Listener to install the License certificate when the project is started.

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 the certificate ** when the project starts@author zifangsky
 * @date 2018/4/24
 * @since1.0.0 * /
@Component
public class LicenseCheckListener implements ApplicationListener<ContextRefreshedEvent> {
    private static Logger logger = LogManager.getLogger(LicenseCheckListener.class);

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

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

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

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

    /** * Keystore storage 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 the certificate ++++++++");

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

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

                logger.info("++++++++ Certificate installation completed ++++++++"); }}}}Copy the code

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

Configure the License
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
Copy the code

(4) Add an interceptor to verify the License certificate during login:

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
 * @since1.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();

        // Verify that 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 apply for a new certificate!");

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

            return false; }}}Copy the code

(5) Add login page and test:

Add a login page that displays an error message when the license verification fails:

<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" Function enterlogin(e) {var key = window.event? E.keycode: e.white; if (key === 13) { userLogin(); Function userLogin() {var username = $("#username").val(); var password = $("#password").val(); If (username = = null | | username = = = "") {$(" # errMsg"). The text (" please enter the login user name!" ); $("#errMsg").attr("style", "display:block"); return; } the if (password = = null | | password = = = "") {$(" # errMsg"). The text (" please input 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: <div class="form-group col-md-offset-9"> <button type="submit" class=" BTN btn-success The pull - right "name =" submit "onclick =" userLogin () "> login < / button > < / div > < / div > < / div > < / div > < / div > < / body > < / HTML >Copy the code

I) After the project is started, the generated license certificate can be used normally:

When visiting http://127.0.0.1:7080/login, can normal login:

Ii) Re-generate the license certificate and set the validity period to a short period.

Iii) Restart ClientDemo and log in again. You can find the following message:

This concludes the discussion on generating and verifying licenses using TrueLicense. Classes not covered in this article can be found in the sample source code, thanks for reading.