使用责任链模式实现登录风险控制

news2024/11/25 11:58:56

责任链模式

责任链模式是是设计模式中的一种行为型模式。该模式下,多个对象通过next属性进行关系关联,从而形成一个对象执行链表。当发起执行请求时,会从首个节点对象开始向后依次执行,如果一个对象不能处理该请求或者完成了请求工作(需要结合具体的业务场景),那么它会把相同的请求传给下一个接收者,依此类推。

责任链上的每个节点的处理者负责处理请求,用户只需要将请求发送到责任链上即可,无须关心请求的处理细节和请求的传递,所以责任链将请求的发送者和请求的处理者解耦了,即使以后多加一些责任节点,也可以做到很好的扩展。

应用

 

上部分简单的介绍了责任链模式,那么现在就结合实际的业务场景来使用该模式。刚好这两天公司的产品经理就提出非常应景的需求。在用户登录时,需要判断登录账号存在的风险,比如在短时间内输入密码错误次数达到预设值,在短时间内,同一账号的登录所在地不属于同一个城市,登录ip地址不属于白名单范围内等。

当满足这些风险规则时,那么就需要根据需求对账号做进一步的处理,例如阻断登录,发送短信提醒或者禁用账号等。下面就使用责任链模式来实现这个需求功能。首先需要确定一个抽象处理类Handler,该处理类包含抽象处理方法和一个后继连接。

其次需要有若干个具体处理类XXXHandler,这个具体处理类需要继承抽象处理类Handler并且实现抽象处理者的处理方法,判断能否处理本次请求,如果可以处理请求则处理,否则将该请求转给它的后继者。最后需要有一个执行器或者客户端类来确定执行顺序,它不关心处理细节和请求的传递过程。

 

1创建抽象父类

在正式编码前,需要确认有哪些数据表和对象。

  • 风险规则类RiskRule: 用于记录不同规则信息,如触发条件,处置措施等。

  • 登录日志类LoginLog: 用于记录登录日志,其中包含登录地区,登录ip等。

  • 账户类UserAccount: 简单的账号account和密码password。

@Data
public class RiskRule {

    private Integer id;

    /**
     * 风险名称
     */
    private String riskName;

    /**
     * 白名单ip
     */
    private String acceptIp;

    /**
     * 触发次数
     */
    private Integer triggerNumber;

    /**
     * 触发时间
     */
    private Integer triggerTime;

    /**
     * 触发时间类型
     */
    private Integer triggerTimeType;

    /**
     * 异常登录时间 (json)
     */
    private String unusualLoginTime;

    /**
     * 采取的操作措施 1:提示 2:发送短信  3:阻断登录  4:封号
     */
    private Integer operate;

}
@Data
public class LoginLog {

    @TableId(type = IdType.AUTO)
    private Integer id;

    private String account;

    private Integer result;

    private String cityCode;

    private String ip;

    private Date time;
}

在确认完需要的对象后,现在可以编写登录风险处理抽象父类AbstractLoginHandle,该类需要包含一个nextHandle对象和filterRisk方法。filterRisk主要处理风险控制的规则并筛选出满足触发条件的规则对象,用于最后统一处理。

/**
* 登录风险处理抽象父类
*/
public abstract class AbstractLoginHandle {

    public AbstractLoginHandle nextHandle; // 下一个执行节点
 
    public void setNextHandle(AbstractLoginHandle nextHandle){
        this.nextHandle = nextHandle;
    }


/**
 * 具体的执行方法,过滤出满足风控的规则
 * @param filter 满足风控的规则
 * @param ruleMap 所有规则集合
 * @param account 登录账户
 */
    public abstract void filterRisk(List<RiskRule> filter, Map<Integer,RiskRule> ruleMap, UserAccount account);

}

2密码错误次数

在创建完抽象父类后,下面开始实现具体的子类。首先是常见的密码错误次数,实现起来简单,需要去登录日志表中按照对应规则配置的规定时间来查询密码错误的日志即可。如果查询出的数量大于等于该规则的触发数量,那么就将该RiskRule对象添加到filter中,最后继续向下执行。

