背景
最近接到一个需求,在海外客户还款之前,都是通过一个还款链接去还款,但是还款链接内没有任何的客户信息,所以需要还款之前,进入一个前置信息确认页面,就需要后端先提供一个查询接口给前端,但是需要参数明文传递给前端,其中包含订单号,金额及还款类型。但是给到前端之后,在url中可以直接修改请求参数,所以就会有安全风险,为了避免这种,使用以下两种方案:
1、请求参数映射
比如我的还款参数如下:
{"orderId": "123","repayAmount": 1000,"repaymentType": 1
}
生成的明文链接为:
http://pay.com/repayment?repaymentAmount=100&repaymentType=1&orderId=123
我将在发起还款请求之前,分配一个唯一ID来标识本次请求,如ID = 67ad2778-d5e4-4466-bf9f-45b0170a8b79,所以生成的链接就是
http://pay.com/repayment?67ad2778-d5e4-4466-bf9f-45b0170a8b79
当请求到达后端之后,去查询当次请求的参数,然后去生成真实的还款链接给客户还款。
示例代码:
@RequestMapping("/repayment/{id}")public RepaymentResp repayment(@PathVariable Long id) {RepaymentResp repaymentResp = new RepaymentResp();RepaymentReq req = repaymentService.checkOrder(id);if (Objects.isNull(req)) {throw new BusinessException(ErrorCodeEnum.DATA_NOT_EXISTS);}return repaymentService.repayment(req);}
2、接口参数验签
在原本的请求链接不变的情况下,加一个sign参数,针对当前参数进行签名,在请求后端的时候,需要先验证签名,如果验签不通过,则直接返回error,否则认定参数没有被修改,继续生成还款链接。
但是常见的不单单是只有一个sign,一般还有一个商户号,商户密钥,时间戳,随机字符串,业务参数做的一个加密后的签名。对此我们做一下改造,因为密钥不适合直接给到前端,都是服务端交互才会这么使用,去除商户号,密钥之后的链接示例如下:
http://pay.com/repayment-order?repaymentAmount=2013&repaymentType=1&caseNo=123&ts=1711788063587&nonceStr=LrmxjdKD50mmhFwCG7UOa6q&sign=24c96ee54e91ee79f475c9a672758ac84ea34d84365a44b2ee23e74b
编写一个加密工具类:
@Slf4j
public class RiskSignUtils {public static final String SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";public static final Random RANDOM = new SecureRandom();public static final long REQ_HEADER_TIMEOUT_MIN = 20;public static final Cache<String, String> NONCE_POOL = Caffeine.newBuilder().expireAfterWrite(REQ_HEADER_TIMEOUT_MIN, TimeUnit.MINUTES).maximumSize(20000).build();public static final String HEADER_API_NONCE = "x-api-nonce";public static final String HEADER_API_TIMESTAMP = "x-api-ts";public static final String HEADER_API_SIGN = "x-api-sign";public static final String APP_ID = "appId";public static final String TS = HEADER_API_TIMESTAMP;public static final String NONCE = HEADER_API_NONCE;public static final String SIGN = HEADER_API_SIGN;/*** 在header中增加如下字段:* x-api-ts:时间戳,20分钟内有效,提交类方法20分钟失效* x-api-nonce:防重,10位以上* appid:线下统一分配,为每一个消息发出方配置坐标* appSecret:线下统一分配,密钥* signature:上述字段的加密*/public static String generateNonceStr() {char[] nonceChars = new char[32];for (int index = 0; index < nonceChars.length; ++index) {nonceChars[index] = SYMBOLS.charAt(RANDOM.nextInt(SYMBOLS.length()));}return new String(nonceChars);}public