fix(backend): 合并 K16 注册限流配置返工
This commit is contained in:
commit
d3b347aa09
7 files changed
+179
-13
No files matched your search
+4
-1
@@ -125,7 +125,10 @@ app.register.max-per-ip-per-day
|
|||||||
app.register.max-per-device-per-day
|
app.register.max-per-device-per-day
|
||||||
```
|
```
|
||||||
|
|
||||||
键缺失或值不是整数时回退默认 5 / 3;值小于等于 0 表示对应维度不限制。
|
运行时优先读取 `sys_config`。键缺失或值不是整数时,回退 `application.yml`
|
||||||
|
中的 `fruitshorts.auth.register-max-per-ip-per-day` /
|
||||||
|
`fruitshorts.auth.register-max-per-device-per-day`,缺省为 5 / 3;
|
||||||
|
值小于等于 0 表示对应维度不限制。
|
||||||
开发种子配置为 0 / 0,生产建议恢复为 5 / 3。
|
开发种子配置为 0 / 0,生产建议恢复为 5 / 3。
|
||||||
|
|
||||||
**残留风险**:IP 取自 `X-Forwarded-For`、设备号由客户端上报,都能伪造,只能挡脚本批量;
|
**残留风险**:IP 取自 `X-Forwarded-For`、设备号由客户端上报,都能伪造,只能挡脚本批量;
|
||||||
|
|||||||
@@ -9,14 +9,15 @@ app.register.max-per-ip-per-day
|
|||||||
app.register.max-per-device-per-day
|
app.register.max-per-device-per-day
|
||||||
```
|
```
|
||||||
|
|
||||||
键缺失或非整数时回退默认 5/3;值小于等于 0 时跳过对应 guard。注册接口、
|
运行时优先读取后台配置;键缺失或非整数时回退 `@Value` 注入值(默认 5/3);
|
||||||
错误码和错误文案不变。
|
值小于等于 0 时跳过对应 guard。注册接口、错误码和错误文案不变。
|
||||||
|
|
||||||
## 实现
|
## 实现
|
||||||
|
|
||||||
1. `AppAuthService` 注入 `ConfigService`,从 `sys_config` 读取两个键。
|
1. `AppAuthService` 注入 `ConfigService`,从 `sys_config` 优先读取两个键;
|
||||||
|
再用 `@Value` 注入 yml 值作为缺失/非法时的 fallback。
|
||||||
2. `sql/data-phase1.sql` 幂等写入开发默认 0/0 和说明。
|
2. `sql/data-phase1.sql` 幂等写入开发默认 0/0 和说明。
|
||||||
3. 不使用 `application.yml` 固化限流值。
|
3. `application.yml` 只声明 fallback(默认 5/3),运行时不以后台固化值为准。
|
||||||
4. `docs/api-phase1.md` §1.1 补充后台配置键、缺省/非法回退和 0=不限制口径。
|
4. `docs/api-phase1.md` §1.1 补充后台配置键、缺省/非法回退和 0=不限制口径。
|
||||||
5. 测试 mock `ConfigService`,覆盖自定义、0、缺省/非数字回退。
|
5. 测试 mock `ConfigService`,覆盖自定义、0、缺省/非数字回退。
|
||||||
6. 确认后台「系统设置」页面可访问 `/admin/configs`;若现有表单未展示新键,
|
6. 确认后台「系统设置」页面可访问 `/admin/configs`;若现有表单未展示新键,
|
||||||
@@ -28,6 +29,7 @@ app.register.max-per-device-per-day
|
|||||||
- `mvn -pl fruitshorts-app-api -am package -DskipTests`
|
- `mvn -pl fruitshorts-app-api -am package -DskipTests`
|
||||||
- 停止当前 8081 服务,启动新 jar,`/healthz` 通过
|
- 停止当前 8081 服务,启动新 jar,`/healthz` 通过
|
||||||
- 使用新设备号连续注册至少 4 次,确认不再 429(接口自检)
|
- 使用新设备号连续注册至少 4 次,确认不再 429(接口自检)
|
||||||
|
- 后台把 IP 上限临时改为 2:前两次成功,第三次返回 429;恢复 0 后继续注册成功
|
||||||
- `feature/backend-register-rate-config-fix` 以 `--no-ff` 合并回 `develop` 并 push
|
- `feature/backend-register-rate-config-fix` 以 `--no-ff` 合并回 `develop` 并 push
|
||||||
- PROGRESS 记录命令、原始输出和解读
|
- PROGRESS 记录命令、原始输出和解读
|
||||||
|
|
||||||
|
|||||||
+19
-1
@@ -55,7 +55,7 @@
|
|||||||
- 提交作者与提交者统一 `Zhou Weidong <502333560@qq.com>`;从干净 develop 拉 `feature/backend-register-rate-config`,完成后 `git merge --no-ff` 回 develop 并 push。
|
- 提交作者与提交者统一 `Zhou Weidong <502333560@qq.com>`;从干净 develop 拉 `feature/backend-register-rate-config`,完成后 `git merge --no-ff` 回 develop 并 push。
|
||||||
- 留痕写入 `FruitServer/docs/plans/PROGRESS.md`。
|
- 留痕写入 `FruitServer/docs/plans/PROGRESS.md`。
|
||||||
|
|
||||||
### 四、K16 完成回执(2026-10-17)
|
### 四、K16 首版回执(历史,已被返工回执替代)
|
||||||
|
|
||||||
**结论**:注册 IP/设备限流已改为读取后台 `sys_config`,开发两键为 0/0,
|
**结论**:注册 IP/设备限流已改为读取后台 `sys_config`,开发两键为 0/0,
|
||||||
支持 0=不限制;后台系统设置页可修改。注册接口与返回结构未变。
|
支持 0=不限制;后台系统设置页可修改。注册接口与返回结构未变。
|
||||||
@@ -75,6 +75,24 @@
|
|||||||
命令、日志和服务自检原始输出见
|
命令、日志和服务自检原始输出见
|
||||||
`docs/plans/PROGRESS.md`「32. K16 注册限流后台配置化」。
|
`docs/plans/PROGRESS.md`「32. K16 注册限流后台配置化」。
|
||||||
|
|
||||||
|
### 五、K16 返工回执(2026-10-17)
|
||||||
|
|
||||||
|
**结论**:已按最新口径改为“后台 `sys_config` 优先,`@Value` fallback”,
|
||||||
|
后台「系统配置」可按 IP/设备修改;开发库两键均为 0。
|
||||||
|
|
||||||
|
- 返工提交 `35d92c0 fix(backend): 后台注册限流配置优先并保留注入兜底`。
|
||||||
|
- `AppAuthService` 读取 `app.register.max-per-ip-per-day` /
|
||||||
|
`app.register.max-per-device-per-day`;缺失或非数字时回退
|
||||||
|
`fruitshorts.auth.register-max-*` 的 `@Value` 注入值。
|
||||||
|
- `sql/data-phase1.sql` 幂等种子已执行;查询结果为 IP 0、设备 0。
|
||||||
|
- `/admin/configs` 实测返回两键,现有系统设置页可修改。
|
||||||
|
- 临时把 IP 上限改为 2:前两次注册成功,第三次返回 `code=429`;
|
||||||
|
恢复 0 后再次注册成功。
|
||||||
|
- `AppAuthServiceTest` 18 项通过,全量 `mvn test` BUILD SUCCESS。
|
||||||
|
- 新 jar 已重启,8081 PID `94657`,`/healthz` 返回 HTTP 200。
|
||||||
|
|
||||||
|
原始输出与解读见 `docs/plans/PROGRESS.md`「33. K16 返工」。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## [待处理] 2026-10-17 | K14 客户端文档:README + 任务清单 + 文件说明
|
## [待处理] 2026-10-17 | K14 客户端文档:README + 任务清单 + 文件说明
|
||||||
|
|||||||
@@ -3158,6 +3158,137 @@ uid=UDAYJR-_E
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-10-17 K16 返工:sys_config 优先 + @Value 兜底
|
||||||
|
|
||||||
|
### 33.1 返工范围
|
||||||
|
|
||||||
|
K16 首版合并后,用户要求明确区分后台运行配置与 yml fallback:
|
||||||
|
|
||||||
|
- `AppAuthService` 运行时优先读取 `sys_config`:
|
||||||
|
`app.register.max-per-ip-per-day`、`app.register.max-per-device-per-day`;
|
||||||
|
- 键缺失或非整数时,回退 `@Value` 注入的
|
||||||
|
`fruitshorts.auth.register-max-*`;
|
||||||
|
- yml 中的值只作 fallback,默认 5 / 3;≤0 仍表示不限制。
|
||||||
|
|
||||||
|
返工分支:`fix/k16-register-config-fallback`。
|
||||||
|
|
||||||
|
### 33.2 TDD 与测试
|
||||||
|
|
||||||
|
先把非数字配置用例改为验证 @Value fallback=2,旧实现红灯:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Could not find field 'maxRegisterPerIpPerDay'
|
||||||
|
Tests run: 18, Failures: 0, Errors: 1, Skipped: 0
|
||||||
|
BUILD FAILURE
|
||||||
|
```
|
||||||
|
|
||||||
|
增加注入字段并接入 registerLimit 后,定向测试转绿:
|
||||||
|
|
||||||
|
```text
|
||||||
|
RUNNING AppAuthServiceTest
|
||||||
|
Tests run: 18, Failures: 0, Errors: 0, Skipped: 0
|
||||||
|
BUILD SUCCESS
|
||||||
|
```
|
||||||
|
|
||||||
|
全量测试:
|
||||||
|
|
||||||
|
```text
|
||||||
|
$ mvn test
|
||||||
|
[INFO] fruitshorts-module-user ............................ SUCCESS
|
||||||
|
[INFO] fruitshorts-app-api ............................... SUCCESS
|
||||||
|
[INFO] BUILD SUCCESS
|
||||||
|
```
|
||||||
|
|
||||||
|
代码提交:
|
||||||
|
|
||||||
|
```text
|
||||||
|
35d92c0 fix(backend): 后台注册限流配置优先并保留注入兜底
|
||||||
|
```
|
||||||
|
|
||||||
|
### 33.3 数据库与后台接口
|
||||||
|
|
||||||
|
重新执行 `sql/data-phase1.sql` 中的幂等插入:
|
||||||
|
|
||||||
|
```text
|
||||||
|
INSERT 0 0
|
||||||
|
```
|
||||||
|
|
||||||
|
查询结果:
|
||||||
|
|
||||||
|
```text
|
||||||
|
config_key | config_value | remark
|
||||||
|
app.register.max-per-device-per-day | 0 | 0=不限制;生产建议 3
|
||||||
|
app.register.max-per-ip-per-day | 0 | 0=不限制;生产建议 5
|
||||||
|
(2 rows)
|
||||||
|
```
|
||||||
|
|
||||||
|
使用本机开发管理员登录后,`GET /admin/configs` 返回:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "ok",
|
||||||
|
"configs": [
|
||||||
|
{
|
||||||
|
"key": "app.register.max-per-device-per-day",
|
||||||
|
"value": "0",
|
||||||
|
"remark": "0=不限制;生产建议 3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "app.register.max-per-ip-per-day",
|
||||||
|
"value": "0",
|
||||||
|
"remark": "0=不限制;生产建议 5"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
现有 `SettingsView.vue` 已使用 `listConfigsApi` / `updateConfigsApi`,
|
||||||
|
页面上的两个数值输入项可直接读写这两键。
|
||||||
|
|
||||||
|
### 33.4 阈值 2 往返验证
|
||||||
|
|
||||||
|
通过后台 `PUT /admin/configs` 把 IP 上限临时设为 2、设备上限设为 0,
|
||||||
|
使用同一条测试 IP 连续注册:
|
||||||
|
|
||||||
|
```text
|
||||||
|
attempt 1 HTTP 200 code=0 message=ok
|
||||||
|
attempt 2 HTTP 200 code=0 message=ok
|
||||||
|
attempt 3 HTTP 200 code=429 message=当前网络注册过于频繁,请稍后再试
|
||||||
|
```
|
||||||
|
|
||||||
|
随后通过同一后台接口恢复 IP=0,再注册一次:
|
||||||
|
|
||||||
|
```text
|
||||||
|
attempt 4 HTTP 200 code=0 message=ok
|
||||||
|
```
|
||||||
|
|
||||||
|
说明:当前统一响应包装对外使用 HTTP 200,业务码在 `code` 字段返回 429。
|
||||||
|
|
||||||
|
### 33.5 打包与 8081
|
||||||
|
|
||||||
|
```text
|
||||||
|
$ mvn -pl fruitshorts-app-api -am package -DskipTests
|
||||||
|
[INFO] BUILD SUCCESS
|
||||||
|
```
|
||||||
|
|
||||||
|
通过 `launchctl kickstart -k gui/501/com.fruitshorts.app-api` 重启:
|
||||||
|
|
||||||
|
```text
|
||||||
|
before PID: 80642
|
||||||
|
after PID: 94657
|
||||||
|
java 94657 ... TCP *:8081 (LISTEN)
|
||||||
|
```
|
||||||
|
|
||||||
|
健康检查:
|
||||||
|
|
||||||
|
```text
|
||||||
|
{"code":0,"message":"ok","data":{"status":"UP","app":"fruitshorts-app-api","profiles":"","time":"2026-10-17T23:38:32.363463"}}
|
||||||
|
HTTP 200
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 2026-10-17 iOS 构建阻塞:固定 shared_preferences_foundation 2.5.6
|
## 2026-10-17 iOS 构建阻塞:固定 shared_preferences_foundation 2.5.6
|
||||||
|
|
||||||
### 25.1 现象与根因
|
### 25.1 现象与根因
|
||||||
|
|||||||
@@ -43,6 +43,10 @@ springdoc:
|
|||||||
path: /v3/api-docs
|
path: /v3/api-docs
|
||||||
|
|
||||||
fruitshorts:
|
fruitshorts:
|
||||||
|
auth:
|
||||||
|
# 仅作 sys_config 缺失/非法时的兜底;运行时优先读取后台「系统配置」
|
||||||
|
register-max-per-ip-per-day: ${REGISTER_MAX_PER_IP_PER_DAY:5}
|
||||||
|
register-max-per-device-per-day: ${REGISTER_MAX_PER_DEVICE_PER_DAY:3}
|
||||||
interaction:
|
interaction:
|
||||||
# MACHINE_FIRST:机审通过即发布,命中 REVIEW 级敏感词才转人工
|
# MACHINE_FIRST:机审通过即发布,命中 REVIEW 级敏感词才转人工
|
||||||
# PRE_REVIEW :全部先审后发(生产建议)
|
# PRE_REVIEW :全部先审后发(生产建议)
|
||||||
|
|||||||
+4
-3
@@ -26,6 +26,7 @@ import com.fruitshorts.user.vo.TokenRespVO;
|
|||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
@@ -193,9 +194,9 @@ class AppAuthServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void should_fallback_to_default_limit_when_config_is_not_numeric() {
|
void should_fallback_to_default_limit_when_config_is_not_numeric() {
|
||||||
setRegisterLimits(5, 3);
|
ReflectionTestUtils.setField(authService, "maxRegisterPerIpPerDay", 2);
|
||||||
given(configService.get("app.register.max-per-ip-per-day", "5")).willReturn("invalid");
|
given(configService.get("app.register.max-per-ip-per-day", "2")).willReturn("invalid");
|
||||||
given(userMapper.selectCount(any())).willReturn(5L);
|
given(userMapper.selectCount(any())).willReturn(2L);
|
||||||
|
|
||||||
assertThatThrownBy(() -> authService.register(emailRegister("abcd1234", null), "203.0.113.8"))
|
assertThatThrownBy(() -> authService.register(emailRegister("abcd1234", null), "203.0.113.8"))
|
||||||
.isInstanceOf(ServiceException.class)
|
.isInstanceOf(ServiceException.class)
|
||||||
|
|||||||
+11
-4
@@ -26,6 +26,7 @@ import com.fruitshorts.user.vo.LogoutReqVO;
|
|||||||
import com.fruitshorts.user.vo.RegisterReqVO;
|
import com.fruitshorts.user.vo.RegisterReqVO;
|
||||||
import com.fruitshorts.user.vo.TokenRespVO;
|
import com.fruitshorts.user.vo.TokenRespVO;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.dao.DuplicateKeyException;
|
import org.springframework.dao.DuplicateKeyException;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -61,6 +62,14 @@ public class AppAuthService {
|
|||||||
private final PasswordEncoder passwordEncoder;
|
private final PasswordEncoder passwordEncoder;
|
||||||
private final ConfigService configService;
|
private final ConfigService configService;
|
||||||
|
|
||||||
|
/** 后台 sys_config 缺失或非法时使用;小于等于 0 表示不限制。 */
|
||||||
|
@Value("${fruitshorts.auth.register-max-per-ip-per-day:5}")
|
||||||
|
private int maxRegisterPerIpPerDay = 5;
|
||||||
|
|
||||||
|
/** 后台 sys_config 缺失或非法时使用;小于等于 0 表示不限制。 */
|
||||||
|
@Value("${fruitshorts.auth.register-max-per-device-per-day:3}")
|
||||||
|
private int maxRegisterPerDevicePerDay = 3;
|
||||||
|
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public TokenRespVO register(RegisterReqVO req, String clientIp) {
|
public TokenRespVO register(RegisterReqVO req, String clientIp) {
|
||||||
String targetType = AppIdentifier.normalizeType(req.getTargetType());
|
String targetType = AppIdentifier.normalizeType(req.getTargetType());
|
||||||
@@ -305,8 +314,6 @@ public class AppAuthService {
|
|||||||
|
|
||||||
private static final String REGISTER_IP_LIMIT_KEY = "app.register.max-per-ip-per-day";
|
private static final String REGISTER_IP_LIMIT_KEY = "app.register.max-per-ip-per-day";
|
||||||
private static final String REGISTER_DEVICE_LIMIT_KEY = "app.register.max-per-device-per-day";
|
private static final String REGISTER_DEVICE_LIMIT_KEY = "app.register.max-per-device-per-day";
|
||||||
private static final int DEFAULT_REGISTER_IP_LIMIT = 5;
|
|
||||||
private static final int DEFAULT_REGISTER_DEVICE_LIMIT = 3;
|
|
||||||
|
|
||||||
private static final int PASSWORD_MIN_LENGTH = 8;
|
private static final int PASSWORD_MIN_LENGTH = 8;
|
||||||
private static final int PASSWORD_MAX_LENGTH = 64;
|
private static final int PASSWORD_MAX_LENGTH = 64;
|
||||||
@@ -339,8 +346,8 @@ public class AppAuthService {
|
|||||||
*/
|
*/
|
||||||
private void guardRegisterRisk(String clientIp, String deviceId) {
|
private void guardRegisterRisk(String clientIp, String deviceId) {
|
||||||
LocalDateTime since = LocalDateTime.now().minusDays(1);
|
LocalDateTime since = LocalDateTime.now().minusDays(1);
|
||||||
int ipLimit = registerLimit(REGISTER_IP_LIMIT_KEY, DEFAULT_REGISTER_IP_LIMIT);
|
int ipLimit = registerLimit(REGISTER_IP_LIMIT_KEY, maxRegisterPerIpPerDay);
|
||||||
int deviceLimit = registerLimit(REGISTER_DEVICE_LIMIT_KEY, DEFAULT_REGISTER_DEVICE_LIMIT);
|
int deviceLimit = registerLimit(REGISTER_DEVICE_LIMIT_KEY, maxRegisterPerDevicePerDay);
|
||||||
String ip = AppIdentifier.nullSafe(clientIp);
|
String ip = AppIdentifier.nullSafe(clientIp);
|
||||||
if (ipLimit > 0 && StringUtils.hasText(ip)) {
|
if (ipLimit > 0 && StringUtils.hasText(ip)) {
|
||||||
Long byIp = appUserMapper.selectCount(Wrappers.<AppUser>lambdaQuery()
|
Long byIp = appUserMapper.selectCount(Wrappers.<AppUser>lambdaQuery()
|
||||||
|
|||||||
Reference in new issue
Block a user