登录终端限制

v1.4.1
Parker 5 years ago
parent a584a50ab7
commit 97db30d2b1

@ -9,9 +9,10 @@ package org.opsli.common.enums;
public enum AlertType {
/** alert 弹出 */
ALERT(),
ALERT,
/** JSON 回推*/
JSON,
;
}

@ -0,0 +1,40 @@
/**
* Copyright 2020 OPSLI https://www.opsli.com
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package org.opsli.common.enums;
/**
* @BelongsProject: opsli-boot
* @BelongsPackage: org.opsli.common.enums
* @Author: Parker
* @CreateTime: 2020-09-17 23:40
* @Description:
*/
public enum LoginLimitRefuse {
/**
*
*/
BEFORE,
/**
*
*/
AFTER;
;
}

@ -2,6 +2,7 @@ package org.opsli.core.autoconfigure.properties;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.opsli.common.enums.LoginLimitRefuse;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@ -129,6 +130,12 @@ public class GlobalProperties {
@EqualsAndHashCode(callSuper = false)
public static class Login {
/** 限制登录数量 -1 为无限大 */
private Integer limitCount;
/** 限制登录拒绝策略 after为后者 before为前者 */
private LoginLimitRefuse limitRefuse = LoginLimitRefuse.AFTER;
/** 失败次数 */
private Integer slipCount;
@ -138,7 +145,9 @@ public class GlobalProperties {
/** 失败锁定时间(秒) */
private Integer slipLockSpeed;
}
}
/**

@ -30,6 +30,8 @@ public enum TokenMsg implements BaseMsg {
* Token
*/
EXCEPTION_TOKEN_CREATE_ERROR(12000,"生成Token失败"),
EXCEPTION_TOKEN_CREATE_LIMIT_ERROR(12001,"您的账号已在其他设备登录"),
EXCEPTION_TOKEN_LOSE_EFFICACY(401,"Token失效请重新登录"),

@ -20,7 +20,9 @@ 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 io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.crypto.hash.Md5Hash;
@ -29,6 +31,7 @@ import org.opsli.api.wrapper.system.user.UserModel;
import org.opsli.common.constants.SignConstants;
import org.opsli.common.constants.TokenConstants;
import org.opsli.common.constants.TokenTypeConstants;
import org.opsli.common.enums.LoginLimitRefuse;
import org.opsli.common.exception.TokenException;
import org.opsli.core.autoconfigure.properties.GlobalProperties;
import org.opsli.core.cache.local.CacheUtil;
@ -41,7 +44,6 @@ 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;
@ -65,13 +67,10 @@ public class UserTokenUtil {
public static final String ACCOUNT_SLIP_COUNT_PREFIX;
/** 账号失败锁定KEY */
public static final String ACCOUNT_SLIP_LOCK_PREFIX;
/** 账号失败阈值 */
public static int ACCOUNT_SLIP_COUNT;
/** 账号失败N次后弹出验证码 */
public static int ACCOUNT_SLIP_VERIFY_COUNT;
/** 账号锁定时间 */
public static int ACCOUNT_SLIP_LOCK_SPEED;
/** 限制登录数量 -1 为无限大 */
public static final int ACCOUNT_LIMIT_INFINITE = -1;
/** 登录配置信息 */
public static GlobalProperties.Auth.Login LOGIN_PROPERTIES;
/** Redis插件 */
private static RedisPlugin redisPlugin;
@ -89,15 +88,31 @@ public class UserTokenUtil {
* @param user
* @return
*/
public static ResultVo<Map<String,Object>> createToken(UserModel user) {
public static ResultVo<UserTokenUtil.TokenRet> createToken(UserModel user) {
if (user == null) {
// 生成Token失败
throw new TokenException(TokenMsg.EXCEPTION_TOKEN_CREATE_ERROR);
}
Map<String,Object> map = Maps.newHashMapWithExpectedSize(2);
try {
// 如果当前登录开启 数量限制
if(LOGIN_PROPERTIES.getLimitCount() > ACCOUNT_LIMIT_INFINITE){
// 当前用户已存在 Token数量
Long ticketLen = redisPlugin.sSize(TICKET_PREFIX + ":" + user.getUsername());
if(ticketLen !=null && ticketLen >= LOGIN_PROPERTIES.getLimitCount()){
// 如果是拒绝后者 则直接抛出异常
if(LoginLimitRefuse.AFTER == LOGIN_PROPERTIES.getLimitRefuse()){
// 生成Token失败 您的账号已在其他设备登录
throw new TokenException(TokenMsg.EXCEPTION_TOKEN_CREATE_LIMIT_ERROR);
}
// 如果是拒绝前者 则弹出前者
else {
redisPlugin.sPop(TICKET_PREFIX + ":" + user.getUsername());
}
}
}
// 生效时间
int expire = Integer.parseInt(
String.valueOf(JwtUtil.EXPIRE)
@ -106,9 +121,6 @@ public class UserTokenUtil {
// 生成 Token 包含 username userId timestamp
String signToken = JwtUtil.sign(TokenTypeConstants.TYPE_SYSTEM, user.getUsername(), user.getId());
// 生成MD5 16进制码 用于缩减存储
String signTokenHex = new Md5Hash(signToken).toHex();
// 获得当前时间戳时间
long timestamp = Convert.toLong(
JwtUtil.getClaim(signToken, SignConstants.TIMESTAMP));
@ -119,12 +131,19 @@ public class UserTokenUtil {
long endTimestamp = dateTime.getTime();
// 在redis存一份 token 是为了防止 人为造假
boolean tokenFlag = redisPlugin.put(TICKET_PREFIX + signTokenHex, endTimestamp, expire);
if(tokenFlag){
map.put("token", signToken);
map.put("expire", endTimestamp);
return ResultVo.success(map);
// 保存用户token
Long saveLong = redisPlugin.sPut(TICKET_PREFIX + ":" + user.getUsername(), signToken);
if(saveLong != null && saveLong > 0){
// 设置该用户全部token失效时间 如果这时又有新设备登录 则续命
redisPlugin.expire(TICKET_PREFIX + ":" + user.getUsername(), expire);
TokenRet tokenRet = new TokenRet();
tokenRet.setToken(signToken);
tokenRet.setEndTimestamp(endTimestamp);
return ResultVo.success(tokenRet);
}
}catch (Exception e){
log.error(e.getMessage() , e);
}
@ -175,20 +194,23 @@ public class UserTokenUtil {
return;
}
try {
// 生成MD5 16进制码 用于缩减存储
String signTokenHex = new Md5Hash(token).toHex();
redisPlugin.del(TICKET_PREFIX + signTokenHex);
// 删除相关信息
// 获得要推出用户
String userId = getUserIdByToken(token);
UserModel user = UserUtil.getUser(userId);
if(user != null){
// 删除Token信息
redisPlugin.sRemove(TICKET_PREFIX + ":" + user.getUsername(), token);
// 如果缓存中 无该用户任何Token信息 则删除用户缓存
Long size = redisPlugin.sSize(TICKET_PREFIX + ":" + user.getUsername());
if(size == null || size == 0L){
// 删除相关信息
UserUtil.refreshUser(user);
UserUtil.refreshUserRoles(user.getId());
UserUtil.refreshUserAllPerms(user.getId());
UserUtil.refreshUserMenus(user.getId());
}
}
}catch (Exception ignored){}
}
@ -211,12 +233,12 @@ public class UserTokenUtil {
// 2. 校验当前缓存中token是否失效
// 生成MD5 16进制码 用于缩减存储
String signTokenHex = new Md5Hash(token).toHex();
boolean hasKey = redisPlugin.hasKey(TICKET_PREFIX + signTokenHex);
if(!hasKey){
// 删除相关信息
String username = getUserNameByToken(token);
boolean hashKey = redisPlugin.sHashKey(TICKET_PREFIX + ":" + username, token);
if(!hashKey){
return false;
}
// JWT 自带过期校验 无需多做处理
} catch (Exception e){
@ -238,7 +260,7 @@ public class UserTokenUtil {
Date currDate = new Date();
DateTime loseDate = DateUtil.date(loseTimeMillis);
// 偏移5分钟
DateTime currLoseDate = DateUtil.offsetSecond(loseDate, ACCOUNT_SLIP_LOCK_SPEED);
DateTime currLoseDate = DateUtil.offsetSecond(loseDate, LOGIN_PROPERTIES.getSlipLockSpeed());
// 计算失效剩余时间( 分 )
long betweenM = DateUtil.between(currLoseDate, currDate, DateUnit.MINUTE);
@ -265,14 +287,14 @@ public class UserTokenUtil {
Long slipNum = redisPlugin.increment(ACCOUNT_SLIP_COUNT_PREFIX + username);
if (slipNum != null){
// 设置失效时间为 5分钟
redisPlugin.expire(ACCOUNT_SLIP_COUNT_PREFIX + username, ACCOUNT_SLIP_LOCK_SPEED);
redisPlugin.expire(ACCOUNT_SLIP_COUNT_PREFIX + username, LOGIN_PROPERTIES.getSlipLockSpeed());
// 如果确认 都失败 则存入临时缓存
if(slipNum >= ACCOUNT_SLIP_COUNT){
if(slipNum >= LOGIN_PROPERTIES.getSlipCount()){
long currentTimeMillis = System.currentTimeMillis();
// 存入Redis
redisPlugin.put(ACCOUNT_SLIP_LOCK_PREFIX + username,
currentTimeMillis, ACCOUNT_SLIP_LOCK_SPEED);
currentTimeMillis, LOGIN_PROPERTIES.getSlipLockSpeed());
}
}
@ -334,18 +356,28 @@ public class UserTokenUtil {
if(globalProperties != null && globalProperties.getAuth() != null
&& globalProperties.getAuth().getLogin() != null
){
// 账号失败阈值
UserTokenUtil.ACCOUNT_SLIP_COUNT = globalProperties.getAuth()
.getLogin().getSlipCount();
// 账号失败N次后弹出验证码
UserTokenUtil.ACCOUNT_SLIP_VERIFY_COUNT = globalProperties.getAuth()
.getLogin().getSlipVerifyCount();
// 账号锁定时间
UserTokenUtil.ACCOUNT_SLIP_LOCK_SPEED = globalProperties.getAuth()
.getLogin().getSlipLockSpeed();
// 登录配置信息
UserTokenUtil.LOGIN_PROPERTIES = globalProperties.getAuth().getLogin();
}
// Redis 插件
UserTokenUtil.redisPlugin = redisPlugin;
}
// =====================
@Data
@EqualsAndHashCode(callSuper = false)
public static class TokenRet {
/** Token */
@ApiModelProperty(value = "Token")
private String token;
/** 失效时间戳 */
@ApiModelProperty(value = "失效时间戳")
private Long endTimestamp;
}
}

@ -173,6 +173,18 @@
"description": "认证类 Token 排除URL."
},
{
"name": "opsli.auth.login.limit-count",
"sourceType": "org.opsli.core.autoconfigure.properties.GlobalProperties$Auth$Login",
"type": "java.lang.Integer",
"defaultValue": -1,
"description": "限制登录数量 -1 为无限大."
},
{
"name": "opsli.auth.login.limit-refuse",
"sourceType": "org.opsli.core.autoconfigure.properties.GlobalProperties$Auth$Login",
"description": "限制登录拒绝策略 after为后者 before为前者."
},
{
"name": "opsli.auth.login.slip-count",
"sourceType": "org.opsli.core.autoconfigure.properties.GlobalProperties$Auth$Login",

@ -21,6 +21,7 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.opsli.api.base.result.ResultVo;
import org.opsli.core.autoconfigure.properties.GlobalProperties;
import org.opsli.core.utils.ValidationUtil;
import org.opsli.api.wrapper.system.tenant.TenantModel;
import org.opsli.api.wrapper.system.user.UserModel;
@ -38,6 +39,7 @@ import org.opsli.core.security.shiro.realm.JwtRealm;
import org.opsli.core.utils.*;
import org.opsli.modulars.system.login.entity.LoginForm;
import org.opsli.modulars.system.user.service.IUserService;
import org.opsli.plugins.redis.RedisPlugin;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
@ -68,7 +70,6 @@ public class LoginRestController {
@Autowired
private IUserService iUserService;
/**
*
*/
@ -76,7 +77,8 @@ public class LoginRestController {
@InterfaceEncryptAndDecrypt(responseEncrypt = false)
@ApiOperation(value = "登录", notes = "登录")
@PostMapping("/sys/login")
public ResultVo<?> login(@RequestBody LoginForm form, HttpServletRequest request){
public ResultVo<UserTokenUtil.TokenRet> login(@RequestBody LoginForm form, HttpServletRequest request){
// 非空验证
if(form == null){
throw new TokenException(TokenMsg.EXCEPTION_LOGIN_NULL);
@ -92,7 +94,7 @@ public class LoginRestController {
long slipCount = UserTokenUtil.getSlipCount(form.getUsername());
// 失败次数超过 验证次数阈值 开启验证码验证
if(slipCount >= UserTokenUtil.ACCOUNT_SLIP_VERIFY_COUNT){
if(slipCount >= UserTokenUtil.LOGIN_PROPERTIES.getSlipVerifyCount()){
CaptchaUtil.validate(form.getUuid(), form.getCaptcha());
}
@ -124,13 +126,13 @@ public class LoginRestController {
}
// 失败次数超过 验证次数阈值 开启验证码验证
if(slipCount >= UserTokenUtil.ACCOUNT_SLIP_VERIFY_COUNT){
if(slipCount >= UserTokenUtil.LOGIN_PROPERTIES.getSlipVerifyCount()){
// 删除验证过后验证码
CaptchaUtil.delCaptcha(form.getUuid());
}
//生成token并保存到Redis
ResultVo<Map<String, Object>> resultVo = UserTokenUtil.createToken(user);
ResultVo<UserTokenUtil.TokenRet> resultVo = UserTokenUtil.createToken(user);
if(resultVo.isSuccess()){
// 异步保存IP
AsyncProcessQueueReFuse.execute(()->{
@ -170,7 +172,7 @@ public class LoginRestController {
// 获得当前失败次数
long slipCount = UserTokenUtil.getSlipCount(username);
Map<String, Object> ret = Maps.newHashMap();
ret.put("base", UserTokenUtil.ACCOUNT_SLIP_VERIFY_COUNT);
ret.put("base", UserTokenUtil.LOGIN_PROPERTIES.getSlipVerifyCount());
ret.put("curr", slipCount);
return ResultVo.success(ret);
}
@ -212,7 +214,7 @@ public class LoginRestController {
);
}
// =================
public static void main(String[] args) {
String passwordStr = "Aa123456";

@ -198,6 +198,10 @@ opsli:
# 登录设置
login:
# 限制登录数量 -1 为无限大
limit-count: 4
# 限制登录拒绝策略 after为后者 before为前者
limit-refuse: after
# 失败次数
slip-count: 5
# 失败N次后弹出验证码 (超过验证码阈值 弹出验证码)

Loading…
Cancel
Save