互联网API对接如何保证系统安全

目录

前言

网络方面的安全策略

系统部署

专线 

数据传输方面安全策略

文件传输

实时接口

使用https协议

添加白名单

接口进行验签

接口数据进行加密

防止重放攻击

其它注意事项

幂等性

重复提交


前言

互联网金融系统经常需要提供一些对外的接口供第三方调用,这些接口暴露在互联网中。如何才能保证API系统对接的安全性?本文主要从网络及数据传输两个方面进行讲解。

网络方面的安全策略

系统部署

 在互联网区域部署一个前置机,提供接口的服务部署在内网。外网不能直接调用内网的服务,所有的请求必须经过前置机进行转发。前置机不做任何的逻辑处理,只做一些简单的请求转发。这样有效的做到了网络隔离。一般和银行对接都会有相应的前置机,专业术语叫做银企直连。大概的架构图如下:

专线 

网络专线就是网络服务提供商给用户提供专用的信道,让用户的数据传输变得可靠可信。这种方式一般银行,保险使用的比较多。

专线的优点就是安全性好,QoS可以得到保证。不过,专线租用价格也相对比较高,而且管理也许要专业人员。

专线如何提高安全性?这主要是通过信道技术,信道主要有两种:

  1.   物理专用信道。物理专用信道就是在服务商到用户之间铺设有一条专用的线路,线路只给用户独立使用,其他的数据不能进入此线路,而一般的线路就允许多用户共享信道。
  2.   虚拟专用信道;虚拟专用信道就是在一般的信道上为用户保留一定的带宽,使用户可以独享这部分带宽,就像在公用信道上又开了一个通道,只让相应用户使用,而且用户的数据是加密的,以此来保证可靠性与安全性。

数据传输方面安全策略

数据传输分为两种方式,一种是通过文件进行交互,一种是通过实时接口的方式。下面分别讨论一下两种方式的具体实现形式

文件传输

文件交互外网一般使用的是SFTP,SFTP(SSH File Transfer Protocol,也称 Secret File Transfer Protocol)是一种安全的文件传输协议,一种通过网络传输文件的安全方法。它确保使用私有和安全的数据流来安全地传输数据。给每个合作渠道分配一个sftp的用户名和密码,然后通过用户名分配文件目录的查看和读写的权限。安全方面主要通过白名单进行控制,或者通过rsa密钥认证的方式进行登录。

内网一般使用FTP,FTP 是File Transfer Protocol(文件传输协议)的英文简称,而中文简称为“文传协议”。用于Internet上的控制文件的双向传输。同时,它也是一个应用程序(Application)。内网由于是内部使用,可以只用设置用户名和密码。然后通过用户名分配文件目录的查看和读写的权限。

两者的主要区别:
链接方式:FTP使用TCP端口21上的控制连接建立连接。而SFTP是在客户端和服务器之间通过SSH协议(TCP端口22)建立的安全连接来传输文件。
安全性:SFTP使用加密传输认证信息和传输的数据,所以使用SFTP相对于FTP是非常安全。
效率:SFTP这种传输方式使用了加密解密技术,所以传输效率比普通的FTP要低得多。

实时接口

实时接口是服务端提供相应的接口供外部调用,并且实时返回相应的处理结果。实时接口通过使用如下方式保证系统的安全

使用https协议

HTTPS在HTTP的基础上添加了TSL/SSL安全协议,不过,HTTPS也不是绝对安全的,也存在被劫持的可能,但相对HTTP毋庸置疑是更加安全的。
缺点:服务器对HTTPS的配置相对有点复杂,还需要到CA申请证书,而且一般还是收费的。而且,HTTPS效率也比较低。一般,只有安全要求比较高的系统才会采用HTTPS,比如银行。而大部分对安全要求没那么高的App还是采用HTTP的方式

添加白名单

对接口的访问设置相应的IP白名单,仅允许IP白名单内的IP访问,其余的IP均不允许访问。

接口进行验签

接口验签主要是保证接口的安全性,防止被篡改。一般我们会给每个合作方(或者称为商户)分配一个秘钥,合作方调用接口时使用 MD5对密钥值key+相应的字符串( 字符串可以是接口中的报文参数的值)按照字典(比如 abc)顺序排序(具体排序方法直接调用 JAVA Arrays.sort 方法)后进行签名。服务端对真实报文进行同样签名后与接收到的签名报文进行比对。验证结果为 true 则验证成功,否则验证未通过。

