java实现用户登录异常统计、锁定及解锁功能

写在前面

        现在很多互联网项目、app等都会有登录异常提醒、登录异常次数限制,基本都是5次异常后就会锁定一定时间的账户,让该账户无法进行登录操作。需要注册用户使用安全验证手段(如动态验证码等),解除锁定后才能进行新一轮登录操作。这么做无非就是两个目的:

1. 为了注册用户的账户安全;

2. 更多的是防止黑客攻击,维护网站等的安全。

项目需求

        项目开发结束,进入测试阶段,很多项目或者开发就进入到了边测试边修改bug的阶段了。不过,我们公司的项目还需要进行安全测试,对不符合要求的项目需要进行安全隐患的整改。这不,我就“被迫”的研究起了如何避免被无差别攻击,防止通过无限攻击次数、穷举等破解密码的手段了。

整体思路

1. 验证及提示

获取并验证账号登录异常信息,提示登录异常。代码片段如下

        // 验证账户是否封锁
        String usercode = userLoginDto.getUsercode();
        ValueOperations<String, String> opsForValue = stringRedisTemplate.opsForValue();
        // 如果这个账号登录异常,则在登录页面提醒。
        String shiroLoginCount = opsForValue.get(SHIRO_LOGIN_COUNT + usercode);
        if (!StringUtils.isEmpty(shiroLoginCount) && Integer.parseInt(shiroLoginCount) >= 10) {
            if ("LOCK".equals(opsForValue.get(SHIRO_IS_LOCK + usercode))) {
                // 计数大于10次,设置用户被锁定5分钟
                String msg = "由于输入错误次数大于10次,帐号5分钟内已经禁止登录!";
                log.info(msg);
                return PlatformResult.failure(msg);
            }
        }

2. 计数

用户的每次异常登录(其实就是密码错误)时,将账户及错误次数记录到redis中。代码片段如下

            // 登录失败计数
            opsForValue.increment(SHIRO_LOGIN_COUNT + usercode, 1); // 每次增加1
            log.info(usercode + ":账号登录异常次数:" + opsForValue.get(SHIRO_LOGIN_COUNT + usercode));

3. 检查及限制

检查累积阈值错误次数(本系统设置为10次)时,锁定该用户,并限制一定时间内(本系统设置为5分钟)无法进行登录操作。代码片段如下

            // 登录异常次数超限实现锁定
            if (Integer.parseInt(opsForValue.get(SHIRO_LOGIN_COUNT + usercode)) >= 10) {
                opsForValue.set(SHIRO_IS_LOCK + usercode, "LOCK"); // 锁住这个账号,值是LOCK。
                stringRedisTemplate.expire(SHIRO_IS_LOCK + usercode, 5, TimeUnit.MINUTES); // expire 变量存活期限
            }

4. 解锁及清空

在错误10次(可根据业务需要进行调整)前登录成功,则解锁该用户异常状态,清空登录错误次数和解锁账户。代码片段如下

    /**
     * 清空登录计数
     * 
     * @author: caip
     * @date: 2021-04-07 15:45:57
     * @param userName
     */
    private void clearLoginCount(String userName) {
        log.info("UserLoginServiceImpl clearLoginCount start");
        ValueOperations<String, String> opsForValue = stringRedisTemplate.opsForValue();
        // 清空登录计数
        opsForValue.set(SHIRO_LOGIN_COUNT + userName, "0");
        // 清空锁
        opsForValue.set(SHIRO_IS_LOCK + userName, "");
        log.info("UserLoginServiceImpl clearLoginCount end");
    }

整体代码

1. 参数对象

用于前后交互传参

package cn.xxx.rdc.fi.dto;

import lombok.Getter;
import lombok.Setter;

/**
 * @author xxx
 * @date: 2021-02-25 10:54:02
 * @Copyright: Copyright (c) 2006 - 2021
 * @Company: 公司
 * @Version: V1.0
 */
