小工具知识网

当前位置: 首页 > 行业知识 > 对接第三方接口鉴权(Spring Boot+Aop+注解实现Api接口签名验证)

行业知识

对接第三方接口鉴权(Spring Boot+Aop+注解实现Api接口签名验证)

2026-11-10 21:06:26 来源:互联网转载

前言

一个web系统,从接口的使用范围也可以分为对内和对外两种,对内的接口主要限于一些我们内部系统的调用,多是通过内网进行调用,往往不用考虑太复杂的鉴权操作。但是,对于对外的接口,我们就不得不重视这个问题,外部接口没有做鉴权的操作就直接发布到互联网,而这不仅有暴露数据的风险,同时还有数据被篡改的风险,严重的甚至是影响到系统的正常运转

方案一:Spring Boot+Aop+注解实现Api接口签名验证

方案二:在已有接口上,拦截器拦截,接口路径,白名单匹配

Spring Boot+Aop+注解实现Api接口签名验证

appId和secret+token+时间戳

token的生成过程中在加入时间戳,校验token正确性之前先校验时间戳是否在一定时间窗口内(比如说1分钟),如果超过一分钟,直接拒绝请求,通过后再校验token。

新接口加上注解

由于项目需要开发第三方接口给多个供应商,为保证Api接口的安全性,遂采用Api接口签名验证。

一、为什么需要 API 接口签名

对外开放的 API 接口都会面临一些安全问题,例如伪装攻击、篡改攻击、重放攻击以及数据信息泄漏的风险。利用 API 接口签名能方便的防范这些安全问题和风险。在设计 API 接口签名时主要考虑以下几点:

请求发起时间得在限制范围内

请求的用户是否真实存在

是否存在重复请求

请求参数是否被篡改

1.保证请求数据正确

当请求中的某一个字段的值变化时,原有的签名结果就会发生变化。所以,只要参数发生变化,签名就要发生变化,否则请求将会是一个无效的请求。

2.保证请求来源合法

一般情况下,生成签名的算法都会成对出现一个 appKey 和一个 appSecret,根据 appKey 能识别出调用者身份;根据 appSecret 能识别出签名是否合法。

3.识别接口的时效性

一般情况下,签名和参数中会包含时间戳,这样服务端就可以验证客户端请求是否在有效时间内,从而避免接口被长时间的重复调用

4.是否存在重复请求

API 接口签名验签实现机制

签名验签流程图

1 客户端向服务端申请 appKey,appSecret ,服务端下发 appKey,appSecret。

2 客户端集成 SDK 产生 sign,将 appKey,请求参数,时间戳,sign,随机数nonce 发送到服务端,服务端根据请求参数使用 SDK 中的签名规则生成签名来验证sign的合法性,之后返回结果。

实现思路:

我们按照主要防御措施先后顺序来实现,首先已知我们得到以下四个参数:

// 供应商的id,验证用户的真实性String appid = request.getHeader("appid");// 请求发起的时间String timestamp = request.getHeader("timestamp");// 随机数String nonce = request.getHeader("nonce");// 签名算法生成的签名String sign = request.getHeader("sign"); 1.请求发起时间得在限制范围内  像这种比较简单,就是获取服务器的当前时间去跟请求发起时间比较。// 限制为(含)60秒以内发送的请求long time = 60;long now = System.currentTimeMillis() / 1000;if (now - Long.valueOf(timestamp) > time) {    return ObjectResponse.fail("请求发起时间超过服务器限制时间");}

2.请求的用户是否真实存在

一般会有以下两个场景

场景一:在前后端分离的模式中,用户登录后得到token,用户调用接口时传递token来确保用户的真实性。

场景二:接口调用方不需要登录,那么我们接口提供方可以提供appid(调用时需要传递)与secret(在签名算法中使用)给接口调用方来验证用户的真实性。

这里我主要说一下场景二,如下:

// 查询appid是否正确来验证用户的真实性

CoreApiKey apiKey = coreApiKeyService.selectByAppid(appid);if (apiKey == null) {    return ObjectResponse.fail("appid参数错误");}
  1. 是否存在重复请求

    这里利用nonce参数,每次请求时先判断nonce在redis是否存在,存在则认为是重复请求,不存在就存放到redis中。但是这会有一个问题,随着请求的 次数越来越多,那么redis存放的nonce集合会越来越大,这肯定不是我们所期望的。这时我们可以巧妙的利用在请求发起时间得在限制范围内中的time(服务器限制60秒以内发生的请求),因为此步骤主要是验证请求是否重复,如果timestamp时间戳变了,那就不是重复请求了,所以我们可以在nonce存放到redis时给它设置一个过期时间(60秒),这样既保证了nonce的唯一性也不会发生nonce集合的无限大。

