开放接口签名(Signature)实现

news2024/11/19 1:27:52

开放接口签名(Signature)实现方案

既然是对外开放,那么调用者一定没有我们系统的Token,就需要对调用者进行签名验证,签名验证采用主流的验证方式,采用Signature 的方式。

字段

类型

必传

说明

appid

String

应用id

timestamp

String

时间戳

nonce

String

随机数、不少于10位

signature

String

签名

signature: 生成方式

将参数appId=wx123456789&nonce=155121212121&timestamp=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&timestamp=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);

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/604123.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

windows server安全设置

Windows服务器安全策略设置 1. Windows服务器安全策略设置 操作系统关闭不必要的服务如smartd&#xff08;一个守护进程&#xff08;帮助程序&#xff09;、Print Spoole&#xff08;管理所有本地和网络打印队列及控制所有打印工作&#xff09;&#xff0c;操作系统关闭默认盘…

名著《MySQL必知必会》讲了个啥

文章目录 第一章 了解SQL第二章 检索数据第三章 排序第四章 过滤数据第五章 高级数据过滤第六章 多表查询内连接&#xff08;交集&#xff09;外连接多表连接UNIONUNION ALL 第七章 单行函数日期和时间函数获取日期、时间日期与时间戳的转换获取月份、星期、星期数、天数等函数…

chatgpt赋能python:Python中创建空列表的两种方法

Python中创建空列表的两种方法 在Python编程中&#xff0c;创建空列表是一项非常常见的任务。Python提供了两种主要的方法来创建一个空列表&#xff0c;分别是“中括号法”和“list()函数法”。本文将介绍这两种方法&#xff0c;它们的优缺点以及如何在你的代码中使用它们。 …

2023年5月Web3行业月度发展报告区块链篇 | 陀螺科技会员专享

5月&#xff0c;市场大部熊市与局部牛市并存&#xff0c;一方面&#xff0c;BRC-20与Meme币引领热点涨势&#xff0c;代表项目Ordinals与Pepe涨幅上千倍&#xff0c;相继引发比特币与以太坊拥堵&#xff0c;市场情绪高涨&#xff1b;另一方面&#xff0c;主流币种持续震荡&…

【终结】Plsql 监听失败+链接Oracle ORA-12545:因目标主机不存在

折腾我好久&#xff01;翻阅千山万水也没有命中问题&#xff01;今天我要公布于世&#xff01;如何解决&#xff01; 起因&#xff1a;服务器那边重启了我运行良好的主机导致上述问题&#xff01; 感谢各位CSDN默默贡献的大神&#xff0c;是你们促进互联网的发展&#xff01;…

无需魔法、无需账户!可免费直接使用微软新必应

大家好&#xff0c;我是校长。 如果你不会魔法上网&#xff0c;如果你没有 ChatGPT 账号&#xff0c;无法体验大语言模型生成式 AI &#xff0c;别怕&#xff0c;我给大家推荐一个好的开源的应用站点&#xff0c;它可以让你体验一番。 前几天&#xff0c;我刷 GitHub 的时候&am…

Transformer工业部署落地!超越ResNet、CSWin(附源码)

关注并星标 从此不迷路 计算机视觉研究院 公众号ID&#xff5c;ComputerVisionGzq 学习群&#xff5c;扫码在主页获取加入方式 论文地址&#xff1a;https://arxiv.org/pdf/2207.05501.pdf 计算机视觉研究院专栏 作者&#xff1a;Edison_G 一种用于在现实工业场景中高效部署的下…

读改变未来的九大算法笔记02_数据库

1. 基础思想 1.1. 预写日志记录 1.2. 两阶段提交 1.3. 关系数据库 2. 两个事实 2.1. 计算机程序会崩溃 2.1.1. 当一个程序崩溃时&#xff0c;它会丢掉所有正在处理的东西 2.1.2. 只有安放在计算机文件系统中的信息会得到保存 2.1.3. 崩溃相当宽泛&#xff1a;包括任何可…

助力智能制造数字化转型 | 5.31 IoTDB 中航机载制造行业客户分享会回顾

5 月 31 日&#xff0c;IoTDB & 中航机载智能制造实践分享会在线上举办。IoTDB 与中航机载的两位产品、技术专家&#xff0c;结合 EMQ 与深南电路的两位技术大拿&#xff0c;针对制造行业智能化痛点带来了一场方案实践分享&#xff0c;与线上直播中上千人次的智能制造关注者…

