国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

軟件產(chǎn)品license的簡(jiǎn)單實(shí)現(xiàn)java

這篇具有很好參考價(jià)值的文章主要介紹了軟件產(chǎn)品license的簡(jiǎn)單實(shí)現(xiàn)java。希望對(duì)大家有所幫助。如果存在錯(cuò)誤或未考慮完全的地方,請(qǐng)大家不吝賜教,您也可以點(diǎn)擊"舉報(bào)違法"按鈕提交疑問(wèn)。

軟件License簡(jiǎn)介

我們?cè)谑褂靡恍┬枰?gòu)買(mǎi)版權(quán)的軟件產(chǎn)品時(shí),或者我們做的商業(yè)軟件需要進(jìn)行售賣(mài),為了收取費(fèi)用,一般需要一個(gè)軟件使用許可證,然后輸入這個(gè)許可到軟件里就能夠使用軟件。簡(jiǎn)單的是一串序列碼或者一個(gè)許可證文件,復(fù)雜的是一個(gè)定制化插件包。于是有的小伙伴就開(kāi)始好奇這個(gè)許可是怎么實(shí)現(xiàn)的,特別是在離線情況下它是怎么給軟件授權(quán),同時(shí)又能避免被破解的。

License控制內(nèi)容

本文主要介紹的是許可證形式的授權(quán)。

1. 如何控制只在指定服務(wù)器上使用

如果不控制指定設(shè)備,那么下發(fā)了許可證,只要把軟件復(fù)制多份安裝則可到處使用,不利于版權(quán)維護(hù),每個(gè)設(shè)備都有唯一標(biāo)識(shí):mac地址,ip地址,主板序列號(hào)等,在許可證中指定唯一標(biāo)識(shí)則只能指定設(shè)備使用。

2. 如何控制軟件使用期限

為了版權(quán)可持續(xù)性收益,對(duì)軟件使用設(shè)置期限,到期續(xù)費(fèi)等,則需要在許可證中配置使用起止日期。

3. 如何控制按收費(fèi)等級(jí)分模塊使用功能

售賣(mài)的軟件產(chǎn)品可能進(jìn)行分級(jí)收費(fèi),不同的收費(fèi)標(biāo)準(zhǔn)可使用的軟件功能范圍不同。許可證中配置可使用的功能模塊節(jié)點(diǎn),前提是軟件產(chǎn)品在設(shè)計(jì)開(kāi)發(fā)時(shí),需要有對(duì)應(yīng)的模塊結(jié)構(gòu)代碼。

實(shí)現(xiàn)方案

一、流程設(shè)計(jì)

  • 形式:許可證以文件形式下發(fā),放在服務(wù)器指定位置
  • 內(nèi)容:以上控制內(nèi)容以dom節(jié)點(diǎn)形式放在文件中
  • 流程:將控制項(xiàng)加密后寫(xiě)入license文件節(jié)點(diǎn),部署到客戶機(jī)器,客戶機(jī)使用時(shí)再讀取license文件內(nèi)容與客戶機(jī)實(shí)際參數(shù)進(jìn)行匹配校驗(yàn)
  • 工具:
    私用工具:包含生成秘鑰對(duì)和通過(guò)秘鑰生成加密內(nèi)容
    客戶工具:包含獲取客戶機(jī)唯一標(biāo)識(shí)內(nèi)容、校驗(yàn)license內(nèi)容、守護(hù)線程獲取及存儲(chǔ)結(jié)果
  • 時(shí)序圖
    軟件產(chǎn)品license的簡(jiǎn)單實(shí)現(xiàn)java
    二、文件防破解
  • 防止篡改:文件內(nèi)容加密,使用AES加密,但是AES加密解密都是使用同一個(gè)key;使用非對(duì)稱(chēng)公私鑰(本文使用的RSA)對(duì)內(nèi)容加密解密,但是對(duì)內(nèi)容長(zhǎng)度有限制;綜合方案,將AES的key(內(nèi)部定義)用RSA加密,公鑰放在加密工具中,內(nèi)部持有,私鑰放在解密工具中,引入軟件產(chǎn)品解密使用。
  • 防止修改系統(tǒng)時(shí)間繞過(guò)許可證使用時(shí)間:許可證帶上發(fā)布時(shí)間戳,并定時(shí)修改運(yùn)行時(shí)間記錄到文件,如果系統(tǒng)時(shí)間小于這個(gè)時(shí)間戳,就算大于許可證限制的起始時(shí)間也無(wú)法使用
  • 提高破解難度:懂技術(shù)的可以將代碼反編譯過(guò)來(lái)修改代碼文件直接繞過(guò)校驗(yàn),所以需要進(jìn)行代碼混淆,有測(cè)試過(guò)xjar的混淆效果比較好。

實(shí)現(xiàn)流程,直接上圖?。?!
軟件產(chǎn)品license的簡(jiǎn)單實(shí)現(xiàn)java

代碼示例講解

私用工具端:
RSA秘鑰對(duì)生成

package com.license.tools.licensecreate.utils;

import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.Base64;

/**
 * @Description 生成公鑰私鑰對(duì)
 * @createDate 2022/05/05
 * @createTime 14:25
 */
public class KeyGenerator {

    /**
     * 私鑰
     */
    private static byte[] privateKey;

    /**
     * 公鑰
     */
    private static byte[] publicKey;

    /**
     * 加密算法
     */
    private static final String KEY_ALGORITHM = "RSA";

