From bcd93a7a4e3de85e197371731ca9f3dc4e75936c Mon Sep 17 00:00:00 2001 From: smallchill Date: Fri, 14 Mar 2025 10:21:31 +0800 Subject: [PATCH] =?UTF-8?q?:tada:=204.5.0.RELEASE=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E8=B4=A6=E5=8F=B7=E9=94=81=E5=AE=9A=E4=B8=8E=E8=A7=A3=E9=94=81?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=96=B0=E5=A2=9E=E8=85=BE=E8=AE=AF?= =?UTF-8?q?=E4=BA=91=E5=AF=B9=E8=B1=A1=E5=AD=98=E5=82=A8=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 23 +++++++------- .../auth/granter/CaptchaTokenGranter.java | 23 +++++++++++++- .../auth/granter/PasswordTokenGranter.java | 26 ++++++++++++++++ .../org/springblade/auth/utils/TokenUtil.java | 1 + .../springblade/common/cache/CacheNames.java | 22 ++++++++++++- .../system/controller/UserController.java | 31 ++++++++++++++++--- doc/nacos/blade.yaml | 4 +-- pom.xml | 4 +-- script/docker/.env | 2 +- script/kuboard/kuboard_spring-blade.yaml | 24 +++++++------- 10 files changed, 125 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 0d316b6..f64f14a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- Downloads + Downloads Build Status Build Status Coverage Status @@ -81,19 +81,20 @@ SpringBlade ## 官方产品 -| 简介 | 演示地址 | -|---------------|------------------------------------------------------| -| BladeX企业级开发平台 | [https://saber3.bladex.cn](https://saber3.bladex.cn) | -| BladeX可视化数据大屏 | [https://data.bladex.cn](https://data.bladex.cn) | -| BladeX物联网开发平台 | [https://iot.bladex.cn](https://iot.bladex.cn) | +| 简介 | 演示地址 | +|-----------------|------------------------------------------------------| +| BladeX企业级开发平台 | [https://saber3.bladex.cn](https://saber3.bladex.cn) | +| BladeX可视化数据大屏 | [https://data.bladex.cn](https://data.bladex.cn) | +| BladeX物联网开发平台 | [https://iot.bladex.cn](https://iot.bladex.cn) | +| BladeXAI大模型平台 | [https://aigc.bladex.cn/](https://aigc.bladex.cn/) | ## 前端项目 -| 简介 | 地址 | -|--------------------|----------------------------------------------------------------------------------------------------| -| 前端框架Sword(基于React) | [https://gitee.com/smallc/Sword](https://gitee.com/smallc/Sword) | -| 前端框架Saber(基于Vue2) | [https://gitee.com/smallc/Saber](https://gitee.com/smallc/Saber) | -| 前端框架Saber3(基于Vue3) | [https://gitee.com/smallc/Saber3](https://gitee.com/smallc/Saber/tree/3.x/) | +| 简介 | 地址 | +|--------------------|------------------------------------------------------------------------------| +| 前端框架Saber3(基于Vue3) | [https://gitee.com/smallc/Saber3](https://gitee.com/smallc/Saber) | +| 前端框架Saber(基于Vue2) | [https://gitee.com/smallc/Saber2](https://gitee.com/smallc/Saber/tree/vue2/) | +| 前端框架Sword(基于React) | [https://gitee.com/smallc/Sword](https://gitee.com/smallc/Sword) | ## 后端项目 | 简介 | 地址 | diff --git a/blade-auth/src/main/java/org/springblade/auth/granter/CaptchaTokenGranter.java b/blade-auth/src/main/java/org/springblade/auth/granter/CaptchaTokenGranter.java index 4bcadba..a078197 100644 --- a/blade-auth/src/main/java/org/springblade/auth/granter/CaptchaTokenGranter.java +++ b/blade-auth/src/main/java/org/springblade/auth/granter/CaptchaTokenGranter.java @@ -16,6 +16,7 @@ package org.springblade.auth.granter; import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springblade.auth.enums.BladeUserEnum; import org.springblade.auth.utils.TokenUtil; import org.springblade.common.cache.CacheNames; @@ -30,16 +31,20 @@ import org.springframework.stereotype.Component; import jakarta.servlet.http.HttpServletRequest; +import java.time.Duration; + /** * 验证码TokenGranter * * @author Chill */ +@Slf4j @Component @AllArgsConstructor public class CaptchaTokenGranter implements ITokenGranter { public static final String GRANT_TYPE = "captcha"; + public static final Integer FAIL_COUNT = 5; private IUserClient userClient; private BladeRedis bladeRedis; @@ -53,7 +58,7 @@ public class CaptchaTokenGranter implements ITokenGranter { String key = request.getHeader(TokenUtil.CAPTCHA_HEADER_KEY); String code = request.getHeader(TokenUtil.CAPTCHA_HEADER_CODE); // 获取验证码 - String redisCode = Func.toStr(bladeRedis.get(CacheNames.CAPTCHA_KEY + key)); + String redisCode = Func.toStr(bladeRedis.getAndDel(CacheNames.CAPTCHA_KEY + key)); // 判断验证码 if (code == null || !StringUtil.equalsIgnoreCase(redisCode, code)) { throw new ServiceException(TokenUtil.CAPTCHA_NOT_CORRECT); @@ -62,6 +67,14 @@ public class CaptchaTokenGranter implements ITokenGranter { String tenantId = tokenParameter.getArgs().getStr("tenantId"); String account = tokenParameter.getArgs().getStr("account"); String password = tokenParameter.getArgs().getStr("password"); + + // 判断登录是否锁定 + int cnt = Func.toInt(bladeRedis.get(CacheNames.tenantKey(tenantId, CacheNames.USER_FAIL_KEY, account)), 0); + if (cnt >= FAIL_COUNT) { + log.error("用户登录失败次数过多, 账号:{}, IP:{}", account, WebUtil.getIP()); + throw new ServiceException(TokenUtil.USER_HAS_TOO_MANY_FAILS); + } + UserInfo userInfo = null; if (Func.isNoneBlank(account, password)) { // 获取用户类型 @@ -80,6 +93,14 @@ public class CaptchaTokenGranter implements ITokenGranter { } userInfo = result.isSuccess() ? result.getData() : null; } + + if (userInfo == null || userInfo.getUser() == null) { + // 增加错误锁定次数 + bladeRedis.setEx(CacheNames.tenantKey(tenantId, CacheNames.USER_FAIL_KEY, account), cnt + 1, Duration.ofMinutes(30)); + } else { + // 成功则清除登录缓存 + bladeRedis.del(CacheNames.tenantKey(tenantId, CacheNames.USER_FAIL_KEY, account)); + } return userInfo; } diff --git a/blade-auth/src/main/java/org/springblade/auth/granter/PasswordTokenGranter.java b/blade-auth/src/main/java/org/springblade/auth/granter/PasswordTokenGranter.java index 1db1498..201d809 100644 --- a/blade-auth/src/main/java/org/springblade/auth/granter/PasswordTokenGranter.java +++ b/blade-auth/src/main/java/org/springblade/auth/granter/PasswordTokenGranter.java @@ -16,28 +16,38 @@ package org.springblade.auth.granter; import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springblade.auth.enums.BladeUserEnum; import org.springblade.auth.utils.TokenUtil; +import org.springblade.common.cache.CacheNames; +import org.springblade.core.log.exception.ServiceException; +import org.springblade.core.redis.cache.BladeRedis; import org.springblade.core.secure.props.BladeAuthProperties; import org.springblade.core.tool.api.R; import org.springblade.core.tool.utils.DigestUtil; import org.springblade.core.tool.utils.Func; +import org.springblade.core.tool.utils.WebUtil; import org.springblade.system.user.entity.UserInfo; import org.springblade.system.user.feign.IUserClient; import org.springframework.stereotype.Component; +import java.time.Duration; + /** * PasswordTokenGranter * * @author Chill */ +@Slf4j @Component @AllArgsConstructor public class PasswordTokenGranter implements ITokenGranter { public static final String GRANT_TYPE = "password"; + public static final Integer FAIL_COUNT = 5; private IUserClient userClient; + private BladeRedis bladeRedis; private BladeAuthProperties authProperties; @@ -46,6 +56,14 @@ public class PasswordTokenGranter implements ITokenGranter { String tenantId = tokenParameter.getArgs().getStr("tenantId"); String account = tokenParameter.getArgs().getStr("account"); String password = tokenParameter.getArgs().getStr("password"); + + // 判断登录是否锁定 + int cnt = Func.toInt(bladeRedis.get(CacheNames.tenantKey(tenantId, CacheNames.USER_FAIL_KEY, account)), 0); + if (cnt >= FAIL_COUNT) { + log.error("用户登录失败次数过多, 账号:{}, IP:{}", account, WebUtil.getIP()); + throw new ServiceException(TokenUtil.USER_HAS_TOO_MANY_FAILS); + } + UserInfo userInfo = null; if (Func.isNoneBlank(account, password)) { // 获取用户类型 @@ -64,6 +82,14 @@ public class PasswordTokenGranter implements ITokenGranter { } userInfo = result.isSuccess() ? result.getData() : null; } + + if (userInfo == null || userInfo.getUser() == null) { + // 增加错误锁定次数 + bladeRedis.setEx(CacheNames.tenantKey(tenantId, CacheNames.USER_FAIL_KEY, account), cnt + 1, Duration.ofMinutes(30)); + } else { + // 成功则清除登录缓存 + bladeRedis.del(CacheNames.tenantKey(tenantId, CacheNames.USER_FAIL_KEY, account)); + } return userInfo; } diff --git a/blade-auth/src/main/java/org/springblade/auth/utils/TokenUtil.java b/blade-auth/src/main/java/org/springblade/auth/utils/TokenUtil.java index e8d71f9..daf6794 100644 --- a/blade-auth/src/main/java/org/springblade/auth/utils/TokenUtil.java +++ b/blade-auth/src/main/java/org/springblade/auth/utils/TokenUtil.java @@ -47,6 +47,7 @@ public class TokenUtil { public final static String HEADER_KEY = "Authorization"; public final static String HEADER_PREFIX = "Basic "; public final static String ENCRYPT_PREFIX = "04"; + public final static String USER_HAS_TOO_MANY_FAILS = "用户登录失败次数过多"; public final static String DEFAULT_AVATAR = "https://bladex.cn/images/logo.png"; /** diff --git a/blade-common/src/main/java/org/springblade/common/cache/CacheNames.java b/blade-common/src/main/java/org/springblade/common/cache/CacheNames.java index 1d72d07..3640896 100644 --- a/blade-common/src/main/java/org/springblade/common/cache/CacheNames.java +++ b/blade-common/src/main/java/org/springblade/common/cache/CacheNames.java @@ -27,6 +27,26 @@ public interface CacheNames { String DICT_VALUE = "dict:value"; String DICT_LIST = "dict:list"; - String CAPTCHA_KEY = "blade:auth::captcha:"; + /** + * 验证码key + */ + String CAPTCHA_KEY = "blade:auth::blade:captcha:"; + + /** + * 登录失败key + */ + String USER_FAIL_KEY = "blade:user::blade:fail:"; + + /** + * 返回租户格式的key + * + * @param tenantId 租户编号 + * @param cacheKey 缓存key + * @param cacheKeyValue 缓存key值 + * @return tenantKey + */ + static String tenantKey(String tenantId, String cacheKey, String cacheKeyValue) { + return tenantId.concat(":").concat(cacheKey).concat(cacheKeyValue); + } } diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/controller/UserController.java b/blade-service/blade-system/src/main/java/org/springblade/system/controller/UserController.java index b2969fa..dc5c35e 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/controller/UserController.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/controller/UserController.java @@ -20,6 +20,7 @@ import com.alibaba.excel.EasyExcel; import com.alibaba.excel.read.builder.ExcelReaderBuilder; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -30,8 +31,10 @@ import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import lombok.AllArgsConstructor; import lombok.SneakyThrows; +import org.springblade.common.cache.CacheNames; import org.springblade.core.mp.support.Condition; import org.springblade.core.mp.support.Query; +import org.springblade.core.redis.cache.BladeRedis; import org.springblade.core.secure.BladeUser; import org.springblade.core.secure.annotation.PreAuth; import org.springblade.core.secure.utils.SecureUtil; @@ -39,6 +42,7 @@ import org.springblade.core.tool.api.R; import org.springblade.core.tool.constant.BladeConstant; import org.springblade.core.tool.constant.RoleConstant; import org.springblade.core.tool.utils.Func; +import org.springblade.core.tool.utils.StringUtil; import org.springblade.system.user.entity.User; import org.springblade.system.excel.UserExcel; import org.springblade.system.excel.UserImportListener; @@ -69,6 +73,7 @@ import java.util.Map; public class UserController { private IUserService userService; + private BladeRedis bladeRedis; /** * 查询单条 @@ -85,7 +90,7 @@ public class UserController { /** * 查询单条 */ - @ApiOperationSupport(order =2) + @ApiOperationSupport(order = 2) @Operation(summary = "查看详情", description = "传入id") @GetMapping("/info") public R info(BladeUser user) { @@ -212,7 +217,7 @@ public class UserController { @Operation(summary = "导入用户", description = "传入excel") public R importUser(MultipartFile file, Integer isCovered) { String filename = file.getOriginalFilename(); - if (StringUtils.isEmpty(filename)) { + if (StringUtil.isBlank(filename)) { throw new RuntimeException("请上传文件!"); } if ((!StringUtils.endsWithIgnoreCase(filename, ".xls") && !StringUtils.endsWithIgnoreCase(filename, ".xlsx"))) { @@ -240,14 +245,14 @@ public class UserController { @PreAuth(RoleConstant.HAS_ROLE_ADMIN) public void exportUser(@Parameter(hidden = true) @RequestParam Map user, BladeUser bladeUser, HttpServletResponse response) { QueryWrapper queryWrapper = Condition.getQueryWrapper(user, User.class); - if (!SecureUtil.isAdministrator()){ + if (!SecureUtil.isAdministrator()) { queryWrapper.lambda().eq(User::getTenantId, bladeUser.getTenantId()); } queryWrapper.lambda().eq(User::getIsDeleted, BladeConstant.DB_NOT_DELETED); List list = userService.exportUser(queryWrapper); response.setContentType("application/vnd.ms-excel"); response.setCharacterEncoding(StandardCharsets.UTF_8.name()); - String fileName = URLEncoder.encode("用户数据导出", StandardCharsets.UTF_8.name()); + String fileName = URLEncoder.encode("用户数据导出", StandardCharsets.UTF_8); response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx"); EasyExcel.write(response.getOutputStream(), UserExcel.class).sheet("用户数据表").doWrite(list); } @@ -263,7 +268,7 @@ public class UserController { List list = new ArrayList<>(); response.setContentType("application/vnd.ms-excel"); response.setCharacterEncoding(StandardCharsets.UTF_8.name()); - String fileName = URLEncoder.encode("用户数据模板", StandardCharsets.UTF_8.name()); + String fileName = URLEncoder.encode("用户数据模板", StandardCharsets.UTF_8); response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx"); EasyExcel.write(response.getOutputStream(), UserExcel.class).sheet("用户数据表").doWrite(list); } @@ -279,4 +284,20 @@ public class UserController { } + /** + * 用户解锁 + */ + @PostMapping("/unlock") + @ApiOperationSupport(order = 16) + @Operation(summary = "账号解锁") + @PreAuth(RoleConstant.HAS_ROLE_ADMIN) + public R unlock(String userIds) { + if (StringUtil.isBlank(userIds)) { + return R.fail("请至少选择一个用户"); + } + List userList = userService.list(Wrappers.lambdaQuery().in(User::getId, Func.toLongList(userIds))); + userList.forEach(user -> bladeRedis.del(CacheNames.tenantKey(user.getTenantId(), CacheNames.USER_FAIL_KEY, user.getAccount()))); + return R.success("操作成功"); + } + } diff --git a/doc/nacos/blade.yaml b/doc/nacos/blade.yaml index 67a7376..0f78c0c 100644 --- a/doc/nacos/blade.yaml +++ b/doc/nacos/blade.yaml @@ -67,13 +67,13 @@ knife4j: language: zh_cn enableFooter: false enableFooterCustom: true - footerCustomContent: Copyright © 2024 SpringBlade All Rights Reserved + footerCustomContent: Copyright © 2025 SpringBlade All Rights Reserved #swagger配置信息 swagger: title: SpringBlade 接口文档系统 description: SpringBlade 接口文档系统 - version: 4.4.0 + version: 4.5.0 license: Powered By SpringBlade licenseUrl: https://bladex.cn terms-of-service-url: https://bladex.cn diff --git a/pom.xml b/pom.xml index 80fefb8..4514578 100644 --- a/pom.xml +++ b/pom.xml @@ -9,9 +9,9 @@ pom - 4.4.0 + 4.5.0 - 4.4.2 + 4.5.0 17 3.11.0 diff --git a/script/docker/.env b/script/docker/.env index caee97d..c3bd047 100644 --- a/script/docker/.env +++ b/script/docker/.env @@ -1,2 +1,2 @@ REGISTER=192.168.0.157/blade -TAG=4.4.0 +TAG=4.5.0 diff --git a/script/kuboard/kuboard_spring-blade.yaml b/script/kuboard/kuboard_spring-blade.yaml index bc97837..557a6c3 100644 --- a/script/kuboard/kuboard_spring-blade.yaml +++ b/script/kuboard/kuboard_spring-blade.yaml @@ -152,7 +152,7 @@ spec: spec: containers: - name: blade-admin - image: 'swr.cn-east-2.myhuaweicloud.com/blade/blade-admin:4.4.0' + image: 'swr.cn-east-2.myhuaweicloud.com/blade/blade-admin:4.5.0' args: - '--spring.profiles.active=${PROFILE}' - '--spring.cloud.nacos.config.server-addr=${NACOS_SERVER_ADDR}' @@ -386,7 +386,7 @@ spec: spec: containers: - name: blade-auth - image: 'swr.cn-east-2.myhuaweicloud.com/blade/blade-auth:4.4.0' + image: 'swr.cn-east-2.myhuaweicloud.com/blade/blade-auth:4.5.0' args: - '--spring.profiles.active=${PROFILE}' - '--spring.cloud.nacos.config.server-addr=${NACOS_SERVER_ADDR}' @@ -625,7 +625,7 @@ spec: spec: containers: - name: blade-desk - image: 'swr.cn-east-2.myhuaweicloud.com/blade/blade-desk:4.4.0' + image: 'swr.cn-east-2.myhuaweicloud.com/blade/blade-desk:4.5.0' args: - '--spring.profiles.active=${PROFILE}' - '--spring.cloud.nacos.config.server-addr=${NACOS_SERVER_ADDR}' @@ -864,7 +864,7 @@ spec: spec: containers: - name: blade-develop - image: 'swr.cn-east-2.myhuaweicloud.com/blade/blade-develop:4.4.0' + image: 'swr.cn-east-2.myhuaweicloud.com/blade/blade-develop:4.5.0' args: - '--spring.profiles.active=${PROFILE}' - '--spring.cloud.nacos.config.server-addr=${NACOS_SERVER_ADDR}' @@ -1096,7 +1096,7 @@ spec: spec: containers: - name: blade-gateway - image: 'swr.cn-east-2.myhuaweicloud.com/blade/blade-gateway:4.4.0' + image: 'swr.cn-east-2.myhuaweicloud.com/blade/blade-gateway:4.5.0' args: - '--spring.profiles.active=${PROFILE}' - '--spring.cloud.nacos.config.server-addr=${NACOS_SERVER_ADDR}' @@ -1331,7 +1331,7 @@ spec: spec: containers: - name: blade-log - image: 'swr.cn-east-2.myhuaweicloud.com/blade/blade-log:4.4.0' + image: 'swr.cn-east-2.myhuaweicloud.com/blade/blade-log:4.5.0' args: - '--spring.profiles.active=${PROFILE}' - '--spring.cloud.nacos.config.server-addr=${NACOS_SERVER_ADDR}' @@ -1565,7 +1565,7 @@ spec: spec: containers: - name: blade-report - image: 'swr.cn-east-2.myhuaweicloud.com/blade/blade-report:4.4.0' + image: 'swr.cn-east-2.myhuaweicloud.com/blade/blade-report:4.5.0' args: - '--spring.profiles.active=${PROFILE}' - '--spring.cloud.nacos.config.server-addr=${NACOS_SERVER_ADDR}' @@ -1799,7 +1799,7 @@ spec: spec: containers: - name: blade-resource - image: 'swr.cn-east-2.myhuaweicloud.com/blade/blade-resource:4.4.0' + image: 'swr.cn-east-2.myhuaweicloud.com/blade/blade-resource:4.5.0' args: - '--spring.profiles.active=${PROFILE}' - '--spring.cloud.nacos.config.server-addr=${NACOS_SERVER_ADDR}' @@ -2033,7 +2033,7 @@ spec: spec: containers: - name: blade-system - image: 'swr.cn-east-2.myhuaweicloud.com/blade/blade-system:4.4.0' + image: 'swr.cn-east-2.myhuaweicloud.com/blade/blade-system:4.5.0' args: - '--spring.profiles.active=${PROFILE}' - '--spring.cloud.nacos.config.server-addr=${NACOS_SERVER_ADDR}' @@ -2262,7 +2262,7 @@ spec: spec: containers: - name: saber-web - image: 'swr.cn-east-2.myhuaweicloud.com/blade/saber-web:4.4.0' + image: 'swr.cn-east-2.myhuaweicloud.com/blade/saber-web:4.5.0' ports: - name: web containerPort: 80 @@ -2487,7 +2487,7 @@ spec: spec: containers: - name: blade-swagger - image: 'swr.cn-east-2.myhuaweicloud.com/blade/blade-swagger:4.4.0' + image: 'swr.cn-east-2.myhuaweicloud.com/blade/blade-swagger:4.5.0' args: - '--spring.profiles.active=${PROFILE}' - '--spring.cloud.nacos.config.server-addr=${NACOS_SERVER_ADDR}' @@ -3515,7 +3515,7 @@ spec: spec: containers: - name: mysql - image: 'swr.cn-east-2.myhuaweicloud.com/blade/saber-db:v4.4.0' + image: 'swr.cn-east-2.myhuaweicloud.com/blade/saber-db:v4.5.0' ports: - name: mysql containerPort: 3306