Spring Boot项目中使用 TrueLicense 生成和验证License(服务器许可)

2023-11-16

一 简介

License,即版权许可证,一般用于收费软件给付费用户提供的访问许可证明。根据应用部署位置的不同,一般可以分为以下两种情况讨论:

  • 应用部署在开发者自己的云服务器上。这种情况下用户通过账号登录的形式远程访问,因此只需要在账号登录的时候校验目标账号的有效期、访问权限等信息即可。
  • 应用部署在客户的内网环境。因为这种情况开发者无法控制客户的网络环境,也不能保证应用所在服务器可以访问外网,因此通常的做法是使用服务器许可文件,在应用启动的时候加载证书,然后在登录或者其他关键操作的地方校验证书的有效性。

注:限于文章篇幅,这里只讨论代码层面的许可限制,暂不考虑逆向破解等问题。此外,在下面我只讲解关键代码实现,完整代码可以参考:LicenseDemo: 在基于Spring的项目中使用 TrueLicense 生成和验证License(服务器许可)的示例代码

二 使用 TrueLicense 生成License

(1)使用Spring Boot构建测试项目ServerDemo,用于为客户生成License许可文件:

注:这个完整的Demo项目可以参考:LicenseDemo: 在基于Spring的项目中使用 TrueLicense 生成和验证License(服务器许可)的示例代码 - Gitee.com

i)在pom.xml中添加关键依赖:

1

2

3

4

5

6

<dependency>

    <groupId>de.schlichtherle.truelicense</groupId>

    <artifactId>truelicense-core</artifactId>

    <version>1.33</version>

    <scope>provided</scope>

</dependency>

ii)校验自定义的License参数:

TrueLicense的 de.schlichtherle.license.LicenseManager 类自带的verify方法只校验了我们后面颁发的许可文件的生效和过期时间,然而在实际项目中我们可能需要额外校验应用部署的服务器的IP地址、MAC地址、CPU序列号、主板序列号等信息,因此我们需要复写框架的部分方法以实现校验自定义参数的目的。

首先需要添加一个自定义的可被允许的服务器硬件信息的实体类(如果校验其他参数,可自行补充):

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

package cn.zifangsky.license;

import java.io.Serializable;

import java.util.List;

/**

* 自定义需要校验的License参数

*

* @author zifangsky

* @date 2018/4/23

* @since 1.0.0

*/

public class LicenseCheckModel implements Serializable{

    private static final long serialVersionUID = 8600137500316662317L;

    /**

     * 可被允许的IP地址

     */

    private List<String> ipAddress;

    /**

     * 可被允许的MAC地址

     */

    private List<String> macAddress;

    /**

     * 可被允许的CPU序列号

     */

    private String cpuSerial;

    /**

     * 可被允许的主板序列号

     */

    private String mainBoardSerial;

    //省略setter和getter方法

    @Override

    public String toString() {

        return "LicenseCheckModel{" +

                "ipAddress=" + ipAddress +

                ", macAddress=" + macAddress +

                ", cpuSerial='" + cpuSerial + '\'' +

                ", mainBoardSerial='" + mainBoardSerial + '\'' +

                '}';

    }

}

其次,添加一个License生成类需要的参数:

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

98

99

package cn.zifangsky.license;

import com.fasterxml.jackson.annotation.JsonFormat;

import java.io.Serializable;

import java.util.Date;

/**

* License生成类需要的参数

*

* @author zifangsky

* @date 2018/4/19

* @since 1.0.0

*/

public class LicenseCreatorParam implements Serializable {

    private static final long serialVersionUID = -7793154252684580872L;

    /**

     * 证书subject

     */

    private String subject;

    /**

     * 密钥别称

     */

    private String privateAlias;

    /**

     * 密钥密码(需要妥善保管,不能让使用者知道)

     */

    private String keyPass;

    /**

     * 访问秘钥库的密码

     */

    private String storePass;

    /**

     * 证书生成路径

     */

    private String licensePath;

    /**

     * 密钥库存储路径

     */

    private String privateKeysStorePath;

    /**

     * 证书生效时间

     */

    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")

    private Date issuedTime = new Date();

    /**

     * 证书失效时间

     */

    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")

    private Date expiryTime;

    /**

     * 用户类型

     */

    private String consumerType = "user";

    /**

     * 用户数量

     */

    private Integer consumerAmount = 1;

    /**

     * 描述信息

     */

    private String description = "";

    /**

     * 额外的服务器硬件校验信息

     */

    private LicenseCheckModel licenseCheckModel;

    //省略setter和getter方法

    @Override

    public String toString() {

        return "LicenseCreatorParam{" +

                "subject='" + subject + '\'' +

                ", privateAlias='" + privateAlias + '\'' +

                ", keyPass='" + keyPass + '\'' +

                ", storePass='" + storePass + '\'' +

                ", licensePath='" + licensePath + '\'' +

                ", privateKeysStorePath='" + privateKeysStorePath + '\'' +

                ", issuedTime=" + issuedTime +

                ", expiryTime=" + expiryTime +

                ", consumerType='" + consumerType + '\'' +

                ", consumerAmount=" + consumerAmount +

                ", description='" + description + '\'' +

                ", licenseCheckModel=" + licenseCheckModel +

                '}';

    }

}

添加抽象类AbstractServerInfos,用户获取服务器的硬件信息:

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

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

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;

/**

* 用于获取客户服务器的基本信息,如:IP、Mac地址、CPU序列号、主板序列号等

*

* @author zifangsky

* @date 2018/4/23

* @since 1.0.0

*/

public abstract class AbstractServerInfos {

    private static Logger logger = LogManager.getLogger(AbstractServerInfos.class);

    /**

     * 组装需要额外校验的License参数

     * @author zifangsky

     * @date 2018/4/23 14:23

     * @since 1.0.0

     * @return demo.LicenseCheckModel

     */

    public LicenseCheckModel getServerInfos(){

        LicenseCheckModel result = new LicenseCheckModel();

        try {

            result.setIpAddress(this.getIpAddress());

            result.setMacAddress(this.getMacAddress());

            result.setCpuSerial(this.getCPUSerial());

            result.setMainBoardSerial(this.getMainBoardSerial());

        }catch (Exception e){

            logger.error("获取服务器硬件信息失败",e);

        }

        return result;

    }

