开放接口签名(Signature)实现方案
既然是对外开放,那么调用者一定没有我们系统的Token,就需要对调用者进行签名验证,签名验证采用主流的验证方式,采用Signature 的方式。
|   字段  |   类型  |   必传  |   说明  | 
|   appid  |   String  |   是  |   应用id  | 
|   timestamp  |   String  |   是  |   时间戳  | 
|   nonce  |   String  |   是  |   随机数、不少于10位  | 
|   signature  |   String  |   是  |   签名  | 
signature: 生成方式
将参数appId=wx123456789&nonce=155121212121×tamp=1684565287668&key=35AB7ECF665EF5EF44CF8640EC136300 进行拼接 key指的是appid对应的appSecret
将上述参数进行MD5加密

二、流程
1、通过应用设置模块添加应用分配appid和appsecret,针对不同的调用方分配不同的appid和appsecret。
2、加入timestamp(时间戳),有效时间内内数据有效。
3、 加入随机字符串,则认为接口为重复调用,返回错误信息(防止重复提交)。
4、加入signature,所有数据的签名信息。

三、实现
简单来说,调用者调用接口业务参数在body中传递,header中额外增加四个参数signature、appid、timestamp,随机字符串。
我们在后台取到四个参数,其后三个参数加上调用者分配的appSecret,使用字典排序并使用MD5加密后与第一个参数signature进行比对,一致既表示调用者有权限调用。
接口异常:
|   403  |   签名不一致  | 
|   403  |   appId或appSecret不正确  | 
|   422  |   参数timestamp不能为空  | 
|   408  |   请求时间超过规定范围时间10分钟  | 
|   422  |   随机串nonce不能为空  | 
|   422  |   随机串nonce长度最少为10位  | 
|   407  |   不允许重复请求  | 
代码实现:
自定义注解:
/**
 * 签名算法实现=>指定哪些接口或者哪些实体需要进行签名
 */
@Target({TYPE, METHOD})
@Retention(RUNTIME)
@Documented
public @interface Signature {
    //允许重复请求
    boolean resubmit() default true;
} 
 
 
签名工具类:
/**
 * 开放接口签名工具类
 * @author ShawnWang
 * @datetime 2023-05-19
 * @desc 接口校验工具类
 *  生成有序map,签名,验签
 *  通过appId、timestamp、appSecret做签名
 * @menu
 */
@Slf4j
public class SignUtil {
    /**
     * 生成签名sign
     * 加密前:appid=wx123456789&nonce=155121212121×tamp=1684565287668&key=35AB7ECF665EF5EF44CF8640EC136300
     * 加密后:4CD98E261F46AA75E8935695C864A26D
     */
    public static String createSign(SortedMap<String, String> params, String key){
        StringBuilder sb = new StringBuilder();
        Set<Map.Entry<String, String>> es =  params.entrySet();
        Iterator<Map.Entry<String,String>> it =  es.iterator();
        //生成
        while (it.hasNext()){
            Map.Entry<String,String> entry = it.next();
            String k = entry.getKey();
            String v = entry.getValue();
            if(null != v && !"".equals(v) && !"signature".equals(k) && !"key".equals(k)){
                sb.append(k+"="+v+"&");
            }
        }
        sb.append("key=").append(key);
        System.err.println("生成密钥前 " + sb.toString());
        String sign = MD5(sb.toString()).toUpperCase();
        return sign;
    }
    /**
     * 校验签名
     */
    public static Boolean isCorrectSign(SortedMap<String, String> params, String key){
        String sign = createSign(params,key);
        //这是前端带过来的
        String requestSign = params.get("signature").toUpperCase();
        log.info("通过用户发送数据获取新签名:{}", sign);
        return requestSign.equals(sign);
    }
    /**
     * md5常用工具类
     */
    public static String MD5(String data){
        try {
            MessageDigest md5 = MessageDigest.getInstance("MD5");
            byte [] array = md5.digest(data.getBytes("UTF-8"));
            StringBuilder sb = new StringBuilder();
            for (byte item : array) {
                sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
            }
            return sb.toString().toUpperCase();
        }catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }
    /**
     * 生成uuid
     */
    public static String generateUUID(){
        String uuid = UUID.randomUUID().toString().replaceAll("-","").substring(0,32);
        return uuid;
    }
    public static void main(String[] args) {
        /**
         * 模拟如下
         */
        //第一步:用户端发起请求,生成签名后发送请求
        //appId 和 appSecret 由生成者提供
        String appSecret = "35AB7ECF665EF5EF44CF8640EC136300";
        String appId = "wx123456789";
        String timestamp = new Date().getTime() + "";
        String Id = ObjectId.next();
        //生成签名 注意map顺序
        SortedMap<String, String> sortedMap = new TreeMap<>();
        sortedMap.put("appid", appId);
        sortedMap.put("timestamp", timestamp);
        sortedMap.put("nonce", Id);
        //通过sortedMap的参数 以及 appSecret 生成签名
        String sign = SignUtil.createSign(sortedMap, appSecret);
        System.out.println(appId + "生成签名:"+ sign);
        /**
         * 模拟服务端接受参数   并处理校验签名
         */
        //服务端接受到的参数
        String appid = sortedMap.get("appid");
        String timestamp1 = sortedMap.get("timestamp");
        String nonce = sortedMap.get("nonce");
        String websign = sign;
        //2.组装参数,
        SortedMap<String, String> sortedMap12 = new TreeMap<>();
        sortedMap12.put("appid", appid);
        sortedMap12.put("timestamp", timestamp1);
        sortedMap12.put("nonce", nonce);
        sortedMap12.put("signature", websign);
        //3.校验签名
        //sortedMap12模拟客户请求 ,appSecret表示数据库中存储的密钥
        Boolean flag = SignUtil.isCorrectSign(sortedMap12, appSecret);
        if(flag){
            System.out.println("签名验证通过");
        }else {
            System.out.println("签名验证未通过");
        }
    }
} 
 
