SpringBoot3实战教程之实现接口签名验证功能

 更新时间:2025年04月09日 09:03:19   作者:江南一点雨  
接口签名是一种重要的安全机制,用于确保 API 请求的真实性、数据的完整性以及防止重放攻击,这篇文章主要介绍了SpringBoot3实战教程之实现接口签名验证功能,需要的朋友可以参考下

有时候我们要把自己的服务暴露给第三方去调用,为了防止接口不被授权访问,我们一般采用接口签名的方式去保护接口。

接下来松哥和大家聊一聊这个话题。

一 场景分析

什么时候需要接口签名?

接口签名是一种重要的安全机制,用于确保 API 请求的真实性、数据的完整性以及防止重放攻击。

当我们需要保护 API 接口不被未授权访问、确保传输数据在过程中不被篡改,或者需要防止恶意用户利用 API 进行攻击时,就需要使用接口签名。

松哥来举几个需要做接口签名的例子:

  • 开放 API 给第三方使用:当你的 API 需要对外开放,让第三方应用或服务调用时,接口签名可以验证请求方的身份,确保只有拥有有效签名的请求才能被接受。
  • 数据完整性校验:在数据传输过程中,接口签名可以确保数据不被篡改。通过将请求数据与密钥一起进行哈希运算,生成签名值,接收方收到数据后可以用相同的方法生成签名值进行对比,如果一致则数据未被篡改。
  • 防止重放攻击:通过在签名中加入时间戳或随机数等动态元素,接口签名可以防止攻击者截获并重复发送有效的 API 请求。
  • 接口防刷:为了防止接口被恶意调用,通常会采用一些防刷策略,比如限制请求频率、使用验证码等。接口签名可以作为防刷策略的一部分,确保请求的合法性。
  • 敏感操作验证:对于涉及敏感数据或重要操作的 API,如支付、转账等,接口签名提供了额外的安全保障,确保请求的安全性。
  • API 安全合规:在某些行业,如金融、医疗等,法律法规可能要求对 API 进行严格的安全控制,接口签名是满足这些合规要求的一种方式。

这里有一个很重要的点,就是我们的接口是暴露给对方服务端调用的,而不是暴露给前端调用的,这样的场景需要做接口签名。

二 签名步骤

一般来说,接口签名的步骤是这样的:

  • 构造待签名字符串:将请求方法、请求 URI、请求参数(包括查询参数和请求体中的参数)、时间戳等关键信息按照一定的规则拼接成待签名字符串。
  • 生成签名:使用客户端持有的私钥(或密钥)对待签名字符串进行加密(或哈希运算),生成签名。
  • 发送请求:将生成的签名作为请求的一部分(如请求头)发送给服务器。
  • 验证签名:服务器收到请求后,使用相同的规则构造待签名字符串,并使用对应的公钥(或密钥)进行验证。如果签名验证通过,则处理请求;否则,拒绝请求。

实现接口签名时,需要注意密钥管理、时间戳检查、错误处理和日志记录等安全实践。

三 代码实践

接下来,基于 SpringBoot3,松哥来给大家演示一个接口签名案例。

首先我们需要一个签名和验签的工具类。这里我们采用 HmacSHA1 算法。

HmacSHA1 是一种基于 SHA-1 哈希算法的加密哈希消息认证码(Hash-based Message Authentication Code,简称 HMAC)算法。HMAC 是一种用于验证数据完整性和认证消息发送者身份的机制。它结合了加密哈希函数和加密密钥,从而提供了一种安全的方式来确认数据的完整性和真实性。

public class SignUtils {
    /**
     * 使用 HmacSHA1 算法进行签名
     * @param secretKey 密钥
     * @param data 数据
     * @return
     */
    public static String signWithHmacSha1(String secretKey, String data) {
        try {
            SecretKeySpec signingKey = new SecretKeySpec(secretKey.getBytes("UTF-8"), "HmacSHA1");
            Mac mac = Mac.getInstance("HmacSHA1");
            mac.init(signingKey);
            return Base64.getEncoder().encodeToString(mac.doFinal(data.getBytes("UTF-8")));
        } catch (NoSuchAlgorithmException | InvalidKeyException | UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return null;
    }
    /**
     * 验证签名
     * @param secretKey 密钥
     * @param data 数据
     * @param hmac 签名
     * @return
     */
    public static boolean verify(String secretKey, String data, String hmac) {
        String calculatedHmac = signWithHmacSha1(secretKey, data);
        return calculatedHmac.equals(hmac);
    }
}

这个类很简单,一个用来生成签名的方法,这个方法按理说可以封装到 SDK 中给到调用者,或者告诉调用者思路,由调用者自行实现。

第二个方法则是一个签名验证的方法,对用户传来的签名信息进行校验。

接下来我需要一个 App 信息的查询类:

@Service
public class AppService {
    private static final Map<String, String> APP_INFO = Map.of("app1", "sign1", "app2", "sign2");
    public String getAppKey(String appId) {
        return APP_INFO.get(appId);
    }
}

这个类的作用是这样的:比如我们想要接入微信公众号后台,我们需要先在微信公众号后台配置我们自己的应用信息,配置完成后,微信公众号会给我们一个 appId 和 appSecret,微信自己会把这两个信息存入到数据库中,将来用户请求来的时候,用户会携带上 appId,但是不会携带 appSecret,微信公众号可以根据用户携带的 appId 去数据库中查询到 appSecret,然后进行验签。

松哥这个案例简化了,直接模拟了两个 appId 和 appSecret 存入到 Map 中,这里提供一个根据 appId 查询 appSecret 的函数。

接下来我们定义一个拦截器,在拦截器中对签名进行验证:

public class SignInterceptor implements HandlerInterceptor {
    public final static Logger logger = LoggerFactory.getLogger(SignInterceptor.class);
    AppService appService;
    public SignInterceptor(AppService appService) {
        this.appService = appService;
    }
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String appId = request.getHeader("appId");
        String timestamp = request.getHeader("timestamp");
        String sign = request.getHeader("sign");
        if (StringUtils.hasText(appId) && StringUtils.hasText(timestamp) && StringUtils.hasText(sign)) {
            if (LocalDateTime.now().compareTo(LocalDateTime.ofInstant(Instant.ofEpochMilli(Long.parseLong(timestamp)), ZoneId.systemDefault()).plusMinutes(1L)) < 0) {
                String originalSign = appId + "-" + appService.getAppKey(appId) + "-" + timestamp;
                if (SignUtils.verify(appService.getAppKey(appId), originalSign, sign)) {
                    return true;
                } else {
                    logger.error("签名验证失败");
                }
            } else {
                logger.error("签名已过期");
            }
        } else {
            logger.error("签名信息不完整");
        }
        response.setStatus(401);
        return false;
    }
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }
}