    /**

     * 获取IP地址

     * @author zifangsky

     * @date 2018/4/23 11:32

     * @since 1.0.0

     * @return java.util.List<java.lang.String>

     */

    protected abstract List<String> getIpAddress() throws Exception;

    /**

     * 获取Mac地址

     * @author zifangsky

     * @date 2018/4/23 11:32

     * @since 1.0.0

     * @return java.util.List<java.lang.String>

     */

    protected abstract List<String> getMacAddress() throws Exception;

    /**

     * 获取CPU序列号

     * @author zifangsky

     * @date 2018/4/23 11:35

     * @since 1.0.0

     * @return java.lang.String

     */

    protected abstract String getCPUSerial() throws Exception;

    /**

     * 获取主板序列号

     * @author zifangsky

     * @date 2018/4/23 11:35

     * @since 1.0.0

     * @return java.lang.String

     */

    protected abstract String getMainBoardSerial() throws Exception;

    /**

     * 获取当前服务器所有符合条件的InetAddress

     * @author zifangsky

     * @date 2018/4/23 17:38

     * @since 1.0.0

     * @return java.util.List<java.net.InetAddress>

     */

    protected List<InetAddress> getLocalAllInetAddress() throws Exception {

        List<InetAddress> result = new ArrayList<>(4);

        // 遍历所有的网络接口

        for (Enumeration networkInterfaces = NetworkInterface.getNetworkInterfaces(); networkInterfaces.hasMoreElements(); ) {

            NetworkInterface iface = (NetworkInterface) networkInterfaces.nextElement();

            // 在所有的接口下再遍历IP

            for (Enumeration inetAddresses = iface.getInetAddresses(); inetAddresses.hasMoreElements(); ) {

                InetAddress inetAddr = (InetAddress) inetAddresses.nextElement();

                //排除LoopbackAddress、SiteLocalAddress、LinkLocalAddress、MulticastAddress类型的IP地址

                if(!inetAddr.isLoopbackAddress() /*&& !inetAddr.isSiteLocalAddress()*/

                        && !inetAddr.isLinkLocalAddress() && !inetAddr.isMulticastAddress()){

                    result.add(inetAddr);

                }

            }

        }

        return result;

    }

    /**

     * 获取某个网络接口的Mac地址

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

                }

                //将十六进制byte转化为字符串

                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;

    }

}

获取客户Linux服务器的基本信息:

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

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;

/**

* 用于获取客户Linux服务器的基本信息

*

* @author zifangsky

* @date 2018/4/23

* @since 1.0.0

*/

public class LinuxServerInfos extends AbstractServerInfos {

    @Override

    protected List<String> getIpAddress() throws Exception {

        List<String> result = null;

        //获取所有网络接口

        List<InetAddress> inetAddresses = getLocalAllInetAddress();

        if(inetAddresses != null && inetAddresses.size() > 0){

            result = inetAddresses.stream().map(InetAddress::getHostAddress).distinct().map(String::toLowerCase).collect(Collectors.toList());

        }

        return result;

    }

    @Override

    protected List<String> getMacAddress() throws Exception {

        List<String> result = null;

        //1. 获取所有网络接口

        List<InetAddress> inetAddresses = getLocalAllInetAddress();

        if(inetAddresses != null && inetAddresses.size() > 0){

            //2. 获取所有网络接口的Mac地址

            result = inetAddresses.stream().map(this::getMacByInetAddress).distinct().collect(Collectors.toList());

        }

        return result;

    }

    @Override

    protected String getCPUSerial() throws Exception {

        //序列号

        String serialNumber = "";

        //使用dmidecode命令获取CPU序列号

        String[] shell = {"/bin/bash","-c","dmidecode -t processor | grep 'ID' | awk -F ':' '{print $2}' | head -n 1"};

        Process process = Runtime.getRuntime().exec(shell);

        process.getOutputStream().close();

        BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));

        String line = reader.readLine().trim();

        if(StringUtils.isNotBlank(line)){

            serialNumber = line;

        }

        reader.close();

        return serialNumber;

    }

    @Override

    protected String getMainBoardSerial() throws Exception {

        //序列号

        String serialNumber = "";

        //使用dmidecode命令获取主板序列号

        String[] shell = {"/bin/bash","-c","dmidecode | grep 'Serial Number' | awk -F ':' '{print $2}' | head -n 1"};

        Process process = Runtime.getRuntime().exec(shell);

        process.getOutputStream().close();

        BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));

        String line = reader.readLine().trim();

        if(StringUtils.isNotBlank(line)){

            serialNumber = line;

        }

        reader.close();

        return serialNumber;

    }

}

获取客户Windows服务器的基本信息:

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

package cn.zifangsky.license;

import java.net.InetAddress;

import java.util.List;

import java.util.Scanner;

import java.util.stream.Collectors;

/**

* 用于获取客户Windows服务器的基本信息

*

* @author zifangsky

* @date 2018/4/23

* @since 1.0.0

*/

public class WindowsServerInfos extends AbstractServerInfos {

    @Override

    protected List<String> getIpAddress() throws Exception {

        List<String> result = null;

        //获取所有网络接口

        List<InetAddress> inetAddresses = getLocalAllInetAddress();

        if(inetAddresses != null && inetAddresses.size() > 0){

            result = inetAddresses.stream().map(InetAddress::getHostAddress).distinct().map(String::toLowerCase).collect(Collectors.toList());

        }

        return result;

    }

    @Override

    protected List<String> getMacAddress() throws Exception {

        List<String> result = null;

        //1. 获取所有网络接口

        List<InetAddress> inetAddresses = getLocalAllInetAddress();

        if(inetAddresses != null && inetAddresses.size() > 0){

            //2. 获取所有网络接口的Mac地址

            result = inetAddresses.stream().map(this::getMacByInetAddress).distinct().collect(Collectors.toList());

        }

        return result;

    }

    @Override

