• 给javaweb项目设置一个授权


    给javaweb项目设置一个授权

    背景

    JavaWeb项目发布后希望持续可控,比如:

    1. 发布体验版本,有授权期限,过期后不可正常访问
    2. 服务部署后,只允许在部署服务器运行,更换服务器后不可正常访问
    3. 支持离线授权

    实现思路

    1. 给项目颁发一个licence,包含用户信息、授权时间等信息,使用非对称加密对这些信息进行数字签名。
    2. 使用拦截器校验licence的有效性,根据情况返回授权无效、授权过期等信息。

    具体实现

    重要的说明:

    ** 以下代码实现基于springboot**

    依赖hutool

    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.3.8</version>
    </dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    准备非对称加密证书

    本例使用java自带的keytool来获取证书文件,简单记录一下keytool的使用。

    keytool是个密钥和证书管理工具,位置在%JAVA_HOME%\bin\keytool.exe,直接使用即可。

    keytool --help
    -----------------------------------------------
    Key and Certificate Management Tool
    
    Commands:
    
     -certreq            Generates a certificate request
     -changealias        Changes an entry's alias
     -delete             Deletes an entry
     -exportcert         Exports certificate
     -genkeypair         Generates a key pair
     -genseckey          Generates a secret key
     -gencert            Generates certificate from a certificate request
     -importcert         Imports a certificate or a certificate chain
     -importpass         Imports a password
     -importkeystore     Imports one or all entries from another keystore
     -keypasswd          Changes the key password of an entry
     -list               Lists entries in a keystore
     -printcert          Prints the content of a certificate
     -printcertreq       Prints the content of a certificate request
     -printcrl           Prints the content of a CRL file
     -storepasswd        Changes the store password of a keystore
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    生成秘钥,更多参数:

    -alias 产生别名
    -keystore 指定密钥库的名称(就像数据库一样的证书库,可以有很多个证书,cacerts这个文件是jre自带的,你也可以使用其它文件名字,如果没有这个文件名字,它会创建这样一个)
    -storepass 指定密钥库的密码 (获取keystore信息所需的密码)
    -keypass 指定别名条目的密码**(私钥的密码)**
    -list 显示密钥库中的证书信息
    -v 显示密钥库中的证书详细信息
    -export 将别名指定的证书导出到文件
    -file 参数指定导出到文件的文件名
    -delete 删除密钥库中某条目
    -import 将已签名数字证书导入密钥库
    -keypasswd 修改密钥库中指定条目口令
    -dname 指定证书拥有者信息
    -keyalg 指定密钥的算法
    -validity 指定创建的证书有效期多少
    -keysize 指定密钥长度

    -dname 拥有者信息一般格式

    CN 名称
    OU 组织单位
    O 组织
    L 区域
    ST 城市
    C 国家

    生成一个秘钥:

    keytool -genkey -keystore "D:\keytools\test.keystore" -alias mytest -keyalg RSA -validity 30 -storepass spas123 -keypass kpas123 -dname "CN=libai, OU=com.tang, O=com.tang, L=xa, ST=sx, C=CN"
    
    • 1

    补充说明:

    -genkey: 表示生成密钥对(公钥和私钥)

    -keystore:每个 keytool 命令都有一个 -keystore 选项,用于指定 keytool 管理的密钥仓库的永久密钥仓库文件名称及其位置。如果不指定 -keystore 选项,则缺省密钥仓库将是宿主目录中(由系统属性的"user.home"决定)名为 .keystore 的文件。如果该文件并不存在,则它将被创建。

    查看秘钥:

    keytool -list -v -keystore test.keystore -storepass spas123
    
    • 1

    结果示例:

    密钥库类型: jks
    密钥库提供方: SUN

    您的密钥库包含 1 个条目

    别名: mytest
    创建日期: 2022-7-31
    条目类型: PrivateKeyEntry
    证书链长度: 1
    证书[1]:
    所有者: CN=libai, OU=com.ping, O=com.ping, L=xa, ST=sx, C=CN
    发布者: CN=libai, OU=com.ping, O=com.ping, L=xa, ST=sx, C=CN
    序列号: 21882e41
    生效时间: Sun Jul 31 15:25:36 CST 2022, 失效时间: Tue Aug 30 15:25:36 CST 2022
    证书指纹:
    SHA1: 3B:AA:C8:AE:13:F8:47:15:A4:F1:64:6D:DE:B7:ED:C1:15:67:CE:D3
    SHA256: 5D:7C:3A:D1:93:CE:FB:81:85:AC:79:B3:83:ED:5F:01:07:11:7E:71:40:C4:FA:03:EC:E6:60:D8:AD:52:7A:7C
    签名算法名称: SHA256withRSA
    主体公共密钥算法: 2048 位 RSA 密钥
    版本: 3

    扩展:

    #1: ObjectId: 2.5.29.14 Criticality=false
    SubjectKeyIdentifier [
    KeyIdentifier [
    0000: B3 A9 B4 C8 BA B7 6B FA F9 88 0F 22 0C 51 73 72 …k…".Qsr
    0010: B3 46 F2 89 .F…
    ]
    ]



    参数说明:

    -list 列出证书
    -v 显示详细信息
    -keystore 指定密钥库
    -storepass 指定密钥库的解密密码
    -rfc 以可编码方式打印证书

    keytool -list -rfc -keystore ./test.keystore -storepass spas123
    
    • 1

    结果示例:

    密钥库类型: jks
    密钥库提供方: SUN

    您的密钥库包含 1 个条目

    别名: mytest
    创建日期: 2022-7-31
    条目类型: PrivateKeyEntry
    证书链长度: 1
    证书[1]:
    -----BEGIN CERTIFICATE-----
    MIIDWTCCAkGgAwIBAgIEIYguQTANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJD
    TjELMAkGA1UECBMCc3gxCzAJBgNVBAcTAnhhMREwDwYDVQQKEwhjb20ucGluZzER
    MA8GA1UECxMIY29tLnBpbmcxDjAMBgNVBAMTBWxpYmFpMB4XDTIyMDczMTA3MjUz
    NloXDTIyMDgzMDA3MjUzNlowXTELMAkGA1UEBhMCQ04xCzAJBgNVBAgTAnN4MQsw
    CQYDVQQHEwJ4YTERMA8GA1UEChMIY29tLnBpbmcxETAPBgNVBAsTCGNvbS5waW5n
    MQ4wDAYDVQQDEwVsaWJhaTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
    AINpyuhbVXcDHgs6vflvG+/vvzHwWsH9j5GJU2pzT6+mOjpX3OO56CyHiRbZeTI+
    FwhQ1xbptBGy2OFBbTGW1lJQHVta1y3liuV3r5aUYDSUpt/RU82XzUCmF5mzAwYc
    2Xm5dDZp7PsFpJ1z7vP42nAW+Sk6LYWUsrciZhIOkNDKxqHUofomc31RlcF6Xv2a
    CK3eEmp+pf8Hh+kpfcOr01qcj08jjVAARMeQPnbcDOh+S41KPM0NrXUqcWwHTRl1
    Y3xIs6UON1RKS7W+P8irnCjgqCq2s9LYVrUFpbdepUVnnTcK2A/ebr9eXd+bdIR2
    doGBwowjulgqmRgUBv6qd9kCAwEAAaMhMB8wHQYDVR0OBBYEFLOptMi6t2v6+YgP
    IgxRc3KzRvKJMA0GCSqGSIb3DQEBCwUAA4IBAQAShoUVISzFj/yzTQFmAFzRyKaF
    4m4+NzLUogk2zux/9A4uiazx/Ml1sXj8IVHF6uq4PhIu0nhL5h06fQvuFxgZSdyu
    OfpvA0YlMIm3Nl1qUpt0u1XvCXYajJkKQY/zMyuuAtCXakEwhTdb7mhCJ7GEgHF9
    1zguEt9CyhjrTsiASmxJ4ll6tqMsDpE17oaELk3yqwjzgI0suLNV6xmRe3zHSBe3
    ibzfLWeVYFpDyHksw0fIziAYgz+9KlPMTjHy6GZcrOJlle97W1O2E13MFSqf8eLZ
    p8OXJ1hiDLiKQjzqyql/1jaPcPjN2R4Ea1I4F6f5cuXXpIfLLVymfG55qeYk
    -----END CERTIFICATE-----



    其它命令:

    # 导入证书
    keytool -import -alias test1 -file ./test.crt -keystore ./test.keystore -storepass spas123
    # 导出证书
    keytool -export -alias mytest -keystore ./test.keystore -file ./test.crt -storepass spas123
    # 修改密钥库中指定条目的密码
    keytool -keypasswd -alias 需修改的别名 -keypass 旧密码 -new 新密码 -storepass keystore密码 -keystore 所在的密钥库
    # 修改密钥库的密码
    keytool -storepasswd -keystore ./yushan.keystore(需修改口令的keystore) -storepass 123456(原始密码) -new yushan(新密码)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    生成证书文件:

    keytool -exportcert -alias mytest -keystore ./test.keystore -storepass spas123 -file ./test.cer
    
    • 1

    证书文件包含公钥,将来发送给客户使用

    拟定licence文件

    准备一个licence授权文件,将签名 + 证书同项目一起发布,通过licence文件和证书判断权限。我们用一个JSON文件来做licence,java bean如下:

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @Accessors(chain = true)
    public class Licence {
    
        /**
         * 授权序列号
         */
        private String licenceId;
    
        /**
         * 供应商
         */
        private String vendor;
        /**
         * 过期时间
         */
        private Date expiration;
        
        /**
         * 服务器mac地址 保证只在指定的机器运行,缺点是生成秘钥是需要知道此地址,当然可以通过ipconfig查看,然后参数传入生成licence
         */
        private String macAddress;
    
        /**
         * 数字签名
         */
        private String signature;
    
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    如果想分模块授权,可以适当增加模块参数。

    生成和校验licence

    签名需要使用证书,先准备加载证书的方法(证书信息存放在yml配置文件中):

    yml局部:

    # 认证证书逻辑
    licence:
      base-path: D://KeyStore
      key-store: ${licence.base-path}/test.keystore
      key-alias: mytest
      cert: ${licence.base-path}/test.cer
      key-store-pwd: spas123
      key-pwd: kpas123
      auth-file: ${licence.base-path}/licence.json
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    准备工具类加载证书:

    /**
     * 证书加载工具
     */
    @Component
    public class KeyTools {
    
    
        // 私钥存放路径
        @Value("${licence.key-store}")
        public String PRIVATE_KEY_FILE_PATH;
    
        //Cer证书存放路径
        @Value("${licence.cert}")
        public String CER_FILE_PATH;
    
        //私钥别名
        @Value("${licence.key-alias}")
        public String PRIVATE_ALIAS;
    
        //获取keystore密码
        @Value("${licence.key-store-pwd}")
        public String KEYSTORE_PASSWORD;
        //获取私钥所需密码
        @Value("${licence.key-pwd}")
        public String KEY_PASSWORD;
    
    
        /**
         * 获取私钥
         * @return
         */
        public PrivateKey getPrivateKey() {
    
            FileInputStream is = null;
            PrivateKey privateKey = null;
            try {
                KeyStore keyStore = KeyStore.getInstance("JKS");
                is = new FileInputStream(PRIVATE_KEY_FILE_PATH);
                keyStore.load(is, KEYSTORE_PASSWORD.toCharArray());
                privateKey = (PrivateKey) keyStore.getKey(PRIVATE_ALIAS, KEY_PASSWORD.toCharArray());
    
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                try {
                    if (is != null) {
                        is.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
    
            return privateKey;
        }
    
        /**
         * 通过 cer证书获取公钥
         */
        public PublicKey getPublicKey(){
            PublicKey publicKey = null;
            FileInputStream in = null;
            try {
                CertificateFactory cf = CertificateFactory.getInstance("X.509");
                in = new FileInputStream(CER_FILE_PATH);
                Certificate c = cf.generateCertificate(in);
                publicKey = c.getPublicKey();
            } catch (CertificateException | FileNotFoundException e) {
                e.printStackTrace();
            } finally {
                try {
                    if (in != null) {
                        in.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return publicKey;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82

    准备一个工具类 完成签名生成和验签

    重要的约定:

    将证书中的值以字典序排列,然后生成签名

    
    import cn.hutool.core.util.CharsetUtil;
    import com.alibaba.fastjson.JSON;
    import com.alibaba.fastjson.JSONObject;
    import lombok.extern.slf4j.Slf4j;
    
    import java.io.UnsupportedEncodingException;
    import java.nio.charset.StandardCharsets;
    import java.security.*;
    import java.util.Base64;
    import java.util.Collection;
    import java.util.Objects;
    import java.util.stream.Collectors;
    
    @Slf4j
    public class MySignUtil {
    
        private final static String PARAM_SIGN = "signature";
        private static final String KEY_ALGORITHM = "SHA1withRSA";
        private static Signature signature;
    
        static {
            try {
                signature = Signature.getInstance(KEY_ALGORITHM);
            } catch (NoSuchAlgorithmException e) {
                e.printStackTrace();
            }
        }
    
        /**
         * 获取签名
         *
         * @param data
         * @param privateKey
         * @return
         */
        public static String sign(byte[] data, PrivateKey privateKey) {
            try {
                signature.initSign(privateKey);
                signature.update(data);
                return new String(Base64.getEncoder().encode(signature.sign()), CharsetUtil.UTF_8);
            } catch (InvalidKeyException | SignatureException | UnsupportedEncodingException e) {
                e.printStackTrace();
            }
            return null;
        }
    
        public static String sign(Object data, PrivateKey privateKey) {
            return sign(getParams(data), privateKey);
        }
    
        /**
         * 验签
         *
         * @param data
         * @param sign
         * @param publicKey
         * @return
         */
        public static boolean verify(byte[] data, byte[] sign, PublicKey publicKey) {
            try {
                signature.initVerify(publicKey);
                signature.update(data);
                return signature.verify(Base64.getDecoder().decode(sign));
            } catch (InvalidKeyException | SignatureException e) {
                e.printStackTrace();
            }
            return false;
        }
    
        public static boolean verify(Object data, String signature, PublicKey publicKey) {
            return verify(getParams(data), signature.getBytes(StandardCharsets.UTF_8), publicKey);
        }
    
        /**
         * 获取参数
         *
         * @param obj
         * @return
         */
        private static byte[] getParams(Object obj) {
            if (Objects.isNull(obj)) {
                log.error("签名获取失败, 传入对象为null");
                return null;
            }
            // 获取非sign参数值
            JSONObject js = (JSONObject) JSON.toJSON(obj);
            js.remove(PARAM_SIGN);
    
            Collection<Object> values = js.values();
            String params = values.stream().map(String::valueOf).sorted().collect(Collectors.joining());
    
            return params.getBytes(StandardCharsets.UTF_8);
        }
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97

    准备好内容, 转换为JSON字符串写入文件即为授权文件,生成过程。生成licence:

    import org.jeecg.modules.licence.entity.Licence;
    
    import java.io.File;
    
    public interface ILicenceService {
    
        /**
         * 生成授权文件
         *
         * @param licence
         * @return
         */
        File generateLicence(Licence licence);
    
        /**
         * 实现签名校验
         *
         * @param licence
         * @return
         */
        boolean verifySign(Licence licence);
    
        /**
         * 签名校验 检验默认位置签名
         *
         * @return
         */
        boolean verifySign();
    
        /**
         * 从磁盘中获取licence
         *
         * @return
         */
        Licence getLicence();
    
        /**
         * 当前licence是否有效
         *
         * @return
         */
        boolean isEffective();
    
        /**
         * 清空licence缓存
         *
         * @return
         */
        boolean clearEffectiveCache();
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    import cn.hutool.core.io.FileUtil;
    import cn.hutool.core.util.CharsetUtil;
    import cn.hutool.json.JSONObject;
    import cn.hutool.json.JSONUtil;
    import org.jeecg.modules.licence.entity.Licence;
    import org.jeecg.modules.licence.service.ILicenceService;
    import org.jeecg.modules.licence.util.KeyTools;
    import org.jeecg.modules.licence.util.MySignUtil;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.cache.annotation.CacheEvict;
    import org.springframework.cache.annotation.Cacheable;
    import org.springframework.stereotype.Service;
    
    import java.io.File;
    import java.nio.charset.StandardCharsets;
    
    @Service
    public class LicenceServiceImpl implements ILicenceService {
    
        @Value("${licence.auth-file}")
        private String licencePath;
    
        @Autowired
        private KeyTools keyTools;
    
        @Override
        public File generateLicence(Licence licence) {
            // 生成签名
            String sign = MySignUtil.sign(licence, keyTools.getPrivateKey());
            licence.setSignature(sign);
            // 生成licence文件
            String jsonStr = JSONUtil.toJsonStr(licence);
            File file = FileUtil.writeString(jsonStr, licencePath, CharsetUtil.UTF_8);
            return file;
        }
    
        @Override
        public boolean verifySign(Licence licence) {
            return MySignUtil.verify(licence, licence.getSignature(), keyTools.getPublicKey());
        }
    
        @Override
        public boolean verifySign() {
            Licence licence = getLicence();
            return MySignUtil.verify(licence, licence.getSignature(), keyTools.getPublicKey());
        }
    
        @Override
        public Licence getLicence() {
            String licenceStr = FileUtil.readString(licencePath, StandardCharsets.UTF_8);
            JSONObject jsonObject = JSONUtil.parseObj(licenceStr);
            Licence licence = jsonObject.toBean(Licence.class);
            return licence;
        }
    
    
        @Override
        // 这里增加了一个缓存, 后面会添加拦截器, 如果每个请求都重新验证一遍证书, 会带来性能上的压力
        @Cacheable(value = "licence", unless = "#result == false ")
        public boolean isEffective() {
            // 证书时间和licence时间是一致的, 如果证书过期签名校验不通过, licence肯定过期
            boolean signVerify = this.verifySign();
            if(!signVerify) {
                return false;
            }
    
            Licence licence = getLicence();
            if (StrUtil.isNotEmpty(licence.getMacAddress())) {
                // 校验Mac地址
                String macAddress = NetUtil.getLocalMacAddress();
                if (!StrUtil.equals(macAddress, licence.getMacAddress())) {
                    return false;
                }
            }
            return true;
        }
    
        @Override
        @CacheEvict(value = "licence")
        public boolean clearEffectiveCache() {
            return true;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84

    准备一个controller测试过程

    
    import cn.hutool.core.date.DateUtil;
    import cn.hutool.core.lang.UUID;
    import org.jeecg.common.api.vo.Result;
    import org.jeecg.modules.licence.entity.Licence;
    import org.jeecg.modules.licence.service.ILicenceService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    
    /**
     * 证书相关信息
     */
    @Controller
    @RequestMapping("test/licence")
    public class LicenceController {
    
        @Autowired
        private ILicenceService licenceService;
    	
        /**
         * 生成licence
         * @return
         */
        @RequestMapping(value = "generate", method = {RequestMethod.GET, RequestMethod.POST})
        public Result generateLicence() {
            Licence licence = new Licence();
    
            licence.setLicenceId(UUID.fastUUID().toString(true))
                    .setExpiration(DateUtil.offsetDay(DateUtil.date(), 30))
                    .setVendor("XX科技有限责任公司");
            
            // 这里直接获取了本机的网卡Mac 实际操作中可以通过参数掺入的方式
            String macAddress = NetUtil.getLocalMacAddress();
            licence.setMacAddress(macAddress);
    
            licenceService.generateLicence(licence);
            return Result.OK(licence);
        }
    	
        /**
         * 校验licence  验签  这里证书过期或者licence被修改都会导致验签失败
         * @return
         */
        @RequestMapping(value = "verify", method = {RequestMethod.GET, RequestMethod.POST})
        public Result verifyLicence() {
            return Result.OK(licenceService.verifySign());
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51

    添加licence拦截器

    拦截所有的请求, 校验授权是否有效:

    
    import cn.hutool.core.date.DateUtil;
    import com.alibaba.fastjson.JSONObject;
    import lombok.extern.slf4j.Slf4j;
    import org.jeecg.common.api.vo.Result;
    import org.jeecg.modules.licence.entity.Licence;
    import org.jeecg.modules.licence.service.ILicenceService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    import org.springframework.web.servlet.HandlerInterceptor;
    import org.springframework.web.servlet.ModelAndView;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.io.PrintWriter;
    
    /**
     * 授权校验拦截器, 校验licence是否合法
     */
    
    @Slf4j
    @Component
    public class LicenceInterceptor implements HandlerInterceptor {
    
        @Autowired
        private ILicenceService licenceService;
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    
            if (licenceService.isEffective()) {
                return true;
            }else{
                Licence licence = licenceService.getLicence();
                if (licence == null) {
                    output(Result.error("项目未授权"), response);
                }else{
                    if (DateUtil.compare(DateUtil.date(), DateUtil.parseDate(licence.getExpiration())) >= 0) {
                        // 有授权, 但是过期了
                        output(Result.error("授权已过期"), response);
                    }else{
                        output(Result.error("项目授权无效"), response);
                    }
                }
            }
            return false;
        }
    
        @Override
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
            HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
        }
    
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
            HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
        }
    
    
        /**
         * 向页面返回信息
         *
         * @param result
         * @param response
         */
        public void output(Result result, HttpServletResponse response) throws IOException {
            response.setContentType("application/json; charset=utf-8");
            PrintWriter writer = response.getWriter();
            writer.print(JSONObject.toJSONString(result));
            writer.close();
            response.flushBuffer();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74

    当然记得注册一下拦截器。这样就完成了web项目授权,只有授权有效的情况下才能正确返回。

    其他注意事项

    1. 向客户发布项目时,只附带licence.json和test.cer,自己留好keystore文件。
    2. 应该给用户一些友好的提示,比如在证书快过期时提醒用户及时获取授权;已过期时提醒用户是因为授权原因导致服务不可用等。
  • 相关阅读:
    去除upload的抖动效果
    electron.js入门-为生产环境构建应用程序
    Zepoch 销量即将突破800个,生态发展信心十足
    《C++ 并发编程实战 第二版》:条件变量唤醒丢失与虚假唤醒
    线性二分类——机器学习
    Multitor:一款带有负载均衡功能的多Tor实例创建工具
    99页4万字XX大数据湖项目建设方案
    Vite 的基本原理,和 webpack 在开发阶段的比较
    java架构师禁止在项目中使用继承,合理吗?
    【Demo】SpringBoot集合Mybatis修改账户密码逻辑
  • 原文地址:https://blog.csdn.net/Lee_0220/article/details/126112205