如何保证对外接口的安全?

前言

1.什么是安全接口?
通常来说要将暴露在外网的 API 接口视为安全接口,需要实现防篡改和防重放的功能。

1.1 什么是篡改问题?
由于 HTTP 是一种无状态协议,服务端无法确定客户端发送的请求是否合法,也不了解请求中的参数是否正确。以一个充值接口为例:

http://127.0.0.1:8080/api/user/recharge?user_id=1001&amount=10

如果非法用户通过抓包获取接口参数并修改 user_id 或 amount 的值,就能为任意账户添加余额。

1.2 如何解决篡改问题?
虽然使用 HTTPS 协议能对传输的明文进行加密,但黑客仍可截获数据包进行重放攻击。两种通用解决方案是:

  1. 使用 HTTPS 加密接口数据传输,即使被黑客破解,也需要耗费大量时间和精力。
  2. 在接口后台对请求参数进行签名验证,以防止黑客篡改。

签名的实现过程如下图所示:
在这里插入图片描述
步骤1:客户端使用约定好的规则对传输的参数进行加密,得到签名值sign1,并且将签名值也放入请求的参数中,随请求发送至服务端。
步骤2:服务端接收到请求后,使用约定好的规则对请求的参数再次进行签名,得到签名值 sign2。
步骤3:服务端比对 sign1 和 sign2 的值,若不一致,则认定为被篡改,判定为非法请求。

1.3 什么是重放问题?
防重放也叫防复用。简单来说就是我获取到这个请求的信息之后什么也不改,,直接拿着接口的参数去重复请求这个充值的接口。此时我的请求是合法的, 因为所有参数都是跟合法请求一模一样的。重放攻击会造成两种后果:

  1. 针对插入数据库接口:重放攻击,会出现大量重复数据,甚至垃圾数据会把数据库撑爆。
  2. 针对查询的接口:黑客一般是重点攻击慢查询接口,例如一个慢查询接口1s,只要黑客发起重放攻击,就必然造成系统被拖垮,数据库查询被阻塞死。

1.4 如何解决重放问题?
防重放,业界通常基于 nonce + timestamp 方案实现。每次请求接口时生成 timestamp 和 nonce 两个额外参数,其中 timestamp 代表当前请求时间,nonce 代表仅一次有效的随机字符串。生成这两个字段后,与其他参数一起进行签名,并发送至服务端。服务端接收请求后,先比较 timestamp 是否超过规定时间(如60秒),再查看 Redis 中是否存在 nonce,最后校验签名是否一致,是否有篡改。

在这里插入图片描述

一、生成及校验Token

1.1 生成Token

public static final String equipmentSecret = "Equipment_Secret";

@PostMapping("/getToken/app")
@ApiOperation("获取鉴权token")
public Message.DataRespone<AppTokenVo> getToken(@RequestBody AppTokenRequest appTokenRequest) {
    //兼容正负3分钟
    Date endTime = DateTimeUtils.getDateAfterNow(3, "m");
    Date startTime = DateTimeUtils.getDateAfterNow(-3, "m");
    Date targetTime = new Date(appTokenRequest.getTime());
    if (startTime.after(targetTime) || targetTime.after(endTime)) {
        return Message.Time_Not_In_Use.create();
    }
    PProduct product = productService.getProductByProductKey(appTokenRequest.getProductKey());
    Map<String, String> claims = new HashMap<>();
    claims.put("productKey", appTokenRequest.getProductKey());
    claims.put("time", String.valueOf(appTokenRequest.getTime()));
    String targetSign = SignUtil.sign(claims, product.getProductSecret());
    if (!targetSign.equals(appTokenRequest.getSign())) {
        return Message.Sign_Error.create();
    }
    String token = Jwts.builder()
            .setClaims(claims)
            .setExpiration(DateTimeUtils.getDateAfterNow(2, "H"))
            //采用什么算法是可以自己选择的,不一定非要采用HS512
            .signWith(SignatureAlgorithm.HS512, equipmentSecret)
            .compact();
    AppTokenVo appTokenVo1 = new AppTokenVo();
    appTokenVo1.setToken(token);
    appTokenVo1.setExpiration(DateTimeUtils.getDateAfterNow(2, "H").getTime());
    return Message.Success.createWithData(appTokenVo1);
}

1.2 校验Token

@GetMapping("/checkToken")
@ApiOperation("校验token")
public Message.DataRespone<CheckTokenResultVo> getToken(@RequestParam(required = true, defaultValue = "") String token) {
    CheckTokenResultVo checkTokenResultVo = new CheckTokenResultVo();
    Claims claims = null;
    try {
        claims = Jwts.parser()
                .setSigningKey(equipmentSecret)
                .parseClaimsJws(token)
                .getBody();
    } catch (Exception e) {
        claims = null;
    }
    if (claims == null) {
        return Message.Token_CHECK_ERROR.create();
    }
    String productKey = String.valueOf(claims.get("productKey"));
    checkTokenResultVo.setProductKey(productKey);
    checkTokenResultVo.setExpiration(claims.getExpiration().getTime());
    return Message.Success.createWithData(checkTokenResultVo);
}

1.3 SignUtil 签名工具类

@Deprecated
public class SignUtil {
    /**
     * @param @param  sPara
     * @param @param  appecret
     * @param @return 参数描述
     * @return String    返回类型描述
     * @throws
     * @Title: buildRequestMysign
     * @Description: 签名方法
     */
    public static String sign(Map<String, String> sPara, String appecret) {
        String prestr = SignUtil.createLinkString(paraFilter(sPara)); // 把数组所有元素,按照“参数=参数值”的模式用“&”字符拼接成字符串
        String mysign = "";
        mysign = MD5.sign(prestr, appecret, "utf-8");
        return mysign;
    }

    /**
     * 除去数组中的空值和签名参数
     *
     * @param sArray 签名参数组
     * @return 去掉空值与签名参数后的新签名参数组
     */
    private static Map<String, String> paraFilter(Map<String, String> sArray) {

        Map<String, String> result = new HashMap<String, String>();

        if (sArray == null || sArray.size() <= 0) {
            return result;
        }

        for (String key : sArray.keySet()) {
            String value = sArray.get(key);
            if (value == null || value.equals("") || key.equalsIgnoreCase("sign") || key.equalsIgnoreCase("sign_type")) {
                continue;
            }
            result.put(key, value);
        }

        return result;
    }

    /**
     * 把数组所有元素排序,并按照“参数=参数值”的模式用“&”字符拼接成字符串
     *
     * @param params 需要排序并参与字符拼接的参数组
     * @return 拼接后字符串
     */
    private static String createLinkString(Map<String, String> params) {

        List<String> keys = new ArrayList<String>(params.keySet());
        Collections.sort(keys);

        String prestr = "";

        for (int i = 0; i < keys.size(); i++) {
            String key = keys.get(i);
            String value = params.get(key);

            if (i == keys.size() - 1) {// 拼接时,不包括最后一个&字符
                prestr = prestr + key + "=" + value;
            } else {
                prestr = prestr + key + "=" + value + "&";
            }
        }

        return prestr;
    }
}
  • 6
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值