    protected String getCPUSerial() throws Exception {

        //序列号

        String serialNumber = "";

        //使用WMIC获取CPU序列号

        Process process = Runtime.getRuntime().exec("wmic cpu get processorid");

        process.getOutputStream().close();

        Scanner scanner = new Scanner(process.getInputStream());

        if(scanner.hasNext()){

            scanner.next();

        }

        if(scanner.hasNext()){

            serialNumber = scanner.next().trim();

        }

        scanner.close();

        return serialNumber;

    }

    @Override

    protected String getMainBoardSerial() throws Exception {

        //序列号

        String serialNumber = "";

        //使用WMIC获取主板序列号

        Process process = Runtime.getRuntime().exec("wmic baseboard get serialnumber");

        process.getOutputStream().close();

        Scanner scanner = new Scanner(process.getInputStream());

        if(scanner.hasNext()){

            scanner.next();

        }

        if(scanner.hasNext()){

            serialNumber = scanner.next().trim();

        }

        scanner.close();

        return serialNumber;

    }

}

注:这里使用了模板方法模式,将不变部分的算法封装到抽象类,而基本方法的具体实现则由子类来实现。更多内容可以参考我之前写的文档:模板方法模式

自定义LicenseManager,用于增加额外的服务器硬件信息校验:

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

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

276

277

278

279

280

281

282

283

284

285

286

287

288

289

290

291

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;

/**

* 自定义LicenseManager,用于增加额外的服务器硬件信息校验

*

* @author zifangsky

* @date 2018/4/23

* @since 1.0.0

*/

public class CustomLicenseManager extends LicenseManager{

    private static Logger logger = LogManager.getLogger(CustomLicenseManager.class);

    //XML编码

    private static final String XML_CHARSET = "UTF-8";

    //默认BUFSIZE

    private static final int DEFAULT_BUFSIZE = 8 * 1024;

    public CustomLicenseManager() {

    }

    public CustomLicenseManager(LicenseParam param) {

        super(param);

    }

    /**

     * 复写create方法

     * @author zifangsky

     * @date 2018/4/23 10:36

     * @since 1.0.0

     * @param

     * @return byte[]

     */

    @Override

    protected synchronized byte[] create(

            LicenseContent content,

            LicenseNotary notary)

            throws Exception {

        initialize(content);

        this.validateCreate(content);

        final GenericCertificate certificate = notary.sign(content);

        return getPrivacyGuard().cert2key(certificate);

    }

    /**

     * 复写install方法,其中validate方法调用本类中的validate方法,校验IP地址、Mac地址等其他信息

     * @author zifangsky

     * @date 2018/4/23 10:40

     * @since 1.0.0

     * @param

     * @return de.schlichtherle.license.LicenseContent

     */

    @Override

    protected synchronized LicenseContent install(

            final byte[] key,

            final LicenseNotary notary)

            throws Exception {

        final GenericCertificate certificate = getPrivacyGuard().key2cert(key);

        notary.verify(certificate);

        final LicenseContent content = (LicenseContent)this.load(certificate.getEncoded());

        this.validate(content);

        setLicenseKey(key);

        setCertificate(certificate);

        return content;

    }

    /**

     * 复写verify方法,调用本类中的validate方法,校验IP地址、Mac地址等其他信息

     * @author zifangsky

     * @date 2018/4/23 10:40

     * @since 1.0.0

     * @param

     * @return de.schlichtherle.license.LicenseContent

     */

    @Override

    protected synchronized LicenseContent verify(final LicenseNotary notary)

            throws Exception {

        GenericCertificate certificate = getCertificate();

        // Load license key from preferences,

        final byte[] key = getLicenseKey();

        if (null == key){

            throw new NoLicenseInstalledException(getLicenseParam().getSubject());

        }

        certificate = getPrivacyGuard().key2cert(key);

        notary.verify(certificate);

        final LicenseContent content = (LicenseContent)this.load(certificate.getEncoded());

        this.validate(content);

        setCertificate(certificate);

        return content;

    }

    /**

     * 校验生成证书的参数信息

     * @author zifangsky

     * @date 2018/5/2 15:43

     * @since 1.0.0

     * @param content 证书正文

     */

    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("证书失效时间不能早于当前时间");

        }

        if (null != notBefore && null != notAfter && notAfter.before(notBefore)){

            throw new LicenseContentException("证书生效时间不能晚于证书失效时间");

        }

        final String consumerType = content.getConsumerType();

        if (null == consumerType){

            throw new LicenseContentException("用户类型不能为空");

        }

    }

    /**

     * 复写validate方法,增加IP地址、Mac地址等其他信息校验

     * @author zifangsky

     * @date 2018/4/23 10:40

     * @since 1.0.0

     * @param content LicenseContent

     */

    @Override

    protected synchronized void validate(final LicenseContent content)

            throws LicenseContentException {

        //1. 首先调用父类的validate方法

        super.validate(content);

        //2. 然后校验自定义的License参数

        //License中可被允许的参数信息

        LicenseCheckModel expectedCheckModel = (LicenseCheckModel) content.getExtra();

        //当前服务器真实的参数信息

        LicenseCheckModel serverCheckModel = getServerInfos();

        if(expectedCheckModel != null && serverCheckModel != null){

            //校验IP地址

            if(!checkIpAddress(expectedCheckModel.getIpAddress(),serverCheckModel.getIpAddress())){

                throw new LicenseContentException("当前服务器的IP没在授权范围内");

            }

            //校验Mac地址

            if(!checkIpAddress(expectedCheckModel.getMacAddress(),serverCheckModel.getMacAddress())){

                throw new LicenseContentException("当前服务器的Mac地址没在授权范围内");

            }

            //校验主板序列号

            if(!checkSerial(expectedCheckModel.getMainBoardSerial(),serverCheckModel.getMainBoardSerial())){

                throw new LicenseContentException("当前服务器的主板序列号没在授权范围内");

            }

            //校验CPU序列号

            if(!checkSerial(expectedCheckModel.getCpuSerial(),serverCheckModel.getCpuSerial())){

                throw new LicenseContentException("当前服务器的CPU序列号没在授权范围内");

            }

        }else{

            throw new LicenseContentException("不能获取服务器硬件信息");

        }

    }

    /**

     * 重写XMLDecoder解析XML

     * @author zifangsky

     * @date 2018/4/25 14:02

     * @since 1.0.0

     * @param encoded XML类型字符串

     * @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解析XML失败",e);

            }

        }

        return null;

    }

    /**

     * 获取当前服务器需要额外校验的License参数

     * @author zifangsky

     * @date 2018/4/23 14:33

     * @since 1.0.0

     * @return demo.LicenseCheckModel

     */

    private LicenseCheckModel getServerInfos(){

        //操作系统类型

        String osName = System.getProperty("os.name").toLowerCase();

        AbstractServerInfos abstractServerInfos = null;

        //根据不同操作系统类型选择不同的数据获取方法

        if (osName.startsWith("windows")) {

            abstractServerInfos = new WindowsServerInfos();

        } else if (osName.startsWith("linux")) {

            abstractServerInfos = new LinuxServerInfos();

        }else{//其他服务器类型

            abstractServerInfos = new LinuxServerInfos();

        }

        return abstractServerInfos.getServerInfos();

    }

    /**

     * 校验当前服务器的IP/Mac地址是否在可被允许的IP范围内<br/>

     * 如果存在IP在可被允许的IP/Mac地址范围内,则返回true

     * @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;

        }

    }

    /**

     * 校验当前服务器硬件(主板、CPU等)序列号是否在可允许范围内

     * @author zifangsky

     * @date 2018/4/24 14:38

     * @since 1.0.0

     * @return boolean

     */

    private boolean checkSerial(String expectedSerial,String serverSerial){

        if(StringUtils.isNotBlank(expectedSerial)){

            if(StringUtils.isNotBlank(serverSerial)){

                if(expectedSerial.equals(serverSerial)){

                    return true;

                }

            }

            return false;

        }else{

            return true;

        }

    }

}

