接口请求安全措施

一、敏感参数加密

一般情况下,我们针对一些敏感的参数,例如密码、身份证号等,给它加密,防止报文明文传输,加密可以分为大体的两类,对称加密和非对称加密,下面,简单介绍下这两种方式。

1、对称加密

对称加密:加密使用的密钥和解密使用的密钥是同一个,例如sm4加密
接口请求安全措施_第1张图片
这样的加密方式简单,只需要加解密双方都有密钥即可,但是这样很不安全,一旦密钥泄漏,数据就会被解密。

2、非对称加密

非对称加密:顾名思义,非对称加密就是加密使用一个密钥(一般称为公钥),解密使用另一个密钥(一般称为私钥),常见的算法有RSA算法、sm2算法
接口请求安全措施_第2张图片
这种情况下,私钥一般由解密方独立保存,极大提高了数据的安全性。如果要对所有请求参数加密,推荐使用https请求,因为https请求原理上也是非对称加密实现的,这里不做过多赘述。

二、加签验签

我们对参数进行了加密,那么数据是否安全了呢?答案是否定的,因为我们只是保证了传入参数不被别人知道,但是我们的请求或响应是可以被篡改拦截的,那么,就需要引入新的方案,加签验签。

1、加签

加签:用Hash函数把原始报文生成报文摘要,然后用私钥对这个摘要进行加密,就得到这个报文对应的数字签名。一般情况下,客户端会将签名和原始报文一起发给服务端。
接口请求安全措施_第3张图片
客户端加签

//accessKey理解为一个盐值,signPriKey是加密私钥,map是请求参数
public static String sign(String accessKey, String signPriKey, Map map) {
		//sort方法主要用于参数排序及过滤,过滤掉key为sign的参数
		String paramStr = ParamSort.sort(map);
		//生成的摘要
		String abstractText = SM3Digest.sm3Encry(paramStr + accessKey);
		//非对称加密生成签名
		return SMHelper.sm2Sign(signPriKey, abstractText);
	}

获取摘要的hash方法

public static String sort(String jsonString){
		JSONObject jsonObject = JSON.parseObject(jsonString);
        String aa = jsonObject.toJSONString();
		List list = new ArrayList();
		for(Entry entry : jsonObject.entrySet()){
			String key = entry.getKey();
			//主要关注这里,排除了sign参数,因为sign签也是要作为参数传递给服务端的,但是客户端加签时还没有sign签
			if ("sign".equals(key)){
				continue;
			}
			String value = null;
			if (entry.getValue() instanceof JSONObject || entry.getValue() instanceof JSONArray){
				value = JSON.toJSONString(entry.getValue());
			} else {
				value = (String)entry.getValue();
			}
			String str = key+value;
			list.add(str);
		}
		Collections.sort(list, new Comparator() {
			@Override
			public int compare(String o1, String o2) {
				try {
					String s1 = new String(o1.toString().getBytes("UTF-8"), "ISO-8859-1");
					String s2 = new String(o2.toString().getBytes("UTF-8"), "ISO-8859-1");
					return s1.compareTo(s2);
				} catch (Exception e) {
					e.printStackTrace();
				}
				return 0;
			}
		});
		StringBuffer paramStr = new StringBuffer();
		for(String param : list){
			paramStr.append(param);
		}
		return paramStr.toString();
	}

获取到sign签后,记得传递给服务端的参数加上sign签

map.put("sign",sign);

2、验签

验签:接收方拿到原始报文和sign签名后,用同一个Hash函数从报文中生成服务端摘要。然后用对方提供的公钥对数字签名进行解密,得到客户端摘要,对比两个摘要是否相同,就可以得知报文有没有被篡改过。
接口请求安全措施_第4张图片
服务端验签

//ca是证书,存储了验签公钥等信息,sign是客户端的签名
private boolean sign(AuthSecCa ca, String sign, Map param) {
		//相同的排序hash方法
        String paramStr = Sort.sort(param);
        //生成服务端摘要
        String design = SM3Digest.SM3Encry(paramStr + ca.getAccessKey());
        // 验签
        boolean b = SMHelper.sm2Verify(ca.getSignPubKey(), design, sign);
        return b;
    }

三、请求时间戳验证

上面我们做了数据加密和请求加签验签,可以防止请求被抓包之后篡改请求,但是如果攻击者只是拦截数据包之后恶意请求怎么办呢?答案就是增加时间戳验证。大体思路就是请求参数加上一个请求时间戳dataStamp,服务端获取到这个时间戳后,获取一个当前的时间戳serverStamp,然后这两个时间戳的差值少于多长时间才算有效请求。

private boolean verifyDataStamp(String time) {
        boolean flag = false;
        long nowTime = System.currentTimeMillis();
        if (StringUtils.isNotEmpty(time)) {
            long t = Long.parseLong(time);
            //时间间隔超过1分钟
            int stampInt = stamp;
            if (Math.abs(nowTime - t) > stampInt * 1000) {
                flag = true;
            }
        }
        return flag;
    }

四、随机数验证

上面虽然做了时间戳验证,但是还是有漏洞的,只要攻击者在对应的时间范围(例如上面的一分钟)内恶意攻击还是能影响到我们的系统的,因此,我们需要给请求加上一个唯一的随机数nonce,每次请求过来把nonce拿到,判断是否已经又过了,来考虑是否放行请求,但是如果存储大量的nonce对我们的系统来说也是巨大的压力,因此配合时间戳一起使用,例如,时间戳是一分钟,我们可以设置nonce的有效期为两分钟(大于一分钟即可,避免极端情况),这样,我们把nonce存到缓存即可。

private boolean verifyNonce(String nonce) {
        if (StringUtils.isEmpty(nonce)) {
            return true;
        }
        if (缓存.isExist(nonce)) {
            return false;
        }
        缓存.setWithExpire(nonce, 120);
        return true;
    }

五、服务限流

这里介绍一个常见的限流算法,令牌桶限流。它的思路为:

  1. 对于每个要限流的对象(比如一个user,或者一个接入证书),分配一个bucket;
  2. bucket里的tokens以一个固定的速率在增加,bucket有个最大容量,到了最大容量就不再增加了;
  3. 每个请求会消耗一定的token数量,如果bucket内的token数量超过需要消耗的token数量,请求通过;否则拒绝请求;
    下面是通过引入guava的单机版限流RateLimiter做的一个限流demo

  com.google.guava
  guava
  31.1-jre

public class test {
	//每秒钟生成4个token
    private static final RateLimiter rateLimiter = RateLimiter.create(4);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
            	//每次请求消耗一个token
                if(rateLimiter.tryAcquire()){
                    System.out.println("请求成功");
                }else{
                    System.out.println("限流了");
                }
            }).start();
            //每个请求相隔1/5秒
            Thread.sleep(200);
        }
    }
}

可以设想到每五个请求会有一个被限流,实际运行结果也是这样,这里的打印顺序和多线程的打印有关,并不是限流的问题
接口请求安全措施_第5张图片

六、黑、白名单机制

我们可以在本身的后台管理系统中添加黑名单及白名单的相关配置,对于黑名单发起的请求,直接返回错误码;对于一些特别敏感的操作,例如涉及到转账等,只有在白名单中的请求才可以操作。

总结

前文提到了几种保证接口安全的措施,在实际项目应用过程中,可以将其串联起来使用,例如我们做一个全局的拦截器,拦截全部请求,然后在拦截器里将上述措施串联起来,用来保证接口请求的安全性。

你可能感兴趣的:(工作记录,java,安全,web安全)