专访瑞声科技应用软件开发总监陆其明:当一名老兵决定重新上路

编者按&#xff1a;从互联网公司到智能终端解决方案公司&#xff0c;陆其明的这次转变可能难以被人理解。但经济大环境的影响和个人的技术困境还是让他义无反顾地走向一个未知的世界。正如黄仁勋日前所言&#xff0c;“撤退”对聪明人来说并不容易。然而&#xff0c;战略性的撤…

刷题记录:一维前缀和 | leetcode-2559. 统计范围内的元音字符串数 2023/6/2

leetcode-2559. 统计范围内的元音字符串数 这道题的思路并不难找&#xff0c;一开始我有点看出是一维前缀和问题&#xff0c;但没有很确定&#xff0c;因此也就没有直接从这个思路走下去。还是想着先做暴力版本的吧&#xff01; 这是暴力版本的代码&#xff1a; class Solut…

零基础搭建私有云盘并内网穿透远程访问

文章目录 摘要视频教程1. 环境搭建2. 测试局域网访问3. 内网穿透3.1 ubuntu本地安装cpolar3.2 创建隧道3.3 测试公网访问 4 配置固定http公网地址4.1 保留一个二级子域名4.1 配置固定二级子域名4.3 测试访问公网固定二级子域名 转载自cpolar极点云的文章&#xff1a;使用Nextcl…

小白入门C#初探Web简易页面显示信息小案例

1、创建新项目 选择ASP.NET Core Web应用&#xff08;模型-视图-控制器&#xff09;&#xff0c;然后点击下一步。 然后在项目名称里面填写CSharpDemo&#xff0c;点击下一步&#xff0c;直至创建即可。 目录结构&#xff1a; Connected Services&#xff1a;是Visual S…

计算机网络-网络层1.2

IPv6 IP地址耗尽&#xff0c;CIDR和NAT只是延长了IPv4地址分配结束的时间 IPv6从根本上解决了IP地址的耗尽问题 与IPv4的比较 扩大了地址空间移除校验和字段&#xff0c;减少了每跳的处理时间将IPv4的可选字段移出首部&#xff0c;变成拓展首部&#xff0c;路由器不对拓展首…

NIO vs BIO模型解读

目录 stream vs channel IO模型 零拷贝 传统IO NIO优化 stream vs channel stream 不会自动缓冲数据&#xff0c;channel 会利用系统提供的发送缓冲区、接收缓冲区&#xff08;更为底层&#xff09;stream 仅支持阻塞 API&#xff0c;channel 同时支持阻塞、非阻塞 API&a…

计算机网络-网络层1.1

IPv4 网络层打包传输数据时&#xff0c;数据量小则称IP数据报&#xff0c;数据量大则分片&#xff0c;每一片称为IPv4分组 分组格式 固定部分长20B&#xff0c;可变部分用于提供错误检测和安全等机制 版本&#xff1a;指IP版本首部长度&#xff1a;以4B为单位&#xff0c…

Java --- springboot3之web的自动配置原理

目录 一、自动配置 二、默认效果 三、WebMvcAutoConfiguration原理 3.1、生效条件 3.2、效果 3.3、WebMvcConfigurer接口 一、自动配置 1、导入web的pom依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-s…

计算机网络-层级架构

计网概念 计算机网络将分散的、具有独立功能的计算机系统&#xff0c;通过通信设备与线路连接起来&#xff0c;由软件实现资源共享和信息传递&#xff0c;同时也是一些互联的、自治的计算机系统的集合 计网组成的三种划分方式 硬件、软件、协议 硬件包含主机&#xff08;端系…

Redis底层学习(四)—存储类型-Hash篇

文章目录 特点具体服务器操作命令底层结构应用场景 特点 1.适⽤场景&#xff1a;存储⽆序字典的数据&#xff0c;⽐如&#xff1a;适合存储对象类型、存储猪⾁价格等。 2.它的内部采⽤ 数组 链表 的结构&#xff0c;类似java⾥的HashMap。 hash的key值只能是字符串。将对象存…

从C语言到C++_15(vector的模拟实现)+迭代器失效问题

目录 1. vector的基本框架 1.1 构造析构和容量 1.2 push_back&#xff0c;reserve和operator[ ] 2. vector的迭代器 2.1 四个基本迭代器 2.2 迭代器区间初始化 2.3 迭代器的分类 3. vector的其它接口函数 3.1 修改后的reserve 3.2 resize 3.3 pop_back 4. insert和…