请求头中主要有三个信息:

  • 应用 id
  • 时间戳
  • 生成的签名

我们先从请求头中取出来这三个信息,检查是否为空;

然后判断一下这个时间戳,要求必须是 1 分钟之内的请求,这个判断目的主要是为了防止重放攻击。

接下来,根据 appId,以及根据 appId 查询出来的 appSecret,以及 timestamp,组成一个字符串,调用验签方法进行验证,如果验证通过,就说明请求没问题。

最后我们配置一下,让这个拦截器生效:

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    AppService appService;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new SignInterceptor(appService))
                //只拦截需要接口验签的请求
                .addPathPatterns("/app/**");
    }
}

OK,大功告成,接下来就可以写接口进行测试了:

@RestController
public class UserController {
    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }
}

至于调用方要如何生成签名呢?松哥也给大家一个例子:

@Autowired
AppService appService;
@Test
void contextLoads() {
    String appId = "app1";
    long timeMillis = System.currentTimeMillis();
    String appSecret = appService.getAppKey(appId);
    String sign = SignUtils.signWithHmacSha1(appSecret, appId + "-" + appSecret + "-" + timeMillis);
    System.out.println("timeMillis = " + timeMillis);
    System.out.println("sign = " + sign);
}

appId 和 appSecret 则是对方从我们这里申请得到的。

postman 上测试时,类似这样:

到此这篇关于SpringBoot3实战:实现接口签名验证的文章就介绍到这了,更多相关SpringBoot接口签名验证内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • MyBatis缓存实现原理及代码实例解析

    MyBatis缓存实现原理及代码实例解析

    这篇文章主要介绍了MyBatis缓存实现原理及代码实例解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-08-08
  • eclipse创建一个基于maven的web项目详细步骤

    eclipse创建一个基于maven的web项目详细步骤

    开始学习maven,并用maven创建了第一个属于自己的web项目,下面这篇文章主要给大家介绍了关于eclipse创建一个基于maven的web项目的相关资料,文中通过图文介绍的非常详细,需要的朋友可以参考下
    2023-12-12
  • SpringCloud eureka(server)微服务集群搭建过程

    SpringCloud eureka(server)微服务集群搭建过程

    这篇文章主要介绍了微服务SpringCloud-eureka(server)集群搭建, 项目搭建的主要步骤和配置就是创建项目和引入pom依赖,本文通过图文示例代码相结合给大家介绍的非常详细,需要的朋友可以参考下
    2022-07-07
  • SpringBoot中使用@Async注解失效场景及说明

    SpringBoot中使用@Async注解失效场景及说明

    在Spring Boot中,@Async注解就像一把刀,能帮你轻松处理那些耗时的任务,让主线程可以继续忙别的事儿,不过,跟所有强大的工具一样,用不好它也可能出岔子,为了避免这些坑,咱们得深入了解下@Async注解,接下来,咱们就来聊聊7种常见的@Async失效情况,需要的朋友可以参考下
    2024-07-07
  • JavaWeb 简单分页实现代码

    JavaWeb 简单分页实现代码

    这篇文章主要介绍了JavaWeb 简单分页实现代码的相关资料,需要的朋友可以参考下
    2016-11-11
  • 如何扩展Spring Cache实现支持多级缓存

    如何扩展Spring Cache实现支持多级缓存

    这篇文章主要介绍了如何扩展Spring Cache实现支持多级缓存,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-11-11
  • java联系人管理系统简单设计

    java联系人管理系统简单设计

    这篇文章主要为大家详细介绍了java联系人管理系统简单设计,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2016-10-10
  • 利用Spring Session和redis对Session进行共享详解

    利用Spring Session和redis对Session进行共享详解

    这篇文章主要给大家介绍了关于利用Spring、Session和redis对Session进行共享的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧。
    2017-09-09
  • Spring中的父子容器原理解析

    Spring中的父子容器原理解析

    这篇文章主要为大家介绍了Spring中的父子容器原理解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-07-07
  • 日历显示读出输入的年月的java代码

    日历显示读出输入的年月的java代码

    这篇文章主要介绍了日历显示读出输入的年月的java代码,有需要的朋友可以参考一下
    2013-12-12

最新评论