【项目杂记】安全问题--核心数据加密、签名

之前做过一个项目,是一个主营蔬果商户平台,后来要扩展一个优惠券服务。鉴于优惠券的实现逻辑并不复杂,所以最后决定采用伪分布式架构;即现有数据库不做改变,为优惠券模块单独创建一个用户优惠券服务,单独拥有一个数据库;目的是为以后项目服务拆分,整体向分布式改造打前站。

这里记录一下用户使用优惠券时需要注意的点,首先,用户使用优惠券这里做的只是一个校验,有点发送验证码然后校验的意思,这里是判断能不能使用,然后返回对应的折扣。几个基本判断包括:

  • 用户是否有该优惠券
  • 优惠券是否过期
  • 如果优惠券有 token,那么 token 是否有效(判断当前 token 是否属于已使用的 token)

1.安全问题

但是,如果只是这样就忽略了一个很关键的问题,安全问题。

  • 对于请求,要防止请求被拦截后
    • templateId 被窃取,然后返回一个错误响应,黑客自己根据 templateId 伪造优惠券
    • templateId 被篡改,然后重新发送请求,最终返回了一个面值大的优惠券的折扣(所以商户给优惠券上传token也很重要啊)
  • 对于返回,要防止响应被拦截后,返回的折扣值被篡改,最终返回了一个大的折扣

注:如果只是个简单的单体架构就不涉及这个问题,因为所有逻辑一次请求就都做完了

2.常规方案

所以,上面就涉及了两个基本的问题,保密和防篡改

  • 对于保密,
    • 响应方(发送):通过请求方的公钥对明文加密,然后传输密文(注:公钥是请求方请求时发送过来的)

    • 请求方(接收):收到密文后,根据自己的秘钥进行解密

      PS:对于防止公钥被篡改,可以了解一下 PKI 技术,这里就不说了。

  • 对于防篡改,
    • 响应方(发送):通过自己的私钥添加数字签名,然后将数据和自己的公钥发送给请求方
    • 请求方(接收):通过发送方的公钥验证签名,因为签名是私钥加密的,所以如果中途被篡改,在解密时就会出错

3.简单实现

下面,我就不整什么 RSA、DSA、椭圆曲线了,就拿上面说的优惠券结算,通过移位密码简单模拟一下。

PS:解释一下,移位密码就是根据 ACSII 码表,所有字符都向后移动 n 位,n 就是秘钥,是最简单的一种加密算法。

  1. 请求方:请求时除了携带 templateId,再带上商品Id(goodsId,相当于签名),将 templateId 和 goodsId 拼装成一个字符串,然后通过移位加密算法进行加密
  2. 响应方:用户优惠券服务受到请求后,解密出 templateId 和 goodsId;然后再根据 templateId 查询出相应优惠券,校验与请求参数的 goodsId 是否相同(校验签名)
  3. 响应方:返回时除了折扣值,同样再带上 goodsId;将 templateId 和 goodsId 拼装成一个字符串,然后通过移位加密算法进行加密(可取不同秘钥 n)
  4. 请求方:结算模块收到响应后,解密出 templateId 和 goodsId;然后再判断当前商品 goodsId 是否和参数的 goodsId 相同(校验签名)

4.具体代码

使用优惠券的接口,入参是 PassRequest,返回是 PassResponse

/**
 * 用户使用优惠券
 * @param request {@link PassRequest}
 * @return {@link PassReponse}
 */
@PostMapping("/usepasstemplate/{templateId}")
Response usePassTemplate(@RequestBody PassRequest request) throws Exception {
     
    return passOperatorService.usePassTemplate(request);
}

PassResponse 请求校验

@Data
@AllArgsConstructor
@NoArgsConstructor
public class PassRequest {
     

    /** 要使用优惠券的商品 ID & 优惠券 ID*/
    private String passRequest;

    @Value("${llxs.yiwei.decrpt}") // 解密秘钥在配置文件
    private int decrptKey;

    /**
     * 解密
     */
    public String decrpt() {
     
        char[] chars = this.passRequest.toCharArray();
        for (int i = 0; i < chars.length; i++) {
     
            chars[i] -= decrptKey;
        }
        return String.valueOf(chars);
    }

    /**
     * 获取 templateId:
     *      1.解密
     *      2.校验携带的goodsId和对应template的goodsId
     */
    public long getTemplateId(PassTemplateDao passTemplateDao) {
     

        String decrpt = decrpt();
        String[] split = decrpt.split("&");
        long goodsId = Long.valueOf(split[0]);
        long templateId = Long.valueOf(split[1]);

        // 校验携带的goodsId和对应template的goodsId(校验签名)
        PassTemplate template = passTemplateDao.findById(templateId);
        if(template.getGoodsId() == goodsId) {
     
            throw new RuntimeException("args error");
        }

        return templateId;
    }
}

PassResponse 响应校验

@Data
@AllArgsConstructor
@NoArgsConstructor
public class PassReponse {
     

    /** 要使用优惠券的商品 ID & 折扣*/
    private String passResponse;

    @Value("${llxs.yiwei.encrpt}") // 加密秘钥在配置文件
    private int encrptKey;

    /**
     * 对返回结果加密
     */
    public String encrpt(long goodsId, int discount) {
     
		// 带上 goodsId 然后加密相当于给数据添加签名
        passResponse = String.valueOf(goodsId) + "&" + String.valueOf(discount);
        char[] chars = passResponse.toCharArray();
        for (int i = 0; i < chars.length; i++) {
     
            chars[i] += encrptKey;
        }

        return passResponse;
    }
}

举个例子:

POST: 127.0.0.1:8080/passbook/usepasstemplate/
    {
        "passRequest":";::;0ACB:B:B:" // 1001&79808080 移位加密后
    }

你可能感兴趣的:(项目杂记,加密解密)