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

责任链模式

责任链模式是是设计模式中的一种行为型模式。该模式下,多个对象通过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即可,这样就可以与主体的登录逻辑代码实现解耦。

小结

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

侧击雷如何检测预防

侧击雷是一种雷击的形式&#xff0c;指的是雷电从建筑物的侧面打来的直接雷击。侧击雷对高层建筑物的防雷保护提出了更高的要求&#xff0c;因为一般的避雷带或避雷针不能完全保护住建筑物的侧面。侧击雷可能会对建筑物的结构、设备和人员造成严重的损害&#xff0c;甚至引发火…

酷开科技丨酷开系统,带你进入惊喜不断的影视世界!

随着科技的迅速发展&#xff0c;智能电视已经成为家庭娱乐的重要组成部分。而要说到智能电视&#xff0c;就不得不提到酷开系统&#xff0c;作为一款智能电视操作系统&#xff0c;酷开系统以其独特的功能和出色的使用体验&#xff0c;让观众们看到了到惊喜不断的影视世界。 如…

CRM系统:助力数据服务企业,打造核心竞争力

近年来&#xff0c;数据服务企业开始走入大众视野。作为企业管理应用热门选手——CRM客户管理系统&#xff0c;可以助力企业实时数据应用先行者&#xff0c;提升业务转化与协同效率&#xff0c;进一步打造核心竞争力。下面我们说说&#xff0c;CRM系统对数据服务企业的作用。 …

Stable Diffusion 是否使用 GPU?

在线工具推荐&#xff1a; Three.js AI纹理开发包 - YOLO合成数据生成器 - GLTF/GLB在线编辑 - 3D模型格式在线转换 - 3D数字孪生场景编辑器 Stable Diffusion 已迅速成为最流行的生成式 AI 工具之一&#xff0c;用于通过文本到图像扩散模型创建图像。但是&#xff0c;它需…

使用 Stable Diffusion Img2Img 生成、放大、模糊和增强

在线工具推荐&#xff1a; Three.js AI纹理开发包 - YOLO合成数据生成器 - GLTF/GLB在线编辑 - 3D模型格式在线转换 - 3D数字孪生场景编辑器 Stable Diffusion 2022.1 Img5Img 于 2 年发布&#xff0c;是一款革命性的深度学习模型&#xff0c;正在重新定义和推动照片级真实…

云原生Kubernetes系列 | 通过容器互联搭建wordpress博客系统

云原生Kubernetes系列 | 通过容器互联搭建wordpress博客系统 通过容器互联搭建一个wordpress博客系统。wordpress系统是需要连接到数据库上的&#xff0c;所以wordpress和mysql的镜像都是需要的。wordpress在创建过程中需要指定一些参数。创建mysql容器时需要把mysql的数据保存…

linux系统下文件操作常用的命令

一、是什么 Linux 是一个开源的操作系统&#xff08;OS&#xff09;&#xff0c;是一系列Linux内核基础上开发的操作系统的总称&#xff08;常见的有Ubuntu、centos&#xff09; 系统通常会包含以下4个主要部分 内核shell文件系统应用程序 文件系统是一个目录树的结构&…

PyQt中QFrame窗口中的组件不显示的原因

文章目录 问题代码&#xff08;例&#xff09;原因和解决方法 问题代码&#xff08;例&#xff09; from PyQt5.QtWidgets import * from PyQt5.QtGui import QFont, QIcon, QCursor, QPixmap import sysclass FrameToplevel(QFrame):def __init__(self, parentNone):super().…

【Python基础篇】变量

博主&#xff1a;&#x1f44d;不许代码码上红 欢迎&#xff1a;&#x1f40b;点赞、收藏、关注、评论。 格言&#xff1a; 大鹏一日同风起&#xff0c;扶摇直上九万里。 文章目录 一 Python中变量的定义二 Python中变量的使用三 Python中变量的类型四 Python中变量的删除五 …

[数据结构大作业]HBU 河北大学校园导航

校园导航实验报告 问题描述&#xff1a; 以我校为例&#xff0c;设计一个校园导航系统&#xff0c;主要为来访的客人提供信息查询。系统有两类登陆账号&#xff0c;一类是游客&#xff0c;使用该系统方便校内路线查询&#xff1b;一类是管理员&#xff0c;可以使用该系统查询…