最后是License生成类,用于生成License证书:

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

98

99

100

101

102

103

104

105

106

107

108

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生成类

*

* @author zifangsky

* @date 2018/4/19

* @since 1.0.0

*/

public class LicenseCreator {

    private static Logger logger = LogManager.getLogger(LicenseCreator.class);

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

    private LicenseCreatorParam param;

    public LicenseCreator(LicenseCreatorParam param) {

        this.param = param;

    }

    /**

     * 生成License证书

     * @author zifangsky

     * @date 2018/4/20 10:58

     * @since 1.0.0

     * @return boolean

     */

    public boolean generateLicense(){

        try {

            LicenseManager licenseManager = new CustomLicenseManager(initLicenseParam());

            LicenseContent licenseContent = initLicenseContent();

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

            return true;

        }catch (Exception e){

            logger.error(MessageFormat.format("证书生成失败:{0}",param),e);

            return false;

        }

    }

    /**

     * 初始化证书生成参数

     * @author zifangsky

     * @date 2018/4/20 10:56

     * @since 1.0.0

     * @return de.schlichtherle.license.LicenseParam

     */

    private LicenseParam initLicenseParam(){

        Preferences preferences = Preferences.userNodeForPackage(LicenseCreator.class);

        //设置对证书内容加密的秘钥

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

        KeyStoreParam privateStoreParam = new CustomKeyStoreParam(LicenseCreator.class

                ,param.getPrivateKeysStorePath()

                ,param.getPrivateAlias()

                ,param.getStorePass()

                ,param.getKeyPass());

        LicenseParam licenseParam = new DefaultLicenseParam(param.getSubject()

                ,preferences

                ,privateStoreParam

                ,cipherParam);

        return licenseParam;

    }

    /**

     * 设置证书生成正文信息

     * @author zifangsky

     * @date 2018/4/20 10:57

     * @since 1.0.0

     * @return de.schlichtherle.license.LicenseContent

     */

    private LicenseContent initLicenseContent(){

        LicenseContent licenseContent = new LicenseContent();

        licenseContent.setHolder(DEFAULT_HOLDER_AND_ISSUER);

        licenseContent.setIssuer(DEFAULT_HOLDER_AND_ISSUER);

        licenseContent.setSubject(param.getSubject());

        licenseContent.setIssued(param.getIssuedTime());

        licenseContent.setNotBefore(param.getIssuedTime());

        licenseContent.setNotAfter(param.getExpiryTime());

        licenseContent.setConsumerType(param.getConsumerType());

        licenseContent.setConsumerAmount(param.getConsumerAmount());

        licenseContent.setInfo(param.getDescription());

        //扩展校验服务器硬件信息

        licenseContent.setExtra(param.getLicenseCheckModel());

        return licenseContent;

    }

}

iii)添加一个生成证书的Controller:

这个Controller对外提供了两个RESTful接口,分别是「获取服务器硬件信息」和「生成证书」,示例代码如下:

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

98

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;

/**

*

* 用于生成证书文件,不能放在给客户部署的代码里

* @author zifangsky

* @date 2018/4/26

* @since 1.0.0

*/

@RestController

@RequestMapping("/license")

public class LicenseCreatorController {

    /**

     * 证书生成路径

     */

    @Value("${license.licensePath}")

    private String licensePath;

    /**

     * 获取服务器硬件信息

     * @author zifangsky

     * @date 2018/4/26 13:13

     * @since 1.0.0

     * @param osName 操作系统类型,如果为空则自动判断

     * @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) {

        //操作系统类型

        if(StringUtils.isBlank(osName)){

            osName = System.getProperty("os.name");

        }

        osName = osName.toLowerCase();

        AbstractServerInfos abstractServerInfos = null;

        //根据不同操作系统类型选择不同的数据获取方法

        if (osName.startsWith("windows")) {

            abstractServerInfos = new WindowsServerInfos();

        } else if (osName.startsWith("linux")) {

            abstractServerInfos = new LinuxServerInfos();

        }else{//其他服务器类型

            abstractServerInfos = new LinuxServerInfos();

        }

        return abstractServerInfos.getServerInfos();

    }

    /**

     * 生成证书

     * @author zifangsky

     * @date 2018/4/26 13:13

     * @since 1.0.0

     * @param param 生成证书需要的参数,如:{"subject":"ccx-models","privateAlias":"privateKey","keyPass":"5T7Zz5Y0dJFcqTxvzkH5LDGJJSGMzQ","storePass":"3538cef8e7","licensePath":"C:/Users/zifangsky/Desktop/license.lic","privateKeysStorePath":"C:/Users/zifangsky/Desktop/privateKeys.keystore","issuedTime":"2018-04-26 14:48:12","expiryTime":"2018-12-31 00:00:00","consumerType":"User","consumerAmount":1,"description":"这是证书描述信息","licenseCheckModel":{"ipAddress":["192.168.245.1","10.0.5.22"],"macAddress":["00-50-56-C0-00-01","50-7B-9D-F9-18-41"],"cpuSerial":"BFEBFBFF000406E3","mainBoardSerial":"L1HF65E00X9"}}

     * @return java.util.Map<java.lang.String,java.lang.Object>

     */

