锋盈数科-知识库 Logo
首页
软件开发
计算机基础
Hello Halo
新手必读
关于本知识库
登录 →
锋盈数科-知识库 Logo
首页 软件开发 计算机基础 Hello Halo 新手必读 关于本知识库
登录
  1. 首页
  2. 软件开发
  3. JAVA
  4. Spring Boot实现接口签名验证

Spring Boot实现接口签名验证

0
  • JAVA
  • 发布于 2024-08-15
  • 0 次阅读
黄健
黄健

本文由 简悦 SimpRead 转码, 原文地址 blog.csdn.net

项目场景:

        开放接口是指不需要登录凭证就允许被第三方系统调用的接口。为了防止开放接口被恶意调用,开放接口一般都需要验签才能被调用。

        在Spring Boot中实现接口校验签名通常是为了保证接口请求的安全性和数据的完整性。签名校验通常涉及对请求参数的签名计算和验证,以确保请求是由可信的发送方发送,并且在传输过程中没有被篡改。下面,我将详细介绍如何在Spring Boot应用中实现接口校验签名的过程。

解决方案:

1. 配置签名密钥

签名密钥是用于生成和验证签名的秘密信息,生成规则自己定义,需要把生成的密钥提供给第三方。

比如:

appId:360aa3a3ba074da6a7bb17ae55e72d26
appSecret:81343DC5-6E80-483A-A427-E3DF5FA4E5F3

/**
 * 生成应用id和密钥
 */
@RequestMapping(value = "/getSecret", method = RequestMethod.GET)
public Map<String, String> getSecret() {
	//生成应用id和密钥提供给第三方使用,具体生成规则自己定
	String appId = UUID.randomUUID().toString().replace("-", "").toLowerCase();
	String appSecret = UUID.randomUUID().toString().toUpperCase();
	System.out.println("appId:"+appId);
	System.out.println("appSecret:"+appSecret);
 
	Map<String, String> map = new HashMap<>();
	map.put("appId", appId);
	map.put("appSecret", appSecret);
	return map;
}

 2. 定义签名算法

        一个用于生成签名,另一个用于验证签名。生成签名的方法通常将请求参数按照特定规则计算出一个签名值。常见的签名算法有 HMAC-SHA1、HMAC-SHA256 等。

        验证签名的方法则是对接收到的请求参数进行同样的处理,并计算出一个签名值,然后与请求中携带的签名值进行比对。

package com.test.utils;
 
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
 
public class Signature {
    /**
     * 获取签名
     * @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  true:签名一致
     */
    public static boolean verify(String secretKey, String data, String hmac) {
        String calculatedHmac = signWithHmacSha1(secretKey, data);
        return calculatedHmac.equals(hmac);
    }
}

3. 拦截器或过滤器实现

使用 Spring 的拦截器(Interceptor)或过滤器(Filter)来实现对接口请求的签名校验。在拦截器或过滤器中,你可以获取到请求的参数,并调用签名验证方法来校验签名的有效性。

package com.test.aop;
 
import com.test.utils.Signature;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
 
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
 
/**
 * 签名拦截器
 */
@Component
@Slf4j
public class SignInterceptor implements HandlerInterceptor {
 
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("text/html;charset=UTF-8");
 
        //分配的应用id
        String appId = request.getHeader("appId");
        //时间戳
        String timestampStr = request.getHeader("timestamp");
        //签名
        String signature = request.getHeader("signature");
        if(StringUtils.isBlank(appId) || StringUtils.isBlank(timestampStr) || StringUtils.isBlank(signature)){
            response.setStatus(500);
            response.getWriter().println("参数错误!");
            return false;
        }
 
        //这个密钥实际应该根据appId到数据库里查出来
        String appSecret = "81343DC5-6E80-483A-A427-E3DF5FA4E5F3";
        //如果密钥没查到
//        if(flag){
//            response.setStatus(500);
//            response.getWriter().println("密钥不存在!");
//        }
 
        //拼接数据
        String origin = appId + "\n" + appSecret + "\n" + timestampStr;
        if(!Signature.verify(appSecret, origin, signature)){
            response.setStatus(500);
            response.getWriter().println("签名错误!");
            return false;
        }
 