// 验证请求是否重复if (redisService.hasKeyHashItem("third_party_key", apiKey.getAppid() + nonce)) {    return ObjectResponse.fail("请不要发送重复的请求");} else {    // 如果nonce没有存在缓存中,则加入,并设置失效时间(秒)    redisService.setHashItem("third_party_key", apiKey.getAppid() + nonce, nonce, time);}
  1. 请求参数是否被篡改

    利用签名算法来生成签名。主要就是接口调用方的签名算法必须与接口提供方的签名算法一致。签名算法可以自己捣鼓捣鼓,我这里是先对key进行字典序排序,然后以url的参数格式进行拼接(secret在最后拼接),最后进行md5加密,以下一个Api接口签名验证就大功告成啦!

JSONObject signObj = new JSONObject();signObj.put("appid", appid);signObj.put("timestamp", timestamp);signObj.put("nonce", nonce);String mySign = getSign(signObj, apiKey.getSecret());// 验证签名if (!mySign.equals(sign)) {    return ObjectResponse.fail("签名信息错误");}/** * 获取签名信息 * @param data * @param secret * @return */private static String getSign(JSONObject data, String secret) {    // 由于map是无序的,这里主要是对key进行排序(字典序)    Set keySet = data.keySet();    String[] keyArr = keySet.toArray(new String[keySet.size()]);    Arrays.sort(keyArr);    StringBuilder sbd = new StringBuilder();    for (String k : keyArr) {        if (StringUtil.isNotEmpty(data.getString(k))) {            sbd.append(k + "=" + data.getString(k) + "&");        }    }    // secret最后拼接    sbd.append("secret=").append(secret);    return MD5Util.encode(sbd.toString());} 5.基于SringBoot以及Redis使用Aop来实现Api接口签名验证的源码  @Component@Aspect@Slf4jpublic class ThridPartyApiAspect {    @Autowired    private HttpServletRequest request;    @Autowired    private HttpServletResponse response;    @Autowired    private RedisService redisService;    @Autowired    private CoreApiKeyService coreApiKeyService;    /**     * 表示匹配带有自定义注解的方法     */    @Pointcut("@annotation(com.stan.framework.anno.ThridPartyApi)")    public void pointcut() {    }    @Around("pointcut()")    public Object around(ProceedingJoinPoint point) {        try {            // 供应商的id,验证用户的真实性            String appid = request.getHeader("appid");            // 请求发起的时间            String timestamp = request.getHeader("timestamp");            // 随机数            String nonce = request.getHeader("nonce");            // 签名算法生成的签名            String sign = request.getHeader("sign");            if (StringUtil.isEmpty(appid) || StringUtil.isEmpty(timestamp) || StringUtil.isEmpty(nonce) || StringUtil.isEmpty(sign)) {                return ObjectResponse.fail("请求头参数不能为空");            }            // 限制为(含)60秒以内发送的请求            long time = 60;            long now = System.currentTimeMillis() / 1000;            if (now - Long.valueOf(timestamp) > time) {                return ObjectResponse.fail("请求发起时间超过服务器限制时间");            }            // 查询appid是否正确            CoreApiKey apiKey = coreApiKeyService.selectByAppid(appid);            if (apiKey == null) {                return ObjectResponse.fail("appid参数错误");            }            // 验证请求是否重复            if (redisService.hasKeyHashItem("third_party_key", apiKey.getAppid() + nonce)) {                return ObjectResponse.fail("请不要发送重复的请求");            } else {                // 如果nonce没有存在缓存中,则加入,并设置失效时间(秒)                redisService.setHashItem("third_party_key", apiKey.getAppid() + nonce, nonce, time);            }            JSONObject signObj = new JSONObject();            signObj.put("appid", appid);            signObj.put("timestamp", timestamp);            signObj.put("nonce", nonce);            String mySign = getSign(signObj, apiKey.getSecret());            // 验证签名            if (!mySign.equals(sign)) {                return ObjectResponse.fail("签名信息错误");            }            try {                return point.proceed();            } catch (Throwable throwable) {                throwable.printStackTrace();            }        } catch (Exception e) {            e.printStackTrace();            return ObjectResponse.fail("解析请求参数异常");        }        return null;    }    /**     * 获取签名信息     * @param data     * @param secret     * @return     */    private static String getSign(JSONObject data, String secret) {        // 由于map是无序的,这里主要是对key进行排序(字典序)        Set keySet = data.keySet();        String[] keyArr = keySet.toArray(new String[keySet.size()]);        Arrays.sort(keyArr);        StringBuilder sbd = new StringBuilder();        for (String k : keyArr) {            if (StringUtil.isNotEmpty(data.getString(k))) {                sbd.append(k + "=" + data.getString(k) + "&");            }        }        // secret最后拼接        sbd.append("secret=").append(secret);        return MD5Util.encode(sbd.toString());    }}6. 测试签名/** * @Author lc * @description: * @Date 2022/4/26 15:19 * @Version 1.0 */@RestController@Api(tags = "对外接口")@RequestMapping("/test")public class ThirdPartyApiAspectController  {    @ThirdPartyApi    @ApiOperation("对接接口测试")    @PostMapping(value = "/test")    public ResponseVO test(HttpServletRequest request){        return new ResponseVO ("签名校验");    }}

上一篇:宝塔面板怎么选?

下一篇:服务器怎么播放媒体?