    @RequestMapping(value = "/generateLicense",produces = {MediaType.APPLICATION_JSON_UTF8_VALUE})

    public Map<String,Object> generateLicense(@RequestBody(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","证书文件生成失败!");

        }

        return resultMap;

    }

}

(2)使用JDK自带的 keytool 工具生成公私钥证书库:

假如我们设置公钥库密码为:public_password1234,私钥库密码为:private_password1234,则生成命令如下:

1

2

3

4

5

6

7

8

#生成命令

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"

#导出命令

keytool -exportcert -alias "privateKey" -keystore "privateKeys.keystore" -storepass "public_password1234" -file "certfile.cer"

#导入命令

keytool -import -alias "publicCert" -file "certfile.cer" -keystore "publicCerts.keystore" -storepass "public_password1234"

上述命令执行完成之后,会在当前路径下生成三个文件,分别是:privateKeys.keystore、publicCerts.keystore、certfile.cer。其中文件certfile.cer不再需要可以删除,文件privateKeys.keystore用于当前的 ServerDemo 项目给客户生成license文件,而文件publicCerts.keystore则随应用代码部署到客户服务器,用户解密license文件并校验其许可信息。

(3)为客户生成license文件:

将 ServerDemo  项目部署到客户服务器,通过以下接口获取服务器的硬件信息(等license文件生成后需要删除这个项目。当然也可以通过命令手动获取客户服务器的硬件信息,然后在开发者自己的电脑上生成license文件):

注:上图使用的是Firefox的RESTClient插件

然后生成license文件:

请求时需要在Header中添加一个 Content-Type ,其值为:application/json;charset=UTF-8。参数示例如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

{

    "subject": "license_demo",

    "privateAlias": "privateKey",

    "keyPass": "private_password1234",

    "storePass": "public_password1234",

    "licensePath": "C:/Users/zifangsky/Desktop/license_demo/license.lic",

    "privateKeysStorePath": "C:/Users/zifangsky/Desktop/license_demo/privateKeys.keystore",

    "issuedTime": "2018-07-10 00:00:01",

    "expiryTime": "2019-12-31 23:59:59",

    "consumerType": "User",

    "consumerAmount": 1,

    "description": "这是证书描述信息",

    "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"

    }

}

如果请求成功,那么最后会在 licensePath 参数设置的路径生成一个 license.lic 的文件,这个文件就是给客户部署代码的服务器许可文件。

三 给客户部署的应用中添加License校验

(1)使用Spring Boot构建测试项目ServerDemo,用于模拟给客户部署的应用:

注:这个完整的Demo项目可以参考:LicenseDemo: 在基于Spring的项目中使用 TrueLicense 生成和验证License(服务器许可)的示例代码 - Gitee.com

(2)添加License校验类需要的参数:

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

package cn.zifangsky.license;

/**

* License校验类需要的参数

*

* @author zifangsky

* @date 2018/4/20

* @since 1.0.0

*/

public class LicenseVerifyParam {

    /**

     * 证书subject

     */

    private String subject;

    /**

     * 公钥别称

     */

    private String publicAlias;

    /**

     * 访问公钥库的密码

     */

    private String storePass;

    /**

     * 证书生成路径

     */

    private String licensePath;

    /**

     * 密钥库存储路径

     */

    private String publicKeysStorePath;

    public LicenseVerifyParam() {

    }

    public LicenseVerifyParam(String subject, String publicAlias, String storePass, String licensePath, String publicKeysStorePath) {

        this.subject = subject;

        this.publicAlias = publicAlias;

        this.storePass = storePass;

        this.licensePath = licensePath;

        this.publicKeysStorePath = publicKeysStorePath;

    }

    //省略setter和getter方法

    @Override

    public String toString() {

        return "LicenseVerifyParam{" +

                "subject='" + subject + '\'' +

                ", publicAlias='" + publicAlias + '\'' +

                ", storePass='" + storePass + '\'' +

                ", licensePath='" + licensePath + '\'' +

                ", publicKeysStorePath='" + publicKeysStorePath + '\'' +

                '}';

    }

}

然后再添加License校验类:

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

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校验类

*

* @author zifangsky

* @date 2018/4/20

* @since 1.0.0

*/

public class LicenseVerify {

    private static Logger logger = LogManager.getLogger(LicenseVerify.class);

    /**

     * 安装License证书

     * @author zifangsky

     * @date 2018/4/20 16:26

     * @since 1.0.0

     */

    public synchronized LicenseContent install(LicenseVerifyParam param){

        LicenseContent result = null;

        DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

        //1. 安装证书

        try{

            LicenseManager licenseManager = LicenseManagerHolder.getInstance(initLicenseParam(param));

            licenseManager.uninstall();

            result = licenseManager.install(new File(param.getLicensePath()));

            logger.info(MessageFormat.format("证书安装成功,证书有效期:{0} - {1}",format.format(result.getNotBefore()),format.format(result.getNotAfter())));

        }catch (Exception e){

            logger.error("证书安装失败!",e);

        }

        return result;

    }

    /**

     * 校验License证书

     * @author zifangsky

     * @date 2018/4/20 16:26

     * @since 1.0.0

     * @return boolean

     */