/**
 * 密码错误次数风险实现
 */
@Component
public class PasswordErrorRiskHandle extends AbstractLoginHandle {

    // 配置触发时间间隔类型是秒
    private static final Integer SEC = 1;

   // 配置触发时间间隔类型是分钟
    private static final Integer MIN = 2;

   // 配置触发时间间隔类型是小时
    private static final Integer HOU = 3;

    @Resource
    private LoginLogService loginLogService;

    @Override
    public void filterRisk(List<RiskRule> filter, Map<Integer, RiskRule> ruleMap, UserAccount account) {
        if (MapUtil.isNotEmpty(ruleMap)) {
            //获取密码错误的规则信息
            RiskRule passwordRisk = ruleMap.get(1);
            if (passwordRisk != null) {
                //触发次数
                Integer triggerNumber = passwordRisk.getTriggerNumber();
                //触发时间
                Integer triggerTime = passwordRisk.getTriggerTime();
                //时间类型
                Integer triggerTimeType = passwordRisk.getTriggerTimeType();

                Date endTime = new Date();

                Date startTime;
                
                if (triggerTimeType == SEC) {
                    startTime = DateUtil.offsetSecond(endTime, -triggerTime);
                } else if (triggerTimeType == MIN) {
                    startTime = DateUtil.offsetMinute(endTime, -triggerTime);
                } else {
                    startTime = DateUtil.offsetHour(endTime, -triggerTime);
                }
                // 查询范围时间内密码错误的次数
                Integer count = loginLogService.lambdaQuery().eq(LoginLog::getResult, 2)
                        .eq(LoginLog::getAccount, account.getAccount())
                        .between(LoginLog::getTime, startTime, endTime)
                        .count();
                 // 如果达到触发规则,则记录
                if (count != null && count.intValue() >= triggerNumber.intValue()) {
                    filter.add(passwordRisk);
                }
            }
        }
        //是否有下一个节点 , 如果有,继续向下执行  
        if (this.nextHandle != null) {
            this.nextHandle.filterRisk(filter, ruleMap, account);
        }
    }

}

3异常时间登录

到底什么时间登录才算异常时间登录,这个需要根据公司,系统来做判断。如果一家公司从来不加班,用的也都是些OA系统,正常的登录时间段都在早上8点到下午6点这样。如果有一天,一个账号突然在凌晨两三点进行了登录,那么这就可以算作异常登录。

当然,具体的时间段可以根据实际的需求进行设置。为了方便,这些时间段直接以json的方式存在的数据表中,具体格式如下。

[
    {
    "week":0,
    "startTime":"12:00:00",
    "endTime":"14:00:00"
    },
    {
    "week":1,
    "startTime":"12:00:00",
    "endTime":"14:00:00"
    }
]

这个需求实现也非常简单,只需要判断当前的登录时间是否在配置的异常登录时间范围内即可,如果在这个范围为内,那么就将该风险规则添加到filter中。

/**
 * 异常时间登录风险实现
 */
@Component
public class UnusualLoginRiskHandle extends AbstractLoginHandle {

