diff --git a/opsli-base-support/opsli-common/src/main/java/org/opsli/common/enums/AlertType.java b/opsli-base-support/opsli-common/src/main/java/org/opsli/common/enums/AlertType.java index 2498f98d..80370548 100644 --- a/opsli-base-support/opsli-common/src/main/java/org/opsli/common/enums/AlertType.java +++ b/opsli-base-support/opsli-common/src/main/java/org/opsli/common/enums/AlertType.java @@ -9,9 +9,10 @@ package org.opsli.common.enums; public enum AlertType { /** alert 弹出 */ - ALERT(), + ALERT, /** JSON 回推*/ JSON, + ; } diff --git a/opsli-base-support/opsli-common/src/main/java/org/opsli/common/enums/LoginLimitRefuse.java b/opsli-base-support/opsli-common/src/main/java/org/opsli/common/enums/LoginLimitRefuse.java new file mode 100644 index 00000000..dcc903bc --- /dev/null +++ b/opsli-base-support/opsli-common/src/main/java/org/opsli/common/enums/LoginLimitRefuse.java @@ -0,0 +1,40 @@ +/** + * Copyright 2020 OPSLI 快速开发平台 https://www.opsli.com + *

+ * 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 + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * 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; + + ; + +} diff --git a/opsli-base-support/opsli-core/src/main/java/org/opsli/core/autoconfigure/properties/GlobalProperties.java b/opsli-base-support/opsli-core/src/main/java/org/opsli/core/autoconfigure/properties/GlobalProperties.java index 93de1f30..8ba74fbf 100644 --- a/opsli-base-support/opsli-core/src/main/java/org/opsli/core/autoconfigure/properties/GlobalProperties.java +++ b/opsli-base-support/opsli-core/src/main/java/org/opsli/core/autoconfigure/properties/GlobalProperties.java @@ -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; + } + } /** 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 5df7730a..f4a2b3a6 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 @@ -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失效,请重新登录"), 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 4048d6b7..a9aa8056 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 @@ -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> createToken(UserModel user) { + public static ResultVo createToken(UserModel user) { if (user == null) { // 生成Token失败 throw new TokenException(TokenMsg.EXCEPTION_TOKEN_CREATE_ERROR); } - Map 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,19 +194,22 @@ 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){ - UserUtil.refreshUser(user); - UserUtil.refreshUserRoles(user.getId()); - UserUtil.refreshUserAllPerms(user.getId()); - UserUtil.refreshUserMenus(user.getId()); + // 删除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; + + } } diff --git a/opsli-base-support/opsli-core/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/opsli-base-support/opsli-core/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 5f7bd407..c17fb5b9 100644 --- a/opsli-base-support/opsli-core/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/opsli-base-support/opsli-core/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -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", 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 c4b847ab..d2809ce7 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 @@ -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 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> resultVo = UserTokenUtil.createToken(user); + ResultVo resultVo = UserTokenUtil.createToken(user); if(resultVo.isSuccess()){ // 异步保存IP AsyncProcessQueueReFuse.execute(()->{ @@ -170,7 +172,7 @@ public class LoginRestController { // 获得当前失败次数 long slipCount = UserTokenUtil.getSlipCount(username); Map 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"; diff --git a/opsli-starter/src/main/resources/application.yaml b/opsli-starter/src/main/resources/application.yaml index 09e43af9..c7b7c267 100644 --- a/opsli-starter/src/main/resources/application.yaml +++ b/opsli-starter/src/main/resources/application.yaml @@ -198,6 +198,10 @@ opsli: # 登录设置 login: + # 限制登录数量 -1 为无限大 + limit-count: 4 + # 限制登录拒绝策略 after为后者 before为前者 + limit-refuse: after # 失败次数 slip-count: 5 # 失败N次后弹出验证码 (超过验证码阈值 弹出验证码)