    public void generater() {
        try {
            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(KEY_ALGORITHM);
            keyPairGenerator.initialize(1024);
            KeyPair keyPair = keyPairGenerator.genKeyPair();
            RSAPublicKey pubKey = (RSAPublicKey) keyPair.getPublic();
            RSAPrivateKey priKey = (RSAPrivateKey) keyPair.getPrivate();
            privateKey = Base64.getEncoder().encode(priKey.getEncoded());
            publicKey = Base64.getEncoder().encode(pubKey.getEncoded());
            System.out.println("公鑰:" + new String(publicKey));
            System.out.println("私鑰:" + new String(privateKey));
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            System.out.println("生成密鑰對(duì)失??!");
        }
    }

    public static void main(String[] args) {
        KeyGenerator keyGenerator = new KeyGenerator();
        keyGenerator.generater();
    }

}

運(yùn)行main方法,生成的秘鑰對(duì)填入RSAUtils的puk和prk,公鑰用于加密,私鑰用于解密

package com.license.tools.licensecreate.utils;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;

/**
 * @Description 對(duì)AES密碼加密
 * @createDate 2022/05/05
 * @createTime 14:28
 */
public class RSAUtils {

    /**
     * 公鑰base64
     */
    private static String puk = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCdfujgTmG4aOa4oK2VysmKvAI+hurN/wuKQjzgJTo3ct6TH5NHFHncb9KXijC1xk2Po+pJ8UjU4XGjU4gq5yhTdeSYPYR6hj5jqLy8fkWpFzeC6RvM4bLDe1lDNKphpcUoo5ZO7T77w9fX2lgJSyy/8LxdBThc4Megga3KW1/W4wIDAQAB";

    /**
     * 加密
     *
     * @return
     * @throws Exception
     */
    protected static String encrypt(String content) throws Exception {

        byte[] publicKeyBytes = puk.getBytes();
        X509EncodedKeySpec x = new X509EncodedKeySpec(Base64.getDecoder().decode(publicKeyBytes));
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PublicKey pubKey = keyFactory.generatePublic(x);
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.ENCRYPT_MODE, pubKey);

        byte[] result = cipher.doFinal(content.getBytes("UTF-8"));
        return Base64.getEncoder().encodeToString(result);
    }

    /**
     * 私鑰base64
     */
    private static String prk = "MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAJ1+6OBOYbho5rigrZXKyYq8Aj6G6s3/C4pCPOAlOjdy3pMfk0cUedxv0peKMLXGTY+j6knxSNThcaNTiCrnKFN15Jg9hHqGPmOovLx+RakXN4LpG8zhssN7WUM0qmGlxSijlk7tPvvD19faWAlLLL/wvF0FOFzgx6CBrcpbX9bjAgMBAAECgYA8uRWohg//PdLXFHxY6JrUNrDW0sXtLoyQfgFimnfbsRpHt0DdgvOJHkQf0VP+gbqdyyEl6TWfflyGEErL39wX1rrosy+LpiN0HeISERJuwJtuiGeR+0qw+Xz2M7VE+e5oD94dRtlzERft2mcDbQAQYUCFNgUBtd1dCJgMJPZJYQJBANHxKKHqMbsH91JsGP8eCu+yeMah0X8cT79nwD71SJRc03W5P1MPKhRyGWJj0M+Wax32pAPCMTfbj19scLplJpUCQQDADD5OuSLYRVqx68/CYbFVK3ye/YD4Cgc+0kT9SoI9bLB10JumHT0seDGeXQqwUPAF3bBZGI8pW2bdtzDj8YGXAkABQXgEv+ncPIf2Lj9YB035cQ/X4E/oerrfYjd8KOtuN7/sDFecn5KY3LXaKM6u7y9k1nzUqOyycNXCtFtYQhKhAkBvgyxyvaFz/uFoyko6zksP705Pa1eFrx0B50pT4P26+O+FmXmnfPbWaXw2PkREmNqmLVGGinImS4JxXzuuP79FAkAFQejjE+5Twi8oSCcNwse7FFP86U6jgcc+S+XCUUkLXQ5SPlkyb037hwoV1lEEJpcyI2tSFRxBKT89KZN0Nfat";

    protected static String decrypt(String signEncrypt) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
        byte[] privateKeyBytes = prk.getBytes();
        PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKeyBytes));
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PrivateKey priKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);

        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.DECRYPT_MODE, priKey);

        byte[] result = cipher.doFinal(Base64.getDecoder().decode(signEncrypt));
        return new String(result);
    }

    public static void main(String[] args) throws Exception {
        String password = "123456";
        String a = encrypt(password);
        System.out.println("AES加密秘鑰:" + a);
    }
}

main方法輸入AES需要的明文(示例:123456),用RSA加密成密文,做為AESUtils的aesKey

package com.license.tools.licensecreate.utils;

import org.springframework.util.Base64Utils;

import javax.crypto.KeyGenerator;
import javax.crypto.*;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;

/**
 * @version V1.0
 * @desc AES 加密工具類(lèi)
 */
public class AESUtils {

    /**
     * RSA加密后的AES秘鑰
     */
    private static String aesEncyptPwd="ZIkun+KvXFWLZLYUwXqFWazQeRe119AkcGcl+p8Erzi4EEaHBFYcQuGuKthIE+1IWSQxoUpUJkT0T1+xtoRi3txDnBikdrFhccGZdRpqwRv58q5nqxJX4wVrq0Ms02KBKgQRTqqlzfYLzQcYPyhv8KPE8JDVkttic+W+j5pFles=";

    private static final String KEY_ALGORITHM = "AES";

    private static final String DEFAULT_CIPHER_ALGORITHM = "AES/ECB/PKCS5Padding";