        //业务的时间戳
        long timestamp = Long.parseLong(timestampStr);
        //当前时间戳
        long currentTimestamp = System.currentTimeMillis() / 1000;
        //10分钟内有效
        long timeDifference = 10 * 60;
        if(Math.abs(timestamp - currentTimestamp) > timeDifference){
            response.setStatus(500);
            response.getWriter().println("签名过期!");
            return false;
        }
 
        //放行
        return true;
    }
}

配置拦截器

package com.test.config;
 
import com.test.aop.SignInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
 
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
 
/**
 * WebMvc配置
 */
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
	@Resource
	private SignInterceptor signInterceptor;
 
	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		//不拦截的地址
		List<String> excludedList = new ArrayList<>();
		//swagger地址
		excludedList.add("/swagger-ui.html");
		excludedList.add("/swagger-ui.html/**");
		excludedList.add("/webjars/**");
		excludedList.add("/swagger/**");
		excludedList.add("/doc.html");
		excludedList.add("/doc.html/**");
		excludedList.add("/swagger-resources/**");
		excludedList.add("/v2/**");
		excludedList.add("/favicon.ico");
		//生成应用id和密钥接口不拦截
		excludedList.add("/getSecret");
 
 
		registry.addInterceptor(signInterceptor)
				.addPathPatterns("/**")//拦截所有请求
				.excludePathPatterns(excludedList);//排除的请求
		super.addInterceptors(registry);
	}
}

4. 测试接口 

controller 定义一个测试接口

/**
 * 测试签名
 */
@RequestMapping(value = "/sign", method = RequestMethod.GET)
public String sign() {
	return "success";
}

模拟第三方调用: 

我们需要把接口请求头所需参数和签名的方法告知第三方。

 接口请求头参数如下:

参数名称中文参数值
appId应用 id360aa3a3ba074da6a7bb17ae55e72d26
timestamp当前时间戳,精确到秒

1713838208

signature签名bjvXebFiHi2+I93BNs+8+Tl2I7k=

签名方法: 

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
 