    @Override
    public void filterRisk(List<RiskRule> filter, Map<Integer, RiskRule> ruleMap, UserAccount account) {
        if (MapUtil.isNotEmpty(ruleMap)) {
            RiskRule loginTimeExe = ruleMap.get(2);
            if (loginTimeExe != null) {
            // 将json转为异常时间对象
                List<UnusualLoginTime> unusualLoginTimes = JSONUtil.toList(loginTimeExe.getUnusualLoginTime(), UnusualLoginTime.class);
                Date now = new Date();
                // 判断当前时间是周几
                int dayOfWeek = DateUtil.dayOfWeek(now);
                for (UnusualLoginTime unusualLoginTime : unusualLoginTimes) {
                   // 如果当前的周数与配置的周数相等,那么判断当前的具体时间
                    if (unusualLoginTime.getWeek() == dayOfWeek) {
                        DateTime startTime = DateUtil.parseTimeToday(unusualLoginTime.getStartTime());
                        DateTime endTime = DateUtil.parseTimeToday(unusualLoginTime.getEndTime());
                        // 如果当前的时间,在配置的时间范围内,那么将算作异常时间登录
                        if (DateUtil.isIn(now, startTime, endTime)) {
                            filter.add(loginTimeExe);
                            break;
                        }
                    }
                }
            }
        }
        // 是否有下一个节点 , 如果有,继续向下执行  
        if (this.nextHandle != null) {
            this.nextHandle.filterRisk(filter, ruleMap, account);
        }
    }

    @Data
    public static class UnusualLoginTime {

        private int week;

        private String startTime;

        private String endTime;
    }
}

4IP白名单

在使用一些阿里云服务时,有时需要配置一些ip白名单才可以访问,非白名单内的ip将会阻断连接。这也是一种保证系统服务安全的一种方式,实现起来也比较容易。从数据库中读取ip白名单,如果是多个,可以使用英文逗号进行分割。

用户登录时,通过HttpServletRequest来获取用户的ip(这里为了方便测试,将ip作为一个字段放在了account中),如果这个ip不在白名单内,那么将这个风险规则添加到filter中。

/**
 * 登录ip风险实现
 */
@Component
public class IPRiskHandle extends AbstractLoginHandle {
    
    @Override
    public void filterRisk(List<RiskRule> filter, Map<Integer, RiskRule> ruleMap, UserAccount account) {
        if (MapUtil.isNotEmpty(ruleMap)) {
            RiskRule ipRisk = ruleMap.get(3);
            //判断是否配置登录ip白名单
            if (null != ipRisk && StrUtil.isNotEmpty(ipRisk.getAcceptIp())) {
                List<String> acceptIpList = Arrays.asList(ipRisk.getAcceptIp().split(","));
                //当前登录ip是否在白名单内,如果不在,则添加到filter中
                if (!acceptIpList.contains(account.getIp())) {
                    filter.add(ipRisk);
                }
            }
        }
        if (this.nextHandle != null) {
            this.nextHandle.filterRisk(filter, ruleMap, account);
        }
    }
}

5异常地区登录

如果一个账号在短时间内在不同地区进行了登录操作,比如上一秒在北京登录,下一秒就在上海进行了登录。

那么这就可能出现了账号盗取情况,需要采取一定的处置措施,比如输入短信验证码,输入密保,封号等。

/**
 * 登录地区风险实现
 */
@Component
public class LoginAreaRiskHandle extends AbstractLoginHandle {

    private static final Integer SEC = 1;

    private static final Integer MIN = 2;

    private static final Integer HOU = 3;

    @Resource
    private LoginLogService loginLogService;

    @Override
    public void filterRisk(List<RiskRule> filter, Map<Integer, RiskRule> ruleMap, UserAccount account) {
        if (MapUtil.isNotEmpty(ruleMap)) {
            RiskRule areaRisk = ruleMap.get(4);
            if (null != areaRisk) {
                Integer triggerTime = areaRisk.getTriggerTime();
                Integer triggerTimeType = areaRisk.getTriggerTimeType();
                Integer triggerNumber = areaRisk.getTriggerNumber();
                Date endTime = new Date();
                Date startTime;
                //获取查询时间范围的开始时间
                if (triggerTimeType == SEC) {
                    startTime = DateUtil.offsetSecond(endTime, -triggerTime);
                } else if (triggerTimeType == MIN) {
                    startTime = DateUtil.offsetMinute(endTime, -triggerTime);
                } else {
                    startTime = DateUtil.offsetHour(endTime, -triggerTime);
                }
                // 指定时间范围内,登录地区是否超过指定个数
                List<LoginLog> loginLogList = loginLogService.lambdaQuery().select(LoginLog::getCityCode).between(LoginLog::getTime, startTime, endTime)
                        .eq(LoginLog::getResult, 1)
                        .eq(LoginLog::getAccount, account.getAccount())
                        .list();
                long areaCount = CollUtil.emptyIfNull(loginLogList).stream().map(LoginLog::getCityCode).distinct().count();
                //如果超过指定个数,则将该风险策略添加到filter
                if (areaCount >= triggerNumber.longValue()) {
                    filter.add(areaRisk);
                }
            }
        }
        if (this.nextHandle != null) {
            this.nextHandle.filterRisk(filter, ruleMap, account);
        }
    }
}

