系列学习互联网安全架构第 8 篇 —— API 接口增加签名(验签)

 

通过学习之前的博客:

API 接口安全设计:https://blog.csdn.net/BiandanLoveyou/article/details/117487626

信息加密技术(对称加密、非对称加密):https://blog.csdn.net/BiandanLoveyou/article/details/117524716

现在我们来讲解 API 接口增加签名的内容。不像网上讲解的那种那么难理解,不用那么复杂。

如何保证 API 接口的安全性,一般来说,解决办法有以下几种:

1、API 请求使用加签名方式,防止篡改数据。

2、使用Https 协议加密传输。

3、搭建OAuth2.0认证授权平台。

4、使用令牌方式 accessToken。

5、搭建网关实现黑名单和白名单。

我们今天讲解第 1 种。API 接口增加验签功能,可以防止篡改数据。我们验签不通过,就认为数据被篡改,拒绝处理本次请求的数据。

 

下载本次案例的脚手架:https://pan.baidu.com/s/1bxw5TMJo6vVLFt1kru0JAg  提取码:wetr

 

本案例中,我们创建一个项目 study-sign,有3个模块:send-server(发送端服务),receive-server(接收端服务),common-core(公共包)。

公共依赖 pom.xml 

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.study</groupId>
    <artifactId>study-sign</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>send-server</module>
        <module>receive-server</module>
        <module>common-core</module>
    </modules>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.2.RELEASE</version>
    </parent>

    <dependencies>
        <!--Spring boot 集成包-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <!-- SpringBoot整合Web组件 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- 阿里巴巴 fastjson -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.75</version>
        </dependency>
        <!-- feignClient 支持 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <!-- apache commons -->
        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
            <version>1.15</version>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Hoxton.SR2</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

</project>

 

签名的工具类:

package com.study.util;

import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.util.StringUtils;

import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;

/**
 * 签名工具类
 */
public class SignUtil {

    //要加入签名的字段
    private static final String[] strArr = new String[]{"timestamp", "appId", "key"};

    /**
     * 生成签名
     *
     * @param appId
     * @param key
     * @return
     * @throws Exception
     */
    public static String createSign(String appId, String key) {
        if (StringUtils.isEmpty(appId) || StringUtils.isEmpty(key)) {
            return null;
        }

        Long time = System.currentTimeMillis();//当前时间戳
        //毫秒数除以5000得到加密的需要参数,精确到5秒内
        String timestamp = String.valueOf(time / 5000);
        System.out.println("timestamp="+timestamp);

        StringBuffer sb = new StringBuffer();
        Arrays.sort(strArr);//把需要加密的字符串按照字母排序
        for (String str : strArr) {
            if ("appId".equals(str)) {
                sb.append(str).append("=").append(appId).append("&");
            }

            if ("key".equals(str)) {
                sb.append(str).append("=").append(key).append("&");
            }

            if ("timestamp".equals(str)) {
                sb.append(str).append("=").append(timestamp).append("&");
            }
        }
        //去掉最后一个 & 符号
        String data = sb.toString().substring(0, sb.length() - 1);
        //使用 MD5 加密,并全部转成大写,得到签名
        String sign = DigestUtils.md5Hex(data).toUpperCase();
        return sign;
    }

    /**
     * 校验签名是否正确
     * @param appId
     * @param key
     * @param sign
     * @return
     */
    public static Boolean checkSign(String appId, String key, String sign) {
        if (StringUtils.isEmpty(appId) || StringUtils.isEmpty(key) || StringUtils.isEmpty(sign)) {
            return false;
        }

        Long time = System.currentTimeMillis();//当前时间戳
        //避免临界值的出现:比如发送方生成签名的时候是5秒,通过网络传输后6秒到达,因此允许误差5-10秒内都有效
        Long timestamp = time / 5000;
        Long timestamp_sub = timestamp - 1;

        System.out.println("timestamp="+timestamp+",timestamp_sub="+timestamp_sub);

        String timestamp_1 = String.valueOf(timestamp);
        String timestamp_2 = String.valueOf(timestamp_sub);

        StringBuffer sb_1 = new StringBuffer();
        StringBuffer sb_2 = new StringBuffer();
        Arrays.sort(strArr);//把需要加密的字符串按照字母排序
        for (String str : strArr) {
            if ("appId".equals(str)) {
                sb_1.append(str).append("=").append(appId).append("&");
                sb_2.append(str).append("=").append(appId).append("&");
            }

            if ("key".equals(str)) {
                sb_1.append(str).append("=").append(key).append("&");
                sb_2.append(str).append("=").append(key).append("&");
            }

            if ("timestamp".equals(str)) {
                sb_1.append(str).append("=").append(timestamp_1).append("&");
                sb_2.append(str).append("=").append(timestamp_2).append("&");
            }
        }
        //去掉最后一个 & 符号
        String data_1 = sb_1.toString().substring(0, sb_1.length() - 1);
        String data_2 = sb_2.toString().substring(0, sb_2.length() - 1);
        //使用 MD5 加密,并全部转成大写,得到签名
        String sign_1 = DigestUtils.md5Hex(data_1).toUpperCase();
        String sign_2 = DigestUtils.md5Hex(data_2).toUpperCase();
        if(sign.equals(sign_1) || sign.equals(sign_2)){
            return true;
        }
        return false;
    }


    public static void main(String[] args) throws Exception {

        String sign = SignUtil.createSign("good123", "123456");
        System.out.println("生成签名:sign=" + sign);

        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        for (int i = 0; i < 10; i++) {
            Long time = System.currentTimeMillis();//当前时间戳
            //毫秒数除以5000得到加密的需要参数,精确到5秒内
            String timestamp = String.valueOf(time / 5000);
            System.out.println("time=" + time + ",除以 5000 timestamp=" + timestamp+",时间:" + sdf.format(new Date()));
            Boolean flag = SignUtil.checkSign("good123", "123456", sign);
            System.out.println("校验签名:flag=" + flag);
            System.out.println("****************************");
            Thread.sleep(1000);
        }

    }

}

