From e98b06e7fea7951cd776ba8a4fab5ffe64e78a07 Mon Sep 17 00:00:00 2001 From: Parker Date: Sun, 6 Dec 2020 19:17:39 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=B1=E8=B4=A5=E6=AC=A1=E6=95=B0=E5=A4=A7?= =?UTF-8?q?=E4=BA=8E=E7=B3=BB=E7=BB=9F=E8=A7=84=E5=AE=9A=E9=98=88=E5=80=BC?= =?UTF-8?q?,=E5=BC=80=E5=90=AF=E9=AA=8C=E8=AF=81=E7=A0=81=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/opsli/core/conf/ShiroConfig.java | 1 + .../java/org/opsli/core/msg/TokenMsg.java | 1 + .../org/opsli/core/utils/UserTokenUtil.java | 117 ++++++++++++++++- .../system/login/web/LoginRestController.java | 121 +++++++----------- .../src/main/resources/application.yaml | 2 + 5 files changed, 167 insertions(+), 75 deletions(-) diff --git a/opsli-base-support/opsli-core/src/main/java/org/opsli/core/conf/ShiroConfig.java b/opsli-base-support/opsli-core/src/main/java/org/opsli/core/conf/ShiroConfig.java index adadee9..d7972f7 100644 --- a/opsli-base-support/opsli-core/src/main/java/org/opsli/core/conf/ShiroConfig.java +++ b/opsli-base-support/opsli-core/src/main/java/org/opsli/core/conf/ShiroConfig.java @@ -70,6 +70,7 @@ public class ShiroConfig { filterMap.put("/druid/**", "anon"); filterMap.put("/app/**", "anon"); filterMap.put("/sys/login", "anon"); + filterMap.put("/sys/slipCount", "anon"); filterMap.put("/swagger/**", "anon"); filterMap.put("/v2/api-docs", "anon"); filterMap.put("/doc.html", "anon"); diff --git a/opsli-base-support/opsli-core/src/main/java/org/opsli/core/msg/TokenMsg.java b/opsli-base-support/opsli-core/src/main/java/org/opsli/core/msg/TokenMsg.java index a626b7f..b4ab31d 100644 --- a/opsli-base-support/opsli-core/src/main/java/org/opsli/core/msg/TokenMsg.java +++ b/opsli-base-support/opsli-core/src/main/java/org/opsli/core/msg/TokenMsg.java @@ -43,6 +43,7 @@ public enum TokenMsg implements BaseMsg { EXCEPTION_LOGOUT_SUCCESS(12104,"登出成功!"), EXCEPTION_LOGIN_ACCOUNT_LOCK(12104,"账号已锁定,请{}后,再次尝试"), EXCEPTION_LOGIN_TENANT_NOT_USABLE(12105,"租户未启用,请联系管理员"), + EXCEPTION_LOGIN_NULL(12106,"请输入账号密码"), /** * 其他 diff --git a/opsli-base-support/opsli-core/src/main/java/org/opsli/core/utils/UserTokenUtil.java b/opsli-base-support/opsli-core/src/main/java/org/opsli/core/utils/UserTokenUtil.java index c6489e0..5270c68 100644 --- a/opsli-base-support/opsli-core/src/main/java/org/opsli/core/utils/UserTokenUtil.java +++ b/opsli-base-support/opsli-core/src/main/java/org/opsli/core/utils/UserTokenUtil.java @@ -15,8 +15,11 @@ */ package org.opsli.core.utils; +import cn.hutool.core.convert.Convert; import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUnit; import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; import com.google.common.collect.Maps; import lombok.extern.slf4j.Slf4j; 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.common.constants.SignConstants; 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.plugins.redis.RedisPlugin; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.context.annotation.Lazy; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; +import java.util.Date; import java.util.Map; import static org.opsli.common.constants.OrderConstants.UTIL_ORDER; @@ -53,13 +61,31 @@ public class UserTokenUtil { /** token 缓存名 */ public static final String TOKEN_NAME = TokenConstants.ACCESS_TOKEN; - /** 缓存前缀 */ 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插件 */ 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 * @param user @@ -196,6 +222,95 @@ public class UserTokenUtil { 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 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); + } + // ========================== diff --git a/opsli-modulars/opsli-modulars-system/src/main/java/org/opsli/modulars/system/login/web/LoginRestController.java b/opsli-modulars/opsli-modulars-system/src/main/java/org/opsli/modulars/system/login/web/LoginRestController.java index c52296d..678e2b2 100644 --- a/opsli-modulars/opsli-modulars-system/src/main/java/org/opsli/modulars/system/login/web/LoginRestController.java +++ b/opsli-modulars/opsli-modulars-system/src/main/java/org/opsli/modulars/system/login/web/LoginRestController.java @@ -15,11 +15,9 @@ */ package org.opsli.modulars.system.login.web; -import cn.hutool.core.date.DateTime; -import cn.hutool.core.date.DateUnit; -import cn.hutool.core.date.DateUtil; -import cn.hutool.core.util.StrUtil; +import cn.hutool.core.convert.Convert; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.google.common.collect.Maps; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; 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.wrapper.system.user.UserModel; import org.opsli.common.api.TokenThreadLocal; +import org.opsli.common.exception.TokenException; import org.opsli.common.utils.IPUtil; import org.opsli.core.msg.TokenMsg; 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.service.ITenantService; 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.Value; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -52,7 +49,6 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.awt.image.BufferedImage; import java.io.IOException; -import java.util.Date; import java.util.List; import java.util.Map; @@ -69,93 +65,52 @@ import java.util.Map; @RestController 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 private ITenantService iTenantService; @Autowired private IUserService iUserService; - /** * 登录 */ @ApiOperation(value = "登录", notes = "登录") @PostMapping("/sys/login") public ResultVo login(@RequestBody LoginForm form, HttpServletRequest request){ - boolean captcha = CaptchaUtil.validate(form.getUuid(), form.getCaptcha()); - // 验证码不正确 - if(!captcha){ - return ResultVo.error(TokenMsg.EXCEPTION_LOGIN_CAPTCHA.getCode(), - TokenMsg.EXCEPTION_LOGIN_CAPTCHA.getMessage()); + if(form == null){ + throw new TokenException(TokenMsg.EXCEPTION_LOGIN_NULL); } // 判断账号是否临时锁定 - 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); + 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()); + // 验证码不正确 + if(!captcha){ + throw new TokenException(TokenMsg.EXCEPTION_LOGIN_CAPTCHA); } } - //用户信息 + // 用户信息 UserModel user = UserUtil.getUserByUserName(form.getUsername()); - //账号不存在、密码错误 + // 账号不存在、密码错误 if(user == null || !user.getPassword().equals(UserUtil.handlePassword(form.getPassword(), user.getSecretkey()))) { - - // 如果失败次数 超过阈值 则锁定账号 - 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()); + // 判断是否需要锁定账号 这里没有直接抛异常 而是返回错误信息, 其中包含 是否开启验证码状态 + return UserTokenUtil.lockAccount(form.getUsername()); } - // 删除失败次数记录 - redisPlugin.del(ACCOUNT_SLIP_COUNT_PREFIX + form.getUsername()); + // 如果验证成功, 则清除锁定信息 + UserTokenUtil.clearLockAccount(form.getUsername()); - //账号锁定 + // 账号锁定 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"); List tenants = iTenantService.findList(queryWrapper); if(tenants == null || tenants.isEmpty()){ - return ResultVo.error(TokenMsg.EXCEPTION_LOGIN_TENANT_NOT_USABLE.getMessage()); + throw new TokenException(TokenMsg.EXCEPTION_LOGIN_TENANT_NOT_USABLE); } } - // 删除验证过后验证码 - CaptchaUtil.delCaptcha(form.getUuid()); + // 失败次数超过 验证次数阈值 开启验证码验证 + if(slipCount >= UserTokenUtil.ACCOUNT_SLIP_VERIFY_COUNT){ + // 删除验证过后验证码 + CaptchaUtil.delCaptcha(form.getUuid()); + } //生成token,并保存到Redis ResultVo> resultVo = UserTokenUtil.createToken(user); @@ -183,8 +141,8 @@ public class LoginRestController { String clientIpAddress = IPUtil.getClientIpAddress(request); user.setLoginIp(clientIpAddress); iUserService.updateLoginIp(user); - }catch (Exception ignored){} - finally { + }catch (Exception ignored){ + }finally { // 清空 token缓存 TokenThreadLocal.remove(); } @@ -209,6 +167,21 @@ public class LoginRestController { 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 ret = Maps.newHashMap(); + ret.put("base", UserTokenUtil.ACCOUNT_SLIP_VERIFY_COUNT); + ret.put("curr", slipCount); + return ResultVo.success(ret); + } + + /** * 验证码 */ diff --git a/opsli-starter/src/main/resources/application.yaml b/opsli-starter/src/main/resources/application.yaml index 6efd464..0f59858 100644 --- a/opsli-starter/src/main/resources/application.yaml +++ b/opsli-starter/src/main/resources/application.yaml @@ -180,6 +180,8 @@ opsli: login: # 失败次数 slip-count: 5 + # 失败N次后弹出验证码 (超过验证码阈值 弹出验证码) + slip-verify-count: 3 # 失败锁定时间(秒) slip-lock-speed: 300