6组合链路节点

再将上面的各种情况实现完成后,需要有一个执行器来聚合这些handle。让这些hande节点有一定的执行顺序。

并且在所有节点执行完成后,对触发的风险规则进行处理。我自己定义的执行顺序是密码错误次数->异常时间登录->ip白名单->异常地区登录。

@Slf4j
@Component
public class LoginHandleManage {

    @Resource
    private RiskRuleService riskRuleService;
    
    @Resource
    private LoginLogService loginLogService;

    @Resource
    private IPRiskHandle ipRiskHandle;

    @Resource
    private LoginAreaRiskHandle loginAreaRiskHandle;

    @Resource
    private PasswordErrorRiskHandle passwordErrorRiskHandle;

    @Resource
    private UnusualLoginRiskHandle unusualLoginRiskHandle;


    /**
     * 构建执行顺序
     * passwordErrorRiskHandle -> unusualLoginRiskHandle -> ipRiskHandle -> loginAreaRiskHandle
     */

    @PostConstruct
    public void init() {
        passwordErrorRiskHandle.setNextHandle(unusualLoginRiskHandle);
        unusualLoginRiskHandle.setNextHandle(ipRiskHandle);
        ipRiskHandle.setNextHandle(loginAreaRiskHandle);
    }


    /**
     * 执行链路入口
     * @param account
     * @throws Exception
     */
    public void execute(UserAccount account) throws Exception {
    //获取所有风险规则 
        List<RiskRule> riskRules = riskRuleService.lambdaQuery().list();
        Map<Integer, RiskRule> riskRuleMap = riskRules.stream().collect(Collectors.toMap(RiskRule::getId, r -> r));
        List<RiskRule> filterRisk = new ArrayList<>();
        //开始从首节点执行
        passwordErrorRiskHandle.filterRisk(filterRisk, riskRuleMap, account);
        if (CollUtil.isNotEmpty(filterRisk)) {
            // 获取最严重处置措施的规则
            Optional<RiskRule> optional = filterRisk.stream().max(Comparator.comparing(RiskRule::getOperate));
            if (optional.isPresent()) {
                RiskRule riskRule = optional.get();
                handleOperate(riskRule);//处置

                //TODO 记录日志

            }
        }
    }

    /**
     * 处置风险
     * @param riskRule
     * @throws Exception
     */

    public void handleOperate(RiskRule riskRule) throws Exception {
        int operate = riskRule.getOperate().intValue();
        if (operate == OperateEnum.TIP.op) { //1
            log.info("========执行提示逻辑========");
        } else if (operate == OperateEnum.SMS.op) {//2
            log.info("========执行短信提醒逻辑========");
        } else if (operate == OperateEnum.BLOCK.op) {//3
            log.info("========执行登录阻断逻辑========");
            throw new Exception("登录存在风险!");
        } else if (operate == OperateEnum.DISABLE.op) {//4
            log.info("========执行封号逻辑========");
            throw new Exception("登录存在风险,账号被封!");
        }
    }
}

现在所有的逻辑已经搞定了,那么在登录的实现方法中只需要注入LoginHandleManage并调用execute即可,这样就可以与主体的登录逻辑代码实现解耦。

小结