    public boolean verify(){

        LicenseManager licenseManager = LicenseManagerHolder.getInstance(null);

        DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

        //2. 校验证书

        try {

            LicenseContent licenseContent = licenseManager.verify();

//            System.out.println(licenseContent.getSubject());

            logger.info(MessageFormat.format("证书校验通过,证书有效期:{0} - {1}",format.format(licenseContent.getNotBefore()),format.format(licenseContent.getNotAfter())));

            return true;

        }catch (Exception e){

            logger.error("证书校验失败!",e);

            return false;

        }

    }

    /**

     * 初始化证书生成参数

     * @author zifangsky

     * @date 2018/4/20 10:56

     * @since 1.0.0

     * @param param License校验类需要的参数

     * @return de.schlichtherle.license.LicenseParam

     */

    private LicenseParam initLicenseParam(LicenseVerifyParam param){

        Preferences preferences = Preferences.userNodeForPackage(LicenseVerify.class);

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

        KeyStoreParam publicStoreParam = new CustomKeyStoreParam(LicenseVerify.class

                ,param.getPublicKeysStorePath()

                ,param.getPublicAlias()

                ,param.getStorePass()

                ,null);

        return new DefaultLicenseParam(param.getSubject()

                ,preferences

                ,publicStoreParam

                ,cipherParam);

    }

}

(3)添加Listener,用于在项目启动的时候安装License证书:

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

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;

/**

* 在项目启动时安装证书

*

* @author zifangsky

* @date 2018/4/24

* @since 1.0.0

*/

@Component

public class LicenseCheckListener implements ApplicationListener<ContextRefreshedEvent> {

    private static Logger logger = LogManager.getLogger(LicenseCheckListener.class);

    /**

     * 证书subject

     */

    @Value("${license.subject}")

    private String subject;

    /**

     * 公钥别称

     */

    @Value("${license.publicAlias}")

    private String publicAlias;

    /**

     * 访问公钥库的密码

     */

    @Value("${license.storePass}")

    private String storePass;

    /**

     * 证书生成路径

     */

    @Value("${license.licensePath}")

    private String licensePath;

    /**

     * 密钥库存储路径

     */

    @Value("${license.publicKeysStorePath}")

    private String publicKeysStorePath;

    @Override

    public void onApplicationEvent(ContextRefreshedEvent event) {

        //root application context 没有parent

        ApplicationContext context = event.getApplicationContext().getParent();

        if(context == null){

            if(StringUtils.isNotBlank(licensePath)){

                logger.info("++++++++ 开始安装证书 ++++++++");

                LicenseVerifyParam param = new LicenseVerifyParam();

                param.setSubject(subject);

                param.setPublicAlias(publicAlias);

                param.setStorePass(storePass);

                param.setLicensePath(licensePath);

                param.setPublicKeysStorePath(publicKeysStorePath);

                LicenseVerify licenseVerify = new LicenseVerify();

                //安装证书

                licenseVerify.install(param);

                logger.info("++++++++ 证书安装结束 ++++++++");

            }

        }

    }

}

注:上面代码使用参数信息如下所示:

1

2

3

4

5

6

7

#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

(4)添加拦截器,用于在登录的时候校验License证书:

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

package cn.zifangsky.license;

import com.alibaba.fastjson.JSON;

import org.apache.logging.log4j.LogManager;

import org.apache.logging.log4j.Logger;

import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import java.util.HashMap;

import java.util.Map;

/**

* LicenseCheckInterceptor

*

* @author zifangsky

* @date 2018/4/25

* @since 1.0.0

*/

public class LicenseCheckInterceptor extends HandlerInterceptorAdapter{

    private static Logger logger = LogManager.getLogger(LicenseCheckInterceptor.class);

    @Override

    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        LicenseVerify licenseVerify = new LicenseVerify();

        //校验证书是否有效

        boolean verifyResult = licenseVerify.verify();

        if(verifyResult){

            return true;

        }else{

            response.setCharacterEncoding("utf-8");

            Map<String,String> result = new HashMap<>(1);

            result.put("result","您的证书无效,请核查服务器是否取得授权或重新申请证书!");

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

            return false;

        }

    }

}

(5)添加登录页面并测试:

添加一个登录页面,可以在license校验失败的时候给出错误提示:

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

<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>登录页面</title>

    <script src="https://cdn.bootcss.com/jquery/2.2.4/jquery.min.js"></script>

    <link href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">

    <link href="https://cdn.bootcss.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">

    <script src="https://cdn.bootcss.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>

    <link rel="stylesheet" th:href="@{/css/style.css}"/>

    <script>

        //回车登录

        function enterlogin(e) {

            var key = window.event ? e.keyCode : e.which;

            if (key === 13) {

                userLogin();

            }

        }

        //用户密码登录

        function userLogin() {

            //获取用户名、密码

            var username = $("#username").val();

            var password = $("#password").val();

            if (username == null || username === "") {

                $("#errMsg").text("请输入登陆用户名!");

                $("#errMsg").attr("style", "display:block");

                return;

            }

            if (password == null || password === "") {

                $("#errMsg").text("请输入登陆密码!");

                $("#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 οnkeydοwn="enterlogin(event);">

<div class="container">

    <div class="form row">

        <div class="form-horizontal col-md-offset-3" id="login_form">

            <h3 class="form-title">LOGIN</h3>

            <div class="col-md-9">

                <div class="form-group">

                    <i class="fa fa-user fa-lg"></i>

                    <input class="form-control required" type="text" placeholder="Username" id="username"

                           name="username" autofocus="autofocus" maxlength="20"/>

                </div>

                <div class="form-group">

                    <i class="fa fa-lock fa-lg"></i>

                    <input class="form-control required" type="password" placeholder="Password" id="password"

                           name="password" maxlength="8"/>

                </div>

                <div class="form-group">

                    <span class="errMsg" id="errMsg" style="display: none">错误提示</span>

                </div>

                <div class="form-group col-md-offset-9">

                    <button type="submit" class="btn btn-success pull-right" name="submit" οnclick="userLogin()">登录

                    </button>

                </div>

            </div>

        </div>

    </div>

</div>

</body>

</html>

i)启动项目,可以发现之前生成的license证书可以正常使用:

这时访问 http://127.0.0.1:7080/login ,可以正常登录:

ii)重新生成license证书,并设置很短的有效期。

iii)重新启动ClientDemo,并再次登录,可以发现爆以下提示信息:

