前文
前文讲了《风控系统基于"会话"构建可信任的验证降级机制(理论篇)》mp.weixin.qq.com/s/V4EZTZOzm...
大概说明了风控系统决策时可以基于"会话"做验证降级机制,上篇是理论,那这篇就来实践吧。
验证结果通知
首先系统增加了一个系统字段String SESSION_ID = "N_S_sessionId";,所有会话降级基于此字段。前文也讲了,此字段不局限于会话,还可以订单号、子会话,等等。
增加了一个新接口用于调用方同步验证结果,声明如下:
seqId:单次决策生成
disposalCode:对应系统的code列表中的一个
verifyTime:验证完成时间
expireSeconds:本次验证过期时间
java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class VerifyReq {
private String seqId;
private String disposalCode;
private LocalDateTime verifyTime;
private Integer expireSeconds;
}
seqId非常关键,因为这是单次决策生成的,只有真正发送决策方能拿到,能避免一点点攻击
然后验证结果通知的流程大概是:
1、查询seqId对应的事件
有:继续
无:结束,伪造的?
2、找到当笔事件,对比验证结果时间和事件发生时间
时差过大:结束
范围内:继续
3、找到当笔事件的会话信息,及sessionId
有:继续
无:结束
4、以sessionId,获取对应缓存
有:
比较缓存和验证结果的disposalCode,本次等级更高则更新disposalCode和expireSeconds,否则不更新
无:
以sessionId缓存本次disposalCode并设置expireSeconds
至此,注意上面也只是有了验证结果,只是set,没有get
所以真正要做"会话"降级用还是要修改决策流程
验证结果接受代码示例如下:
当然一些可以做参数管理,比如时间差、最大时间保护等等
java
public void verifyResult(VerifyReq verifyReq) {
try {
// 1、查询seqId对应的事件
SearchResponse<EventData> searchResponse = elasticsearchClient.search(s -> s
.index("index_name")
.query(q ->
q.term(t -> t.field(EventConstant.ZD + FieldCode.SEQ_ID).value(verifyReq.getSeqId())))
.sort(so -> so
.field(f -> f.field(EventConstant.ZD + FieldCode.EVENT_TIME_STAMP).order(SortOrder.Desc))), EventData.class
);
// 有记录继续
if (CollUtil.isNotEmpty(searchResponse.hits().hits())) {
// 取第一个
EventData eventData = searchResponse.hits().hits().get(0).source();
LocalDateTime eventTime = LocalDateTimeUtil.parse(eventData.getZd().get(FieldCode.EVENT_TIME).toString(), DatePattern.NORM_DATETIME_FORMATTER);
// 判断verifyTime是否在eventTime之后的300s 内
if (eventTime.plus(Duration.ofSeconds(300)).isAfter(verifyReq.getVerifyTime())) {
// 获取sessionId
Object getSessionId = eventData.getZd().get(FieldCode.SESSION_ID);
if (getSessionId != null) {
String sessionId = getSessionId.toString();
log.info("sessionId:{}", sessionId);
Disposal byCode = disposalService.getByCode(verifyReq.getDisposalCode());
// 获取sessionId对应的缓存
RBucket<Disposal> rBucket = redissonClient.getBucket(DecisionKey.VERIFY + sessionId);
Disposal preDisposal = rBucket.get();
if (preDisposal != null) {
// 当前等级更高则更新
if (byCode != null && byCode.getGrade() >= preDisposal.getGrade()) {
// TODO 保护系统应该设置最大时间限制
rBucket.set(byCode, Duration.ofSeconds(verifyReq.getExpireSeconds()));
}
} else {
rBucket.set(byCode, Duration.ofSeconds(verifyReq.getExpireSeconds()));
}
}
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
决策流程修改
就是在修改决策流程时发现改动要比想象中大很多,这次不仅仅是修改了决策流程,增加了"会话"降级的判断而已,还动了上下文、事件数据、输出数据,另外还改动了异步决策的机制,使用了Kafka间接接受数据。
下面这张图大致说明了现在的同步/异步决策流程,带上Kafka生产消费和决策中的上下文、事件数据、输出数据等概念。后续有时间再介绍整个架构和数据流的设计和实现吧😂
这个张图好像也比较清晰了,那就不过多废话讲解了
关于异步流程的示例代码如下:
java
if (!AccessMode.ASYNC.equals(mode)) {
DeStep deStep5 = new DeStep().setName("策略集");
stopWatch.start("策略集");
// 执行策略集
policySetService.policySet();
// 非异步决策时才有策略上下文
PolicyContext policyContext = DecisionContextHolder.getPolicyContext();
PolicySetResult policySetResult = policyContext.getPolicySetResult();
eventData.setPolicySetResult(policySetResult);
// 生成事件结果
EventResult eventResult = new EventResult(policySetResult.getDisposalName(), policySetResult.getDisposalCode(), policySetResult.getDisposalGrade());
// 降级豁免开关
if (true) {
String sessionId = fieldContext.getData2String(FieldCode.SESSION_ID);
if (sessionId != null) {
RBucket<Disposal> bucket = redissonClient.getBucket(DecisionKey.VERIFY + sessionId);
Disposal predisposal = bucket.get();
if (predisposal != null) {
if (predisposal.getGrade() >= policySetResult.getDisposalGrade()) {
eventResult.setDisposalName(DisposalConstant.PASS_NAME);
eventResult.setDisposalCode(DisposalConstant.PASS_CODE);
eventResult.setDisposalGrade(DisposalConstant.PASS_GRADE);
fieldContext.setDataByType(FieldCode.EXEMPTION, "会话降级豁免", FieldType.STRING);
}
}
}
}
eventData.setEventResult(eventResult);
decisionResult.setEventResult(eventResult);
// 策略结果
decisionResult.setPolicySetResult(policySetResult);
stopWatch.stop();
deData.addDeStep(deStep5.setSpendTime(stopWatch.getLastTaskTimeMillis()));
}
测试
发送一笔数据,触发一种决策如下

拿到决策返回的seqid作为请求参数,并设置合适的时间和有效期

查看redis中有了该笔数据

然后再以相同会话id发送一笔,7bcfe5f0-1028-4def-8d02-6e2a8d118b02

查到两笔看到最近的一笔属于"会话降级豁免"

另外很重要的一点,如果你看了前面的示例代码,其实这笔也是跑所有的策略规则的,在测试中这两笔结果是一模一样的,只是最终结果取的是事件结果而不是策略结果,这也是保留灵活性和审查策略运行的一种方式吧。

小结
其实这段时间做的还有其他如:消费者幂等处理、手动提交位点策略、ES索引设计、apikey实践应用、dify结合mcp工具等等,还有如上的策略展示,之后需要将规则明细也展示出来,es索引方面考虑索引生命周期管理或者数据流。
see you!