责任链模式使用了委托的思想构建了一个链表,通过遍历链表来挨个询问链表中的每一个节点是否可以胜任某件事情,如果某个节点能够胜任,则直接处理,否则继续向下传递。责任链会造成处理的时延,但是能够很好的解耦合,提高可扩展性,可以结合具体场景,选择性使用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1202631.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

福建科立讯通信 指挥调度管理平台RCE漏洞复现

0x01 产品简介 福建科立讯通信指挥调度管理平台是一个专门针对通信行业的管理平台。该产品旨在提供高效的指挥调度和管理解决方案&#xff0c;以帮助通信运营商或相关机构实现更好的运营效率和服务质量。该平台提供强大的指挥调度功能&#xff0c;可以实时监控和管理通信网络设…

一文让你了解网络刷卡器的特点和优势

网络刷卡器一款高性能的多协议电子标签读写器&#xff0c;保持高识读率的同时实现对电子标签的快速读写处理&#xff0c;广泛应用于物流追踪、个人身份识别、人员管理、智能停车场、门禁考勤、公交一卡通、餐饮、金融等多个领域。 特点和优势&#xff1a; 1&#xff09;低功耗、…

知识管理工具zotero的数据迁移工作

工作中一直使用zotero来管理一些有价值的文档&#xff0c;时间长了这个数据库的大小也是很大的&#xff0c;如果有调整电脑的需求&#xff0c;那么这个数据库就要随之迁移&#xff0c;zotero有两种数据迁移方式&#xff0c;一种是云同步&#xff0c;需要买zotero的容量&#xf…

【FastCAE源码阅读8】调用gmsh生成网格

FastCAE使用gmsh进行网格划分&#xff0c;划分的时候直接启动一个新的gmsh进程&#xff0c;个人猜测这么设计是为了规避gmsh的GPL协议风险。 进行网格划分时&#xff0c;其大体运行如下图&#xff1a; 一、Python到gmshModule模块 GUI操作到Python这步不再分析&#xff0c;比…

C++学习第三十七天----第十章--对象和类

10.2.2 C中的类 类是一种将抽象转换未用户定义类型的C工具&#xff0c;它将数据表示和操作数据的方法合成一个整洁的包。 接口&#xff1a;一个共享框架&#xff0c;供两个系统交互时使用。 1.访问控制 使用类对象的程序可以直接访问类的公有部分&#xff0c;但只能通过公有…

单片机定时器讲解和实现

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、计数器是什么&#xff1f;二、单片机定时器结构2.1***两个8位如何合成16位&#xff0c;16位如何分成两个8位***2.2 计数器的位数组合&#xff1f;2.3 定时功…

用python将csv表格数据做成热力图

python的开发者为处理表格和画图提供了库的支持&#xff0c;使用pandas库可以轻松完成对csv文件的读写操作&#xff0c;使用matplotlib库提供了画热力图的各种方法。实现这个功能首先需要读出csv数&#xff0c;然后设置自定义色条的各种属性如颜色&#xff0c;位置&#xff0c;…

[Android]新建项目使用AppCompatActivity后运行闪退

报错 日志&#xff1a; Caused by: java.lang.IllegalStateException: You need to use a Theme.AppCompat theme (or descendant) with this activity. FATAL EXCEPTION: main Process: com.example.gatestdemol, PID: 26071 java.lang.RuntimeException: Unable to start a…

Maya v2024(3D动画制作软件)

Maya 2024是一款三维计算机图形动画制作软件。它被广泛应用于电影、电视、游戏、动画等领域中&#xff0c;用于创建各种三维模型、场景、特效和动画。 以下是Maya的主要特点&#xff1a; 强大的建模工具&#xff1a;Maya提供了各种建模工具&#xff0c;如多边形建模、NURBS建模…

VsCode 安装 GitHub Copilot插件 (最新)

##在线安装&#xff1a; 打开Vscode扩展商店&#xff0c;输入 "GitHub Copilot " ,选择下载人数最多的那个。&#xff08;这个是你写一部分代码或者注释&#xff0c;Ai自动帮你提示/补全代码&#xff09;,建议选择这个 注意下面有个和他类似的 "GitHub Copilo…