@Getter
@Setter
public class UserLoginDto {

    /** 账号. */
    private String usercode;

    /** 密码. */
    private String password;

    /** 记住密码. */
    private String remarkid;

    /** 用户类型. */
    private Integer userType;

    /** 微信userid. */
    private String wxuserid;

    /** 是否加密:1.是;0.否. */
    private String isEncryption;
}

2. 接口

定义交互标准

    @Autowired
    private IUserLoginService userLoginService;

    @ApiOperation(value = "用户登录", notes = "用户登录")
    @PostMapping("/userLogin")
    public PlatformResult<JSONObject> userLogin(@RequestBody UserLoginDto userLoginDto, ServletResponse response) {
        return userLoginService.userLogin(userLoginDto, response);
    }

3. 服务定义

定义服务标准

    /**
     * 用户登录
     * 
     * @author: xxx
     * @date: 2021-02-24 17:17:23
     * @param userLoginDto 用户登录对象
     * @return
     */
    PlatformResult<JSONObject> userLogin(UserLoginDto userLoginDto, ServletResponse response);

4. 服务实现

定义服务具体实现

package cn.xxx.rdc.knowledge.service.impl;

import java.util.concurrent.TimeUnit;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;

import com.alibaba.druid.util.StringUtils;
import com.alibaba.fastjson.JSONObject;

import cn.xxx.BootComm.Contants;
import cn.xxx.BootComm.utils.PlatformResult;
import cn.xxx.rdc.knowledge.dto.UserLoginDto;
import cn.xxx.rdc.knowledge.service.IUserService;
import cn.xxx.rdc.knowledge.utils.HttpClientUtil;
import cn.xxx.rdc.knowledge.utils.HttpRequestUtil;
import cn.xxx.rdc.knowledge.utils.RSAUtil;
import lombok.extern.slf4j.Slf4j;

/**
 * 用户登录服务类
 * 
 * @author xxx
 * @date: 2021-02-24 17:17:53
 * @Copyright: Copyright (c) 2006 - 2021
 * @Company: xxx
 * @Version: V1.0
 */
@Slf4j
@Service
public class UserServiceImpl implements IUserService {

    @Value("${user.login.url}")
    private String userLoginUrl;

    @Value("${sso.verifyUrl}")
    private String ssoVerifyUrl;

    @Value("${sso.defaultPrikey}")
    private String ssoDefaultPrikey;

    @Value("${user.logout.url}")
    private String userLogoutUrl;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    // 用户登录次数计数 redisKey 前缀
    private final String SHIRO_LOGIN_COUNT = "kb_shiro_login_count_";
    // 用户登录是否被锁定 redisKey 前缀
    private final String SHIRO_IS_LOCK = "kb_shiro_is_lock_";

