之前做过一个项目,是一个主营蔬果商户平台,后来要扩展一个优惠券服务。鉴于优惠券的实现逻辑并不复杂,所以最后决定采用伪分布式架构;即现有数据库不做改变,为优惠券模块单独创建一个用户优惠券服务,单独拥有一个数据库;目的是为以后项目服务拆分,整体向分布式改造打前站。
这里记录一下用户使用优惠券时需要注意的点,首先,用户使用优惠券这里做的只是一个校验,有点发送验证码然后校验的意思,这里是判断能不能使用,然后返回对应的折扣。几个基本判断包括:
- 用户是否有该优惠券
- 优惠券是否过期
- 如果优惠券有 token,那么 token 是否有效(判断当前 token 是否属于已使用的 token)
1.安全问题
但是,如果只是这样就忽略了一个很关键的问题,安全问题。
- 对于请求,要防止请求被拦截后
- templateId 被窃取,然后返回一个错误响应(监听),黑客自己根据 templateId 伪造优惠券(重放攻击)
- templateId 被篡改,然后重新发送请求(中间人攻击),最终返回了一个面值大的优惠券的折扣(所以商户给优惠券上传token也很重要啊)
- 对于返回,要防止响应被拦截后,返回的折扣值被篡改,最终返回了一个大的折扣
注:如果只是个简单的单体架构就不涉及这个问题,因为所有逻辑一次请求就都做完了
2.常规方案
所以,上面就涉及了两个基本的问题,保密和防篡改
- 对于保密,
-
响应方(发送):通过请求方的公钥对明文加密,然后传输密文(注:公钥是请求方请求时发送过来的)
-
请求方(接收):收到密文后,根据自己的秘钥进行解密(注:私钥签名是因为只能让"我"发布签名)
PS:对于防止公钥管理,可以了解一下 PKI 技术,这里就不说了。
-
- 对于防篡改,
- 响应方(发送):通过自己的私钥添加数字签名,然后将数据和自己的公钥发送给请求方
- 请求方(接收):通过发送方的公钥验证签名,因为签名是私钥加密的,所以如果中途被篡改,在解密时就会出错
3.简单实现
下面,我就不整什么 RSA、DSA、椭圆曲线了,就拿上面说的优惠券结算,通过移位密码简单模拟一下。
PS:解释一下,移位密码就是根据 ACSII 码表,所有字符都向后移动 n 位,n 就是秘钥,是最简单的一种加密算法。
- 哈希加密:MD5、SHA1
- 对称加密:AES、DES
- 非对称加密:RSA
关于其余加密算法可以参考这篇文章…
- 请求方:请求时除了携带 templateId,再带上商品Id(goodsId,相当于签名),将 templateId 和 goodsId 拼装成一个字符串,然后通过移位加密算法进行加密
- 响应方:用户优惠券服务受到请求后,解密出 templateId 和 goodsId;然后再根据 templateId 查询出相应优惠券,校验与请求参数的 goodsId 是否相同(校验签名)
- 响应方:返回时除了折扣值,同样再带上 goodsId;将 templateId 和 goodsId 拼装成一个字符串,然后通过移位加密算法进行加密(可取不同秘钥 n)
- 请求方:结算模块收到响应后,解密出 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 移位加密后
}
本文标题:【项目杂记】安全问题--核心数据加密、签名
本文链接:https://blog.quwenai.cn/post/10165.html
版权声明:本文不使用任何协议授权,您可以任何形式自由转载或使用。






还没有评论,来说两句吧...