切面AOP:
@Order(2)
@Aspect
@Component
@Slf4j
/**
 * 通过Aop的方式实现接口签名
 */
public class OpenApiValidatorAspect {
    //同一个请求多长时间内有效 10分钟
    private static final Long EXPIRE_TIME = 60 * 1000 * 10L;
    //同一个nonce 请求多长时间内不允许重复请求 2秒
    private static final Long RESUBMIT_DURATION = 2000L;
    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private ApplySettingMapper applySettingMapper;
    private static ConcurrentHashMap<String, ExpiringMap<String, Integer>> map= new ConcurrentHashMap<>();
    /**
     * 执行OpenApiController 包下所有带注解@annotation的
     *
     * @param pjp
     * @return
     * @throws Throwable
     */
    @Around("execution(" +
            "* com.xx.xx.controller.api.OpenApiController.*(..)) " +
            "&& @annotation(xx.xx.xx.xx.openApiUtils.Signature) " +
            ")"
    )
    public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
        HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
        //如果是对外开放的URL, 进行签名校验
        //获取当前方法的组件
        MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
        Method method = methodSignature.getMethod();
        Signature signature = AnnotationUtils.findAnnotation(method, Signature.class);
        //验证并获取header中的相关参数
        /*
        (1)、appid是否合法
        (2)、根据appid从配置中心中拿到appsecret
        (3)、请求是否已经过时,默认10分钟
        (4)、随机串是否合法
        (5)、是否允许重复请求
        */
        Map<String, String> signatureHeaders = generateSignatureHeaders(signature, request);
        //2.组装参数,
        SortedMap<String, String> sortedMap = new TreeMap<>();
        sortedMap.put("appid", signatureHeaders.get("appid"));
        sortedMap.put("timestamp", signatureHeaders.get("timestamp"));
        sortedMap.put("nonce", signatureHeaders.get("nonce"));
        sortedMap.put("signature", signatureHeaders.get("signature"));
        //3.校验签名
        //sortedMap模拟客户请求 ,appSecret表示数据库中存储的密钥
        Boolean flag = SignUtil.isCorrectSign(sortedMap, signatureHeaders.get("appSecret"));
        //比较客户端与服务端签名
        if (!flag) {
            String message = "签名不一致";
            log.error(message);
            throw new ServiceException("403", message);
        }
        // 获取Map value对象, 如果没有则返回默认值
        //getOrDefault获取参数,获取不到则给默认值
        ExpiringMap<String, Integer> em= map.getOrDefault(signatureHeaders.get("appid"), ExpiringMap.builder().variableExpiration().build());
        Integer count = em.getOrDefault(signatureHeaders.get("appid"), 0);
        if (count >= 10 ) { // 超过次数,不执行目标方法
            throw new ServiceException("422", signatureHeaders.get("appid") + " 接口请求超过次数");
        } else if (count == 0){ // 第一次请求时,设置有效时间
            em.put(signatureHeaders.get("appid"), count + 1, ExpirationPolicy.CREATED,1 , TimeUnit.HOURS);
        } else { // 未超过次数, 记录加一
            em.put(signatureHeaders.get("appid"), count + 1);
        }
        map.put(signatureHeaders.get("appid"), em);
        log.info("签名验证通过, 相关信息: " + signatureHeaders);
        try {
            return pjp.proceed();
        } catch (Throwable e) {
            throw e;
        }
    }
    /**
     * 根据request 中 header值生成SignatureHeaders实体
     * <p>
     * 1.处理header name,通过工具类将header信息绑定到签名实体SignatureHeaders对象上。
     * 2.验证appid是否合法。
     * 3.根据appid拿到appsecret。
     * 4.请求是否已经超时,默认10分钟。
     * 5.随机串是否合法。
     * 6.是否允许重复请求。
     */
    private Map<String, String> generateSignatureHeaders(Signature signature, HttpServletRequest request) throws Exception {
        /**
         * 需要用到的请求参数
         */
        List<String> params = new ArrayList(4);
        params.add("appid");
        params.add("timestamp");
        params.add("nonce");
        params.add("signature");
        /**
         * 获取参数和value 生成Map
         */
        Map<String, String> headerMap = Collections.list(request.getHeaderNames())
                .stream()
                .filter(headerName -> params.contains(headerName))
                .collect(Collectors.toMap(headerName -> headerName, headerName -> request.getHeader(headerName)));
        //根据appId查询数据库
        LambdaQueryWrapper<ApplySetting> applySettingLambdaQueryWrapper = new LambdaQueryWrapper<>();
        LambdaQueryWrapper<ApplySetting> appid = applySettingLambdaQueryWrapper.eq(ApplySetting::getDisabled, EnableStatus.ENABLE).eq(ApplySetting::getStatus, EnableStatus.ENABLE)
                .eq(ApplySetting::getAppId, headerMap.get("appid"));
        ApplySetting applySetting = applySettingMapper.selectOne(appid);
        if (applySetting == null) {
            String errMsg = "未找到appId对应的appSecret, appId=" + headerMap.get("appid");
            log.error(errMsg);
            throw new ServiceException("403", "appId或appSecret不正确");
        } else {
            headerMap.put("appSecret", applySetting.getAppSecret());
        }
        //其他合法性校验
        String timestamp = headerMap.get("timestamp");
        if (StringUtils.isEmpty(timestamp)) {
            throw new ServiceException("422", "参数timestamp不能为空");
        }
        Long now = System.currentTimeMillis();
        Long requestTimestamp = Long.parseLong(headerMap.get("timestamp"));
        if ((now - requestTimestamp) > EXPIRE_TIME) {
            String errMsg = "请求时间超过规定范围时间10分钟, timestamp =" + headerMap.get("timestamp");
            log.error(errMsg);
            throw new ServiceException("408", errMsg);
        }
        /**
         * 随机数位数
         */
        String nonce = headerMap.get("nonce");
        if (StringUtils.isEmpty(nonce)) {
            throw new ServiceException("422", "随机串nonce不能为空");
        }
        if (nonce.length() < 10) {
            String errMsg = "随机串nonce长度最少为10位, nonce=" + nonce;
            log.error(errMsg);
            throw new ServiceException("422", errMsg);
        }
        /**
         * 表单重复提交问题
         */
        if (!signature.resubmit()) {
            String existNonce = (String) redisTemplate.opsForValue().get(nonce);
            if (Objects.isNull(existNonce)) {
                redisTemplate.opsForValue().set(nonce, nonce, RESUBMIT_DURATION, TimeUnit.MILLISECONDS);
            } else {
                String errMsg = "不允许重复请求, nonce=" + nonce;
                log.error(errMsg);
                throw new ServiceException("407", errMsg);
            }
        }
        return headerMap;
    } 
AOP实现中,同时也实现了对接口进行限流
private static ConcurrentHashMap<String, ExpiringMap<String, Integer>> map= new ConcurrentHashMap<>();// 获取Map value对象, 如果没有则返回默认值 //getOrDefault获取参数,获取不到则给默认值 ExpiringMap<String, Integer> em= map.getOrDefault(signatureHeaders.get("appid"), ExpiringMap.builder().variableExpiration().build()); Integer count = em.getOrDefault(signatureHeaders.get("appid"), 0); if (count >= 10 ) { // 超过次数,不执行目标方法 throw new ServiceException("422", signatureHeaders.get("appid") + " 接口请求超过次数"); } else if (count == 0){ // 第一次请求时,设置有效时间 em.put(signatureHeaders.get("appid"), count + 1, ExpirationPolicy.CREATED,1 , TimeUnit.HOURS); } else { // 未超过次数, 记录加一 em.put(signatureHeaders.get("appid"), count + 1); } map.put(signatureHeaders.get("appid"), em);



