说明:①我们加入签名有3个字段{"timestamp", "appId", "key"},当然,可以根据实际情况增加更多,不过需要做非空的判断,就是某个值为空的话不加入签名校验。

②时间戳我们除以 5000(除以1000得到的是秒数),得到误差为±5秒的时间戳。

③Arrays.sort(strArr);//把需要加密的字符串按照字母排序

④校验签名的时候,我们校验了误差为 5~10秒内的签名,避免由于网络传输导致验签不成功,要验签成功还要求双方的机器时钟误差不能太大,因此需要联调。

⑤程序保证请求在5~10秒内都可以验签成功,如图:

 

发送端服务 send-server:

整体代码结构:

pom.xml 配置,引用 common-core 包,因此需要把 core 包 install 到本地仓库,否则找不到依赖!

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>study-sign</artifactId>
        <groupId>com.study</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>send-server</artifactId>

    <properties>
        <com.study.common-core>1.0-SNAPSHOT</com.study.common-core>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.study</groupId>
            <artifactId>common-core</artifactId>
            <version>${com.study.common-core}</version>
        </dependency>
    </dependencies>

</project>

SendTest 类的包层次,需要与启动类的相同,否则报错如下:

main] DEBUG org.springframework.test.context.support.ActiveProfilesUtils - Could not find an 'annotation declaring class' for annotation type [org.springframework.test.context.ActiveProfiles] and class [SendTest]
java.lang.IllegalStateException: Unable to find a @SpringBootConfiguration, you need to use @ContextConfiguration or @SpringBootTest(classes=...) with your test

	at org.springframework.util.Assert.state(Assert.java:73)

SendTest 代码:

package com.study;

import com.alibaba.fastjson.JSONObject;
import com.study.api.SendApi;
import com.study.util.SignUtil;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

/**
 * @author biandan
 * @description 测试类
 * @signature 让天下没有难写的代码
 * @create 2021-06-04 下午 9:56
 */
@RunWith(SpringRunner.class)
@SpringBootTest
public class SendTest {

    @Value("${sign.appId}")
    private String appId;

    @Value("${sign.key}")
    private String key;

    @Autowired
    private SendApi sendApi;

    @Test
    public void testSend(){
        String sign = SignUtil.createSign(appId, key);
        System.out.println(sign);

        String productName = "霸王防脱洗发液";
        Float price = 55.55F;

        JSONObject jsonObject = new JSONObject();
        jsonObject.put("productName",productName);
        jsonObject.put("price",price);

        String result = sendApi.sendToReceive(sign,jsonObject.toJSONString());
        System.out.println("result="+result);

    }
}

SendApi 代码:

package com.study.api;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

/**
 * @author biandan
 * @description 发送给接收服务器
 * @signature 让天下没有难写的代码
 * @create 2021-06-04 下午 11:56
 */
@Component
@FeignClient(url = "http://127.0.0.1:9090",name = "receive-server")
public interface SendApi {

    @PostMapping("/fromSendServer")
    String sendToReceive(@RequestParam("sign") String sign,@RequestParam("data") String data);

}

说明:需要增加注解 FeignClient 远程调用 receive-server,我们这里不启动 Eureka 注册中心,因此需要配置完整的 URL。

启动类 SendApplication:

package com.study;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

/**
 * @author biandan
 * @description
 * @signature 让天下没有难写的代码
 * @create 2021-06-04 下午 6:02
 */
@SpringBootApplication
@EnableFeignClients
public class SendApplication {

    public static void main(String[] args) {
        SpringApplication.run(SendApplication.class,args);
    }
}

注意:需要增加注解 @EnableFeignClients,否则报错,无法使用 @FeignClient。

application.yml 配置

server:
  port: 8080

# 将SpringBoot项目作为单实例部署调试时,不需要注册到注册中心
eureka:
  client:
    fetch-registry: false
    register-with-eureka: false

spring:
  application:
    name: receive-server

sign:
  appId: good123
  key: 123456

 

receive-server 模块:

其它的配置跟 send-server 的差不多,可以看脚手架。这里只列出 Controller 层

package com.study.controller;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.study.util.SignUtil;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author biandan
 * @description
 * @signature 让天下没有难写的代码
 * @create 2021-06-05 上午 12:15
 */
@RestController
public class ReceiveController {

    @Value("${sign.appId}")
    private String appId;

    @Value("${sign.key}")
    private String key;

    @PostMapping(value = "/fromSendServer")
    public String receive(@RequestParam("sign") String sign, @RequestParam("data") String data) {
        Boolean flag = SignUtil.checkSign(appId, key, sign);
        String result;
        if (flag) {
            JSONObject jsonObject = JSON.parseObject(data);
            String productName = jsonObject.getString("productName");
            Float price = Float.parseFloat(jsonObject.getString("price"));
            System.out.println("productName=" + productName + ",price=" + price);
            result = "验签通过,数据已被处理。";
        } else {
            result = "数据被篡改,拒绝处理!";
        }
        System.out.println(result);
        return result;
    }

}

 

OK,先启动接收服务 receive-server,再启动发送服务 send-server 的测试方法。

 

OK,验签通过!

我们在 receive-server 的 Controller 打断点或者线程休眠,模拟超时的情况。

注意 Thread 需要抛出异常。我们让线程休眠 11 秒肯定超时了,然后重启接收服务,继续测试。

 

超过规定的 5~10秒,验签不通过,判断数据被篡改,拒绝处理。

 

OK,验签说到这。

 

 

 

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值