至此,关于使用 TrueLicense 生成和验证License就结束了,文章中没有说到的类可以自行参考示例源码,谢谢阅读。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Spring Boot项目中使用 TrueLicense 生成和验证License(服务器许可) 的相关文章

  • H264 字节流到图像文件

    第一次来这里所以要温柔 我已经在给定的 H 264 字节流上工作了几个星期 一般注意事项 字节流不是来自文件 它是从外部源实时提供给我的 字节流使用 Android 的媒体编解码器进行编码 当将流写入扩展名为 H264的文件时 VLC能够正
  • JBoss AS 5 中的共享库应该放在哪里?

    我是 Jboss 新手 但我有多个 Web 应用程序 每个应用程序都使用 spring hibernate 和其他开源库和 portlet 所以基本上现在每个 war 文件都包含这些 jar 文件 如何将这些 jar 移动到一个公共位置 以
  • WebLogic 10 中的临时目录

    每当 WL 停止时 它都不会删除其临时目录 即 domains mydomain servers myserver tmp WL TEMP APP DOWNLOADS domains mydomain servers myserver tm
  • 使用 JAXB 编组 LocalDate

    我正在构建一系列链接类 我希望能够将其实例编组到 XML 以便我可以将它们保存到文件中并稍后再次读取它们 目前我使用以下代码作为测试用例 import javax xml bind annotation import javax xml b
  • Maven 目标的默认阶段?

    据我了解 在 Maven 中 插件目标可以附加到生命周期阶段 如果没有定义 默认阶段是什么 根据我的经验 这取决于插件的目标 例如 组装 单个 http maven apache org plugins maven assembly plu
  • 空 EntityManager/EJB 注入 MDB

    我有一个消息驱动 bean MDB 部署到 WebLogic 12 1 3 我尝试使用 PersistenceContext 注释将实体管理器注入 MDB 但实体管理器为空 我还尝试注入一个简单的无状态会话 bean 它也是空的 但是 Me
  • 代码编译期间遇到警告消息“使用或覆盖已弃用的 API”

    我编译了我的程序并收到以下错误 我该如何解决呢 Note ClientThreadClients java uses or overrides a deprecated API Note Recompile with Xlint depre
  • BigDecimal 的 JPA @Size 注释

    我该如何使用 SizeMySQL 的注释DECIMAL x y 列 我在用着BigDecimal 但是当我尝试包括 Size max它不起作用 这是我的代码 Size max 7 2 Column name weight private B
  • 通过 JNI 从 Applet 调用 DLL

    我有一个 概念验证 的作品 它跨越了一些不熟悉的领域 我的任务是将 EFTPOS 机器连接到在内联网浏览器中作为小程序运行的应用程序 我暂时忽略了 EFTPOS dll 并用我选择的语言 Delphi 创建了一个简单的 JNI 修饰的 DL
  • 用于防止滥用的 Servlet 过滤器? (DoS、垃圾邮件等)

    我正在寻找一个 Servlet 过滤器库 它可以帮助我保护我们的 Web 服务免受未经授权的使用和 DDoS 攻击 我们的网络服务有 授权客户 因此理想情况下 过滤器将帮助检测未经授权或行为不当的客户 或检测使用同一帐户的多个人 此外 我们
  • 如何使用 Spring MVC 和 Thymeleaf 添加静态文件

    我的问题是如何添加 CSS 和图像文件等静态文件 以便我可以使用它们 我正在使用 Spring MVC 和 Thymeleaf 我查看了有关此主题的各种帖子 但它们对我没有帮助 所以我才来问 根据这些帖子 我将 CSS 和图像文件放在res
  • Scala(或 Java)中泛型函数的特化

    是否可以在 Scala 中专门化泛型函数 或类 例如 我想编写一个将数据写入 ByteBuffer 的通用函数 def writeData T buffer ByteBuffer data T buffer put data 但由于 put
  • MessageDigest MD5 算法未返回我期望的结果

    我脑后的某个东西告诉我 我在这里遗漏了一些明显的东西 我正在将现有的 java 项目与第三方 api 集成 该第三方 api 使用 api 密钥的 md5 哈希进行身份验证 它对我不起作用 在调试过程中我意识到我生成的哈希值与他们提供的示例
  • XSLT:我们可以使用abs值吗?

    我想知道在 XSLT 中我们是否可以使用 math abs 我在某处看到过这个 但它不起作用 我有类似的东西
  • 在多模块项目中访问绑定适配器

    我有一个多模块项目 其中应用程序模块包含我的绑定适配器 而我的功能模块取决于我的应用程序模块 因为它是动态功能模块 应用程序 包含绑定适配器 gt 动态功能模块 存在布局的地方 我在所有模块中启用了数据绑定和 kapt 我无法成功构建应用程
  • setKeyListener 将覆盖 setInputType 并更改键盘

    大家好 我在两个设备之间遇到问题 在实践中使用InputType和KeyListener我正在操纵一个EditText让它从数字键盘接收逗号和数字 有关更多背景信息 请检查我之前的question https stackoverflow c
  • 如何使用maven创建基于spring的可执行jar?

    我有一个基于 Maven 的 Spring WS 客户端项目 我想将其打包为单个 jar 在eclipse中 一切运行正常 当我尝试将其打包为可执行 jar 时 我收到 ClassNotFound 异常 因为 Spring jar 未包含在
  • BoneCP 和 Derby - 如何正确关闭

    I have BoneCP CONNECTION POOL CONNECTION POOL getConfig setJdbcUrl jdbc derby database shutdown true Connection connecti
  • SWT - 与操作系统无关的获取等宽字体的方法

    SWT 有没有一种方法可以简单地获得跨各种操作系统的等宽字体 例如 这适用于 Linux 但不适用于 Windows Font mono new Font parent getDisplay Mono 10 SWT NONE 或者我是否需要
  • 在多线程环境中,Collections.sort 方法有时会抛出 ConcurrentModificationException。列表没有进行结构性修改

    package CollectionsTS import java util ArrayList import java util Collections import java util HashSet import java util