    /**
     * AES 加密操作
     *
     * @param content  待加密內(nèi)容
     * @return 返回Base64轉(zhuǎn)碼后的加密數(shù)據(jù)
     */
    public static String encrypt(String content) {
        try {
            Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
            byte[] byteContent = content.getBytes("utf-8");
            cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(RSAUtils.decrypt(aesEncyptPwd)));
            byte[] result = cipher.doFinal(byteContent);
            return Base64Utils.encodeToString(result);
        } catch (Exception ex) {
            ex.printStackTrace();
            System.out.println("AES加密失敗");
        }

        return null;
    }

    /**
     * AES 解密操作
     *
     * @param content  已加密內(nèi)容
     * @return
     */
    public static String decrypt(String content) {
        try {
            //實(shí)例化
            Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
            //使用密鑰初始化,設(shè)置為解密模式
            cipher.init(Cipher.DECRYPT_MODE, getSecretKey(RSAUtils.decrypt(aesEncyptPwd)));
            //執(zhí)行操作
            byte[] result = cipher.doFinal(Base64Utils.decodeFromString(content));
            return new String(result, "utf-8");
        } catch (Exception ex) {
            ex.printStackTrace();
            System.out.println("AES解密失敗");
        }
        return null;
    }

    /**
     * 生成加密秘鑰
     *
     * @return
     */
    private static SecretKeySpec getSecretKey(String aesKey) {
        //返回生成指定算法密鑰生成器的 KeyGenerator 對(duì)象
        KeyGenerator kg = null;
        try {
            kg = KeyGenerator.getInstance(KEY_ALGORITHM);
            //AES 要求密鑰長(zhǎng)度為 128
            SecureRandom random=SecureRandom.getInstance("SHA1PRNG","SUN");
            random.setSeed(aesKey.getBytes());
            kg.init(128, random);
            //生成一個(gè)密鑰
            SecretKey secretKey = kg.generateKey();
            return new SecretKeySpec(secretKey.getEncoded(), KEY_ALGORITHM);
        } catch (NoSuchAlgorithmException ex) {
            ex.printStackTrace();
            System.out.println("生成加密秘鑰失敗");
        } catch (NoSuchProviderException e) {
            e.printStackTrace();
            System.out.println("生成加密秘鑰失敗");
        }
        return null;
    }
}

生成license文件的方法

package com.license.tools.licensecreate.test;

import com.license.tools.licensecreate.utils.AESUtils;
import com.license.tools.licensecreate.utils.DateUtils;
import org.dom4j.Document;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.XMLWriter;

import java.io.File;
import java.io.FileWriter;
import java.util.Scanner;

/**
 * @Description 生成簽名
 * @createDate 2022/05/05
 * @createTime 17:41
 */
public class CreateSign {

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        //系統(tǒng)標(biāo)識(shí)---由mac地址+cpu序列號(hào),在客戶工具端獲取客戶機(jī)mac地址和cpu序列號(hào)后用AES加密得到
        System.out.println("請(qǐng)輸入系統(tǒng)標(biāo)識(shí)串(部署的服務(wù)獲?。?);
        String systemSign = sc.nextLine();
        System.out.println("請(qǐng)輸入生效起始時(shí)間(格式如:2022-05-05 00:00:00):");
        String generatedTimeStr = sc.nextLine();
        System.out.println("請(qǐng)輸入生效截止時(shí)間(格式如:2022-05-05 00:00:00):");
        String expiredTimeStr = sc.nextLine();
        System.out.println("請(qǐng)輸入上一次校驗(yàn)時(shí)間初始值(格式如:2022-05-05 00:00:00):");
        String lastValidateTimeStr = sc.nextLine();
        System.out.println("請(qǐng)輸入項(xiàng)目部署唯一版本號(hào)(不能帶“-”):");
        String version = sc.nextLine();
        System.out.println("請(qǐng)輸入license文件生成路徑:");
        String path = sc.nextLine();
        createLicense(systemSign, generatedTimeStr, expiredTimeStr, lastValidateTimeStr, version, path);
        System.out.println("license文件生成成功,文件路徑:" + path);
    }