举例如下:

假如请求报文如下:

{ 
    "sign": "f9c1275a497a39b7310c263eb8d47e", 
    "data": { 
        "idcard": "420625198907124562", 
        "appid": "test", 
        "transcode": "00123"
    }
}

该接口我们选取idcard 和transcode作为验签的字符串(验签的字符串最好使用值为英文的并且字段是必填的),分配的秘钥为qaz123,那么sign的值计算方式如下:

public static void main(String[] args) {
        String[] arr = { "qaz123", "420625198907124562", "00123" };
        Arrays.sort(arr);
        StringBuilder content = new StringBuilder();
        for (int i = 0; i < arr.length; i++) {
            content.append(arr[i]);
        }
        System.out.println(Md5Util.MD5Encrypt(content.toString()));
    }

Md5Util代码如下:

package com.springboot.demo.util;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;

public class Md5Util {

    public static String MD5Encrypt(String inStr) {

        MessageDigest md = null;
        String outStr = null;
        try {

            md = MessageDigest.getInstance("MD5"); //可以选中其他的算法如SHA   
            byte[] digest = md.digest(inStr.getBytes());
            //返回的是byet[],要转化为String存储比较方便  
            outStr = bytetoString(digest);
        } catch (NoSuchAlgorithmException nsae) {
            nsae.printStackTrace();
        }
        return outStr;
    }

    public static String bytetoString(byte[] digest) {

        String str = "";
        String tempStr = "";
        for (int i = 1; i < digest.length; i++) {
            tempStr = (Integer.toHexString(digest[i] & 0xff));
            if (tempStr.length() == 1) {
                str = str + "0" + tempStr;
            } else {
                str = str + tempStr;
            }
        }
        return str.toLowerCase();

    }
}

通过如上方法,我们计算出的签名为:f9c1275a497a39b7310c263eb8d47e

 我们把签名与接收到的签名"sign": "f9c1275a497a39b7310c263eb8d47e" 进行对比,发现两个相等,就验证通过。如果不相等就返回失败。sign我们可以放在报文中也可以放在header中。签名的算法有多种,但是原理都是一样。使用非对称加密的方式,双方按照同样的规则进行加密然后比对。

接口数据进行加密

双方约定相应的秘钥,对报文进行对称加密,并且秘钥不在报文中进行传输。加密是为了保证数据传输的安全性,如果不对传输的数据进行加密,报文被截获后就会照成信息泄露。如果进行了加密,即使报文被截获由于没有解密的秘钥也是无法查看的。请求方使用秘钥进行加密,合作方收到请求后使用秘钥进行解密。对称加密一般AES使用的比较多,AES加密解密demo如下:

package com.springboot.demo.util;

import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

public class AesUtil {
    /**
     * 加密
     */
    public static String encrypt(String text, String password) throws Exception {

        Cipher cipher = Cipher.getInstance("AES");
        byte[] byteContent = text.getBytes("utf-8");
        cipher.init(1, genKey(password));
        byte[] result = cipher.doFinal(byteContent);
        return parseByte2HexStr(result);

    }

    /**
     * 解密
     */
    public static String decrypt(String encryptText, String password) throws Exception {

        byte[] decryptFrom = parseHexStr2Byte(encryptText);
        Cipher cipher = Cipher.getInstance("AES");
        cipher.init(2, genKey(password));
        byte[] result = cipher.doFinal(decryptFrom);
        return new String(result);

    }

    private static SecretKeySpec genKey(String password) throws NoSuchAlgorithmException {
        byte[] enCodeFormat = { 0 };
        KeyGenerator kgen = KeyGenerator.getInstance("AES");
        SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
        secureRandom.setSeed(password.getBytes());
        kgen.init(128, secureRandom);
        SecretKey secretKey = kgen.generateKey();
        enCodeFormat = secretKey.getEncoded();
        return new SecretKeySpec(enCodeFormat, "AES");
    }