随机推荐

  • "免费!中文!10个最佳Python学习网站推荐

    分享资料 一起学习 我是小白 微信 tlxx233 备注 888建了个微信程序员学习群 互相解答问题 有需要的同学可以加我微信进群 10个免费学习 Python 的中文网站 如果你想要学习 Python 那么这篇文章将会介绍 10 个免费的
  • Linux和Windows下使用Syslog库

    本文档描述如何在Windows和Linux下使用Syslog库 在Linux下GNU库中已经自带有syslog库 但是在Windows下的标准库中没有syslog库 从网上可以找到syslog的开源代码实现 接口函数基本上与Linux一致
  • 高精度运算c++

    高精度运算c 前言 加法 减法 乘法 除法 求余 全部代码 完 前言 采用c 的stl库实现高精度的加减乘除 以及求余运算 希望可以帮助到大家 加法 string add big string a string b string 加 str
  • String和基本数据类型的比较方式

    package com test author xlj 简单的比较方式 public class Test public static void main String args System out println 192 168 101
  • springboot项目启动时:Failed to retrieve application JMX service URL

    application properties配置有问题 检查即可
  • Cannot find any provider supporting AES/CBC/PKCS5Padding

    1 出现的问题 java lang RuntimeException java security NoSuchAlgorithmException Cannot find any provider supporting AES CBC PK
  • Elastic Search 安装部署最全教程(Docker)

    一 部署单点ES 1 首先创建网络 因为我们还需要部署kibana容器 因此需要让es和kibana容器互联 这里先创建一个网络 docker network create es net 2 加载镜像 docker pull elastic
  • 刀片服务器 如何增加硬盘,IBM为刀片服务器添加新SAS及固态硬盘

    在调整过X64产品线后 我们又收到IBM将为服务器产品线添加新SAS硬盘及固态硬盘的消息 上周IBM刚发布了一款小尺寸的SAS硬盘 它只有2 5英寸 而之前的硬盘基本上都是3 5英寸的SCSI硬盘 因为IBM拥有世界上最好的硬盘研究和生产工
  • 疯壳4900、7072心率血压血氧心电四合一智能手表&模组电容触摸实现

    触摸 该手表的触摸是由RH6015C触摸IC完成的 该IC是一款内置稳压模块的单通道电容式触摸感应控制开关 IC 可以替代传统的机械式开关 RH6015可在有介质 如玻璃 亚克力 塑料 陶瓷等 隔离保护的情况下实现触摸功能 安全性高 RH6
  • delete 和 delete []的真正区别

    c 中对new申请的内存的释放方式有delete和delete 两种方式 到底这两者有什么区别呢 1 我们通常从教科书上看到这样的说明 delete 释放new分配的单个对象指针指向的内存 delete 释放new分配的对象数组指针指向的内
  • ubuntu下解决wps2019缺少字体问题

    准备字体包 链接 https pan baidu com s 1rsqn3CY SWS KWaKc0w83g 提取码 h9cs 复制 解压后的wps symbol fonts zip到 home usr share fonts下 sudo
  • 西门子PLC—用 SCL 编写你的第一个 TIA 代码

    前言 使用梯形图编写程序时 博途编辑器是通过网络段 把程序分成一段一段的 编辑器可以插入若干个网络段 每一个网络段可以有各自的注释 而SCL是文本语言 不分网络段 在LAD FBD语言内增加SCL的除外 这就需要需要用其他的方法来 解决程序
  • 面试总结大全

    预定义变量 0 脚本名 所有的参数 所有的参数 参数的个数 当前进程的PID 上一个后台进程的PID 上一个命令的返回值 0表示成功 for 循环次数是固定的 for i in 取值 范围 1 20 zhangsan lisi wanger
  • 牛客网——华为题库(41~50)

    华为题库 41 称砝码 42 学英语 43 迷宫问题 44 Sudoku 45 名字的漂亮度 46 截取字符串 48 从单向链表中删除指定值的节点 50 四则运算 41 称砝码 include
  • C++通过回车结束循环输入

    试想一个案例 假设需要你输入n行数字 而每一行输入的数字数量都未知 不定 如何通过C 来实现这一操作 本贴笔者给出一个具体案例 首先规定输入的行数 而后在每一行输入不定量的数字 最后将每一个数字对应的值 以及与其匹配的行数输出 例如 输入
  • 实战07- 模型融合:利用AdaBoost元算法提高分类性能

    元算法 meta algorithm 是对其他算法进行组合的一种方式 即模型融合 模型融合主要分为三种 Bagging Boosting和Stacking 思想 将弱分类器融合成强分类器 融合后比最强的弱分类器更好 视频导学 https w
  • 什么是高防CDN,高防CDN是如何防御网络攻击的呢?

    高防CDN是一种新型的网络构建法式 N是构建在现有网络基础之上的智能虚拟网络 依靠部署在各地的边缘服务器 通过中心平台的负载均衡 内容分发 调度等功能模块 使用户就近获取所需内容 降低网络拥塞 提高用户访问响应速度和命中率 CDN的关键技术
  • tensorflow2.1.0安装

    原来一直用1 x的tf 最近安装2 初始源error无法安装 下载本地包后 换清华源之类的 channels defaults show channel urls true default channels https mirrors tu
  • 机器学习(一)

    文章目录 人工智能 人工智能的诞生 人工智能的发展历程 人工智能与机器学习的关系 机器学习 机器学习的发展历程 讨论 机器学习的必要性 机器学习的定义 机器学习的三要素 机器学习的基本概念 作业 人工智能 人工智能的诞生 人工智能诞生于一群
  • Spring Boot项目中使用 TrueLicense 生成和验证License(服务器许可)

    一 简介 License 即版权许可证 一般用于收费软件给付费用户提供的访问许可证明 根据应用部署位置的不同 一般可以分为以下两种情况讨论 应用部署在开发者自己的云服务器上 这种情况下用户通过账号登录的形式远程访问 因此只需要在账号登录的时