小工具      在线工具  汉语词典  css  js  c++  java

接口安全处理

拓展,java 额外说明

收录于:40天前

一、为什么要保证接口安全

在我们的日常开发中,有一些接口是敏感且重要的,比如充值接口。如果您调用充值接口时被人发现,您可以修改充值金额。原来充值10元可以改为充值10w。 ,出现重大生产问题,或者抓包,别人充值10元就可以无限制调用接口,调用几万次也会出现重大问题。那么我们应该如何保证接口的安全呢?

二、接口安全的几种方式

  • 数据参数合法性校验

接口数据的安全性保证,还需要我们的系统,有个数据合法性校验,简单来说就是参数验证,比如身份证长度,手机号长度,是否是数字等等

  • token授权认证方式

一般我们的系统都会使用token认证登录来验证用户的登录状态和用户权限。访问接口前,请验证token的合法性。

在这里插入图片描述

  • 数据加密以防止消息的明文传输

说到数据加密,我们不难想到使用HTTPS进行传输,HTTPS使用了RSA和AES加密的方式保证了数据传输中的安全问题,具体的HTTPS的加密原理,请看HTTPS原理

数据在传输过程中被加密了,理论上,即使被抓包,数据也不会被篡改。但是https 并不绝对安全的哦。还有一个点:https加密的部分只是在外网,然后有很多服务是内网相互跳转的,签名验证也可以在保证不被中间商篡改。,所以一般转账类安全性要求高的接口开发,都需要签名验证

  • 签名验证

https虽然保证了外网数据不被篡改,但无法保证内网数据被篡改的风险,所以需要签名验证。

  1. 客户端按照特定的顺序对参数进行md5加密,形成签名,然后与参数一起传递给服务器。
  2. 服务器接收到签名和参数,同时也将参数按照一定的顺序用md5加密,并比较传递过来的签名来判断是否被篡改。

这样做的好处是,在数据传输过程中,可以保证数据不会被篡改。如果被篡改,就会导致标志不一致,验证无法通过。

但这仅仅解决了篡改问题。如果我拿到请求后不修改参数,多次调用原始数据,仍然会出现问题。这时候我就需要添加一个防重放功能。

  • 时间戳+随机数方案防止重放攻击
  1. Timestamp是一种时间戳超时机制。当请求超过这个时间,则视为无效,需要重新发送请求。默认值为 60 秒。但60s内多次调用不是也会出现问题吗?
  2. 一般来说,从抓包到回放的时间肯定在60秒以上。为了避免此类问题,我们可以在客户端发送请求时随机生成一个nonce。
  3. nonce token是一个随机数,每次请求后都会将其存储在redis中。过期时间为60s,这样每个请求只能请求一次,避免了多次调用的问题。
  • 白名单黑名单

三、防重放和防篡改拦截器

这里我们使用时间戳+随机数+符号对接口进行安全处理

在这里插入图片描述

1. 构建请求头
@Data
@Builder
public class RequestHeader {
    

    /** * 签名 */
    private String sign;
    /** * 时间戳 */
    private Long timestamp;
    /** * 临时的数据 */
    private String nonce;

}
2. 保存请求流对象
public class SignRequestWrapper extends HttpServletRequestWrapper {
    
    //用于将流保存下来
    private byte[] requestBody = null;

    public SignRequestWrapper(HttpServletRequest request) throws IOException {
    
        super(request);
        requestBody = StreamUtils.copyToByteArray(request.getInputStream());
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
    
        final ByteArrayInputStream bais = new ByteArrayInputStream(requestBody);

        return new ServletInputStream() {
    
            @Override
            public boolean isFinished() {
    
                return false;
            }

            @Override
            public boolean isReady() {
    
                return false;
            }

            @Override
            public void setReadListener(ReadListener readListener) {
    

            }

            @Override
            public int read() throws IOException {
    
                return bais.read();
            }
        };

    }

    @Override
    public BufferedReader getReader() throws IOException {
    
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }
}
3. 创建请求数据处理工具
@Slf4j
public class HttpDataUtil {
    
    /** * post请求处理:获取 Body 参数,转换为SortedMap * * @param request */
    public static SortedMap<String, String> getBodyParams(final HttpServletRequest request) throws IOException {
    
        byte[] requestBody = StreamUtils.copyToByteArray(request.getInputStream());
        String body = new String(requestBody);
        return JsonUtils.parseObject(body, SortedMap.class);
    }


