Skip to content

Commit bcd93a7

Browse files
author
smallchill
committed
🎉 4.5.0.RELEASE 新增账号锁定与解锁功能,新增腾讯云对象存储支持
1 parent fb91e26 commit bcd93a7

File tree

10 files changed

+125
-35
lines changed

10 files changed

+125
-35
lines changed

README.md

+12-11
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<p align="center">
2-
<img src="https://img.shields.io/badge/Release-V4.4.0-green.svg" alt="Downloads">
2+
<img src="https://img.shields.io/badge/Release-V4.5.0-green.svg" alt="Downloads">
33
<img src="https://img.shields.io/badge/JDK-17+-green.svg" alt="Build Status">
44
<img src="https://img.shields.io/badge/license-Apache%202-blue.svg" alt="Build Status">
55
<img src="https://img.shields.io/badge/Spring%20Cloud-2023-blue.svg" alt="Coverage Status">
@@ -81,19 +81,20 @@ SpringBlade
8181

8282
## 官方产品
8383

84-
| 简介 | 演示地址 |
85-
|---------------|------------------------------------------------------|
86-
| BladeX企业级开发平台 | [https://saber3.bladex.cn](https://saber3.bladex.cn) |
87-
| BladeX可视化数据大屏 | [https://data.bladex.cn](https://data.bladex.cn) |
88-
| BladeX物联网开发平台 | [https://iot.bladex.cn](https://iot.bladex.cn) |
84+
| 简介 | 演示地址 |
85+
|-----------------|------------------------------------------------------|
86+
| BladeX企业级开发平台 | [https://saber3.bladex.cn](https://saber3.bladex.cn) |
87+
| BladeX可视化数据大屏 | [https://data.bladex.cn](https://data.bladex.cn) |
88+
| BladeX物联网开发平台 | [https://iot.bladex.cn](https://iot.bladex.cn) |
89+
| BladeXAI大模型平台 | [https://aigc.bladex.cn/](https://aigc.bladex.cn/) |
8990

9091
## 前端项目
9192

92-
| 简介 | 地址 |
93-
|--------------------|----------------------------------------------------------------------------------------------------|
94-
| 前端框架Sword(基于React) | [https://gitee.com/smallc/Sword](https://gitee.com/smallc/Sword) |
95-
| 前端框架Saber(基于Vue2) | [https://gitee.com/smallc/Saber](https://gitee.com/smallc/Saber) |
96-
| 前端框架Saber3(基于Vue3) | [https://gitee.com/smallc/Saber3](https://gitee.com/smallc/Saber/tree/3.x/) |
93+
| 简介 | 地址 |
94+
|--------------------|------------------------------------------------------------------------------|
95+
| 前端框架Saber3(基于Vue3) | [https://gitee.com/smallc/Saber3](https://gitee.com/smallc/Saber) |
96+
| 前端框架Saber(基于Vue2) | [https://gitee.com/smallc/Saber2](https://gitee.com/smallc/Saber/tree/vue2/) |
97+
| 前端框架Sword(基于React) | [https://gitee.com/smallc/Sword](https://gitee.com/smallc/Sword) |
9798

9899
## 后端项目
99100
| 简介 | 地址 |

blade-auth/src/main/java/org/springblade/auth/granter/CaptchaTokenGranter.java

+22-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package org.springblade.auth.granter;
1717

1818
import lombok.AllArgsConstructor;
19+
import lombok.extern.slf4j.Slf4j;
1920
import org.springblade.auth.enums.BladeUserEnum;
2021
import org.springblade.auth.utils.TokenUtil;
2122
import org.springblade.common.cache.CacheNames;
@@ -30,16 +31,20 @@
3031

3132
import jakarta.servlet.http.HttpServletRequest;
3233

34+
import java.time.Duration;
35+
3336
/**
3437
* 验证码TokenGranter
3538
*
3639
* @author Chill
3740
*/
41+
@Slf4j
3842
@Component
3943
@AllArgsConstructor
4044
public class CaptchaTokenGranter implements ITokenGranter {
4145

4246
public static final String GRANT_TYPE = "captcha";
47+
public static final Integer FAIL_COUNT = 5;
4348

4449
private IUserClient userClient;
4550
private BladeRedis bladeRedis;
@@ -53,7 +58,7 @@ public UserInfo grant(TokenParameter tokenParameter) {
5358
String key = request.getHeader(TokenUtil.CAPTCHA_HEADER_KEY);
5459
String code = request.getHeader(TokenUtil.CAPTCHA_HEADER_CODE);
5560
// 获取验证码
56-
String redisCode = Func.toStr(bladeRedis.get(CacheNames.CAPTCHA_KEY + key));
61+
String redisCode = Func.toStr(bladeRedis.getAndDel(CacheNames.CAPTCHA_KEY + key));
5762
// 判断验证码
5863
if (code == null || !StringUtil.equalsIgnoreCase(redisCode, code)) {
5964
throw new ServiceException(TokenUtil.CAPTCHA_NOT_CORRECT);
@@ -62,6 +67,14 @@ public UserInfo grant(TokenParameter tokenParameter) {
6267
String tenantId = tokenParameter.getArgs().getStr("tenantId");
6368
String account = tokenParameter.getArgs().getStr("account");
6469
String password = tokenParameter.getArgs().getStr("password");
70+
71+
// 判断登录是否锁定
72+
int cnt = Func.toInt(bladeRedis.get(CacheNames.tenantKey(tenantId, CacheNames.USER_FAIL_KEY, account)), 0);
73+
if (cnt >= FAIL_COUNT) {
74+
log.error("用户登录失败次数过多, 账号:{}, IP:{}", account, WebUtil.getIP());
75+
throw new ServiceException(TokenUtil.USER_HAS_TOO_MANY_FAILS);
76+
}
77+
6578
UserInfo userInfo = null;
6679
if (Func.isNoneBlank(account, password)) {
6780
// 获取用户类型
@@ -80,6 +93,14 @@ public UserInfo grant(TokenParameter tokenParameter) {
8093
}
8194
userInfo = result.isSuccess() ? result.getData() : null;
8295
}
96+
97+
if (userInfo == null || userInfo.getUser() == null) {
98+
// 增加错误锁定次数
99+
bladeRedis.setEx(CacheNames.tenantKey(tenantId, CacheNames.USER_FAIL_KEY, account), cnt + 1, Duration.ofMinutes(30));
100+
} else {
101+
// 成功则清除登录缓存
102+
bladeRedis.del(CacheNames.tenantKey(tenantId, CacheNames.USER_FAIL_KEY, account));
103+
}
83104
return userInfo;
84105
}
85106

blade-auth/src/main/java/org/springblade/auth/granter/PasswordTokenGranter.java

+26
Original file line numberDiff line numberDiff line change
@@ -16,28 +16,38 @@
1616
package org.springblade.auth.granter;
1717

1818
import lombok.AllArgsConstructor;
19+
import lombok.extern.slf4j.Slf4j;
1920
import org.springblade.auth.enums.BladeUserEnum;
2021
import org.springblade.auth.utils.TokenUtil;
22+
import org.springblade.common.cache.CacheNames;
23+
import org.springblade.core.log.exception.ServiceException;
24+
import org.springblade.core.redis.cache.BladeRedis;
2125
import org.springblade.core.secure.props.BladeAuthProperties;
2226
import org.springblade.core.tool.api.R;
2327
import org.springblade.core.tool.utils.DigestUtil;
2428
import org.springblade.core.tool.utils.Func;
29+
import org.springblade.core.tool.utils.WebUtil;
2530
import org.springblade.system.user.entity.UserInfo;
2631
import org.springblade.system.user.feign.IUserClient;
2732
import org.springframework.stereotype.Component;
2833

34+
import java.time.Duration;
35+
2936
/**
3037
* PasswordTokenGranter
3138
*
3239
* @author Chill
3340
*/
41+
@Slf4j
3442
@Component
3543
@AllArgsConstructor
3644
public class PasswordTokenGranter implements ITokenGranter {
3745

3846
public static final String GRANT_TYPE = "password";
47+
public static final Integer FAIL_COUNT = 5;
3948

4049
private IUserClient userClient;
50+
private BladeRedis bladeRedis;
4151

4252
private BladeAuthProperties authProperties;
4353

@@ -46,6 +56,14 @@ public UserInfo grant(TokenParameter tokenParameter) {
4656
String tenantId = tokenParameter.getArgs().getStr("tenantId");
4757
String account = tokenParameter.getArgs().getStr("account");
4858
String password = tokenParameter.getArgs().getStr("password");
59+
60+
// 判断登录是否锁定
61+
int cnt = Func.toInt(bladeRedis.get(CacheNames.tenantKey(tenantId, CacheNames.USER_FAIL_KEY, account)), 0);
62+
if (cnt >= FAIL_COUNT) {
63+
log.error("用户登录失败次数过多, 账号:{}, IP:{}", account, WebUtil.getIP());
64+
throw new ServiceException(TokenUtil.USER_HAS_TOO_MANY_FAILS);
65+
}
66+
4967
UserInfo userInfo = null;
5068
if (Func.isNoneBlank(account, password)) {
5169
// 获取用户类型
@@ -64,6 +82,14 @@ public UserInfo grant(TokenParameter tokenParameter) {
6482
}
6583
userInfo = result.isSuccess() ? result.getData() : null;
6684
}
85+
86+
if (userInfo == null || userInfo.getUser() == null) {
87+
// 增加错误锁定次数
88+
bladeRedis.setEx(CacheNames.tenantKey(tenantId, CacheNames.USER_FAIL_KEY, account), cnt + 1, Duration.ofMinutes(30));
89+
} else {
90+
// 成功则清除登录缓存
91+
bladeRedis.del(CacheNames.tenantKey(tenantId, CacheNames.USER_FAIL_KEY, account));
92+
}
6793
return userInfo;
6894
}
6995

blade-auth/src/main/java/org/springblade/auth/utils/TokenUtil.java

+1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ public class TokenUtil {
4747
public final static String HEADER_KEY = "Authorization";
4848
public final static String HEADER_PREFIX = "Basic ";
4949
public final static String ENCRYPT_PREFIX = "04";
50+
public final static String USER_HAS_TOO_MANY_FAILS = "用户登录失败次数过多";
5051
public final static String DEFAULT_AVATAR = "https://bladex.cn/images/logo.png";
5152

5253
/**

blade-common/src/main/java/org/springblade/common/cache/CacheNames.java

+21-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,26 @@ public interface CacheNames {
2727
String DICT_VALUE = "dict:value";
2828
String DICT_LIST = "dict:list";
2929

30-
String CAPTCHA_KEY = "blade:auth::captcha:";
30+
/**
31+
* 验证码key
32+
*/
33+
String CAPTCHA_KEY = "blade:auth::blade:captcha:";
34+
35+
/**
36+
* 登录失败key
37+
*/
38+
String USER_FAIL_KEY = "blade:user::blade:fail:";
39+
40+
/**
41+
* 返回租户格式的key
42+
*
43+
* @param tenantId 租户编号
44+
* @param cacheKey 缓存key
45+
* @param cacheKeyValue 缓存key值
46+
* @return tenantKey
47+
*/
48+
static String tenantKey(String tenantId, String cacheKey, String cacheKeyValue) {
49+
return tenantId.concat(":").concat(cacheKey).concat(cacheKeyValue);
50+
}
3151

3252
}

blade-service/blade-system/src/main/java/org/springblade/system/controller/UserController.java

+26-5
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import com.alibaba.excel.read.builder.ExcelReaderBuilder;
2121
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
2222
import com.baomidou.mybatisplus.core.metadata.IPage;
23+
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
2324
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
2425
import io.swagger.v3.oas.annotations.Operation;
2526
import io.swagger.v3.oas.annotations.Parameter;
@@ -30,15 +31,18 @@
3031
import jakarta.validation.Valid;
3132
import lombok.AllArgsConstructor;
3233
import lombok.SneakyThrows;
34+
import org.springblade.common.cache.CacheNames;
3335
import org.springblade.core.mp.support.Condition;
3436
import org.springblade.core.mp.support.Query;
37+
import org.springblade.core.redis.cache.BladeRedis;
3538
import org.springblade.core.secure.BladeUser;
3639
import org.springblade.core.secure.annotation.PreAuth;
3740
import org.springblade.core.secure.utils.SecureUtil;
3841
import org.springblade.core.tool.api.R;
3942
import org.springblade.core.tool.constant.BladeConstant;
4043
import org.springblade.core.tool.constant.RoleConstant;
4144
import org.springblade.core.tool.utils.Func;
45+
import org.springblade.core.tool.utils.StringUtil;
4246
import org.springblade.system.user.entity.User;
4347
import org.springblade.system.excel.UserExcel;
4448
import org.springblade.system.excel.UserImportListener;
@@ -69,6 +73,7 @@
6973
public class UserController {
7074

7175
private IUserService userService;
76+
private BladeRedis bladeRedis;
7277

7378
/**
7479
* 查询单条
@@ -85,7 +90,7 @@ public R<UserVO> detail(User user) {
8590
/**
8691
* 查询单条
8792
*/
88-
@ApiOperationSupport(order =2)
93+
@ApiOperationSupport(order = 2)
8994
@Operation(summary = "查看详情", description = "传入id")
9095
@GetMapping("/info")
9196
public R<UserVO> info(BladeUser user) {
@@ -212,7 +217,7 @@ public R<List<UserVO>> userList(User user) {
212217
@Operation(summary = "导入用户", description = "传入excel")
213218
public R importUser(MultipartFile file, Integer isCovered) {
214219
String filename = file.getOriginalFilename();
215-
if (StringUtils.isEmpty(filename)) {
220+
if (StringUtil.isBlank(filename)) {
216221
throw new RuntimeException("请上传文件!");
217222
}
218223
if ((!StringUtils.endsWithIgnoreCase(filename, ".xls") && !StringUtils.endsWithIgnoreCase(filename, ".xlsx"))) {
@@ -240,14 +245,14 @@ public R importUser(MultipartFile file, Integer isCovered) {
240245
@PreAuth(RoleConstant.HAS_ROLE_ADMIN)
241246
public void exportUser(@Parameter(hidden = true) @RequestParam Map<String, Object> user, BladeUser bladeUser, HttpServletResponse response) {
242247
QueryWrapper<User> queryWrapper = Condition.getQueryWrapper(user, User.class);
243-
if (!SecureUtil.isAdministrator()){
248+
if (!SecureUtil.isAdministrator()) {
244249
queryWrapper.lambda().eq(User::getTenantId, bladeUser.getTenantId());
245250
}
246251
queryWrapper.lambda().eq(User::getIsDeleted, BladeConstant.DB_NOT_DELETED);
247252
List<UserExcel> list = userService.exportUser(queryWrapper);
248253
response.setContentType("application/vnd.ms-excel");
249254
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
250-
String fileName = URLEncoder.encode("用户数据导出", StandardCharsets.UTF_8.name());
255+
String fileName = URLEncoder.encode("用户数据导出", StandardCharsets.UTF_8);
251256
response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx");
252257
EasyExcel.write(response.getOutputStream(), UserExcel.class).sheet("用户数据表").doWrite(list);
253258
}
@@ -263,7 +268,7 @@ public void exportUser(HttpServletResponse response) {
263268
List<UserExcel> list = new ArrayList<>();
264269
response.setContentType("application/vnd.ms-excel");
265270
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
266-
String fileName = URLEncoder.encode("用户数据模板", StandardCharsets.UTF_8.name());
271+
String fileName = URLEncoder.encode("用户数据模板", StandardCharsets.UTF_8);
267272
response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx");
268273
EasyExcel.write(response.getOutputStream(), UserExcel.class).sheet("用户数据表").doWrite(list);
269274
}
@@ -279,4 +284,20 @@ public R registerGuest(User user, Long oauthId) {
279284
}
280285

281286

287+
/**
288+
* 用户解锁
289+
*/
290+
@PostMapping("/unlock")
291+
@ApiOperationSupport(order = 16)
292+
@Operation(summary = "账号解锁")
293+
@PreAuth(RoleConstant.HAS_ROLE_ADMIN)
294+
public R unlock(String userIds) {
295+
if (StringUtil.isBlank(userIds)) {
296+
return R.fail("请至少选择一个用户");
297+
}
298+
List<User> userList = userService.list(Wrappers.<User>lambdaQuery().in(User::getId, Func.toLongList(userIds)));
299+
userList.forEach(user -> bladeRedis.del(CacheNames.tenantKey(user.getTenantId(), CacheNames.USER_FAIL_KEY, user.getAccount())));
300+
return R.success("操作成功");
301+
}
302+
282303
}

doc/nacos/blade.yaml

+2-2
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,13 @@ knife4j:
6767
language: zh_cn
6868
enableFooter: false
6969
enableFooterCustom: true
70-
footerCustomContent: Copyright © 2024 SpringBlade All Rights Reserved
70+
footerCustomContent: Copyright © 2025 SpringBlade All Rights Reserved
7171

7272
#swagger配置信息
7373
swagger:
7474
title: SpringBlade 接口文档系统
7575
description: SpringBlade 接口文档系统
76-
version: 4.4.0
76+
version: 4.5.0
7777
license: Powered By SpringBlade
7878
licenseUrl: https://bladex.cn
7979
terms-of-service-url: https://bladex.cn

pom.xml

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99
<packaging>pom</packaging>
1010

1111
<properties>
12-
<revision>4.4.0</revision>
12+
<revision>4.5.0</revision>
1313

14-
<blade.tool.version>4.4.2</blade.tool.version>
14+
<blade.tool.version>4.5.0</blade.tool.version>
1515

1616
<java.version>17</java.version>
1717
<maven.plugin.version>3.11.0</maven.plugin.version>

script/docker/.env

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
REGISTER=192.168.0.157/blade
2-
TAG=4.4.0
2+
TAG=4.5.0

0 commit comments

Comments
 (0)