    private static String parseByte2HexStr(byte[] bytes) {
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < bytes.length; i++) {
            String hex = Integer.toHexString(bytes[i] & 0xFF);
            if (hex.length() == 1) {
                hex = '0' + hex;
            }
            sb.append(hex.toUpperCase());
        }
        return sb.toString();
    }

    private static byte[] parseHexStr2Byte(String hexStr) {
        if (hexStr.length() < 1) {
            return null;
        }
        byte[] result = new byte[hexStr.length() / 2];
        for (int i = 0; i < hexStr.length() / 2; i++) {
            int high = Integer.parseInt(hexStr.substring(i * 2, i * 2 + 1), 16);
            int low = Integer.parseInt(hexStr.substring(i * 2 + 1, i * 2 + 2), 16);

            result[i] = ((byte) (high * 16 + low));
        }
        return result;
    }

    public static void main(String[] args) throws Exception {
        String content = "{\n" + "\"id\": 1,\n" + "\"name\": \"张三\",\n" + "\"address\":\"上海市浦东新区\"\n" + "}";
        String decryptContent = "BABD56DADE25C8170A7BF6BFB77EA7256ED71E9013E803F012DCBA117C693F1EC1B9EEF8BEBCECA60C1A5AB89F5ADC65B6ED8483B141F56EF46FAC1B11D96692CB53B966BB008522E33737F44C63B4E7";
        System.out.println("AES加密" + AesUtil.encrypt(content, "123456"));
        System.out.println("AES解密" + AesUtil.decrypt(decryptContent, "123456"));
    }
}

防止重放攻击

如果请求报文泄露,别人直接使用泄露的报文进行请求,那么接口的验签就不起作用了。如何解决这种情况?从如下两个方面进行介绍:

时间戳方案,在请求的header中添加一个时间戳,程序根据业务实际情况设定只允许一定时间内的请求(比如30秒或者1分钟甚至更长)。在请求的header中添加一个时间戳,每次请求拿到时间戳与当前时间做比对,如果大于规定的值,则提示错误。如下:

      //只支持10分钟内的请求
        Long now = System.currentTimeMillis();
        Long requestTimestamp = Long.parseLong(timeStamp);
        if (Math.abs(now - requestTimestamp) > 600000L) {
            log.warn("验签出错:请求时间超过规定范围时间10分钟");
             ServerException为自定义异常
            throw new ServerException("1", "验签出错:请求时间超过规定范围时间10分钟!");
        }

随机数方案,在请求的header中添加一个唯一的流水号,每次请求传递不同的值,并且把该值放入到redis中设置相应的过期时间。请求前先判断redis是否存在,如果存在则提示错误。如下:

      //不允许重复请求(一次有效)
        String redisKey = uuid;//从hedader中取的随机数
        if (redisService.exists(redisKey)) {
            log.warn("验签出错:不允许重复请求");
            //ServerException为自定义异常
            throw new ServerException("1", "验签出错:不允许重复请求");
        } else {
            redisService.set(redisKey, sign, 900L);
        }

其它注意事项

幂等性

有些接口需要做幂等性校验,比如支付接口。比如用户发起了支付,服务端接收到了请求并且扣款。由于网络的原因,系统没有返回。客户端又重新发起请求,如果不做幂等校验可能就会重复扣款。

  幂等校验的实现方式:数据库设计的时候,表里面需要有一个状态的字段。每次请求完成后,修改相应的状态。接口的请求参数需要有一个唯一的业务号和一个请求的流水号。业务号是查询状态使用,流水号每次请求需要不一样,用于排查问题。以支付为例,每次接收到请求,使用订单号查询状态是否是已支付,如果是已支付就幂等返回。如果是未支付,就扣款。先查询在判断状态,这里需要考虑并发。比如两个请求同时进来,根据订单号查询订单是未支付的状态,然后两个请求又同时扣款。这里可以使用Redis的分布式锁解决并发的问题。

重复提交

有时候用户点击提交按钮,可能由于网络原因没有返回用户又点击了一次提交,这样就可能造成重复提交的现象。可以使用如下两种方式防止重复提交(数据库的方式这里不讨论,比如设置唯一索引,数据库表加锁等):

 方式一:把验签的sign放入到redis中,并设置过期时间。每次请求之前从redis中查询sign是否存在如果存在就是重复请求,返回相应的错误。如果不存在,说明不是重复请求,正常的处理业务逻辑。
 方式二:token机制,每次接口请求前先获取一个token,然后再下次请求的时候在请求的header体中加上这个token,后台进行验证,如果验证通过删除token,下次请求再次判断token。

Guess you like

Origin blog.csdn.net/xinghui_liu/article/details/121260820