    /** * get请求处理:将URL请求参数转换成SortedMap */
    public static SortedMap<String, String> getUrlParams(HttpServletRequest request) {
    
        String param = "";
        SortedMap<String, String> result = new TreeMap<>();

        if (StringUtils.isEmpty(request.getQueryString())) {
    
            return result;
        }

        try {
    
            param = URLDecoder.decode(request.getQueryString(), "utf-8");
        } catch (UnsupportedEncodingException e) {
    
            e.printStackTrace();
        }

        String[] params = param.split("&");
        for (String s : params) {
    
            String[] array = s.split("=");
            result.put(array[0], array[1]);
        }
        return result;
    }
}
4. 签名验证工具
@Slf4j
public class SignUtil {
    

    /** * 验证签名 * 验证算法:把timestamp + JsonUtil.object2Json(SortedMap)合成字符串,然后MD5 */
    @SneakyThrows
    public static boolean verifySign(SortedMap<String, String> map, RequestHeader requestHeader) {
    
        String params = requestHeader.getNonce() + requestHeader.getTimestamp() + JsonUtils.toJsonString(map);
        return verifySign(params, requestHeader);
    }

    /** * 验证签名 */
    public static boolean verifySign(String params, RequestHeader requestHeader) {
    
        log.debug("客户端签名: {}", requestHeader.getSign());
        if (StringUtils.isEmpty(params)) {
    
            return false;
        }
        log.info("客户端上传内容: {}", params);
        String paramsSign = DigestUtils.md5DigestAsHex(params.getBytes()).toUpperCase();
        log.info("客户端上传内容加密后的签名结果: {}", paramsSign);
        return requestHeader.getSign().equals(paramsSign);
    }

}
5. 创建拦截器SignFilter
@Slf4j
public class SignFilter implements Filter {
    

    private static final Long signMaxTime = 60L;

    private static final String NONCE_KEY = "x-nonce-";

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    
        HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;
        log.info("过滤URL:{}", httpRequest.getRequestURI());

        //request数据流只能读取一次,这里保存request流
        HttpServletRequestWrapper requestWrapper = new SignRequestWrapper(httpRequest);

        //构建请求头
        String nonceHeader = httpRequest.getHeader("X-Nonce");
        String timeHeader = httpRequest.getHeader("X-Time");
        String signHeader = httpRequest.getHeader("X-Sign");

        //验证请求头是否存在
        if (StringUtils.isEmpty(nonceHeader) || ObjectUtils.isEmpty(timeHeader) || StringUtils.isEmpty(signHeader)) {
    
            throw new RuntimeException("请求头不存在");
        }

        RequestHeader requestHeader = RequestHeader.builder()
                .nonce(httpRequest.getHeader("X-Nonce"))
                .timestamp(Long.parseLong(httpRequest.getHeader("X-Time")))
                .sign(httpRequest.getHeader("X-Sign")).build();
        /* * 1.验证签名是否过期,防止重放 * 判断timestamp时间戳与当前时间是否操过60s(过期时间根据业务情况设置),如果超过了就提示签名过期。 */
        long now = System.currentTimeMillis() / 1000;
        if (now - requestHeader.getTimestamp() > signMaxTime) {
    
            throw new RuntimeException("签名过期");
        }

        //2. 判断nonce,是否重复发送
        boolean nonceExists = RedisUtils.hasKey(NONCE_KEY + requestHeader.getNonce());
        if (nonceExists) {
    
            //请求重复
            throw new RuntimeException("请求重复");
        } else {
    
            RedisUtils.set(NONCE_KEY + requestHeader.getNonce(), requestHeader.getNonce(), signMaxTime);
        }

        // 3. 验证签名,防止篡改
        boolean accept;
        SortedMap<String, String> paramMap;
        switch (httpRequest.getMethod()) {
    
            case "GET":
                paramMap = HttpDataUtil.getUrlParams(requestWrapper);
                accept = SignUtil.verifySign(paramMap, requestHeader);
                break;
            case "POST":
                paramMap = HttpDataUtil.getBodyParams(requestWrapper);
                accept = SignUtil.verifySign(paramMap, requestHeader);
                break;
            default:
                accept = true;
                break;
        }
        if (accept) {
    
            filterChain.doFilter(requestWrapper, servletResponse);
        } else {
    
            throw new RuntimeException("签名有误,请重新请求");
        }

    }

}
6. 配置拦截器
@Configuration
public class SignFilterConfiguration {
    

