风控系统基于“会话”构建的验证降级机制(实践篇)

前文

前文讲了《风控系统基于"会话"构建可信任的验证降级机制(理论篇)》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!

相关推荐
拾光拾趣录10 分钟前
🔥FormData+Ajax组合拳,居然现在还用这种原始方式?💥
前端·面试
不会笑的卡哇伊19 分钟前
新手必看!帮你踩坑h5的微信生态~
前端·javascript
bysking21 分钟前
【28 - 记住上一个页面tab】实现一个记住用户上次点击的tab,上次搜索过的数据 bysking
前端·javascript
我是不会赢的22 分钟前
使用 decimal 包解决 go float 浮点数运算失真
开发语言·后端·golang·浮点数
Dream耀23 分钟前
跨域问题解析:从同源策略到JSONP与CORS
前端·javascript
前端布鲁伊23 分钟前
【前端高频面试题】面试官: localhost 和 127.0.0.1有什么区别
前端
HANK23 分钟前
Electron + Vue3 桌面应用开发实战指南
前端·vue.js
yuqifang38 分钟前
写一个简单的Java示例
java·后端
極光未晚39 分钟前
Vue 前端高效分包指南:从 “卡成 PPT” 到 “丝滑如德芙” 的蜕变
前端·vue.js·性能优化