一、项目场景
在上线的项目中,需要添加一个定时授权的功能,对系统的进行授权认证,当授权过期时提示用户需要更新授权或获取授权,不让用户无限制的使用软件。
二、方案思路
在查阅相关资料进行整理后,对该场景做了一套解决方案,大致的思路如下:
- 使用smart-license-1.0.3工具生成校验证书文件(会根据输入的时长和密码进行授权),工具已上传至百度网盘。
链接:https://pan.baidu.com/s/1OXNjw_rgPC3POW5UXTxLcQ?pwd=a0pl
提取码:a0pl
-
由于授权证书只允许能够在指定的服务器上使用,所以这里我将授权密码设置为指定服务器的mac地址加上一段自定义的密码,在验证时动态获取软件部署机器的mac地址进行验证(利用mac地址的唯一性)。
-
由于该证书会自动根据授权时长自动生成结束授权时间,所以为了防止用户修改机器时间去无限使用,所以从数据库任意表读取一个最新时间作为基础时间,然后每次访问操作都去更新和比对这个时间,当发现本次操作比上次操作的时间靠前时,让证书失效。
三、实施流程
1.引入库
<!-- swagger2 依赖-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>${knife4j.version}</version>
</dependency>
<!--许可依赖-->
<!--smart-license 1.0.3授权-->
<dependency>
<groupId>org.smartboot.license</groupId>
<artifactId>license-client</artifactId>
<version>1.0.3</version>
</dependency>
2.编写代码
- 先配置一个系统的缓存,对授权证书文件等其他信息进行缓存
package com.starcone.common.syscenter;
import org.smartboot.license.client.LicenseEntity;
import java.io.File;
/**
* @Author: Daisen.Z
* @Date: 2021/7/13 11:41
* @Version: 1.0
* @Description: 系统缓存中需要存储的信息
*/
public class SysCacheInfo {
// 系统加载的证书文件信息
public static LicenseEntity licenseEntity = null;
// 最近一次系统的操作时间
public static long latOptTimestmp;
}
- 提供一个证书许可加载的工具类
package com.starcone.common.util;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.resource.ClassPathResource;
import cn.hutool.core.util.ObjectUtil;
import com.starcone.common.bean.response.ExceptionCast;
import com.starcone.common.bean.response.ResponseResult;
import com.starcone.common.syscenter.SysCacheInfo;
import org.smartboot.license.client.License;
import org.smartboot.license.client.LicenseEntity;
import org.smartboot.license.client.LicenseException;
import org.springframework.util.ResourceUtils;
import java.io.*;
import java.nio.charset.Charset;
/**
* 许可授权拦截器
*/
public class LicenseUtil {
// 加载授权文件的方法,该方法必须为单线程
public synchronized static LicenseEntity loadLocalLEntity(File file) throws Exception {
// 加载证书,在证书文件过期或无效时该方法会报错
License license = new License();
return license.loadLicense(file);
}
public static LicenseCheckResult checkLicenseFile(File file) {
License license = new License();
LicenseEntity licenseEntity = null;
try {
licenseEntity = license.loadLicense(file);
String s1 = Md5Util.encodeByMd5(IpUtil.getMACAddress());
String md5 = licenseEntity.getMd5();
if (!s1.equals(md5)) {
// 校验md5值是否相等
return new LicenseCheckResult(false,"证书文件不匹配");
}
return new LicenseCheckResult(true,"");
} catch (LicenseException e) {
e.printStackTrace();
return new LicenseCheckResult(false,"证书文件无效");
} catch (Exception e) {
e.printStackTrace();
return new LicenseCheckResult(false,"证书文件失效");
}
}
// 校验缓存中的licens信息
public static LicenseCheckResult checkLicense(LicenseEntity licenseEntity) {
// 授权缓存为空时,先将文件加载至缓存
if (ObjectUtil.isEmpty(licenseEntity)) {
return new LicenseCheckResult(false,"未加载证书");
} else {
// 校验授权是否被修改
try {
String s1 = Md5Util.encodeByMd5(IpUtil.getMACAddress());
String md5 = licenseEntity.getMd5();
if (!s1.equals(md5)) {
// 校验md5值是否相等
return new LicenseCheckResult(false,"证书文件不匹配");
}
// 校验授权是否过期
long expireTime = licenseEntity.getExpireTime(); // 到期时间
if (System.currentTimeMillis() > expireTime) { // 当前系统时间大于到期时间,说明已经过期
return new LicenseCheckResult(false,"证书已过期");
}
return new LicenseCheckResult(true,"");
} catch (LicenseException e) {
e.printStackTrace();
return new LicenseCheckResult(false,"证书文件失效");
} catch (Exception e) {
e.printStackTrace();
return new LicenseCheckResult(false,"证书文件失效");
}
}
}
// 加载授权文件的方法,该方法必须为单线程
public synchronized static LicenseEntity loadLicenseToCache() throws Exception {
// 加载证书文件
ClassPathResource classPathResource = new ClassPathResource("license.txt");
File file = classPathResource.getFile();
// 加载证书,在证书文件过期或无效时该方法会报错
License license = new License();
SysCacheInfo.licenseEntity = license.loadLicense(file);
return SysCacheInfo.licenseEntity;
}
public static LicenseCheckResult checkLocalLicenseFile() {
// 校验文件是否有效
ClassPathResource classPathResource = new ClassPathResource("license.txt");
File file = null;
try {
file = classPathResource.getFile();
String absolutePath = ResourceUtils.getFile("classpath:license.txt").getAbsolutePath();
} catch (IOException e) {
throw new RuntimeException(e);
}
return checkLicenseFile(file);
}
// 校验缓存中的licens信息
public static LicenseCheckResult checkLocalLicenseCache() {
return checkLicense(SysCacheInfo.licenseEntity);
}
public static class LicenseCheckResult{
private boolean checkResult;
private String msg;
public LicenseCheckResult(boolean checkResult, String msg) {
this.checkResult = checkResult;
this.msg = msg;
}
public LicenseCheckResult() {
}
public boolean getCheckResult() {
return checkResult;
}
public void setCheckResult(boolean checkResult) {
this.checkResult = checkResult;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
// 更新本地文件
public static void updateLocalLicense(File file) throws IOException {
BufferedInputStream inputStream = FileUtil.getInputStream(file);
InputStreamReader streamReader = new InputStreamReader(inputStream);
BufferedReader reader = new BufferedReader(streamReader);
String line;
StringBuilder stringBuilder = new StringBuilder();
while ((line = reader.readLine()) != null) {
stringBuilder.append(line);
}
reader.close();
ClassPathResource classPathResource = new ClassPathResource("license.txt");
File file1 = null;
try {
file1 = classPathResource.getFile();
String absolutePath = ResourceUtils.getFile("classpath:license.txt").getAbsolutePath();
FileUtil.writeString(String.valueOf(stringBuilder),file1, Charset.forName("UTF-8"));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
- 前后端交互的Controller类
package com.starcone.web.controller;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.resource.ClassPathResource;
import com.starcone.common.bean.response.ExceptionCast;
import com.starcone.common.bean.response.ResponseResult;
import com.starcone.common.syscenter.SysCacheInfo;
import com.starcone.common.util.IpUtil;
import com.starcone.common.util.LicenseUtil;
import com.starcone.common.util.LogHelper;
import com.starcone.common.util.Md5Util;
import com.starcone.service.SysUserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.smartboot.license.client.License;
import org.smartboot.license.client.LicenseEntity;
import org.smartboot.license.client.LicenseException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.util.ResourceUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.nio.charset.Charset;
import java.util.Date;
import java.util.UUID;
/**
* @Author: Daisen.Z
* @Date: 2024/1/17 15:15
* @Version: 1.0
* @Description:
*/
@RestController
@RequestMapping("/licensManager")
@Api(value = "LicensController", tags = {"授权管理接口"})
public class LicensManagerController {
@Autowired
private SysUserService sysUserService;
@Autowired
private LogHelper logHelper;
@GetMapping("/getEuqMac")
public ResponseResult addDemo() throws Exception {
;return ResponseResult.success(IpUtil.getMACAddress());
}
// http://localhost:8080/track/licensManager/reloadLicens
@ApiOperation(value = "重新加载Licens和校准时钟", notes = "授权管理接口", produces = "application/json")
@GetMapping("/reloadLicens")
public ResponseResult reloadLicens() throws Exception {
// 校准一下时钟信息
SysCacheInfo.latOptTimestmp = System.currentTimeMillis();
LicenseUtil.loadLicenseToCache();
;return ResponseResult.success("过期时间"+ DateUtil.format(new Date(SysCacheInfo.licenseEntity.getExpireTime()),"yyyy-MM-dd HH:mm:ss"));
}
/**
* 基站信息上传
* @return
* @throws IOException
*/
@PostMapping("/uploadLicens")
public ResponseResult upload(MultipartFile file){
File file1 = FileUtil.createTempFile(new File(""));
try {
file.transferTo(file1);
} catch (IOException e) {
logHelper.failLog("更新授权","文件上传异常,"+file.getOriginalFilename());
return ResponseResult.error(503,e.getMessage());
}
LicenseUtil.LicenseCheckResult licenseCheckResult = LicenseUtil.checkLicenseFile(file1);
if (!licenseCheckResult.getCheckResult()){ // 如果证书无效
logHelper.failLog("更新授权","证书无效");
return ResponseResult.error(503,licenseCheckResult.getMsg());
}
// 校验通过后更新本地文件
try {
LicenseUtil.updateLocalLicense(file1);
} catch (IOException e) {
logHelper.failLog("更新授权","本地授权文件更新异常"+file.getOriginalFilename());
return ResponseResult.error(503,"文件更新异常");
}
// 加载授权文件至本地缓存
try {
LicenseUtil.loadLicenseToCache();
} catch (Exception e) {
logHelper.failLog("更新授权","本地授权文件加载异常"+file.getOriginalFilename());
return ResponseResult.error(503,"加载本地文件异常");
}
logHelper.successdLog("更新授权","更新成功"+file.getOriginalFilename()+",授权截至日期"+ DateUtil.format(new Date(SysCacheInfo.licenseEntity.getExpireTime()),"yyyy-MM-dd HH:mm:ss"));
if (FileUtil.isNotEmpty(file1)){
FileUtil.del(file1);
}
return ResponseResult.success();
}
}
- 提供一个可以获取软件部署的服务器mac地址的接口(工具类)
package com.starcone.common.util;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import javax.servlet.http.HttpServletRequest;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.UnknownHostException;
import java.util.Enumeration;
/**
* IP地址相关工具类
*/
public class IpUtil {
private static final Log logger = LogFactory.getLog(IpUtil.class);
public static String getIpAddr(HttpServletRequest request) {
String ipAddress;
try {
ipAddress = request.getHeader("x-forwarded-for");
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("WL-Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getRemoteAddr();
if (ipAddress.equals("127.0.0.1")) {
// 根据网卡取本机配置的IP
InetAddress inet = null;
try {
inet = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
logger.error(e.getMessage(), e);
}
ipAddress = inet.getHostAddress();
}
}
// 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
if (ipAddress != null && ipAddress.length() > 15) { // "***.***.***.***".length()
// = 15
if (ipAddress.indexOf(",") > 0) {
ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
}
}
} catch (Exception e) {
ipAddress = "";
}
return ipAddress;
}
// 获取MAC地址的方法
public static String getMACAddress() throws Exception {
// 获得网络接口对象(即网卡),并得到mac地址,mac地址存在于一个byte数组中。
InetAddress ia = InetAddress.getLocalHost();
byte[] mac = NetworkInterface.getByInetAddress(ia).getHardwareAddress();
// 下面代码是把mac地址拼装成String
StringBuffer sb = new StringBuffer();
for (int i = 0; i < mac.length; i++) {
if (i != 0) {
sb.append("-");
}
// mac[i] & 0xFF 是为了把byte转化为正整数
String s = Integer.toHexString(mac[i] & 0xFF);
// System.out.println("--------------");
// System.err.println(s);
sb.append(s.length() == 1 ? 0 + s : s);
}
// 把字符串所有小写字母改为大写成为正规的mac地址并返回
return sb.toString().toUpperCase();
}
}
-
根据mac地址生成证书文件
输入授权时间,校验密码(主机mac加上自定义密码)
生成的授权文件
-
将证书文件放到项目的resource目录
-
编写启动类,在项目启动时加载证书信息,并读取数据库最新的时间作为基础时间,防止修改系统时间和文件篡改
package com.starcone.common.task;
import cn.hutool.core.util.ObjectUtil;
import com.starcone.common.bean.response.ExceptionCast;
import com.starcone.common.syscenter.SysCacheInfo;
import com.starcone.common.util.LicenseUtil;
import com.starcone.domain.SysLog;
import com.starcone.service.SysLogService;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.Date;
/**
* @Author: Daisen.Z
* @Date: 2021/7/13 9:55
* @Version: 1.0
* @Description: 系统启动时要执行的任务
*/
@Component
public class SysStartTask {
private Logger logger = LogManager.getLogger();
@Autowired
private SysLogService sysLogService;
@PostConstruct
public void init() {
logger.info("****************执行系统启动初始化****************");
// 加载认证证书文件信息
if (SysCacheInfo.licenseEntity == null){
try {
LicenseUtil.loadLicenseToCache();
} catch (Exception e) {
ExceptionCast.cast("License load to Cache Exception");
}
}
// 从数据库读取一个最新的时间到缓存中
SysLog sysLog = sysLogService.queryOneByMaxTime();
if (ObjectUtil.isEmpty(sysLog)){
SysCacheInfo.latOptTimestmp = System.currentTimeMillis();
}else {
Date addTime = sysLog.getAddTime();
SysCacheInfo.latOptTimestmp = addTime.getTime();
}
}
}
- 编写拦截器,在访问系统接口时,进行证书校验,并校验系统时间是否被修改
拦截器:
package com.starcone.common.config.auth;
import com.starcone.common.syscenter.SysCacheInfo;
import com.starcone.common.util.LicenseUtil;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @Author: Daisen.Z
* @Date: 2024/1/17 18:57
* @Version: 1.0
* @Description:
*/
@Configuration
public class JarAuthInterceptor implements HandlerInterceptor {
/**
* 在请求处理之前进行调用(Controller方法调用之前)
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String uri = request.getRequestURI();
if (uri.endsWith("login") || uri.endsWith("licens")) {
return true;
}
// 校验证书文件
boolean checkResult = LicenseUtil.checkLocalLicenseCache().getCheckResult();
long timeMillis = System.currentTimeMillis();
boolean timeFlag = ( timeMillis+ 180000) > SysCacheInfo.latOptTimestmp;
if (checkResult && timeFlag){ // 操作时间不能比最近上一次操作系统的时间小超过3分钟
// 更新最近一次操作的时间
SysCacheInfo.latOptTimestmp = System.currentTimeMillis();
return true;
} else {
if (!timeFlag) {// 跳转到请不要修改服务器时钟的页面
if ("XMLHttpRequest".equals (request.getHeader ("X-Requested-With"))) { // ajax跳转
//告诉ajax我是重定向
response.setHeader ("REDIRECT", "REDIRECT");
//告诉ajax我重定向的路径
response.setHeader ("CONTENTPATH", "/licensDate");
response.setStatus (HttpServletResponse.SC_FORBIDDEN);
} else {
// 如果不是ajax请求,直接跳转
response.sendRedirect (request.getContextPath ( ) + "/licensDate");
}
}else {
if ("XMLHttpRequest".equals (request.getHeader ("X-Requested-With"))) {// ajax跳转
//告诉ajax我是重定向
response.setHeader ("REDIRECT", "REDIRECT");
//告诉ajax我重定向的路径
response.setHeader ("CONTENTPATH", "/licens");
response.setStatus (HttpServletResponse.SC_FORBIDDEN);
} else {
response.sendRedirect (request.getContextPath ( ) + "/licens");
}
}
return false;
}
}
}
配置拦截器生效:
package com.starcone.common.config;
import com.starcone.common.config.auth.JarAuthInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.ArrayList;
import java.util.List;
/**
* @Author: Daisen.Z
* @Date: 2024/1/17 18:58
* @Version: 1.0
* @Description:
*/
@Configuration
public class SignAuthConfiguration implements WebMvcConfigurer {
@Autowired
public JarAuthInterceptor jarAuthInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册TestInterceptor拦截器
InterceptorRegistration registration = registry.addInterceptor(jarAuthInterceptor);
registration.addPathPatterns("/**"); //所有路径都被拦截
List<String> excludePath = new ArrayList<>();
excludePath.add("/login");
excludePath.add("/licens");
excludePath.add("/licensDate");
excludePath.add("/licensManager/**");
excludePath.add("/dologin");
excludePath.add("/libs/**");
excludePath.add("/static/**");
excludePath.add("/src/**");
excludePath.add("/js/**");
excludePath.add("/icon/**");
// 许可授权拦截器
registration.excludePathPatterns(excludePath);
}
}
四、拓展
可以根据项目需求进行适当修改,开搞吧!