mysql常用命令-03

今天讲解下mysql中创建表的语法 CREATE TABLE tb_name( 列名 数据类型 [PRIMARY KEY] [AUTO_INCREMENT], 列名 数据类型 [NULL | NOT NULL], ....., 列名 数据类型 ); 1.创建班级表classes,结构如下&#xff1a; 列名数据类型允许空约束其它说明cid INT主键班级编号cname…

医疗器械维修工程师必须重视的方面

彩虹医疗器械维修技能培训开班报名中 长期班低至五折&#xff0c; 打破常规培训模式轻松愉快技术学习&#xff01; 两个多月时间&#xff0c;提升自我&#xff01; 点击进入 彩虹实训基地 理论实践结合教学 小班授课 立即咨询 1 工程师须重视 在医疗行业中&#xff0c;…

【算法与数据结构】46、47、LeetCode全排列I, II

文章目录 一、46.全排列I二、47.全排列II三、完整代码 所有的LeetCode题解索引&#xff0c;可以看这篇文章——【算法和数据结构】LeetCode题解。 一、46.全排列I 思路分析&#xff1a;本题要求是全排列&#xff0c;意味着每次递归的时候startIndex都要从0开始&#xff0c;否则…

mysq,数据库的综合查询

记录一下数据库综合查询&#xff0c;复习加深印象 创建教学数据库中包含四个基本表&#xff1a; 教师情况表Teacher&#xff08;Tno 教师号&#xff0c;TName 教师名&#xff0c;TDept 教师所在的院系&#xff09;&#xff1b;课程基本表Course&#xff08;Cno 课号&#xff…

LeetCode(12)时间插入、删除和获取随机元素【数组/字符串】【中等】

目录 1.题目2.答案3.提交结果截图 链接&#xff1a; 380. O(1) 时间插入、删除和获取随机元素 1.题目 实现RandomizedSet 类&#xff1a; RandomizedSet() 初始化 RandomizedSet 对象bool insert(int val) 当元素 val 不存在时&#xff0c;向集合中插入该项&#xff0c;并返回…

CSS 实现新拟态(Neumorphism) UI 风格

什么是新拟态(Neumorphism) UI 风格&#xff1f;网上似乎还没有一个准确统一的定义。按照我个人的通俗理解&#xff0c;就是将界面的一部分凸起来&#xff0c;另一部分凹下去&#xff0c;形成的一种错落有致的拟物风格。代表作是乌克兰设计师 Alexander Plyuto 在各平台发布的新…

腾讯云服务器购买优惠活动,腾讯云服务器新用户优惠活动

如果你正在寻找一种性价比高、稳定可靠的云服务器&#xff0c;那么腾讯云服务器绝对是一个不错的选择。现在&#xff0c;腾讯云服务器购买优惠活动已经开始了&#xff0c;新用户可以享受到更多的优惠。 腾讯云双十一领9999代金券 https://1111.mian100.cn 腾讯云新用户领2860…

电脑桌面图标打不开?三种方法让你轻松应对

电脑桌面上的图标是我们日常使用电脑的入口&#xff0c;但有时候您可能会遇到一个常见问题&#xff0c;电脑桌面图标打不开。这个问题可能会让您感到困惑&#xff0c;但幸运的是&#xff0c;通常有多种方法可以解决。本文将详细介绍三种常见的解决方法&#xff0c;帮助您恢复桌…

教资笔记(目录)

2023.9.16教资考试 笔试成绩是150分&#xff0c;但是考试折合成120分满分&#xff0c;70分及格。 计划&#xff1a;2024上半年再战科一 名称类型中学科二急救班中学中小学科一模板通用科目二简答题汇总中学教资学习笔记总结中学《综合素质》通用 小学中学科一&#xff08;通…

Node.js进阶-包与模块化

文章目录 一、模块化概念node.js的模块加载模块向外共享模块作用域的成员模块化规范 二、npm与包概念包的下载与使用下载速度慢的解决方法 三、npmnpm初体验包管理配置文件多人协作问题一次性安装所有包卸载包 调试包包的分类项目包全局包 规范包结构开发属于自己的包模块化拆分…