    @Bean
    public FilterRegistrationBean contextFilterRegistrationBean() {
    
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setFilter(new SignFilter());
        registration.addUrlPatterns("/sign/*");
        registration.setName("SignFilter");
        // 设置过滤器被调用的顺序
        registration.setOrder(1);
        return registration;
    }

}
7. 测试
@RequestMapping("")
@RestController
public class SignDemoController {
    

    @PostMapping("/sign/demo1")
    public R demo1(@RequestBody DemoDto demoDto) {
    
        System.out.println("===执行了demo1");
        return R.ok();
    }

    @GetMapping("/demo2")
    public R demo2() {
    
        System.out.println("执行了demo2====");
        return R.ok();
    }

}

@Data
class DemoDto {
    
    private Integer age;

    private String username;

    private Long id;
}
{
    
  "age": 11,
  "username": "zhangsan",
  "id": 1
}

在这里插入图片描述
在这里插入图片描述

. . .

相关推荐

额外说明

每天 4 道算法题 - 第 031 天

目录 1、统计好三元组 2、找出数组游戏的赢家 3、排布二进制网格的最少交换次数 4、最大得分

额外说明

嵌入式Linux内存压力测试

原文参考:添加链接描述 1 前言   内存是电子计算机的最重要组成要素之一。 与内存对应的就是外存,如硬盘、外部存储器等。内存是将外存与CPU连接起来的桥梁,计算机中所有数据都需经过内存进行交互,而且所有应用程序都运行在内存中。可见,内存的重要性。如果内

额外说明

二叉搜索树遍历搜索(Java实现)

1.二叉搜索树的概念 一棵二叉搜索树是以一棵二叉树来组织的,这样的一棵树可以以链表的数据结构来表示,其中的每个结点就是一个对象。 2.二叉搜索树结点对象的属性 (1)key(关键字,用于代表整个对象) (2)基本数据和信息 (3)left、right和p

额外说明

[Eigen中文文档] 编写以特征类型为参数的函数(一)

文档总目录 本文目录 一些开始的示例 EigenBase示例 DenseBase示例 ArrayBase示例 MatrixBase示例 多个模板化参数示例 如何编写通用但非模板化的函数? [Eigen中文文档] 编写以特征类型为参数的函数(二) 英文原文

额外说明

[Eigen中文文档] Matrix类

专栏总目录 本文目录 Matrix前三个模板参数 向量 动态的特殊值 构造函数 访问元素 逗号初始化 重置大小 赋值和重置大小 固定大小与动态大小 可选模板参数 其他常用Matrix类型 本文英文原文链接 在Eigen中,所有矩阵和向量都是Matrix模

额外说明

Python数据结构与算法(1.4)——Python基础之控制结构

Python数据结构与算法(1.4)——Python基础之控制结构 0. 学习目标 1. 代码块与缩进 2. 条件语句 2.1 if 语句 2.2 if 语句的嵌套 2.3 断言 3. 循环 3.1 while 循环 3.2 for 循环 3.3 中断循

额外说明

【Python 随练】输出 “C”

题目: 使用星号(*)输出字母 C 的图案。 简介: 在本篇博客中,我们将使用Python代码生成字母 C 的图案。我们将提供问题的解析,并给出一个完整的代码示例来输出这个图案。 问题分析: 我们需要使用星号(*)来绘制字母 C 的图案。 解决方案: 为

额外说明

Python——cv2图片识别

方法一 import numpy from PIL import Image a = Image.open(r"C:\Users\17567\Desktop\10.png") b = Image.open(r"C:\Users\17567\Deskto

额外说明

spring boot启动环境的配置与更改(dev,local,pro)包含单元测试环境

文件类型介绍 特性 该文件是一种key-value的格式,配置文件的特点是,它的Key-Value一般都是String-String类型的,因此我们完全可以用Map<String, String>来表示它。 用Properties读取配置文件非常简单。J

ads via 小工具