wwh před 1 dnem
rodič
revize
ebe8c48a6a

+ 3 - 3
baqing-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java

@@ -64,7 +64,7 @@ public class SysLoginController
64
     /**
64
     /**
65
      * 管理端登录。
65
      * 管理端登录。
66
      * <p>
66
      * <p>
67
-     * 互联网部署且策略为 SMS_2FA 时,除 username/password/图形验证码外还须传 {@code smsCode}
67
+     * 互联网部署且策略为 SMS_2FA 时,须传 username/password/{@code smsCode}(不要求图形验证码)
68
      * 须先调用 {@link #sendLoginSms} 获取短信验证码。
68
      * 须先调用 {@link #sendLoginSms} 获取短信验证码。
69
      * </p>
69
      * </p>
70
      * 
70
      * 
@@ -85,11 +85,11 @@ public class SysLoginController
85
     /**
85
     /**
86
      * 发送登录短信验证码(互联网双因子第一步)。
86
      * 发送登录短信验证码(互联网双因子第一步)。
87
      * <p>
87
      * <p>
88
-     * 请求体与登录类似,须携带图形验证码 uuid/code 防刷。
88
+     * 请求体仅需 username;sms-2fa 下不校验图形验证码,依赖 IP 发送间隔防刷。
89
      * 用户须已绑定手机号;验证码写入 Redis,开发环境可在日志中查看 Stub 短信内容。
89
      * 用户须已绑定手机号;验证码写入 Redis,开发环境可在日志中查看 Stub 短信内容。
90
      * </p>
90
      * </p>
91
      *
91
      *
92
-     * @param body username + 图形验证码
92
+     * @param body username
93
      */
93
      */
94
     @PostMapping("/login/sms/send")
94
     @PostMapping("/login/sms/send")
95
     public AjaxResult sendLoginSms(@RequestBody LoginSmsSendBody body)
95
     public AjaxResult sendLoginSms(@RequestBody LoginSmsSendBody body)

+ 0 - 3
baqing-admin/src/test/java/com/ruoyi/framework/web/service/LoginSmsServiceTest.java

@@ -55,9 +55,6 @@ class LoginSmsServiceTest
55
     @Mock
55
     @Mock
56
     private SmsGatewayClient smsGatewayClient;
56
     private SmsGatewayClient smsGatewayClient;
57
 
57
 
58
-    @Mock
59
-    private SysLoginService loginService;
60
-
61
     @Mock
58
     @Mock
62
     private LoginInforRecorder loginInforRecorder;
59
     private LoginInforRecorder loginInforRecorder;
63
 
60
 

+ 3 - 3
doc/密码改造/README.md

@@ -26,8 +26,8 @@
26
 | `ruoyi-crypto` 模块 | ✅ Stub | `CryptoService` + BouncyCastle 开发实现 |
26
 | `ruoyi-crypto` 模块 | ✅ Stub | `CryptoService` + BouncyCastle 开发实现 |
27
 | 阿里云短信 SDK | ✅ | `alibabacloud-dysmsapi20170525`,支持 `dry-run` 日志 Stub |
27
 | 阿里云短信 SDK | ✅ | `alibabacloud-dysmsapi20170525`,支持 `dry-run` 日志 Stub |
28
 | `sys_login_policy` | ✅ 代码+DDL | 脚本 `sql/crypto_phase1.sql` |
28
 | `sys_login_policy` | ✅ 代码+DDL | 脚本 `sql/crypto_phase1.sql` |
29
-| UKey 证书登录 | ⏳ 待实施 | 接口与验签服务器对接待二期 |
30
-| 前端登录改造 | ⏳ 待实施 | 可不改前端,用 API 联调后端 |
29
+| UKey 证书登录 | ⏳ 待实施 | 字段/策略枚举已预留(`cert_sn`、`LoginPolicyType.CERT`);接口未实现 |
30
+| 前端登录改造 | ✅ 短信双因子 | 支持 `VUE_APP_LOGIN_AUTH_MODE=ruoyi|sms-2fa` |
31
 | 字段 SM3/SM4 | ⏳ 二期 | MyBatis 拦截器、存量迁移 |
31
 | 字段 SM3/SM4 | ⏳ 二期 | MyBatis 拦截器、存量迁移 |
32
 
32
 
33
 ---
33
 ---
@@ -63,7 +63,7 @@ login:
63
 
63
 
64
 ## 外部依赖
64
 ## 外部依赖
65
 
65
 