    private static void createLicense(String systemSign, String generatedTimeStr, String expiredTimeStr, String lastValidateTimeStr, String version, String path) {
        try {
            //解密系統(tǒng)標(biāo)識(shí)得到mac地址+cpu序列號(hào)
            String macAndCpu = AESUtils.decrypt(systemSign);
            System.out.println("客戶服務(wù)器mac地址和cpu序列號(hào):" + macAndCpu);

            //MAC地址-CPU序列號(hào)-生效起始時(shí)間-生效結(jié)束結(jié)束時(shí)間-軟件產(chǎn)品序列號(hào)(項(xiàng)目版本唯一標(biāo)識(shí))
//            String content = "A8:A1:59:41:89:36-BFEBFBFF000906EA-20220506-20220507-dmoiji3xkoa4p33";
            StringBuilder signBuilder = new StringBuilder(macAndCpu);
            //生效起始時(shí)間
            long generatedTime = DateUtils.getTimeInMillis(generatedTimeStr);
            //生效截止時(shí)間
            long expiredTime = DateUtils.getTimeInMillis(expiredTimeStr);
            //項(xiàng)目唯一標(biāo)識(shí)
            signBuilder.append("-").append(generatedTime).append("-").append(expiredTime).append("-").append(version);

            String sign = AESUtils.encrypt(signBuilder.toString());
            System.out.println("AES加密生成簽名:");
            System.out.println("-----------------------------------------------------------------------------------------------");
            System.out.println(sign);
            System.out.println("-----------------------------------------------------------------------------------------------");

            //生成licence文件
            Document document = DocumentHelper.createDocument();
            //根節(jié)點(diǎn)
            Element rootEle = document.addElement("license");
            //功能數(shù)據(jù)節(jié)點(diǎn),擴(kuò)展參數(shù)時(shí)可在此節(jié)點(diǎn)下擴(kuò)展
            Element dataEle = rootEle.addElement("features");
            Element featureEle = dataEle.addElement("feature");
            featureEle.addAttribute("name", "lastValidateTi");
            featureEle.addAttribute("ti", AESUtils.encrypt(String.valueOf(DateUtils.getTimeInMillis(lastValidateTimeStr))));
            //簽名節(jié)點(diǎn)
            Element signEle = rootEle.addElement("signature");
            signEle.setText(sign);
            System.out.println(document.asXML());
            OutputFormat format = OutputFormat.createPrettyPrint();
            // 設(shè)置編碼格式
            format.setEncoding("UTF-8");
            FileWriter fileWriter = new FileWriter(new File(path));
            XMLWriter xmlWriter = new XMLWriter(fileWriter, format);
            // 設(shè)置是否轉(zhuǎn)義,默認(rèn)使用轉(zhuǎn)義字符
            xmlWriter.setEscapeText(false);
            xmlWriter.write(document);
            xmlWriter.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

其中第一步的系統(tǒng)標(biāo)識(shí)由下面的客戶工具獲取客戶機(jī)的mac地址和cpu序列號(hào)而來(lái),然后將此方法生成的license文件和軟件包同路徑部署(也可修改客戶工具中指定路徑)

客戶工具端:
只有解密方法的RSAUtils

package com.dtranx.tools.license.utils;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;

/**
 * @author penghao
 * @Description 對(duì)AES密碼加密
 * @createDate 2022/05/05
 * @createTime 14:28
 */
public class RSAUtils {

    /**
     * 私鑰base64
     */
    private static String prk = "MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAJ1+6OBOYbho5rigrZXKyYq8Aj6G6s3/C4pCPOAlOjdy3pMfk0cUedxv0peKMLXGTY+j6knxSNThcaNTiCrnKFN15Jg9hHqGPmOovLx+RakXN4LpG8zhssN7WUM0qmGlxSijlk7tPvvD19faWAlLLL/wvF0FOFzgx6CBrcpbX9bjAgMBAAECgYA8uRWohg//PdLXFHxY6JrUNrDW0sXtLoyQfgFimnfbsRpHt0DdgvOJHkQf0VP+gbqdyyEl6TWfflyGEErL39wX1rrosy+LpiN0HeISERJuwJtuiGeR+0qw+Xz2M7VE+e5oD94dRtlzERft2mcDbQAQYUCFNgUBtd1dCJgMJPZJYQJBANHxKKHqMbsH91JsGP8eCu+yeMah0X8cT79nwD71SJRc03W5P1MPKhRyGWJj0M+Wax32pAPCMTfbj19scLplJpUCQQDADD5OuSLYRVqx68/CYbFVK3ye/YD4Cgc+0kT9SoI9bLB10JumHT0seDGeXQqwUPAF3bBZGI8pW2bdtzDj8YGXAkABQXgEv+ncPIf2Lj9YB035cQ/X4E/oerrfYjd8KOtuN7/sDFecn5KY3LXaKM6u7y9k1nzUqOyycNXCtFtYQhKhAkBvgyxyvaFz/uFoyko6zksP705Pa1eFrx0B50pT4P26+O+FmXmnfPbWaXw2PkREmNqmLVGGinImS4JxXzuuP79FAkAFQejjE+5Twi8oSCcNwse7FFP86U6jgcc+S+XCUUkLXQ5SPlkyb037hwoV1lEEJpcyI2tSFRxBKT89KZN0Nfat";

    protected static String decrypt(String signEncrypt) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
        byte[] privateKeyBytes = prk.getBytes();
        PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKeyBytes));
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PrivateKey priKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);

        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.DECRYPT_MODE, priKey);

        byte[] result = cipher.doFinal(Base64.getDecoder().decode(signEncrypt));
        return new String(result);
    }
}

AESUtils

package com.dtranx.tools.license.utils;

import org.springframework.util.Base64Utils;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;

/**
 * @version V1.0
 * @desc AES 加密工具類(lèi)
 */
public class AESUtils {

    /**
     * RSA加密后的AES秘鑰
     */
    private static String aesKey="ZIkun+KvXFWLZLYUwXqFWazQeRe119AkcGcl+p8Erzi4EEaHBFYcQuGuKthIE+1IWSQxoUpUJkT0T1+xtoRi3t" +
            "xDnBikdrFhccGZdRpqwRv58q5nqxJX4wVrq0Ms02KBKgQRTqqlzfYLzQcYPyhv8KPE8JDVkttic+W+j5pFles=";

    private static final String KEY_ALGORITHM = "AES";

    private static final String DEFAULT_CIPHER_ALGORITHM = "AES/ECB/PKCS5Padding";

    /**
     * AES 加密操作
     *
     * @param content  待加密內(nèi)容
     * @return 返回Base64轉(zhuǎn)碼后的加密數(shù)據(jù)
     */
    protected static String encrypt(String content) {
        try {
            Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
            byte[] byteContent = content.getBytes("utf-8");
            cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(RSAUtils.decrypt(aesKey)));
            byte[] result = cipher.doFinal(byteContent);
            return Base64Utils.encodeToString(result);
        } catch (Exception ex) {
            ex.printStackTrace();
            System.out.println("AES加密失敗");
        }

