登陆改造案例分享

    最近接手了登陆模块,要对它进行改造,由原来的用户名 + 密码 + 验证码登陆改为 用户名(手机号)+ 短信验证码登陆。

    由于增加了短信验证码这项功能,为了防止被人恶意攻击,除了前台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(), "登录成功!");
}

注:代码中有许多已经封装好的方法,代码只是逻辑的实现。