66
-- **签名验签服务器**:国密 UKey 证书登录、SM2 验签(待联调
66
+- **签名验签服务器**:国密 UKey 证书登录、SM2 验签(待实施
67
 - **密码机(HSM)**:SM3-HMAC、SM4 加解密(一期 Stub,生产待对接)
67
 - **密码机(HSM)**:SM3-HMAC、SM4 加解密(一期 Stub,生产待对接)
68
 - **短信网关**:阿里云 dysmsapi(一期已集成 SDK)
68
 - **短信网关**:阿里云 dysmsapi(一期已集成 SDK)
69
 
69
 

+ 4 - 4
doc/密码改造/实施计划与验收.md

@@ -17,7 +17,7 @@
17
 
17
 
18
 ## 2. 一期:身份鉴别与密码框架
18
 ## 2. 一期:身份鉴别与密码框架
19
 
19
 
20
-> **进度说明(互联网管理端)**:后端短信双因子、`ruoyi-crypto` Stub、阿里云短信 SDK、`login.auth-mode` 切换已落地;UKey 证书登录与前端改造仍待实施
20
+> **进度说明**:后端短信双因子、`ruoyi-crypto` Stub、阿里云短信 SDK、`login.auth-mode` 切换已落地;UKey 证书登录接口已撤销,仅保留字段与策略枚举预留
21
 
21
 
22
 ### 2.1 任务清单
22
 ### 2.1 任务清单
23
 
23
 
@@ -25,14 +25,14 @@
25
 | --- | --- | --- | --- |
25
 | --- | --- | --- | --- |
26
 | 1.1 | 采购/部署密码机、验签服务器联调环境 | 联调报告 | ⏳ |
26
 | 1.1 | 采购/部署密码机、验签服务器联调环境 | 联调报告 | ⏳ |
27
 | 1.2 | 新建 `ruoyi-crypto` 模块,`CryptoService` 封装 SM3/SM4 | 代码 + 单元测试(Stub) | ✅ |
27
 | 1.2 | 新建 `ruoyi-crypto` 模块,`CryptoService` 封装 SM3/SM4 | 代码 + 单元测试(Stub) | ✅ |
28
-| 1.3 | 对接签名验签服务器,`/login/cert` 接口 | 证书登录可用 | ⏳ |
28
+| 1.3 | 对接签名验签服务器,`/login/cert` 接口 | 证书登录可用 | ⏳ 待实施 |
29
 | 1.4 | 短信双因子登录(互联网) | `/login/sms/send`、登录校验 | ✅ 后端 |
29
 | 1.4 | 短信双因子登录(互联网) | `/login/sms/send`、登录校验 | ✅ 后端 |
30
 | 1.4a | 阿里云短信 SDK 集成 | `AliyunSmsGatewayClient` + `dry-run` | ✅ |
30
 | 1.4a | 阿里云短信 SDK 集成 | `AliyunSmsGatewayClient` + `dry-run` | ✅ |
31
 | 1.4b | 若依默认登录一键切换 | `login.auth-mode: ruoyi` | ✅ |
31
 | 1.4b | 若依默认登录一键切换 | `login.auth-mode: ruoyi` | ✅ |
32
-| 1.5 | `sys_user` 增加 `cert_sn`、`login_policy` 等字段 | `sql/crypto_phase1.sql` | ✅ 脚本已提供 |
32
+| 1.5 | `sys_user` 增加 `cert_sn`、`login_policy` 等字段 | `sql/crypto_phase1.sql` | ✅ 脚本已提供(字段预留) |
33
 | 1.6 | `sys_login_policy` 配置与文档 | 策略可配置 | ✅ |
33
 | 1.6 | `sys_login_policy` 配置与文档 | 策略可配置 | ✅ |
34
 | 1.7 | 安全审计:短信失败记 `sys_logininfor` | 日志可查 | ✅ |
34
 | 1.7 | 安全审计:短信失败记 `sys_logininfor` | 日志可查 | ✅ |
35
-| 1.8 | 前端:登录页短信双因子、`api/login.js` | 待前端改造(后端可先 API 联调) |
35
+| 1.8 | 前端:登录页短信双因子、`api/login.js` |  |
36
 
36
 
37
 ### 2.2 验收标准
37
 ### 2.2 验收标准
38
 
38
 

+ 10 - 8
doc/密码改造/应用密码改造技术方案.md

@@ -84,7 +84,9 @@ flowchart TB
84
 | 业务操作员 | 按角色配置 | 口令 + 可选短信/UKey | 由 `sys_login_policy` 配置 |
84
 | 业务操作员 | 按角色配置 | 口令 + 可选短信/UKey | 由 `sys_login_policy` 配置 |
85
 | 移动端/App | 普通用户 | 现有口令/Token 体系 | 敏感接口可增加短信二次确认 |
85
 | 移动端/App | 普通用户 | 现有口令/Token 体系 | 敏感接口可增加短信二次确认 |
86
 
86
 
87
-### 2.2 管理员 UKey 证书登录流程
87
+### 2.2 管理员 UKey 证书登录流程(⏳ 待实施)
88
+
89
+> 以下为设计方案;本期已撤销接口实现,仅保留 `sys_user.cert_sn` 等字段与 `LoginPolicyType.CERT` 枚举。
88
 
90
 
89
 ```mermaid
91
 ```mermaid
90
 sequenceDiagram
92
 sequenceDiagram
@@ -107,14 +109,14 @@ sequenceDiagram
107
     API->>API: 写 sys_logininfor + 安全审计
109
     API->>API: 写 sys_logininfor + 安全审计
108
 ```
110
 ```
109
 
111
 
110
-**后端改造点**
112
+**后端改造点(待实施)**
111
 
113
 
112
 | 模块 | 改造内容 |
114
 | 模块 | 改造内容 |
113
 | --- | --- |
115
 | --- | --- |
114
 | `SysLoginController` | 新增 `GET /login/cert/challenge`、`POST /login/cert` |
116
 | `SysLoginController` | 新增 `GET /login/cert/challenge`、`POST /login/cert` |
115
-| `sys_user` | 增加 `cert_sn`、`cert_subject`、`login_policy` |
117
+| `sys_user` | 增加 `cert_sn`、`cert_subject`、`login_policy`(**字段已预留**) |
116
 | `TokenService` | 证书登录成功后与普通登录共用 JWT 签发逻辑 |
118
 | `TokenService` | 证书登录成功后与普通登录共用 JWT 签发逻辑 |
117
-| `SysLoginService` | 增加 `loginByCert`、`verifyCertSignature` |
119
+| 登录服务 | 增加证书验签登录与验签服务器对接 |
118
 | 权限 | 超级管理员角色可配置强制证书登录 |
120
 | 权限 | 超级管理员角色可配置强制证书登录 |
119
 
121
 
120
 **验签失败处理**
122
 **验签失败处理**
@@ -126,8 +128,8 @@ sequenceDiagram
126
 
128
 
127
 **流程**
129
 **流程**
128
 
130
 
129
-1. `POST /login/sms/send`:校验图形验证码 → 发送短信 OTP 至 `sys_user.phonenumber`
130
-2. `POST /login`:提交 `username`、`password`、`smsCode`(图形验证码)
131
+1. `POST /login/sms/send`:提交账号 → 发送短信 OTP 至 `sys_user.phonenumber`(不校验图形验证码)
132
+2. `POST /login`:提交 `username`、`password`、`smsCode`(不要求图形验证码)
131
 3. 校验短信 OTP(Redis)→ 校验口令(BCrypt)→ 签发 Token,写登录日志
133
 3. 校验短信 OTP(Redis)→ 校验口令(BCrypt)→ 签发 Token,写登录日志
132
 
134
 
133
 **改造点(一期已实施)**
135
 **改造点(一期已实施)**
@@ -344,8 +346,8 @@ payload = dataType + "|" + keyVer + "|" + field1 + "|" + field2 + "|" ...
344
 
346
 
345
 | 期次 | 内容 | 状态 |
347
 | 期次 | 内容 | 状态 |
346
 | --- | --- | --- |
348
 | --- | --- | --- |
347
-| 一期 | 短信双因子后端、`ruoyi-crypto` Stub、阿里云短信 SDK、`auth-mode` 切换 | ✅ 后端已完成 |
348
-| 一期余 | UKey 证书登录、前端登录页 | ⏳ |
349
+| 一期 | 短信双因子后端、`ruoyi-crypto` Stub、阿里云短信 SDK、`auth-mode` 切换、前端短信双因子 | ✅ |
350
+| 一期余 | UKey 证书登录 | ⏳ 待实施(字段/策略枚举已预留) |
349
 | 二期 | `sys_user` / 权限表 / 日志表 SM3;个人信息 SM4 | ⏳ |
351
 | 二期 | `sys_user` / 权限表 / 日志表 SM3;个人信息 SM4 | ⏳ |
350
 | 三期 | 业务表按映射表改造;存量迁移 | ⏳ |
352
 | 三期 | 业务表按映射表改造;存量迁移 | ⏳ |
351
 | 四期 | 定时巡检、告警中心、密钥轮换 | ⏳ |
353
 | 四期 | 定时巡检、告警中心、密钥轮换 | ⏳ |

+ 10 - 41
doc/密码改造/配置与接口说明.md

@@ -79,36 +79,15 @@ sms:
79
 | `crypto.enabled` | 密码改造总开关;false 时业务可跳过加解密(仅建议迁移双写期) |
79
 | `crypto.enabled` | 密码改造总开关;false 时业务可跳过加解密(仅建议迁移双写期) |
80
 | `crypto.hsm.provider` | `stub`=BouncyCastle 开发实现;生产对接密码机 SDK |
80
 | `crypto.hsm.provider` | `stub`=BouncyCastle 开发实现;生产对接密码机 SDK |
81
 | `crypto.keys.*` | 各数据类型在密码机内的密钥索引(非密钥本身) |
81
 | `crypto.keys.*` | 各数据类型在密码机内的密钥索引(非密钥本身) |
82
-| `login.auth-mode` | `ruoyi` / `sms-2fa`;非空时优先于 `internet-sms.enabled` |
82
+| `login.auth-mode` | `ruoyi` / `sms-2fa`;非空时优先于子开关 |
83
 | `login.network-zone` | 与 `sys_login_policy.network_zone` 配合解析策略 |
83
 | `login.network-zone` | 与 `sys_login_policy.network_zone` 配合解析策略 |
84
 | `login.internet-sms.*` | OTP 长度、Redis TTL、同 IP 发送间隔 |
84
 | `login.internet-sms.*` | OTP 长度、Redis TTL、同 IP 发送间隔 |
85
 | `sms.aliyun.dry-run` | `true` 或未配齐 AccessKey 时仅打日志 Stub,不调阿里云 API |
85
 | `sms.aliyun.dry-run` | `true` 或未配齐 AccessKey 时仅打日志 Stub,不调阿里云 API |
86
 | `sms.aliyun.*` | 阿里云 dysmsapi SendSms 参数 |
86
 | `sms.aliyun.*` | 阿里云 dysmsapi SendSms 参数 |
87
 
87
 
88
-### 1.4 二期及以后配置(草案,尚未实施)
88
+### 1.4 说明
89
 
89
 
90
-```yaml
91
-crypto:
92
-  integrity:
93
-    verify-on-read: true
94
-    verify-before-update: true
95
-    patrol-cron: "0 0 2 * * ?"
96
-    patrol-batch-size: 500
97
-  alert:
98
-    sms-enabled: false
99
-    notify-roles: admin,security
100
-
101
-sign-server:
102
-  enabled: true
103
-  base-url: https://10.0.0.20:8443/sign-api
104
-  app-id: baqing-admin
105
-  connect-timeout-ms: 5000
106
-
107
-login:
108
-  cert:
109
-    enabled: true
110
-    challenge-ttl-seconds: 300
111
-```
90
+> UKey 证书登录(`/login/cert*`、`sign-server.*`)本期已撤销,仅保留库表字段与策略枚举预留;详见技术方案 §2.2。完整性巡检、告警等见实施计划。
112
 
91
 
113
 ---
92
 ---
114
 
93
 
@@ -127,20 +106,18 @@ login:
127
 
106
 
128
 ```json
107
 ```json
129
 {
108
 {
130
-  "username": "admin",
131
-  "uuid": "图形验证码uuid",
132
-  "code": "图形验证码"
109
+  "username": "admin"
133
 }
110
 }
134
 ```
111
 ```
135
 
112
 
113
+> sms-2fa 下不要求图形验证码;防刷依赖同 IP 发送间隔(`login.internet-sms.send-interval-seconds`)。
114
+
136
 **POST /login(互联网双因子)**
115
 **POST /login(互联网双因子)**
137
 
116
 
138
 ```json
117
 ```json
139
 {
118
 {
140
   "username": "admin",
119
   "username": "admin",
141
   "password": "******",
120
   "password": "******",
142
-  "code": "图形验证码",
143
-  "uuid": "图形验证码uuid",
144
   "smsCode": "123456"
121
   "smsCode": "123456"
145
 }
122
 }
146
 ```
123
 ```
@@ -166,22 +143,14 @@ login:
166
 | 发送过频 | 短信发送过于频繁,请稍后再试 |
143
 | 发送过频 | 短信发送过于频繁,请稍后再试 |
167
 | 未绑定手机 | 账号未绑定手机号,无法登录 |
144
 | 未绑定手机 | 账号未绑定手机号,无法登录 |
168
 
145
 
169
-### 2.2 证书登录(一期预留,未实施)
146
+### 2.2 证书登录(⏳ 待实施)
170
 
147
 
171
 | 方法 | 路径 | 说明 | 状态 |
148
 | 方法 | 路径 | 说明 | 状态 |
172
 | --- | --- | --- | --- |
149
 | --- | --- | --- | --- |
173
 | GET | `/login/cert/challenge` | 获取登录随机数 challenge | ⏳ 待实施 |
150
 | GET | `/login/cert/challenge` | 获取登录随机数 challenge | ⏳ 待实施 |
174
 | POST | `/login/cert` | 证书验签登录 | ⏳ 待实施 |
151
 | POST | `/login/cert` | 证书验签登录 | ⏳ 待实施 |
175
 
152
 
176
-**POST /login/cert 请求体(规划)**
177
-
178
-```json
179
-{
180
-  "certBase64": "MIIC...",
181
-  "signedData": "MEUCI...",
182
-  "challenge": "a1b2c3d4-e5f6-..."
183
-}
184
-```
153
+**已预留**:`sys_user.cert_sn` / `cert_subject`、`login_policy=CERT`、`LoginPolicyType.CERT`。本期不提供接口与验签服务器对接。
185
 
154
 
186
 ---
155
 ---
187
 
156
 
@@ -250,7 +219,7 @@ public interface IntegrityService {
250
 | `SmsCodeExpireException` | 200 + 业务错误 | 短信验证码已失效 |
219
 | `SmsCodeExpireException` | 200 + 业务错误 | 短信验证码已失效 |
251
 | `CryptoIntegrityException` | 409 | 数据完整性校验失败(二期) |
220
 | `CryptoIntegrityException` | 409 | 数据完整性校验失败(二期) |
252
 | `CryptoException` | 500 | 系统安全服务暂不可用 |
221
 | `CryptoException` | 500 | 系统安全服务暂不可用 |
253
-| `CertVerifyException` | 401 | 证书验签失败(待实施) |
222
+| `CertVerifyException` | 401 | 证书验签失败 |
254
 
223
 
255
 ---
224
 ---
256
 
225
 
@@ -273,7 +242,7 @@ public interface IntegrityService {
273
 - [x] 阿里云 SDK 集成(dry-run / 真实发送)
242
 - [x] 阿里云 SDK 集成(dry-run / 真实发送)
274
 - [ ] 执行 `sql/crypto_phase1.sql`
243
 - [ ] 执行 `sql/crypto_phase1.sql`
275
 - [ ] 管理员账号绑定手机号
244
 - [ ] 管理员账号绑定手机号
276
-- [ ] 前端登录页接入 `smsCode`(见前端方案,可选)
245
+- [x] 前端登录页接入 `smsCode`
277
 - [ ] 等保测评环境 `auth-mode=sms-2fa` + `dry-run=false`
246
 - [ ] 等保测评环境 `auth-mode=sms-2fa` + `dry-run=false`
278
 
247
 
279
 ### 后续阶段
248
 ### 后续阶段

+ 3 - 3
ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginSmsSendBody.java

@@ -3,7 +3,7 @@ package com.ruoyi.common.core.domain.model;
3
 /**
3
 /**
4
  * 登录短信验证码发送请求体({@code POST /login/sms/send})。
4
  * 登录短信验证码发送请求体({@code POST /login/sms/send})。
5
  * <p>
5
  * <p>
6
- * 字段与 {@link LoginBody} 中图形验证码部分一致,用于在发送短信前完成人机校验,防止接口被恶意刷短信
6
+ * sms-2fa 模式下仅需 {@link #username};{@link #code}/{@link #uuid} 为兼容保留字段,后端不再校验
7
  * 不包含密码与 smsCode。
7
  * 不包含密码与 smsCode。
8
  * </p>
8
  * </p>
9
  */
9
  */
@@ -12,10 +12,10 @@ public class LoginSmsSendBody
12
     /** 待登录的系统账号(sys_user.user_name) */
12
     /** 待登录的系统账号(sys_user.user_name) */
13
     private String username;
13
     private String username;
14
 
14
 
15
-    /** 用户输入的图形验证码 */
15
+    /** 兼容保留:图形验证码(sms-2fa 下不使用) */
16
     private String code;
16
     private String code;
17
 
17
 
18
-    /** 获取图形验证码时接口返回的 uuid,与 Redis 中 captcha_codes:{uuid} 对应 */
18
+    /** 兼容保留:图形验证码 uuid(sms-2fa 下不使用) */
19
     private String uuid;
19
     private String uuid;
20
 
20
 
21
     public String getUsername()
21
     public String getUsername()

+ 8 - 15
ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/LoginSmsService.java

@@ -6,7 +6,6 @@ import java.util.List;
6
 import java.util.concurrent.TimeUnit;
6
 import java.util.concurrent.TimeUnit;
7
 import java.util.stream.Collectors;
7
 import java.util.stream.Collectors;
8
 import org.springframework.beans.factory.annotation.Autowired;
8
 import org.springframework.beans.factory.annotation.Autowired;
9
-import org.springframework.context.annotation.Lazy;
10
 import org.springframework.stereotype.Service;
9
 import org.springframework.stereotype.Service;
11
 import com.ruoyi.common.constant.CacheConstants;
10
 import com.ruoyi.common.constant.CacheConstants;
12
 import com.ruoyi.common.constant.Constants;
11
 import com.ruoyi.common.constant.Constants;
@@ -73,11 +72,6 @@ public class LoginSmsService
73
     @Autowired
72
     @Autowired
74
     private SmsGatewayClient smsGatewayClient;
73
     private SmsGatewayClient smsGatewayClient;
75
 
74
 
76
-    /** 复用图形验证码校验逻辑,与 {@link SysLoginService#validateCaptcha} 保持一致 */
77
-    @Autowired
78
-    @Lazy
79
-    private SysLoginService loginService;
80
-
81
     /** 异步写 sys_logininfor,与 Spring 容器解耦便于单测 */
75
     /** 异步写 sys_logininfor,与 Spring 容器解耦便于单测 */
82
     @Autowired
76
     @Autowired
83
     private LoginInforRecorder loginInforRecorder;
77
     private LoginInforRecorder loginInforRecorder;
@@ -85,20 +79,19 @@ public class LoginSmsService
85
     /**
79
     /**
86
      * 发送登录短信验证码。
80
      * 发送登录短信验证码。
87
      * <p>
81
      * <p>
88
-     * 流程:图形验证码 → 开关/策略校验 → 用户与手机号校验 → IP 频控 → 生成 OTP 写 Redis → 调短信网关。
82
+     * 流程:开关/策略校验 → 用户与手机号校验 → IP 频控 → 生成 OTP 写 Redis → 调短信网关。
83
+     * sms-2fa 模式下不校验图形验证码,依赖 IP 发送间隔防刷。
89
      * 安全注意:用户不存在时仍返回统一错误文案({@link UserNotExistsException}),避免枚举用户名。
84
      * 安全注意:用户不存在时仍返回统一错误文案({@link UserNotExistsException}),避免枚举用户名。
90
      * </p>
85
      * </p>
91
      *
86
      *
92
-     * @param body 含 username、图形验证码 uuid/code
87
+     * @param body 含 username(code/uuid 可省略)
93
      */
88
      */
94
     public void sendLoginSms(LoginSmsSendBody body)
89
     public void sendLoginSms(LoginSmsSendBody body)
95
     {
90
     {
96
         String username = StringUtils.trim(body.getUsername());
91
         String username = StringUtils.trim(body.getUsername());
97
-        // 1. 图形验证码(与 /login 相同规则,防止接口被刷)
98
-        loginService.validateCaptcha(username, body.getCode(), body.getUuid());
99
         assertInternetSmsEnabled();
92
         assertInternetSmsEnabled();
100
 
93
 
101
-        // 2. 用户必须存在且当前策略要求短信双因子
94
+        // 1. 用户必须存在且当前策略要求短信双因子
102
         SysUser user = userService.selectUserByUserName(username);
95
         SysUser user = userService.selectUserByUserName(username);
103
         if (user == null)
96
         if (user == null)
104
         {
97
         {
@@ -110,7 +103,7 @@ public class LoginSmsService
110
             throw new ServiceException("当前账号无需短信验证码登录");
103
             throw new ServiceException("当前账号无需短信验证码登录");
111
         }
104
         }
112
 
105
 
113
-        // 3. 手机号来自 sys_user.phonenumber(二期可改为 SM4 解密后比对)
106
+        // 2. 手机号来自 sys_user.phonenumber(二期可改为 SM4 解密后比对)
114
         String mobile = resolveMobile(user);
107
         String mobile = resolveMobile(user);
115
         if (StringUtils.isEmpty(mobile))
108
         if (StringUtils.isEmpty(mobile))
116
         {
109
         {
@@ -118,7 +111,7 @@ public class LoginSmsService
118
             throw new UserMobileNotBoundException();
111
             throw new UserMobileNotBoundException();
119
         }
112
         }
120
 
113
 
121
-        // 4. 同 IP 发送间隔限制(默认 60s,配置 login.internet-sms.send-interval-seconds)
114
+        // 3. 同 IP 发送间隔限制(默认 60s,配置 login.internet-sms.send-interval-seconds)
122
         String clientIp = IpUtils.getIpAddr();
115
         String clientIp = IpUtils.getIpAddr();
123
         String limitKey = CacheConstants.LOGIN_SMS_LIMIT_KEY + clientIp;
116
         String limitKey = CacheConstants.LOGIN_SMS_LIMIT_KEY + clientIp;
124
         if (redisCache.hasKey(limitKey))
117
         if (redisCache.hasKey(limitKey))
@@ -127,14 +120,14 @@ public class LoginSmsService
127
             throw new SmsSendIntervalException();
120
             throw new SmsSendIntervalException();
128
         }
121
         }
129
 
122
 
130
-        // 5. 生成数字 OTP 并写入 Redis(按手机号维度,非用户名,避免换账号刷同一手机)
123
+        // 4. 生成数字 OTP 并写入 Redis(按手机号维度,非用户名,避免换账号刷同一手机)
131
         String code = generateCode(loginProperties.getInternetSms().getCodeLength());
124
         String code = generateCode(loginProperties.getInternetSms().getCodeLength());
132
         String smsKey = CacheConstants.LOGIN_SMS_CODE_KEY + mobile;
125
         String smsKey = CacheConstants.LOGIN_SMS_CODE_KEY + mobile;
133
         int ttl = loginProperties.getInternetSms().getCodeTtlSeconds();
126
         int ttl = loginProperties.getInternetSms().getCodeTtlSeconds();
134
         redisCache.setCacheObject(smsKey, code, ttl, TimeUnit.SECONDS);
127
         redisCache.setCacheObject(smsKey, code, ttl, TimeUnit.SECONDS);
135
         redisCache.setCacheObject(limitKey, "1", loginProperties.getInternetSms().getSendIntervalSeconds(), TimeUnit.SECONDS);
128
         redisCache.setCacheObject(limitKey, "1", loginProperties.getInternetSms().getSendIntervalSeconds(), TimeUnit.SECONDS);
136
 
129
 
137
-        // 6. 调用短信网关;失败则回滚 Redis 中的 OTP
130
+        // 5. 调用短信网关;失败则回滚 Redis 中的 OTP
138
         boolean sent = smsGatewayClient.sendLoginCode(mobile, code);
131
         boolean sent = smsGatewayClient.sendLoginCode(mobile, code);
139
         if (!sent)
132
         if (!sent)
140
         {
133
         {

+ 14 - 8
ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java

@@ -2,7 +2,6 @@ package com.ruoyi.framework.web.service;
2
 
2
 
3
 import javax.annotation.Resource;
3
 import javax.annotation.Resource;
4
 import org.springframework.beans.factory.annotation.Autowired;
4
 import org.springframework.beans.factory.annotation.Autowired;
5
-import org.springframework.context.annotation.Lazy;
6
 import org.springframework.security.authentication.AuthenticationManager;
5
 import org.springframework.security.authentication.AuthenticationManager;
7
 import org.springframework.security.authentication.BadCredentialsException;
6
 import org.springframework.security.authentication.BadCredentialsException;
8
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
7
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
@@ -24,6 +23,7 @@ import com.ruoyi.common.utils.DateUtils;
24
 import com.ruoyi.common.utils.MessageUtils;
23
 import com.ruoyi.common.utils.MessageUtils;
25
 import com.ruoyi.common.utils.StringUtils;
24
 import com.ruoyi.common.utils.StringUtils;
26
 import com.ruoyi.common.utils.ip.IpUtils;
25
 import com.ruoyi.common.utils.ip.IpUtils;
26
+import com.ruoyi.framework.config.properties.LoginProperties;
27
 import com.ruoyi.framework.manager.AsyncManager;
27
 import com.ruoyi.framework.manager.AsyncManager;
28
 import com.ruoyi.framework.manager.factory.AsyncFactory;
28
 import com.ruoyi.framework.manager.factory.AsyncFactory;
29
 import com.ruoyi.framework.security.context.AuthenticationContextHolder;
29
 import com.ruoyi.framework.security.context.AuthenticationContextHolder;
@@ -35,6 +35,7 @@ import com.ruoyi.system.service.ISysUserService;
35
  * <p>
35
  * <p>
36
  * 若依默认口令登录流程;密码改造一期在互联网管理端增加短信双因子:
36
  * 若依默认口令登录流程;密码改造一期在互联网管理端增加短信双因子:
37
  * 当 {@link LoginSmsService#requiresSmsTwoFactor} 为 true 时,须在口令认证前校验 {@code smsCode}。
37
  * 当 {@link LoginSmsService#requiresSmsTwoFactor} 为 true 时,须在口令认证前校验 {@code smsCode}。
38
+ * {@code login.auth-mode=sms-2fa} 时以短信 OTP 代替图形验证码,不再校验图片验证码。
38
  * </p>
39
  * </p>
39
  * 
40
  * 
40
  * @author ruoyi
41
  * @author ruoyi
@@ -57,9 +58,10 @@ public class SysLoginService
57
     @Autowired
58
     @Autowired
58
     private ISysConfigService configService;
59
     private ISysConfigService configService;
59
 
60
 
60
-    /** 互联网短信双因子;与 LoginSmsService 存在循环依赖,使用 @Lazy 打破 */
61
     @Autowired
61
     @Autowired
62
-    @Lazy
62
+    private LoginProperties loginProperties;
63
+
64
+    @Autowired
63
     private LoginSmsService loginSmsService;
65
     private LoginSmsService loginSmsService;
64
 
66
 
65
     /**
67
     /**
@@ -79,7 +81,8 @@ public class SysLoginService
79
     /**
81
     /**
80
      * 登录验证(支持互联网短信双因子)。
82
      * 登录验证(支持互联网短信双因子)。
81
      * <p>
83
      * <p>
82
-     * 完整顺序:图形验证码 → 用户名/密码格式与黑名单 → <b>短信 OTP(若策略要求)</b> → Spring Security 口令认证 → 签发 Token。
84
+     * 完整顺序:图形验证码(非 sms-2fa)→ 用户名/密码格式与黑名单 → <b>短信 OTP(若策略要求)</b>
85
+     * → Spring Security 口令认证 → 签发 Token。
83
      * {@code smsCode} 对非双因子用户可省略;双因子用户缺失或错误将在口令校验前被拒绝并记登录失败审计。
86
      * {@code smsCode} 对非双因子用户可省略;双因子用户缺失或错误将在口令校验前被拒绝并记登录失败审计。
84
      * </p>
87
      * </p>
85
      *
88
      *
@@ -87,8 +90,11 @@ public class SysLoginService
87
      */
90
      */
88
     public String login(String username, String password, String code, String uuid, String smsCode)
91
     public String login(String username, String password, String code, String uuid, String smsCode)
89
     {
92
     {
90
-        // 验证码校验
91
-        validateCaptcha(username, code, uuid);
93
+        // sms-2fa 以短信 OTP 代替图形验证码
94
+        if (!loginProperties.isSmsTwoFactorActive())
95
+        {
96
+            validateCaptcha(username, code, uuid);
97
+        }
92
         // 登录前置校验
98
         // 登录前置校验
93
         loginPreCheck(username, password);
99
         loginPreCheck(username, password);
94
 
100
 
@@ -135,8 +141,8 @@ public class SysLoginService
135
     /**
141
     /**
136
      * 校验图形验证码。
142
      * 校验图形验证码。
137
      * <p>
143
      * <p>
138
-     * 登录与 {@code POST /login/sms/send} 共用此方法;验证码一次性使用(校验后删 Redis)。
139
-     * 设为 public 供 {@link LoginSmsService} 复用
144
+     * 登录与短信发送共用此方法;验证码一次性使用(校验后删 Redis)。
145
+     * sms-2fa 登录流程不再调用本方法
140
      * </p>
146
      * </p>
141
      * 
147
      * 
142
      * @param username 用户名(仅用于记登录失败日志)
148
      * @param username 用户名(仅用于记登录失败日志)

+ 4 - 0
sql/crypto_phase1.sql

@@ -38,3 +38,7 @@ VALUES ('admin', 'INTERNET', 'SMS_2FA', '1', '互联网超级管理员双因子'
38
 -- 互联网环境:默认策略(未匹配角色时)
38
 -- 互联网环境:默认策略(未匹配角色时)
39
 INSERT INTO sys_login_policy (role_key, network_zone, policy_type, enabled, remark, create_time)
39
 INSERT INTO sys_login_policy (role_key, network_zone, policy_type, enabled, remark, create_time)
40
 VALUES (NULL, 'INTERNET', 'SMS_2FA', '1', '互联网管理端默认双因子', NOW());
40
 VALUES (NULL, 'INTERNET', 'SMS_2FA', '1', '互联网管理端默认双因子', NOW());
41
+
42
+-- 内网/专网:默认口令登录(CERT 策略枚举与 cert_sn 字段一期仅预留,证书登录接口待实施)
43
+INSERT INTO sys_login_policy (role_key, network_zone, policy_type, enabled, remark, create_time)
44
+VALUES (NULL, 'INTERNAL', 'PASSWORD', '1', '内网管理端默认口令登录', NOW());