失败次数大于系统规定阈值,开启验证码校验

v1.4.1
Parker 4 years ago
parent 2c774e628a
commit e98b06e7fe

@ -70,6 +70,7 @@ public class ShiroConfig {
filterMap.put("/druid/**", "anon"); filterMap.put("/druid/**", "anon");
filterMap.put("/app/**", "anon"); filterMap.put("/app/**", "anon");
filterMap.put("/sys/login", "anon"); filterMap.put("/sys/login", "anon");
filterMap.put("/sys/slipCount", "anon");
filterMap.put("/swagger/**", "anon"); filterMap.put("/swagger/**", "anon");
filterMap.put("/v2/api-docs", "anon"); filterMap.put("/v2/api-docs", "anon");
filterMap.put("/doc.html", "anon"); filterMap.put("/doc.html", "anon");

@ -43,6 +43,7 @@ public enum TokenMsg implements BaseMsg {
EXCEPTION_LOGOUT_SUCCESS(12104,"登出成功!"), EXCEPTION_LOGOUT_SUCCESS(12104,"登出成功!"),
EXCEPTION_LOGIN_ACCOUNT_LOCK(12104,"账号已锁定,请{}后,再次尝试"), EXCEPTION_LOGIN_ACCOUNT_LOCK(12104,"账号已锁定,请{}后,再次尝试"),
EXCEPTION_LOGIN_TENANT_NOT_USABLE(12105,"租户未启用,请联系管理员"), EXCEPTION_LOGIN_TENANT_NOT_USABLE(12105,"租户未启用,请联系管理员"),
EXCEPTION_LOGIN_NULL(12106,"请输入账号密码"),
/** /**
* *

@ -15,8 +15,11 @@
*/ */
package org.opsli.core.utils; package org.opsli.core.utils;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.DateTime; import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUnit;
import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@ -25,15 +28,20 @@ import org.opsli.api.base.result.ResultVo;
import org.opsli.api.wrapper.system.user.UserModel; import org.opsli.api.wrapper.system.user.UserModel;
import org.opsli.common.constants.SignConstants; import org.opsli.common.constants.SignConstants;
import org.opsli.common.constants.TokenConstants; import org.opsli.common.constants.TokenConstants;
import org.opsli.common.exception.ServiceException;
import org.opsli.common.exception.TokenException;
import org.opsli.common.utils.Props;
import org.opsli.core.msg.TokenMsg; import org.opsli.core.msg.TokenMsg;
import org.opsli.plugins.redis.RedisPlugin; import org.opsli.plugins.redis.RedisPlugin;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import java.util.Date;
import java.util.Map; import java.util.Map;
import static org.opsli.common.constants.OrderConstants.UTIL_ORDER; import static org.opsli.common.constants.OrderConstants.UTIL_ORDER;
@ -53,13 +61,31 @@ public class UserTokenUtil {
/** token 缓存名 */ /** token 缓存名 */
public static final String TOKEN_NAME = TokenConstants.ACCESS_TOKEN; public static final String TOKEN_NAME = TokenConstants.ACCESS_TOKEN;
/** 缓存前缀 */ /** 缓存前缀 */
private static final String PREFIX = "opsli:ticket:"; private static final String PREFIX = "opsli:ticket:";
/** 账号失败次数 */
public static final String ACCOUNT_SLIP_COUNT_PREFIX = "opsli:account:slip:count:";
/** 账号失败锁定KEY */
public static final String ACCOUNT_SLIP_LOCK_PREFIX = "opsli:account:slip:lock:";
/** 账号失败阈值 */
public static final int ACCOUNT_SLIP_COUNT;
/** 账号失败N次后弹出验证码 */
public static final int ACCOUNT_SLIP_VERIFY_COUNT;
/** 账号锁定时间 */
public static final int ACCOUNT_SLIP_LOCK_SPEED;
/** Redis插件 */ /** Redis插件 */
private static RedisPlugin redisPlugin; private static RedisPlugin redisPlugin;
static {
Props props = new Props("application.yaml");
ACCOUNT_SLIP_COUNT = props.getInt("opsli.login.slip-count", 5);
ACCOUNT_SLIP_VERIFY_COUNT = props.getInt("opsli.login.slip-verify-count", 3);
ACCOUNT_SLIP_LOCK_SPEED = props.getInt("opsli.login.slip-lock-speed", 300);
}
/** /**
* user Token * user Token
* @param user * @param user
@ -196,6 +222,95 @@ public class UserTokenUtil {
return true; return true;
} }
// ============================ 锁账号 操作
/**
*
* @param username
*/
public static void verifyLockAccount(String username){
// 判断账号是否临时锁定
Long loseTimeMillis = (Long) redisPlugin.get(ACCOUNT_SLIP_LOCK_PREFIX + username);
if(loseTimeMillis != null){
Date currDate = new Date();
DateTime loseDate = DateUtil.date(loseTimeMillis);
// 偏移5分钟
DateTime currLoseDate = DateUtil.offsetSecond(loseDate, ACCOUNT_SLIP_LOCK_SPEED);
// 计算失效剩余时间( 分 )
long betweenM = DateUtil.between(currLoseDate, currDate, DateUnit.MINUTE);
if(betweenM > 0){
String msg = StrUtil.format(TokenMsg.EXCEPTION_LOGIN_ACCOUNT_LOCK.getMessage()
,betweenM + "分钟");
throw new TokenException(TokenMsg.EXCEPTION_LOGIN_ACCOUNT_LOCK.getCode(), msg);
}else{
// 计算失效剩余时间( 秒 )
long betweenS = DateUtil.between(currLoseDate, currDate, DateUnit.SECOND);
String msg = StrUtil.format(TokenMsg.EXCEPTION_LOGIN_ACCOUNT_LOCK.getMessage()
,betweenS + "秒");
throw new TokenException(TokenMsg.EXCEPTION_LOGIN_ACCOUNT_LOCK.getCode(), msg);
}
}
}
/**
*
* @param username
*/
public static ResultVo<?> lockAccount(String username){
// 如果失败次数 超过阈值 则锁定账号
Long slipNum = redisPlugin.increment(ACCOUNT_SLIP_COUNT_PREFIX + username);
if (slipNum != null){
// 设置失效时间为 5分钟
redisPlugin.expire(ACCOUNT_SLIP_COUNT_PREFIX + username, ACCOUNT_SLIP_LOCK_SPEED);
// 如果确认 都失败 则存入临时缓存
if(slipNum >= ACCOUNT_SLIP_COUNT){
long currentTimeMillis = System.currentTimeMillis();
// 存入Redis
redisPlugin.put(ACCOUNT_SLIP_LOCK_PREFIX + username,
currentTimeMillis, ACCOUNT_SLIP_LOCK_SPEED);
}
}
Map<String,Boolean> flagMap = Maps.newHashMap();
flagMap.put("izVerify", false);
if(slipNum != null && slipNum >= ACCOUNT_SLIP_VERIFY_COUNT){
flagMap.put("izVerify", true);
}
return ResultVo.error(TokenMsg.EXCEPTION_LOGIN_ACCOUNT_NO.getCode(),
TokenMsg.EXCEPTION_LOGIN_ACCOUNT_NO.getMessage(),
flagMap
);
}
/**
*
* @param username
*/
public static long getSlipCount(String username){
long count = 0L;
Object obj = redisPlugin.get(ACCOUNT_SLIP_COUNT_PREFIX + username);
if(obj != null){
try {
count = Convert.convert(Long.class, obj);
}catch (Exception ignored){}
}
return count;
}
/**
*
* @param username
*/
public static void clearLockAccount(String username){
// 删除失败次数记录
redisPlugin.del(ACCOUNT_SLIP_COUNT_PREFIX + username);
// 删除失败次数记录
redisPlugin.del(ACCOUNT_SLIP_LOCK_PREFIX + username);
}
// ========================== // ==========================

@ -15,11 +15,9 @@
*/ */
package org.opsli.modulars.system.login.web; package org.opsli.modulars.system.login.web;
import cn.hutool.core.date.DateTime; import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.DateUnit;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.google.common.collect.Maps;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@ -27,6 +25,7 @@ import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.opsli.api.base.result.ResultVo; import org.opsli.api.base.result.ResultVo;
import org.opsli.api.wrapper.system.user.UserModel; import org.opsli.api.wrapper.system.user.UserModel;
import org.opsli.common.api.TokenThreadLocal; import org.opsli.common.api.TokenThreadLocal;
import org.opsli.common.exception.TokenException;
import org.opsli.common.utils.IPUtil; import org.opsli.common.utils.IPUtil;
import org.opsli.core.msg.TokenMsg; import org.opsli.core.msg.TokenMsg;
import org.opsli.core.persistence.querybuilder.GenQueryBuilder; import org.opsli.core.persistence.querybuilder.GenQueryBuilder;
@ -38,9 +37,7 @@ import org.opsli.modulars.system.login.entity.LoginForm;
import org.opsli.modulars.system.tenant.entity.SysTenant; import org.opsli.modulars.system.tenant.entity.SysTenant;
import org.opsli.modulars.system.tenant.service.ITenantService; import org.opsli.modulars.system.tenant.service.ITenantService;
import org.opsli.modulars.system.user.service.IUserService; import org.opsli.modulars.system.user.service.IUserService;
import org.opsli.plugins.redis.RedisPlugin;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
@ -52,7 +49,6 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.IOException; import java.io.IOException;
import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -69,93 +65,52 @@ import java.util.Map;
@RestController @RestController
public class LoginRestController { public class LoginRestController {
/** 账号失败次数 */
private static final String ACCOUNT_SLIP_COUNT_PREFIX = "opsli:account:slip:count:";
/** 账号失败锁定KEY */
private static final String ACCOUNT_SLIP_LOCK_PREFIX = "opsli:account:slip:lock:";
/** 失败阈值 */
@Value("${opsli.login.slip-count}")
private int slipCount;
/** 锁定时间 */
@Value("${opsli.login.slip-lock-speed}")
private int slipLockSpeed;
@Autowired
private RedisPlugin redisPlugin;
@Autowired @Autowired
private ITenantService iTenantService; private ITenantService iTenantService;
@Autowired @Autowired
private IUserService iUserService; private IUserService iUserService;
/** /**
* *
*/ */
@ApiOperation(value = "登录", notes = "登录") @ApiOperation(value = "登录", notes = "登录")
@PostMapping("/sys/login") @PostMapping("/sys/login")
public ResultVo<?> login(@RequestBody LoginForm form, HttpServletRequest request){ public ResultVo<?> login(@RequestBody LoginForm form, HttpServletRequest request){
if(form == null){
throw new TokenException(TokenMsg.EXCEPTION_LOGIN_NULL);
}
// 判断账号是否临时锁定
UserTokenUtil.verifyLockAccount(form.getUsername());
// 获得当前失败次数
long slipCount = UserTokenUtil.getSlipCount(form.getUsername());
// 失败次数超过 验证次数阈值 开启验证码验证
if(slipCount >= UserTokenUtil.ACCOUNT_SLIP_VERIFY_COUNT){
boolean captcha = CaptchaUtil.validate(form.getUuid(), form.getCaptcha()); boolean captcha = CaptchaUtil.validate(form.getUuid(), form.getCaptcha());
// 验证码不正确 // 验证码不正确
if(!captcha){ if(!captcha){
return ResultVo.error(TokenMsg.EXCEPTION_LOGIN_CAPTCHA.getCode(), throw new TokenException(TokenMsg.EXCEPTION_LOGIN_CAPTCHA);
TokenMsg.EXCEPTION_LOGIN_CAPTCHA.getMessage()); }
} }
// 判断账号是否临时锁定 // 用户信息
Long loseTimeMillis = (Long) redisPlugin.get(ACCOUNT_SLIP_LOCK_PREFIX + form.getUsername());
if(loseTimeMillis != null){
Date currDate = new Date();
DateTime loseDate = DateUtil.date(loseTimeMillis);
// 偏移5分钟
DateTime currLoseDate = DateUtil.offsetSecond(loseDate, slipLockSpeed);
// 计算失效剩余时间( 分 )
long betweenM = DateUtil.between(currLoseDate, currDate, DateUnit.MINUTE);
if(betweenM > 0){
String msg = StrUtil.format(TokenMsg.EXCEPTION_LOGIN_ACCOUNT_LOCK.getMessage()
,betweenM + "分钟");
return ResultVo.error(TokenMsg.EXCEPTION_LOGIN_ACCOUNT_LOCK.getCode(),
msg);
}else{
// 计算失效剩余时间( 秒 )
long betweenS = DateUtil.between(currLoseDate, currDate, DateUnit.SECOND);
String msg = StrUtil.format(TokenMsg.EXCEPTION_LOGIN_ACCOUNT_LOCK.getMessage()
,betweenS + "秒");
return ResultVo.error(TokenMsg.EXCEPTION_LOGIN_ACCOUNT_LOCK.getCode(),
msg);
}
}
//用户信息
UserModel user = UserUtil.getUserByUserName(form.getUsername()); UserModel user = UserUtil.getUserByUserName(form.getUsername());
//账号不存在、密码错误 // 账号不存在、密码错误
if(user == null || if(user == null ||
!user.getPassword().equals(UserUtil.handlePassword(form.getPassword(), user.getSecretkey()))) { !user.getPassword().equals(UserUtil.handlePassword(form.getPassword(), user.getSecretkey()))) {
// 判断是否需要锁定账号 这里没有直接抛异常 而是返回错误信息, 其中包含 是否开启验证码状态
// 如果失败次数 超过阈值 则锁定账号 return UserTokenUtil.lockAccount(form.getUsername());
Long slipNum = redisPlugin.increment(ACCOUNT_SLIP_COUNT_PREFIX + form.getUsername());
// 设置失效时间为 5分钟
redisPlugin.expire(ACCOUNT_SLIP_COUNT_PREFIX + form.getUsername(),slipLockSpeed);
// 如果确认 都失败 则存入临时缓存
if(slipNum >= slipCount){
long currentTimeMillis = System.currentTimeMillis();
// 存入Redis
redisPlugin.put(ACCOUNT_SLIP_LOCK_PREFIX + form.getUsername(),
currentTimeMillis, slipLockSpeed);
} }
return ResultVo.error(TokenMsg.EXCEPTION_LOGIN_ACCOUNT_NO.getMessage()); // 如果验证成功, 则清除锁定信息
} UserTokenUtil.clearLockAccount(form.getUsername());
// 删除失败次数记录 // 账号锁定
redisPlugin.del(ACCOUNT_SLIP_COUNT_PREFIX + form.getUsername());
//账号锁定
if(user.getLocked() == 1){ if(user.getLocked() == 1){
return ResultVo.error(TokenMsg.EXCEPTION_LOGIN_ACCOUNT_LOCKED.getMessage()); throw new TokenException(TokenMsg.EXCEPTION_LOGIN_ACCOUNT_LOCKED);
} }
// 如果不是超级管理员 需要验证租户是否生效 // 如果不是超级管理员 需要验证租户是否生效
@ -166,12 +121,15 @@ public class LoginRestController {
.eq("iz_usable", "1"); .eq("iz_usable", "1");
List<SysTenant> tenants = iTenantService.findList(queryWrapper); List<SysTenant> tenants = iTenantService.findList(queryWrapper);
if(tenants == null || tenants.isEmpty()){ if(tenants == null || tenants.isEmpty()){
return ResultVo.error(TokenMsg.EXCEPTION_LOGIN_TENANT_NOT_USABLE.getMessage()); throw new TokenException(TokenMsg.EXCEPTION_LOGIN_TENANT_NOT_USABLE);
} }
} }
// 失败次数超过 验证次数阈值 开启验证码验证
if(slipCount >= UserTokenUtil.ACCOUNT_SLIP_VERIFY_COUNT){
// 删除验证过后验证码 // 删除验证过后验证码
CaptchaUtil.delCaptcha(form.getUuid()); CaptchaUtil.delCaptcha(form.getUuid());
}
//生成token并保存到Redis //生成token并保存到Redis
ResultVo<Map<String, Object>> resultVo = UserTokenUtil.createToken(user); ResultVo<Map<String, Object>> resultVo = UserTokenUtil.createToken(user);
@ -183,8 +141,8 @@ public class LoginRestController {
String clientIpAddress = IPUtil.getClientIpAddress(request); String clientIpAddress = IPUtil.getClientIpAddress(request);
user.setLoginIp(clientIpAddress); user.setLoginIp(clientIpAddress);
iUserService.updateLoginIp(user); iUserService.updateLoginIp(user);
}catch (Exception ignored){} }catch (Exception ignored){
finally { }finally {
// 清空 token缓存 // 清空 token缓存
TokenThreadLocal.remove(); TokenThreadLocal.remove();
} }
@ -209,6 +167,21 @@ public class LoginRestController {
return ResultVo.success(TokenMsg.EXCEPTION_LOGOUT_SUCCESS.getMessage()); return ResultVo.success(TokenMsg.EXCEPTION_LOGOUT_SUCCESS.getMessage());
} }
/**
*
*/
@ApiOperation(value = "获得当前登录失败次数", notes = "获得当前登录失败次数")
@GetMapping("/sys/slipCount")
public ResultVo<?> slipCount(String username){
// 获得当前失败次数
long slipCount = UserTokenUtil.getSlipCount(username);
Map<String, Object> ret = Maps.newHashMap();
ret.put("base", UserTokenUtil.ACCOUNT_SLIP_VERIFY_COUNT);
ret.put("curr", slipCount);
return ResultVo.success(ret);
}
/** /**
* *
*/ */

@ -180,6 +180,8 @@ opsli:
login: login:
# 失败次数 # 失败次数
slip-count: 5 slip-count: 5
# 失败N次后弹出验证码 (超过验证码阈值 弹出验证码)
slip-verify-count: 3
# 失败锁定时间(秒) # 失败锁定时间(秒)
slip-lock-speed: 300 slip-lock-speed: 300

Loading…
Cancel
Save