Implementing the Chain of Responsibility Pattern for Login Risk Management in Java
This article explains the Chain of Responsibility design pattern and demonstrates how to apply it in a Java backend to evaluate login risk factors such as password errors, unusual login times, IP whitelist violations, and abnormal login locations, providing full code examples and execution flow.
The Chain of Responsibility pattern is a behavioral design pattern that links multiple handler objects via a next reference, forming a processing chain where each handler can either handle a request or pass it to the next handler.
In a login risk scenario, the system needs to evaluate several rules—excessive password errors, login from unusual times, IP not in a whitelist, and login from disparate regions—to decide whether to block the login, send an SMS, prompt for verification, or disable the account.
First, three domain entities are defined:
@Data
public class RiskRule {
private Integer id;
/** risk name */
private String riskName;
/** whitelist ip */
private String acceptIp;
/** trigger count */
private Integer triggerNumber;
/** trigger time */
private Integer triggerTime;
/** trigger time type */
private Integer triggerTimeType;
/** unusual login time (json) */
private String unusualLoginTime;
/** operation: 1 tip, 2 SMS, 3 block, 4 disable */
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;
}An abstract handler AbstractLoginHandle defines the chain link and the abstract method filterRisk that concrete handlers must implement.
/**
* Login risk abstract handler
*/
public abstract class AbstractLoginHandle {
public AbstractLoginHandle nextHandle; // next node
public void setNextHandle(AbstractLoginHandle nextHandle) {
this.nextHandle = nextHandle;
}
/**
* Concrete execution method, filters risk rules.
*/
public abstract void filterRisk(List
filter, Map
ruleMap, UserAccount account);
}Four concrete handlers are implemented:
1. PasswordErrorRiskHandle checks the number of failed password attempts within a configured time window.
@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
filter, Map
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);
}
}
}2. UnusualLoginRiskHandle parses a JSON list of allowed login time windows and adds the rule if the current login falls inside an “unusual” window.
@Component
public class UnusualLoginRiskHandle extends AbstractLoginHandle {
@Override
public void filterRisk(List
filter, Map
ruleMap, UserAccount account) {
if (MapUtil.isNotEmpty(ruleMap)) {
RiskRule loginTimeExe = ruleMap.get(2);
if (loginTimeExe != null) {
List
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;
}
}3. IPRiskHandle checks whether the login IP is present in a configured whitelist.
@Component
public class IPRiskHandle extends AbstractLoginHandle {
@Override
public void filterRisk(List
filter, Map
ruleMap, UserAccount account) {
if (MapUtil.isNotEmpty(ruleMap)) {
RiskRule ipRisk = ruleMap.get(3);
if (ipRisk != null && StrUtil.isNotEmpty(ipRisk.getAcceptIp())) {
List
acceptIpList = Arrays.asList(ipRisk.getAcceptIp().split(","));
if (!acceptIpList.contains(account.getIp())) {
filter.add(ipRisk);
}
}
}
if (this.nextHandle != null) {
this.nextHandle.filterRisk(filter, ruleMap, account);
}
}
}4. LoginAreaRiskHandle evaluates whether the account has logged in from more distinct regions than allowed within a time window.
@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
filter, Map
ruleMap, UserAccount account) {
if (MapUtil.isNotEmpty(ruleMap)) {
RiskRule areaRisk = ruleMap.get(4);
if (areaRisk != null) {
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
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();
if (areaCount >= triggerNumber.longValue()) {
filter.add(areaRisk);
}
}
}
if (this.nextHandle != null) {
this.nextHandle.filterRisk(filter, ruleMap, account);
}
}
}The handlers are linked in a specific order (password errors → unusual time → IP whitelist → abnormal area) by the LoginHandleManage component, which also retrieves all risk rules, executes the chain, selects the most severe rule, and performs the corresponding operation (tip, SMS, block, or disable).
@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;
/** Build execution order */
@PostConstruct
public void init() {
passwordErrorRiskHandle.setNextHandle(unusualLoginRiskHandle);
unusualLoginRiskHandle.setNextHandle(ipRiskHandle);
ipRiskHandle.setNextHandle(loginAreaRiskHandle);
}
/** Chain entry */
public void execute(UserAccount account) throws Exception {
List
riskRules = riskRuleService.lambdaQuery().list();
Map
riskRuleMap = riskRules.stream()
.collect(Collectors.toMap(RiskRule::getId, r -> r));
List
filterRisk = new ArrayList<>();
passwordErrorRiskHandle.filterRisk(filterRisk, riskRuleMap, account);
if (CollUtil.isNotEmpty(filterRisk)) {
Optional
optional = filterRisk.stream()
.max(Comparator.comparing(RiskRule::getOperate));
if (optional.isPresent()) {
RiskRule riskRule = optional.get();
handleOperate(riskRule);
// TODO: log the handling
}
}
}
/** Execute the selected operation */
public void handleOperate(RiskRule riskRule) throws Exception {
int operate = riskRule.getOperate().intValue();
if (operate == OperateEnum.TIP.op) {
log.info("========执行提示逻辑========");
} else if (operate == OperateEnum.SMS.op) {
log.info("========执行短信提醒逻辑========");
} else if (operate == OperateEnum.BLOCK.op) {
log.info("========执行登录阻断逻辑========");
throw new Exception("登录存在风险!");
} else if (operate == OperateEnum.DISABLE.op) {
log.info("========执行封号逻辑========");
throw new Exception("登录存在风险,账号被封!");
}
}
}Finally, the login service simply injects LoginHandleManage and calls execute(account) before proceeding with the normal authentication flow.
@Resource
private LoginHandleManage loginHandleManage;
public String login(UserAccount account) throws Exception {
// Execute the responsibility chain
loginHandleManage.execute(account);
// TODO: actual login logic
String token = "";
return token;
}The article concludes that the Chain of Responsibility decouples request senders from processors, improves extensibility, and is suitable for scenarios like login risk assessment where multiple independent checks must be performed in a defined order.
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.