Linux系统编程——其他类型数据写到文件并读取

函数原型解读 由前面章节可知&#xff0c;对文件的操作都是基于字符串&#xff0c;但对文件的操作并不仅限于此&#xff0c;这个时候需要重新审视几个文件操作的函数原型&#xff0c;函数原型如下&#xff1a; ssize_t write(int fd, const void *buf, size_t count); ssize_…

月入8.3k,新传文科生转行5G网络优化工程师,张雪峰:这专业,报考就打晕…

新闻传播专业的就业是什么样子的&#xff1f; 考研名师张雪峰说&#xff1a;如果我是家长的话&#xff0c;孩子非要报新闻学&#xff0c;我一定会干一个事&#xff0c;就是把他打晕&#xff0c;然后给他报个别的。 新闻传播专业似乎已经成了一个备受争议的话题&#xff0c;就业…

测试用例之翻页功能

&#x1f4e2;专注于分享软件测试干货内容&#xff0c;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指正&#xff01;&#x1f4e2;交流讨论&#xff1a;欢迎加入我们一起学习&#xff01;&#x1f4e2;资源分享&#xff1a;耗时200小时精选的「软件测试」资…

《Linux从练气到飞升》No.26 Linux中的线程控制

&#x1f57a;作者&#xff1a; 主页 我的专栏C语言从0到1探秘C数据结构从0到1探秘Linux菜鸟刷题集 &#x1f618;欢迎关注&#xff1a;&#x1f44d;点赞&#x1f64c;收藏✍️留言 &#x1f3c7;码字不易&#xff0c;你的&#x1f44d;点赞&#x1f64c;收藏❤️关注对我真的…

目标检测问题总结

目标检测问题总结 目标检测二阶段和一阶段的核心区别目标检测二阶段比一阶段的算法精度高的原因1. 正负样本不平衡2.样本的不一致性 如何解决目标检测中遮挡问题如何解决动态目标检测FPN的作用如何解决训练数据样本过少的问题IOU代码实现NMS代码实现NMS的改进思路 目标检测二阶…

DBC文件解析成C语言

1. 安装python环境 例如Windows安装python3.10版本 下载地址https://cdn.npmmirror.com/binaries/python/3.10.9/python-3.10.9-amd64.exe 2. 安装cantools函数库 打开CMD窗口后&#xff0c;输入pip install cantools 3. 执行dbc文件转为C语言 注意&#xff1a;c文件和…

时间序列预测(2) — 时间序列预测数据集

目录 数据集1&#xff1a;GEFCom2014负荷数据 数据集2&#xff1a;爱奇艺用户留存预测挑战赛数据集 数据集1&#xff1a;GEFCom2014负荷数据 数据集下载&#xff1a; 百度网盘&#xff1a;链接&#xff1a;https://pan.baidu.com/s/1PgCWHx8vYUfGB9UGtCmaVA?pwdktn0 提取码…

儿童HPV感染有哪些症状?皮肤性病科谭巍主任讲述五大要点

HPV&#xff0c;人乳头瘤病毒&#xff0c;是一种常见的感染性强的病毒&#xff0c;相比成人儿童也会感染HPV。家长及时了解儿童感染HPV的症状对于早期发现和治疗至关重要。为了帮助家长们更好预防儿童HPV感染&#xff0c;今日特邀劲松中西医医院皮肤性病科主任谭巍详细介绍儿童…

MoveIt 机械臂运动 学习 01-MoveIt 初次见面

ROS中有针对机器人进行移动操作的一套工具——MoveIt&#xff01;。在主页http://moveit.ros.org 上 包含使用MoveIt&#xff01;的文档、教程、安装说明以及多种机械臂&#xff08;或机器人&#xff09;的示例演示&#xff0c;如一些 移动操作任务&#xff0c;包括抓握、拾取和…