        return null;
    }

    /**
     * AES 解密操作
     *
     * @param content  已加密內(nèi)容
     * @return
     */
    protected static String decrypt(String content) {
        try {
            //實(shí)例化
            Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
            //使用密鑰初始化,設(shè)置為解密模式
            cipher.init(Cipher.DECRYPT_MODE, getSecretKey(RSAUtils.decrypt(aesKey)));
            //執(zhí)行操作
            byte[] result = cipher.doFinal(Base64Utils.decodeFromString(content));
            return new String(result, "utf-8");
        } catch (Exception ex) {
            ex.printStackTrace();
            System.out.println("AES解密失敗");
        }
        return null;
    }

    /**
     * 生成加密秘鑰
     *
     * @return
     */
    private static SecretKeySpec getSecretKey(String aesKey) {
        //返回生成指定算法密鑰生成器的 KeyGenerator 對(duì)象
        KeyGenerator kg = null;
        try {
            kg = KeyGenerator.getInstance(KEY_ALGORITHM);
            //AES 要求密鑰長(zhǎng)度為 128
            SecureRandom random=SecureRandom.getInstance("SHA1PRNG","SUN");
            random.setSeed(aesKey.getBytes());
            kg.init(128, random);
            //生成一個(gè)密鑰
            SecretKey secretKey = kg.generateKey();
            return new SecretKeySpec(secretKey.getEncoded(), KEY_ALGORITHM);
        } catch (NoSuchAlgorithmException ex) {
            ex.printStackTrace();
            System.out.println("生成加密秘鑰失敗");
        } catch (NoSuchProviderException e) {
            e.printStackTrace();
            System.out.println("生成加密秘鑰失敗");
        }
        return null;
    }
}

獲取系統(tǒng)mac地址和cpu序列號(hào)的SystemUtils

package com.dtranx.tools.license.utils;


import lombok.extern.slf4j.Slf4j;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetAddress;
import java.net.InterfaceAddress;
import java.net.NetworkInterface;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
import java.util.stream.Collectors;

/**
 * @author penghao
 */
@Slf4j
public class Systemutils {