    @Override
    public PlatformResult<JSONObject> userLogin(UserLoginDto userLoginDto, ServletResponse response) {
        log.info("UserLoginServiceImpl userLogin start");

        // 获取HttpServletRequest、HttpServletResponse
        HttpServletResponse res = (HttpServletResponse)response;

        // 验证账户是否封锁
        String usercode = userLoginDto.getUsercode();
        ValueOperations<String, String> opsForValue = stringRedisTemplate.opsForValue();
        // 如果这个账号登录异常,则在登录页面提醒。
        String shiroLoginCount = opsForValue.get(SHIRO_LOGIN_COUNT + usercode);
        if (!StringUtils.isEmpty(shiroLoginCount) && Integer.parseInt(shiroLoginCount) >= 10) {
            if ("LOCK".equals(opsForValue.get(SHIRO_IS_LOCK + usercode))) {
                // 计数大于10次,设置用户被锁定5分钟
                String msg = "由于输入错误次数大于10次,帐号5分钟内已经禁止登录!";
                log.info(msg);
                return PlatformResult.failure(msg);
            }
        }

        // 未封锁,则进行登录请求
        userLoginDto.setIsEncryption("1");
        String params = JSONObject.toJSONString(userLoginDto);
        log.info("userLogin urlStr=[" + userLoginUrl + "]");
        log.info("userLogin params=[" + params.toString() + "]");
        // 调用登录http请求
        String result = HttpRequestUtil.httpCrossDomain(userLoginUrl, params);

        // 获取登录返回参数
        JSONObject resultJson = JSONObject.parseObject(result);
        int statusCode = resultJson.getIntValue("statusCode");
        boolean success = resultJson.getBooleanValue("success");
        // 判断是否登录成功
        String loginMsg = resultJson.getString("message");
        if (statusCode != 200 || !success) {
            // 登录失败计数
            opsForValue.increment(SHIRO_LOGIN_COUNT + usercode, 1); // 每次增加1
            log.info(usercode + ":账号登录异常次数:" + opsForValue.get(SHIRO_LOGIN_COUNT + usercode));

            // 登录异常次数超限实现锁定
            if (Integer.parseInt(opsForValue.get(SHIRO_LOGIN_COUNT + usercode)) >= 10) {
                opsForValue.set(SHIRO_IS_LOCK + usercode, "LOCK"); // 锁住这个账号,值是LOCK。
                stringRedisTemplate.expire(SHIRO_IS_LOCK + usercode, 5, TimeUnit.MINUTES); // expire 变量存活期限
            }
            return PlatformResult.failure(loginMsg);
        }

        // 登录成功解锁
        this.clearLoginCount(usercode);

        // 查询本系统用户信息
        JSONObject userObject = resultJson.getJSONObject("object");
        // 判断是否免密登录
        String tokenEncryption = userObject.getString("token");
        log.info("tokenEncryption=" + tokenEncryption);
        // 判断是否调用免密登录
        if (!StringUtils.isEmpty(tokenEncryption) && 36 < tokenEncryption.length()) {
            // 通过加密token,解密后调用sso接口获取用户信息
            String token = RSAUtil.decrypt(ssoDefaultPrikey, tokenEncryption);
            log.info("token=" + token);
            // 调用登录http请求
            String userResult = HttpClientUtil.getInstance().sendHttpGet(ssoVerifyUrl + "?token=" + token);
            log.info("userResult=" + userResult);

            // 获取登录返回参数
            resultJson = JSONObject.parseObject(userResult);
            String code = resultJson.getString("code");
            // 判断是否登录成功
            loginMsg = resultJson.getString("msg");
            if (!"0".equals(code)) {
                return PlatformResult.failure(loginMsg);
            }

            userObject = resultJson.getJSONObject("uid");
        }

        // 设置cookie中的THPMSCookie为登录用户的token
        res.addCookie(new Cookie(Contants.COOKIE_NAME, userObject.getString("token")));

        log.info("UserLoginServiceImpl userLogin result=[" + userObject.toString() + "]");
        log.info("UserLoginServiceImpl userLogin end");
        return PlatformResult.success(userObject);
    }

    /**
     * 清空登录计数
     * 
     * @author: caip
     * @date: 2021-04-07 15:45:57
     * @param userName
     */
    private void clearLoginCount(String userName) {
        log.info("UserLoginServiceImpl clearLoginCount start");
        ValueOperations<String, String> opsForValue = stringRedisTemplate.opsForValue();
        // 清空登录计数
        opsForValue.set(SHIRO_LOGIN_COUNT + userName, "0");
        // 清空锁
        opsForValue.set(SHIRO_IS_LOCK + userName, "");
        log.info("UserLoginServiceImpl clearLoginCount end");
    }

}

补充说明

        以上代码有部分业务代码,整合了自用的SSO(单点登录系统)用于验证账户密码正确性,使用时只需要关心锁定和解锁功能,其他代码直接删除或替换为业务代码即可。

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值