摘要:主要通过sql(oracle)实现连续X次输错密码后,禁止登录。Y小时或隔天后可以登录。
在javaweb项目的登录模块中经常会有连续X次输错密码后禁止登录的需求。这个功能可以通过多种方法来实现。本文只介绍以sql为主的方法,以供参考。
这是从实际项目中扒出来的代码,对一些变量名进行了处理,但是文中将包含全部核心代码。使用框架为struts2,ibatis。
需求:连续输入错误密码5次后,账号进入锁定状态,不可登录。锁定状态将于1小时后,或者下一个自然日(0点)解除。
具体方法描述。
1.建一张表来记录用户id,错误登录次数,上次登录时间。(也可以在user表上添加这三个字段,这样会省去一个初始化的麻烦)
2.登录失败后,进行校验:若已经错误登录5次,返回特定编码以告知前台显示错误信息。若次数不足5次,错误登录次数+1,并返回正常错误信息。若距离上一次错误登录时间已经过了1小时或者到了第二天,错误次数置为1。
3.若密码正确,并可以正常登录,将错误次数置为0。若处于锁定中,返回指定错误信息。
代码
建表语句
create table log_pwd_err(login_id number primary key, err_num number default 0, last_date date default sysdate);
1
java代码
//参数的设置
private static int maxTime = 5; //连续5次后
private static double hour = 1; //一小时内不可登录。
/**
* 登录错误5次,一小时后不可登录
* @param loginId 登录id
* @param password 密码
* @param isJiaMi 对密码进行加密处理
* @return
* @throws DataAccessException
*/
public Map<String, Object> validateByLoginIdLoginErr(String loginId, String password,
boolean isJiaMi) throws DataAccessException {
Map<String, Object> resultMap = new HashMap(2);
User user = getByLoginId(loginId); //一个通过登录id获取用户实体类的方法,文中无源码
String encodePass = "";
if (isJiaMi && null != isJiaMi) {
encodePass = encrypt.encode(password); //对密码进行加密处理,文中无源码
}
if(null != user){ //如果登录id确实存在
if(user.getPassword().equals(encodePass)){ //并且密码输入正确
if(this.checkWhenPwdOk(user)){ //校验该用户是否可以登录
this.setErrZero(user); //如果可以确实的登录,则对表中错误登录次数置零
resultMap.put("user", user);
return resultMap;
}else {
resultMap.put("extMsg", "errMax"); //前台错误信息展现由上层代码实现。
return resultMap;
}
}else {
if(this.checkErrNum(user)){ //密码错误时的一系列操作
//返回true时,错误次数超过了5次,需要返回特定的错误信息
resultMap.put("extMsg", "errMax");
return resultMap;
}else {
//错误次数没有超过5次,返回一个空map,交由上层处理
return resultMap;
}
}
}
return resultMap;
}
private void setErrZero(User user){
//将指定id的错误次数置零
//client是操作数据库的方法,可以认为是SqlMapClientBuilder.buildSqlMapClient(Resources.getResourceAsReader("config/SqlMap.xml")); 这种东西。
this.client.update("login.err.updateForOk", user);
}
/**
* 即使密码正确,也不能登录
* @param user
* @return false 处于锁定中,无法登陆 true 可以正常登录
*/
private boolean checkWhenPwdOk(User user){
Map<String, String> daoMap = new HashMap();
daoMap.put("loginId", user.getLoginId() + "");
daoMap.put("maxTime", maxTime + "");
daoMap.put("hour", hour + "");
String currentNumStr = (String) this.client.queryForObject("login.err.checkWhenPwdOk", daoMap);
if(currentNumStr == null || currentNumStr.length() == 0){
return true; //err表中无数据,可以登录成功
}
if(Integer.valueOf(currentNumStr) >= maxTime){ //超过5次
return false;
}else {
return true;
}
}
//密码错误时的方法
private boolean checkErrNum(User user){
Map<String, String> daoMap = new HashMap();
daoMap.put("loginId", user.getLoginId() + "");
daoMap.put("maxTime", maxTime + "");
daoMap.put("hour", hour + "");
String currentNumStr = (String) this.client.queryForObject("login.err.getCurrentNum", daoMap);
if(currentNumStr == null || currentNumStr.length() == 0){
//错误信息表与用户表并不是同步的,如果是新建的用户,err表中将没有对应数据,需要插入一条新数据
this.client.insert("login.err.insertUser", daoMap);
currentNumStr = "0";
}
int currentNum = Integer.valueOf(currentNumStr);
if(currentNum >= maxTime){
//若次数超过了,将不会修改登陆时间。那样做会导致一小时的校验错误
return true;
}
//执行+1或=1的操作
this.client.update("login.err.updateForErr", daoMap);
return false;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
sql文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE sqlMap PUBLIC "-//iBATIS.com//DTD SQL Map 2.0//EN" "http://www.ibatis.com/dtd/sql-map-2.dtd">
<sqlMap namespace="login.err">
<select id="getCurrentNum" resultClass="java.lang.String">
select decode(trunc(t1.last_date), trunc(sysdate),
case when (sysdate - t1.last_date) * 24 > to_number(#hour#) and t1.err_num >= to_number(#maxTime#)
then 1 <!-- 如果达到5次并且又过了1小时,原则上应该返回1 -->
else t1.err_num end,
1) "flag" <!-- 过了一天还错也返回1 -->
from log_pwd_err t1
where t1.login_id = to_number(#loginId#)
</select>
<insert id="insertUser">
insert into log_pwd_err(login_id, err_num, last_date) values (#userId#, 1, sysdate)
</insert>
<update id="updateForOk">
update log_pwd_err t1
set t1.err_num = 0,
t1.last_date = sysdate
where t1.login_id = to_number(#loginId#)
and t1.err_num != 0
</update>
<update id="updateForErr">
update log_pwd_err t1
set t1.err_num = decode(trunc(t1.last_date), trunc(sysdate),
case when (sysdate - t1.last_date) * 24 > to_number(#hour#)
and t1.err_num >= to_number(#maxTime#)
then 1
else t1.err_num + 1 end,
1),
t1.last_date = sysdate
where t1.login_id = to_number(#loginId#)
</update>
<select id="checkWhenPwdOk" resultClass="java.lang.String">
select case
when trunc(t1.last_date) != trunc(sysdate) then
0
when to_number(#hour#) / 24 > sysdate - t1.last_date and t1.err_num >= to_number(#maxTime#) then
t1.err_num
else
0
end "flag"
from log_pwd_err t1
where t1.login_id = to_number(#loginId#)
</select>
</sqlMap>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
总结:使用sql去完成这种需求最大的好处是逻辑清晰,集中,安全,并且不会出现bug。整个功能连写带测试加部署只花了2小时,比写这篇文章都短……缺点就是会对数据库造成一点点额外的压力。