    protected static String getMacAddress() {
        try {
            java.util.Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces();
            StringBuilder sb = new StringBuilder();
            while (en.hasMoreElements()) {
                NetworkInterface iface = en.nextElement();
                List<InterfaceAddress> addrs = iface.getInterfaceAddresses();
                for (InterfaceAddress addr : addrs) {
                    InetAddress ip = addr.getAddress();
                    NetworkInterface network = NetworkInterface.getByInetAddress(ip);
                    if (network == null) {
                        continue;
                    }
                    if (network.getName().toLowerCase().startsWith("ens")) {
                        byte[] mac = network.getHardwareAddress();
                        if (mac == null) {
                            continue;
                        }
                        for (int i = 0; i < mac.length; i++) {
                            sb.append(String.format("%02X%s", mac[i], (i < mac.length - 1) ? "-" : ""));
                        }
                        String xxy = sb.toString().replaceAll("-", "").toUpperCase();
                        log.info("xxy地址:{}", xxy);
                        return xxy;
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            log.error("讀取本機(jī)系統(tǒng)信息失??!");
        }
        return null;
    }

    protected static String getCpuNum() {
        BufferedReader reader = null;
        InputStreamReader ir = null;
        try {
            String[] linux = {"/bin/bash", "-c", "dmidecode -t processor | grep 'ID' | awk -F ':' '{print $2}' | head -n 1"};
            String[] windows = {"wmic", "cpu", "get", "ProcessorId"};

            // 獲取系統(tǒng)信息
            String property = System.getProperty("os.name");
            Process process = Runtime.getRuntime().exec(property.contains("Window") ? windows : linux);
            process.getOutputStream().close();
            ir = new InputStreamReader(process.getInputStream());
            reader = new BufferedReader(ir);
            String xxw = reader.readLine();
            if (xxw != null) {
                xxw = xxw.replaceAll(" ", "");
            }
            log.info("xxw識(shí)別碼:{}", xxw);
            return xxw;
        } catch (Exception e) {
            e.printStackTrace();
            log.error("獲取系統(tǒng)信息失??!");
        } finally {
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (ir != null) {
                try {
                    ir.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }

    public static void main(String[] args) throws Exception {
//        List<String> macs = getMacAddress();
//        System.out.println("本機(jī)的mac網(wǎng)卡的地址列表" + macs);
        System.out.println(getCpuNum());
    }
}

校驗(yàn)方法

package com.dtranx.tools.license.utils;


import com.dtranx.tools.license.bean.CheckParams;
import com.dtranx.tools.license.bean.ValidateCodeEnum;
import com.dtranx.tools.license.bean.ValidateResult;
import lombok.extern.slf4j.Slf4j;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.SAXReader;
import org.dom4j.io.XMLWriter;
import org.springframework.stereotype.Component;

import java.io.File;
import java.io.FileWriter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author PH
 */
@Slf4j
@Component
public class LicenseManager {

    public static Map<String, ValidateResult> validate() {
        Map<String, ValidateResult> result = new HashMap<String, ValidateResult>();
        CheckParams checkParams = null;
        try {
            checkParams = getCheckParams(result);
            if (checkParams == null) {
                return result;
            }
        } catch (Exception e) {
            e.printStackTrace();
            result.put("Authorize", ValidateResult.error(ValidateCodeEnum.EXCEPTION));
            return result;
        }

        //校驗(yàn)mac地址
        if (!checkParams.getMacAddress().equals(Systemutils.getMacAddress())) {
            result.put("Authorize", ValidateResult.error(ValidateCodeEnum.UNAUTHORIZED));
            return result;
        }
        //校驗(yàn)cpu序列號(hào)
        if (!checkParams.getCpuSerial().equals(Systemutils.getCpuNum())) {
            result.put("Authorize", ValidateResult.error(ValidateCodeEnum.UNAUTHORIZED));
            return result;
        }
        long currentTi = System.currentTimeMillis();
        //校驗(yàn)時(shí)間
        if (notAfterLastValidateTime(checkParams.getLastValidateTime(), currentTi) || notAfter(checkParams.getGeneratedTime(), currentTi)
                || notBefore(checkParams.getExpiredTime(), currentTi)) {
            result.put("Authorize", ValidateResult.error(ValidateCodeEnum.EXPIRED));
            return result;
        }

        result.put("Authorize", ValidateResult.ok());
        return result;
    }

    public static String getSystemSign() {
        String MacAddress = Systemutils.getMacAddress();
        String cpuNum = Systemutils.getCpuNum();
        return AESUtils.encrypt(MacAddress + "-" + cpuNum);
    }


    public static void updateSign(String sign) {
        try {
            Document document = readLicense();
            Element rootElement = document.getRootElement();
            Element signatureEle = rootElement.element("signature");
            signatureEle.setText(sign);
            OutputFormat format = OutputFormat.createPrettyPrint();
            // 設(shè)置編碼格式
            format.setEncoding("UTF-8");
            String path = System.getProperty("user.dir");
            FileWriter fileWriter = new FileWriter(new File(path + File.separator + "license.xml"));
            XMLWriter xmlWriter = new XMLWriter(fileWriter, format);
            // 設(shè)置是否轉(zhuǎn)義,默認(rèn)使用轉(zhuǎn)義字符
            xmlWriter.setEscapeText(false);
            xmlWriter.write(document);
            xmlWriter.close();
            log.info("更新授權(quán)碼成功");
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("更新授權(quán)碼失??!");
        }

    }

    private static boolean notAfterLastValidateTime(long lastValidateTime, long currentTi) {
        return lastValidateTime >= currentTi;
    }

    private static boolean notBefore(Long expiredTime, long currentTi) {
        return expiredTime <= currentTi;
    }

    private static boolean notAfter(long generatedTime, long currentTi) {
        return generatedTime >= currentTi;
    }


    private static CheckParams getCheckParams(Map<String, ValidateResult> result) {
        //讀取license文件
        Document document = readLicense();
        if (document == null) {
            log.error("license 讀取失?。?);
            result.put("Authorize", ValidateResult.error(ValidateCodeEnum.FILE_NOT_EXIST));
            return null;
        }
        Element rootElement = document.getRootElement();
        Element dataEle = rootElement.element("features");
        List<Element> featuresEles = dataEle.elements();
        Element lastValidateTimeEle = featuresEles.get(0);
        //提取上一次驗(yàn)證時(shí)間
        String lastValidateTimeStr = lastValidateTimeEle.attributeValue("ti");
        long lastValidateTime = Long.parseLong(AESUtils.decrypt(lastValidateTimeStr));
        log.debug("上一次校驗(yàn)時(shí)間:{}", lastValidateTime);
        //提取簽名內(nèi)容
        Element signEle = rootElement.element("signature");
        String signStr = signEle.getText();
        String sign = AESUtils.decrypt(signStr);
        if (sign == null) {
            log.error("授權(quán)碼不正確");
            result.put("Authorize", ValidateResult.error(ValidateCodeEnum.ILLEGAL));
            return null;
        }
        log.debug("簽名內(nèi)容:{}", sign);
        String[] signArr = sign.split("-");
        if (signArr.length != 5) {
            log.error("授權(quán)碼不正確");
            result.put("Authorize", ValidateResult.error(ValidateCodeEnum.ILLEGAL));
            return null;
        }

        CheckParams params = CheckParams.builder().lastValidateTime(lastValidateTime).macAddress(signArr[0])
                .cpuSerial(signArr[1]).generatedTime(Long.parseLong(signArr[2])).expiredTime(Long.parseLong(signArr[3]))
                .version(signArr[4]).build();
        return params;
    }

    private static Document readLicense() {
        Document document = null;
        try {
            SAXReader saxReader = new SAXReader();
            String path = System.getProperty("user.dir");
            document = saxReader.read(new File(path + File.separator + "license.xml"));
            return document;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    public static void main(String[] args) {
//        String sign = AESUtils.decrypt("VorZodH/B6eeNLPA09TNJ8fpjlvrsckBk3VW3Pvr2qzhQVdeL38xS8unNFFxzQrjZ70f4wIoi1Tg1wlZq9DFKuVyp2zD20A//lDswyaD8NsmwMR72R2Ua+Gb0dp+PpM3b9gx2iIFIAtKOyaJlMMV8H4az/EKc/d733lyHfY3wbhsmo4vUvsqPYiriaj+psPu7DgO0DsQqw0xjAblpcrfL1xc42E3STEi9NTNbbBTsLU=");

        String s="HPdW5CR3bRzVGEDMkZtsfQMHbcJ6SabTLJqdNsvJ7aU=";
        System.out.println(AESUtils.decrypt(s));
    }
}

守護(hù)線程,定時(shí)獲取校驗(yàn)結(jié)果,校驗(yàn)時(shí)間間隔從配置文件讀取

package com.dtranx.tools.license.utils;

import com.dtranx.tools.license.bean.ValidateResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * @author penghao
 * @createDate 2022/05/11
 * @createTime 16:42
 */
@Component
@Slf4j
public class LicenseThread implements Runnable {

    public static Map<String, ValidateResult> validateResult = null;

    @Value("${xxy.checkTime}")
    private Long checkTime;

    @Bean
    public void startThread() {
        Thread thread = new Thread(this);
        thread.setDaemon(true);
        thread.start();
    }

    public void run() {
        while (true) {
            validateResult = LicenseManager.validate();
            if (validateResult != null) {
                ValidateResult result = validateResult.get("Authorize");
                log.debug("license校驗(yàn)結(jié)果:" + result.getMessage());
            }
            try {
                //正式改為12個(gè)小時(shí)校驗(yàn)一次,保持與登錄同步即可
//                TimeUnit.HOURS.sleep(12);
                //測(cè)試1分鐘校驗(yàn)一次
                TimeUnit.SECONDS.sleep(checkTime);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static boolean validateAfterUpdateSign() {
        validateResult = LicenseManager.validate();
        ValidateResult result = validateResult.get("Authorize");
        return result != null && result.getIsValidate();
    }
}

注意事項(xiàng)

1、獲取cpu序列號(hào)時(shí),實(shí)際是通過(guò)執(zhí)行命令“dmidecode -t processor | grep ‘ID’ | awk -F ‘:’ ‘{print $2}’ | head -n 1”獲取,在docker中運(yùn)行服務(wù),如果找不到dmidecode 命令,需要綁定硬件信息配置到容器內(nèi)
docker 掛載目錄增加

  • /dev/mem:/dev/mem
  • /sbin/dmidecode:/sbin/dmidecode
  • /usr/sbin/dmidecode:/usr/sbin/dmidecode

2、docker網(wǎng)絡(luò)使用非宿主機(jī)網(wǎng)絡(luò)時(shí),docker內(nèi)的MAC地址會(huì)隨著docker的重啟改變,導(dǎo)致之前生成的授權(quán)碼校驗(yàn)不通過(guò)。處理措施有以下幾種:
(1)docker容器內(nèi)使用宿主機(jī)的網(wǎng)絡(luò)“–net=host --privileged=true ”,則mac地址一直跟隨宿主機(jī)
(2)docker啟動(dòng)命令添加指定mac地址“ --mac-address=xx:xx:xx:xx:xx:xx”

源碼

附完整源碼地址(包含使用說(shuō)明):github文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-447511.html

到了這里,關(guān)于軟件產(chǎn)品license的簡(jiǎn)單實(shí)現(xiàn)java的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!

本文來(lái)自互聯(lián)網(wǎng)用戶投稿,該文觀點(diǎn)僅代表作者本人,不代表本站立場(chǎng)。本站僅提供信息存儲(chǔ)空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如若轉(zhuǎn)載,請(qǐng)注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實(shí)不符,請(qǐng)點(diǎn)擊違法舉報(bào)進(jìn)行投訴反饋,一經(jīng)查實(shí),立即刪除!

領(lǐng)支付寶紅包贊助服務(wù)器費(fèi)用

相關(guān)文章

  • 軟件產(chǎn)品認(rèn)定需要準(zhǔn)備什么

    一、軟件著作權(quán) 由于雙軟認(rèn)證并不是一個(gè)資質(zhì),而是\\\"軟件產(chǎn)品登記\\\"和\\\"軟件企業(yè)認(rèn)定\\\"兩種不同資質(zhì)的統(tǒng)稱(chēng).稱(chēng)為\\\"雙軟企業(yè)\\\"。要進(jìn)行軟件產(chǎn)品登記就要有軟件著作權(quán)。 申請(qǐng)軟件著作權(quán)需要營(yíng)業(yè)執(zhí)照(個(gè)人申請(qǐng)憑身份證就行了)、源代碼前后各40頁(yè),不足80頁(yè)的就全部提供、用戶

    2024年02月06日
    瀏覽(26)
  • 創(chuàng)新領(lǐng)航 | 竹云產(chǎn)品入選“年度優(yōu)秀創(chuàng)新軟件產(chǎn)品推廣目錄”

    創(chuàng)新領(lǐng)航 | 竹云產(chǎn)品入選“年度優(yōu)秀創(chuàng)新軟件產(chǎn)品推廣目錄”

    8月31日,由中國(guó)電子信息行業(yè)聯(lián)合會(huì)主辦的第二十五屆中國(guó)國(guó)際軟件博覽會(huì)在天津拉開(kāi)帷幕,本次大會(huì)以“塑造軟件新生態(tài) 賦能發(fā)展新變革”為主題,為全國(guó)乃至全球軟件行業(yè)高質(zhì)量發(fā)展聚智聚“力”。 大會(huì)發(fā)布“2022-2023年度優(yōu)秀創(chuàng)新軟件產(chǎn)品推廣目錄”,涵蓋基礎(chǔ)軟件、

    2024年02月09日
    瀏覽(18)
  • 【火鳥(niǎo)視頻創(chuàng)作軟件】補(bǔ)天云火鳥(niǎo)視頻創(chuàng)作軟件產(chǎn)品用戶手冊(cè) (附軟件下載地址)

    【火鳥(niǎo)視頻創(chuàng)作軟件】補(bǔ)天云火鳥(niǎo)視頻創(chuàng)作軟件產(chǎn)品用戶手冊(cè) (附軟件下載地址)

    1 安裝) 2 全局選項(xiàng)) 3 概念) 4 構(gòu)造視頻) 5 構(gòu)造音頻) 6 圖像構(gòu)造) 7 電子書(shū)構(gòu)造) 8 幫助和手冊(cè)) 9 捐贈(zèng)和廣告) 軟件下載地址: 【免費(fèi)下載免費(fèi)使用】火鳥(niǎo)視頻創(chuàng)作軟件 免費(fèi)+批量生成音視頻+自動(dòng)配音+自動(dòng)配字幕 適用于課程創(chuàng)作+視頻創(chuàng)作+音頻創(chuàng)作+PDF電子書(shū)創(chuàng)作+博客專(zhuān)欄創(chuàng)作

    2024年01月20日
    瀏覽(30)
  • 利用RSA加密打造強(qiáng)大License驗(yàn)證,確保軟件正版合法運(yùn)行

    利用RSA加密打造強(qiáng)大License驗(yàn)證,確保軟件正版合法運(yùn)行

    ? 概述: C#軟件開(kāi)發(fā)中,License扮演著確保軟件合法使用的重要角色。采用RSA非對(duì)稱(chēng)加密方案,服務(wù)端生成帶簽名的License,客戶端驗(yàn)證其有效性,從而實(shí)現(xiàn)對(duì)軟件的授權(quán)與安全保障。 License(許可證)在C#軟件開(kāi)發(fā)中被廣泛應(yīng)用,以確保軟件在合法授權(quán)的環(huán)境中運(yùn)行。常見(jiàn)場(chǎng)景

    2024年02月19日
    瀏覽(23)
  • SOFAStack軟件供應(yīng)鏈安全產(chǎn)品解析——SCA軟件成分分析

    SOFAStack軟件供應(yīng)鏈安全產(chǎn)品解析——SCA軟件成分分析

    近年來(lái),軟件供應(yīng)鏈安全相關(guān)攻擊事件呈快速增長(zhǎng)態(tài)勢(shì),造成的危害也越來(lái)越嚴(yán)重,為了保障軟件供應(yīng)鏈安全,各行業(yè)主管單位也出臺(tái)了諸多政策及技術(shù)標(biāo)準(zhǔn)。基于內(nèi)部多年的實(shí)踐,螞蟻數(shù)科金融級(jí)云原生PaaS平臺(tái)SOFAStack發(fā)布完整的軟件供應(yīng)鏈安全產(chǎn)品及解決方案,包括靜態(tài)代

    2024年02月04日
    瀏覽(26)
  • 云卷云舒:軟件產(chǎn)品質(zhì)量保證思考

    總體產(chǎn)品質(zhì)量觀,從上到下的質(zhì)量規(guī)劃,包括質(zhì)量目標(biāo)、質(zhì)量定義和拆解、質(zhì)量責(zé)任制 關(guān)系到研發(fā)質(zhì)量的各類(lèi)預(yù)防、檢測(cè)手段,如日常研發(fā)規(guī)范,代碼審查,檢測(cè)工具,測(cè)試等 一般指針對(duì)問(wèn)題的改進(jìn),包括根因分析,問(wèn)題溯源,倒逼研發(fā)改進(jìn) 幾個(gè)核心要素: 1,兩撥人:規(guī)

    2024年01月18日
    瀏覽(27)
  • 數(shù)據(jù)蛙恢復(fù)軟件替代產(chǎn)品有哪些?15款頂尖數(shù)據(jù)恢復(fù)軟件清單

    數(shù)據(jù)蛙恢復(fù)軟件替代產(chǎn)品有哪些?15款頂尖數(shù)據(jù)恢復(fù)軟件清單

    數(shù)據(jù)蛙恢復(fù)軟件是一款國(guó)內(nèi)數(shù)據(jù)恢復(fù)軟件,可以在很多品牌的電腦上使用。但是你可能會(huì)遇到數(shù)據(jù)蛙恢復(fù)軟件掃描不到需要恢復(fù)文件的情況。那么有沒(méi)有更專(zhuān)業(yè)的數(shù)據(jù)恢復(fù)軟件可以找到更多誤刪數(shù)據(jù)?本文將為你介紹最值的推薦的15個(gè)數(shù)據(jù)蛙恢復(fù)軟件替代產(chǎn)品。 丟失的文件是

    2024年01月24日
    瀏覽(20)
  • 系統(tǒng)架構(gòu)設(shè)計(jì)高級(jí)技能 · 軟件產(chǎn)品線

    系統(tǒng)架構(gòu)設(shè)計(jì)高級(jí)技能 · 軟件產(chǎn)品線

    現(xiàn)在的一切都是為將來(lái)的夢(mèng)想編織翅膀,讓夢(mèng)想在現(xiàn)實(shí)中展翅高飛。 Now everything is for the future of dream weaving wings, let the dream fly in reality. 點(diǎn)擊進(jìn)入系列文章目錄 軟件產(chǎn)品線 主要由兩部分組成,分別是 核心資源 和 產(chǎn)品集合 。 核心資源 是領(lǐng)域工程的所有結(jié)果的集合,是產(chǎn)品線

    2024年02月09日
    瀏覽(22)
  • 產(chǎn)品設(shè)計(jì)需要學(xué)的8款軟件

    產(chǎn)品設(shè)計(jì)需要學(xué)的8款軟件

    1、即時(shí)設(shè)計(jì): 即時(shí)設(shè)計(jì) 是國(guó)內(nèi)廣受 UI/UX 設(shè)計(jì)師和產(chǎn)品經(jīng)理歡迎的專(zhuān)業(yè)產(chǎn)品設(shè)計(jì)工具。它內(nèi)置了 iOS 和 Android 設(shè)計(jì)系統(tǒng)資源,可幫助用戶快速啟動(dòng)設(shè)計(jì)工作。該工具集成了原型設(shè)計(jì)、UI 設(shè)計(jì)、交互設(shè)計(jì)、交付和資源管理等多種功能,并自帶專(zhuān)業(yè)設(shè)計(jì)工具,提供流暢的創(chuàng)作體驗(yàn)

    2024年02月07日
    瀏覽(21)
  • 探索產(chǎn)品項(xiàng)目管理軟件的種類(lèi)及功能

    隨著科技的不斷發(fā)展,越來(lái)越多的企業(yè)開(kāi)始重視產(chǎn)品項(xiàng)目管理的重要性。產(chǎn)品項(xiàng)目管理軟件作為一種有效的工具,可以幫助企業(yè)更好地規(guī)劃、執(zhí)行和控制項(xiàng)目,提高項(xiàng)目的成功率。本文將分為兩部分,分別介紹產(chǎn)品項(xiàng)目管理軟件的功能以及一些知名的品牌。 \\\"產(chǎn)品項(xiàng)目管理軟

    2024年02月14日
    瀏覽(19)

覺(jué)得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請(qǐng)作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包