一、项目场景

在上线的项目中,需要添加一个定时授权的功能,对系统的进行授权认证,当授权过期时提示用户需要更新授权或获取授权,不让用户无限制的使用软件。


二、方案思路

在查阅相关资料进行整理后,对该场景做了一套解决方案,大致的思路如下:

  • 使用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地址生成证书文件
    SpringBoot项目中添加证书授权认证-LMLPHP
    输入授权时间,校验密码(主机mac加上自定义密码)
    SpringBoot项目中添加证书授权认证-LMLPHP
    生成的授权文件
    SpringBoot项目中添加证书授权认证-LMLPHP

  • 将证书文件放到项目的resource目录
    SpringBoot项目中添加证书授权认证-LMLPHP

  • 编写启动类,在项目启动时加载证书信息,并读取数据库最新的时间作为基础时间,防止修改系统时间和文件篡改

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);
    }
}

四、拓展

可以根据项目需求进行适当修改,开搞吧!

01-20 06:09