public class Signature {
    /**
     * 获取签名
     * @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;
    }
}

 生成签名示例:

public static void main(String[] args) {
   //这里的应用id和密钥已实际分配的为准
   String appId = "360aa3a3ba074da6a7bb17ae55e72d26";
   String appSecret = "81343DC5-6E80-483A-A427-E3DF5FA4E5F3";
   //当前时间戳,精确到秒,示例:1713838208
   long timestamp = System.currentTimeMillis() / 1000;
   //拼接数据:appId、appSecret、timestamp
   String origin = appId + "\n" + appSecret + "\n" + timestamp;
   String signature = Signature.signWithHmacSha1(appSecret, origin);
 
   //需要加到请求头的参数
   System.out.println("appId:"+appId);
   System.out.println("timestamp:"+timestamp);
   System.out.println("signature:"+signature);
}

 当第三方知道接口请求头所需参数和签名的方法后,就可以调用接口了

curl 调用:

curl -X GET \
  http://localhost:8080/testservice/test/sign \
  -H 'appId: 360aa3a3ba074da6a7bb17ae55e72d26' \
  -H 'signature: bjvXebFiHi2+I93BNs+8+Tl2I7k=' \
  -H 'timestamp: 1713843128'

前端 js 生成签名:

在 JavaScript 中,可以使用 crypto-js 库来实现 HMAC-SHA1 签名。

<html>
<head>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js" type="text/javascript"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.9-1/crypto-js.min.js" type="text/javascript"></script>
    <title>HmacSHA1 签名</title>
    <script type="text/javascript">
        function encryptor() {
			var appId = '360aa3a3ba074da6a7bb17ae55e72d26';
			var appSecret = '81343DC5-6E80-483A-A427-E3DF5FA4E5F3';
			var timestamp = parseInt((new Date()).getTime()/1000);
			var converttext = appId + "\n" + appSecret + "\n" + timestamp;
			
			var plaintext = $("#plaintext").val(converttext);
			
			var ciphetext = CryptoJS.HmacSHA1(
					CryptoJS.enc.Utf8.parse(converttext), 
					CryptoJS.enc.Utf8.parse(appSecret)
				).toString(CryptoJS.enc.Base64);
			
            $("#ciphetext").val(ciphetext);
        }
    </script>
    <style>
        h, div {
            margin: 10px 10px;
        }
 
        input[type=text] {
            width: 500px;
        }
 
        input[type=button] {
            margin: 0px 5px;
            padding: 10px;
        }
 
        select {
            width: 250px;
        }
 
        textarea {
            width: 500px;
            height: 150px;
        }
    </style>
</head>
<body>
    <div>
        <div>
            <h>HmacSHA1 签名</h>
        </div>
		<div>
            <input type="button" value="生成签名" onclick="encryptor()" />
        </div>
        <div>
            <label for="plaintext">明文:</label>
            <br />
            <textarea id="plaintext" readonly="readonly" ></textarea>
        </div>
        <div>
            <label for="ciphetext">签名结果:</label>
            <br />
            <textarea id="ciphetext" readonly="readonly" ></textarea>
        </div>
    </div>
</body>
</html>

总结

  1. 这里签名由 appId + "\n" + appSecret + "\n" + timestamp,生成签名字串
  2. 时间戳用于保证签名的有效性,即使签名被盗用,也只能在有效时间内使用
  3. appId、appSecret 自己定义生成规则,保存到数据库中

源码:https://download.csdn.net/download/u011974797/89211980

标签: #JAVA 991 #软件开发 1171
相关文章

Spring 实现 3 种异步接口 2024-10-18 09:07

大家好,我是苏三~ 如何处理比较耗时的接口? 这题我熟,直接上异步接口,使用 Callable、WebAsyncTask 和 DeferredResult、CompletableFuture等均可实现。 但这些方法有局限性,处理结果仅返回单个值。在某些场景下,如果需要接口异步处理的同时,还持续不断地

重学SpringBoot3-集成Redis(五)之布隆过滤器 2024-10-08 11:24

更多SpringBoot3内容请关注我的专栏:《SpringBoot3》 期待您的点赞👍收藏⭐评论✍ 重学SpringBoot3-集成Redis(五)之布隆过滤器 1. 什么是布隆过滤器? * 基本概念 适用场景 2. 使用 Redis 实现布隆过滤器 * 项目依赖 Redis 配置

SpringBoot整合异步任务执行 2024-10-08 11:24

同步任务: 同步任务是在单线程中按顺序执行,每次只有一个任务在执行,不会引发线程安全和数据一致性等 并发问题 同步任务需要等待任务执行完成后才能执行下一个任务,无法同时处理多个任务,响应慢,影响用 户体验 异步任务: 异步任务是在多线程中同时执行,多个任务可以并发执行,同时处理多个请求,响应快,资源

springboot kafka多数据源,通过配置动态加载发送者和消费者 2024-10-08 11:24

前言 最近做项目,需要支持kafka多数据源,实际上我们也可以通过代码固定写死多套kafka集群逻辑,但是如果需要不修改代码扩展呢,因为kafka本身不处理额外逻辑,只是起到削峰,和数据的传递,那么就需要对架构做一定的设计了。 准备test kafka本身非常容易上手,如果我们需要单元测试,引入ja

SpringBoot 集成 Redis 2024-10-08 11:24

一:SpringBoot 集成 Redis ①Redis是一个 NoSQL(not only)数据库, 常作用缓存 Cache 使用。 ②Redis是一个中间件、是一个独立的服务器;常用的数据类型: string , hash ,set ,zset , list ③通过Redis客户端可以使用多种语

SpringBoot整合QQ邮箱 2024-10-08 11:24

SpringBoot可以通过导入依赖的方式集成多种技术,这当然少不了我们常用的邮箱,现在本章演示SpringBoot整合QQ邮箱发送邮件…. 下面按步骤进行: 1.获取QQ邮箱授权码 1.1 登录QQ邮箱 1.2 开启SMTP服务 找到下图中的SMTP服务区域,如果当前账号未开启的话自己手动开启。

目录

IT 外包服务商

  • 意见投递
  • zyf6619

软件开发应用

主菜单

  • 首页
  • 软件开发
  • 计算机基础
  • Hello Halo
  • 新手必读
  • 关于本知识库
Copyright © 2024 your company All Rights Reserved. Powered by Halo.