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

责任链模式

责任链模式是是设计模式中的一种行为型模式。该模式下,多个对象通过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;@Resourceprivate LoginLogService loginLogService;@Overridepublic 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 {@Overridepublic 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);}}@Datapublic 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 {@Overridepublic 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;@Resourceprivate LoginLogService loginLogService;@Overridepublic 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();//如果超过指定个数,则将该风险策略添加到filterif (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 {@Resourceprivate RiskRuleService riskRuleService;@Resourceprivate LoginLogService loginLogService;@Resourceprivate IPRiskHandle ipRiskHandle;@Resourceprivate LoginAreaRiskHandle loginAreaRiskHandle;@Resourceprivate PasswordErrorRiskHandle passwordErrorRiskHandle;@Resourceprivate UnusualLoginRiskHandle unusualLoginRiskHandle;/*** 构建执行顺序* passwordErrorRiskHandle -> unusualLoginRiskHandle -> ipRiskHandle -> loginAreaRiskHandle*/@PostConstructpublic 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) { //1log.info("========执行提示逻辑========");} else if (operate == OperateEnum.SMS.op) {//2log.info("========执行短信提醒逻辑========");} else if (operate == OperateEnum.BLOCK.op) {//3log.info("========执行登录阻断逻辑========");throw new Exception("登录存在风险!");} else if (operate == OperateEnum.DISABLE.op) {//4log.info("========执行封号逻辑========");throw new Exception("登录存在风险,账号被封!");}}
}

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

小结

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

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

联盟 | Quick Creator X HelpLook ,AI助力打造出海企业内容营销

随着人工智能的快速发展&#xff0c;各行各业也加快了人工智能工具的探索&#xff0c;积极将AI融入到行业发展中&#xff0c;出海外贸也不例外。AI渐渐地融入到外贸的各个环节&#xff0c;为企业出海提速增效。 为此&#xff0c;「 Quick Creator 」与 「 HelpLook 」达成战略合…

【Mycat2实战】二、Mycat安装部署

1. Mycat下载 Mycat官网下载地址&#xff0c;点击直接前往&#xff1a;http://www.mycat.org.cn/ Mycat 有提供编译好的安装包&#xff0c;支持 windows、Linux、Mac、 Solaris 等系统上安装与运行。 本文及后续系列的文章都是使用Linux的系统进行操作。 这里我们选择使用文…

华为云优惠券介绍、领取入口及使用教程

华为云是华为的云服务品牌&#xff0c;致力于为用户提供一站式云计算基础设施服务。为了吸引用户&#xff0c;华为云经常推出各种优惠活动&#xff0c;其中就包括优惠券的发放&#xff0c;下面将为大家详细介绍华为云优惠券的作用、领取入口以及使用教程。 一、华为云优惠券介绍…

代码随想录 Day46 动态规划14 LeetCode T392 判断子序列 T115 不同的子序列

LeetCode T392 判断子序列 题目链接:392. 判断子序列 - 力扣&#xff08;LeetCode&#xff09; 题目思路: 本题有两种思路,第一个思路是使用双指针,第二个思路是使用动态规划,结尾笔者会附上两种方法的代码. 1.双指针 首先我们谈双指针的思路,就是让两个指针分别指向s和t字符…

es安装方式

es安装方式 1.下载镜像的方式 分词器 kibana和es和容器互通的方式 docker network create es-net开始拉去镜像的方式 docker pull kibana:7.12.1运行镜像的方式 docker run -d \--name es \-e "ES_JAVA_OPTS-Xms512m -Xmx512m" \-e "discovery.typesingle-…

时间序列预测各类算法探究上篇

前言&#xff1a; 最近项目需要对公司未来业绩进行预测&#xff0c;以便优化决策&#xff0c;so 研究一下时序算法。纯个人理解&#xff0c;记录以便备用&#xff08;只探究一下原理&#xff0c;所有算法都使用基本状态&#xff0c;并未进行特征及参数优化&#xff09;。 环境…

2023年【道路运输企业安全生产管理人员】证考试及道路运输企业安全生产管理人员模拟考试题

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 2023年道路运输企业安全生产管理人员证考试为正在备考道路运输企业安全生产管理人员操作证的学员准备的理论考试专题&#xff0c;每个月更新的道路运输企业安全生产管理人员模拟考试题祝您顺利通过道路运输企业安全生…

【数据结构】直接插入排序

&#x1f466;个人主页&#xff1a;Weraphael ✍&#x1f3fb;作者简介&#xff1a;目前正在学习c和算法 ✈️专栏&#xff1a;数据结构 &#x1f40b; 希望大家多多支持&#xff0c;咱一起进步&#xff01;&#x1f601; 如果文章有啥瑕疵 希望大佬指点一二 如果文章对你有帮助…

纳米软件分享:电源芯片测试指标与测试注意事项

电源芯片测试旨在检测电源管理芯片的质量和性能&#xff0c;保证其可以长期稳定工作。电源芯片测试的参数主要有输入/输出电压、输出电流、效率、温度、功耗等。本文将对电源芯片测试参数以及测试注意事项进行介绍。 电源管理芯片的测试参数 1. 输入电压范围 指电源芯片正常工作…

【论文精读2】R-MVSNet

R-MVSNet【递归多视图立体网络】&#xff0c;论文全名&#xff1a;“Recurrent MVSNet for High-resolution Multi-view Stereo Depth Inference”&#xff0c;CVPR 2019(CCF A) 在MVSNet的基础上做了一些改进&#xff0c;主要解决的问题是代价体正则化&#xff08;Cost Volume…

mysql之搭建MHA架构实现高可用

1、定义 全称是masterhigh avaliabulity。基于主库的高可用环境下可以实现主从复制及故障切换&#xff08;基于主从复制才能故障切换&#xff09; MHA最少要求一主两从&#xff0c;半同步复制模式 2、作用 解决mysql的单点故障问题。一旦主库崩溃&#xff0c;MHA可以在0-30…

入站一个月涨粉80万!B站竖屏UP主如何突出重围?

B站仍然秉持着“内容为王”的社区氛围&#xff0c;这也是众多UP主们一同坚持的事。不管是今年宣布的Story Mode竖屏模式开放还是14周年庆上B站董事长兼CEO陈睿宣布作品播放量改播放分钟数等等改动来看&#xff0c;都能感受到B站在向更多优质创作者招手&#xff0c;并维护着优质…