最近接手了登陆模块,要对它进行改造,由原来的用户名 + 密码 + 验证码登陆改为 用户名(手机号)+ 短信验证码登陆。
由于增加了短信验证码这项功能,为了防止被人恶意攻击,除了前台js进行限制外,后台也要进行限制,防止恶意请求,造成用户手机号泄露,恶意发送短信等等。
基于以上考虑,如何鉴别恶意访问提出了以下解决方案:
前端:
手机验证码的有效时长为:5分钟(测试时间为3分钟),发送验证码成功后界面倒计时时间为1分钟。(验证码存入redis,过期时间5分钟)
涉及到的账号冻结场景为:
30分钟内同一个ip输错6次不存在的手机号(发送验证码或登录时),冻结ip 30分钟,包括发送验证码冻结和登录冻结(测试时间为1分钟内错6次冻结1分钟)。
30分钟内正确手机号输错3次手机验证码,冻结手机号 30分钟,包括发送验证码冻结和登录冻结(测试时间为1分钟内错6次冻结1分钟)
涉及多客户端登录场景:
同一手机号在多个客户端登录,后登录的会踢出先登录的(先登录的在提交数据时会跳回到登录页面)
登录后会话超时时间:2小时。(测试时间为3分钟)
前台流程图:
前端关键代码——验证部分:
function initValidate() {
$("#login-form").validate({
debug: true,
onkeyup: false,
errorClass: "msg-error-tip",
errorPlacement: function (error, element) {
if (!$(".msg").html()) {
$(".msg").html("");
error.appendTo($(".msg"));
}
},
rules: {
telephone: {
required: true,
minlength:11,
digits:true
},
checkCode: {
required: true,
minlength:8,
digits:true
}
},
messages: {
telephone: {
required: "请输入手机号码",
minlength: "手机号必须{0}位",
digits: "手机号必须为数字"
},
checkCode: {
required: "请输入短信验证码",
minlength: "验证码必须{0}位",
digits: "验证码必须为数字"
}
}
});
}
前端关键代码——短信验证码部分:
var InterValObj; //timer变量,控制时间
var count = 60; //间隔函数,1秒执行
var curCount;//当前剩余秒数
var telephone;
var valid_rule = /(1[3456789]\d{9})$/;// 手机号码校验规则
function sendCheckCode() {
if ($("#btnSendCode").hasClass("notOk"))
{
return;
}
curCount = count;
// 设置button效果,开始计时
document.getElementById("btnSendCode").setAttribute("disabled", "true");//设置按钮为禁用状态
document.getElementById("btnSendCode").value = "请在" + curCount + "后再次获取";//更改按钮文字
InterValObj = window.setInterval(setRemainTime, 1000); // 启动计时器timer处理函数,1秒执行一次
var param = {};
param = getFormJson(".login-form");
post2(param, basePath + "/login/sendCheckCode", function (data, info) {
$(".msg").html(info);
$(".msg").show();
}, function (data, code, info) {
//登录失败
curCount = 0;
$(".msg").html(info);
$(".msg").show();
});
}
//timer处理函数
function setRemainTime() {
if (curCount == 0) {
window.clearInterval(InterValObj);// 停止计时器
document.getElementById("btnSendCode").removeAttribute("disabled");//移除禁用状态改为可用
document.getElementById("btnSendCode").value = "重新发送验证码";
} else {
curCount--;
document.getElementById("btnSendCode").value = "请在" + curCount + "秒后再次获取";
}
}
后台流程图——pc端:
后台流程图——app端:
后端关键代码——发送验证码部分:
public ResultVo < Object > sendSms(String telephone, String ipAddress, boolean isApp) {
ipAddress = ipAddress.replaceAll(":", ".");
//校验冻结信息
ResultVo < Object > validLockRs = validLock(ipAddress, telephone);
// ip和手机号冻结校验通过
if (null != validLockRs) {
return validLockRs;
}
// 手机号不在db中(非冻结)
if (!isPhoneInDb(telephone)) {
LockedRedisCache.ipCountIncr(ipAddress);
return new ResultVo < Object > (null, BaseFailedStatusEnum.OBJECT_NOTEXIST.getStateCode(), "账号不存在!");
}
// 验证码存在 则不重复发送
if (!StringUtils.isEmpty(LockedRedisCache.getValidcode(telephone, isApp))) {
return new ResultVo < Object > (null, BaseSuccessStatusEnum.SUCCESS.getStateCode(), "发送成功!");
}
String code = SendLoginCodeUtil.sendCode(telephone, ipAddress);
// 验证码发送失败
if (StringUtils.isEmpty(code)) {
return new ResultVo < Object > (null, BaseFailedStatusEnum.OP_INVALID.getStateCode(), "发送失败,请重试!");
}
// 验证码发送成功
LockedRedisCache.setValidcode(telephone, code, isApp);
return new ResultVo < Object > (null, BaseSuccessStatusEnum.SUCCESS.getStateCode(), "发送成功!");
后端关键代码——登陆部分:
public ResultVo<Object> signIn(String telephone, String ipAddress,
String password, boolean isApp)
{
ipAddress = ipAddress.replaceAll(":", ".");
// 校验冻结信息
ResultVo<Object> validLockRs = validLock(ipAddress, telephone);
if (null != validLockRs)
{
return validLockRs;
}
//根据手机号获取redis中对应的短信验证码
String checkCodeInRedis = LockedRedisCache.getValidcode(telephone, isApp);
// 手机号发过验证码已经失效
if (StringUtils.isEmpty(checkCodeInRedis))
{
LockedRedisCache.ipCountIncr(ipAddress);
return buildUserNotExitOrPwdError();
}
// 验证码错误
if (!checkCodeInRedis.equals(password))
{
//验证码输错,手机冻结列表次数+1
LockedRedisCache.telCountIncr(telephone);
return buildUserNotExitOrPwdError();
}
TokenVo token = null;
//app校验OK后还需要校验下权限信息,识别当前用户是否拥有登录权限
if (isApp)
{
//权限校验
if (!checkPerm(telephone))
{
LockedRedisCache.ipCountIncr(ipAddress);
return buildUserNotExitOrPwdError();
}
//生成令牌 并存入redis中
String tokenStr = LockedRedisCache.setToken(telephone, ipAddress);
token = new TokenVo();
token.setToken(tokenStr);
}
//清除验证码
LockedRedisCache.delValidcode(telephone, isApp);
return new ResultVo<Object>(token, BaseSuccessStatusEnum.SUCCESS.getStateCode(), "登录成功!");
}
注:代码中有许多已经封装好的方法,代码只是逻辑的实现。