爱看书的阿东

赐他一块白色石头,石头上写着新名

一次项目短信验证码整改实验

一次短信验证码整改实验

前言:

讨论内容部分为当初的一些短信验证码的需求细节讨论

这个短信验证码在并发量非常大的情况下有可能会失效,后续会进行整改升级,保证线程安全

需求

短信验证码(要想着怎么把所有的项目都整改起来,不影响原有业务运行) 3天时间,全部替换掉

  • 发送短信

    • 增加【业务类型】
    • 获取短信的时候,增加图片验证码(此处用第三方框架实现)
    • 单独增加短信验证码的 ip访问控制 CheckSendSmsIpVisitHelper ,注意别和 CheckIpVisitHelper 冲突
    • 校验手机号码长度 11 位
    • 60s 根据【手机号码+业务】判断只能发送一次短信,此处将 【手机号码+业务】作为 map 的 key 存储在上下文中
    • 可以灵活配置【手机号码+业务】 每天能够获取短信的次数
  • 校验短信

    • 增加【手机号+短信验证码】的匹配
    • 校验成功,清理掉session中存储的信息

讨论内容

  • 验证码就用第三方的。google kaptcha 这个可以试试
  • 这样 jydd apps 都可以用,2-3天可以完全替换掉所有业务的短信。
  • 页面的修改,直接找建哥提供样式。
  • 把所有业务都一并改了,省的隔三差五的出报告要整改
  • 我要一个完全独立的短信验证码模块,虽然不能在按照项目模块来划分,那就完全独立出来包和功能以及依赖
  • 其实可以直接在apps 的 core里面开发,然后那边直接引用调用?jar包的方式放进去就是的。

处理方案:

第三方图形验证码参考

SpringBoot之配置google kaptcha

SpringMvc项目中使用GoogleKaptcha 生成验证码

happy-captcha

图形验证码:

  1. 调整页面,增加图形验证码的模块(不同模块需要的改动点不一样)

也可以先把功能做出来,再让前端根据实际情况去调整样式

  1. 尝试增加 happy-captcha 或者 google-captcha (实际查询资料发现没有进行维护)
  2. 先不考虑美观问题,以实现功能为主要,后续需要改样式在找前端处理

20200903 已实现

短信校验

  1. 60s 根据【手机号码+业务】判断只能发送一次短信,此处将 【手机号码+业务】作为 map 的 key 存储在上下文中
  1. 将短信的配置独立到一个单独的xml 文件中,方便 spirngboot 项目以及 spinrgmvc 管理
  2. 业务模块按照模板的格式配置,不跟项目走
  3. xml 配置读取参考 节假日的xml 配置读取以及作用
  4. 整个部分可以参考节假日的处理方式,迁移比较方便

使用xml 配置手机+业务模块

  1. 校验手机号码长度 11 位
  1. 写到工具类里面,单独增加一个方法,需要的时候在调用的地方加入(方案一)
  2. 直接在调用的地方补充添加(方案二)

直接在短信接口加入即可

  1. 可以灵活配置【手机号码+业务】 每天能够获取短信的次数
  1. 其实就是业务模块单个手机号码的限制次数

PS: 目前的攻击手段可以用虚拟手机号码 + 肉鸡服务器 实现,手机号+业务的限制作用个人理解来看作用不明显

  1. 大致的工具类设置
  • 初始化读取xml配置
  • 加载一些动态配置到属性里面,包括一些校验次数的限制
  • 单例模式
  • 尽量少的使用三方工具包,尽量使用原生java实现
  • 注意jdk 版本的问题,不以jdk1.8 为基准
  • CheckSendSmsIpVisitHelper 可以是对 CheckIpVisitHelper 的扩展
  • happy-captcha 以此作为参考实现 ,google 的图形化验证码比较老了

实现

  1. 目前先尝试 使用一下是否可行, 如果可行在进行处理
  2. 先不考虑样式问题,先以实现功能保证可用并可以迁移为主
  3. 多测试,保证功能稳定,在考虑迁移到apps

大致流程

  1. 输入手机号码
  2. 在点击发送短信按钮之前,弹出输入图形验证码
  3. 输入正确的图形验证码,发送短信,图形验证取消,回到输入手机验证码界面
  4. 如果输入推行验证码不对,一直重复步骤2

问题:

1. 绕过图形验证码的接口,直接访问短信接口进行攻击,如何避免?

解决方案:

  1. 在 【手机号+业务】中增加一个图形验证码的key, 在校验之前,先校验用户当前提交的手机号和图形验证码是否匹配
    1. 匹配:清空用户当前的图形验证码,需要重新请求图形验证码接口才能进行下一次请求
    2. 超时:图形验证码有效时间为60秒,超过60秒需要重新请求图形验证码,重新请求短信接口
    3. 不匹配,返回错误信息
  2. 设置校验开关,如果需要在短信接口加入图形验证码的校验,则在发送短信之前,需要当前的图形验证码是否吻合(如果没有图形验证码说明没有经过图形验证这一步骤,直接返回失败)
    1. 图形验证码校验关闭,则不会涉及用户图形验证码和手机号的匹配(考虑临时需要关闭这种校验的情况)
    2. 开启,则会进行上面所说的匹配操作
  3. 图形验证码设置为通过校验之后失效,下次请求需要携带新的图形验证码,才能请求通过短信接口

2. 增加【手机号-业务】的配置校验

解决方案:

  1. 短信模块需要在js请求增加模块参数,如果没有模块参数,视为非法请求
  2. 请求带入 手机号-业务-key ,存储当前手机号对应业务的请求次数
  3. 增加判断
    1. 如果请求次数在当天内超过xml配置次数,将不再允许改手机号对应该接口进行请求,不再发送短信
    2. 但是如果超过了一天之后再次请求,需要将请求次数 重置为1,也可以重新发送短信接口请求
  4. 在需要的地方调用工具包即可

实现过程:

成果:

控制器调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 // 开启之后,才做进一步校验
if(PHONE_MODULE_CHECK_ENABLE){
// 添加 【手机+业务模块】校验 以及 【60秒重复调用校验】
boolean checkRequest = CheckSendMailHelper.checkContextMap(result, request, phone);
// 校验不通过的处理办法,可以自定
if (!checkRequest) {
return result;
}
}

//限制用户ip访问短信机获取验证码次数,默认10次
if (IP_CHECK_ENABLE && !CheckIpVisitHelper.check(request)) {
// 校验不通过的处理办法,可以自定
result.put("result", SmsRequestStatusEnum.RESULT_STATUS_6.getCode());
result.put("msg", SmsRequestStatusEnum.RESULT_STATUS_6.getName());
return result;
}

PHONE_MODULE_CHECK_ENABLE:短信-业务模块校验开关

IP_CHECK_ENABLE:限制短信每天的获取次数,也是手机号+业务模块

短信相关的枚举常量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
public enum SmsRequestStatusEnum {

/**
* 返回状态码 表示发送正常
*/
RESULT_STATUS_1(1, "返回状态码 表示发送正常"),
/**
* 60s内只能获取不能重复获取验证码
*/
RESULT_STATUS_2(2, "60s内只能获取不能重复获取验证码"),
/**
* 手机号码长度不正确
*/
RESULT_STATUS_3(3, "手机号码长度不正确"),
// /**
// * 用户session已失效
// */
// RESULT_STATUS_4(4, "用户session已失效"),
/**
* 缺少必要的参数:手机号!
*/
RESULT_STATUS_4(4, "缺少必要的参数:手机号!"),
/**
* 手机号码长度不正确
*/
RESULT_STATUS_5(5, "手机号码长度不正确"),
/**
* 同一个ip请求短信机次数过于频繁
*/
RESULT_STATUS_6(6, "同一个ip请求短信机次数过于频繁!"),

/**
* 60秒内不允许重复请求短信接口
*/
RESULT_STATUS_7(7, "60秒内不允许重复请求短信接口!"),
/**
* 缺少必要的请求参数:短信业务模块名称:phoneModule !
*/
RESULT_STATUS_9(9, "缺少必要的请求参数:短信业务模块名称:phoneModule !"),
/**
* 当前手机号请求超出限制,请等待24小时之后重新请求短信接口
*/
RESULT_STATUS_10(10, "当前手机号请求次数超出限制,请等待24小时之后重新请求短信接口"),
/**
* 图形验证码已失效,请重新请求短信接口!
*/
RESULT_STATUS_8(8, "图形验证码已失效,请重新请求短信接口!");

private int code;
private String name;

SmsRequestStatusEnum(int code, String name) {
this.code = code;
this.name = name;
}

public int getCode() {
return code;
}

public String getName() {
return name;
}

public static String getName(int code) {
for (SmsRequestStatusEnum item : SmsRequestStatusEnum.values()) {
if (item.getCode() == code) {
return item.getName();
}
}
return "";
}
}

IP检测工具类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
/**
* 检测ip访问辅助类,
* 主要处理某个时间段类,
* ip访问次数,以及设置封禁时间、
* 解封等操作,
* 用于防止频繁调用短信机攻击等
* <p>
* <p>
* 重写原理:
* 1. 使用LRUMap key 存储 IP号码,value 存储 访问次数以及时间(使用map)
* 2. 使用servletContext 存储 LRUMap,LRUMap 存储 的 key 为 IP号码-业务模块 VALUE 为 map
* 3. LRUMap 对应的 key IP号码+业务。 value 绑定了访问次数和时间
* 4. 如果没有配置模块,校验将会永久失败,IP的模块和短信的模块使用同一块配置
* 5. ServletContext 生命周期和web的生命周期相同
*
* 2020/09/08 重写工具类,
* 1. 不在暴露 map。
* 2. 使用servletContext 保存 Ip 的 map。Map<String,Object> 形式
* 3. 如果超过IP限制时间,自动进行解锁
*
* @author xd
*/
public class CheckIpVisitHelper {

/**
* 日志使用 短信的key
*/
private static final Logger logger = LoggerFactory.getLogger("phoneCode");

/**
* 手机访问限制初始化的值
*/
private static final int PHONE_REQUEST_INIT_SIZE = 1;

/**
* 封禁的时间(单位毫秒)
*/
private static final int FORBIDEN_TIME = 60 * 1000 * 60;
/**
* 超过访问时间重新计时(单位毫秒)
*/
private static final int MININTEVAL = 60 * 1000 * 60;

/**
* LRU Map 初始化大小
*/
private static final int LRU_MAP_INIT_SIZE = 100000;

/**
* IP 在指定时间内的限制次数
*/
private static final int IP_MAX_VISIST_TIME = Setter.getInt("sms.ip-size");

/**
* ip检测使用的 Map key
*/
private static final String IP_CHECK_MAP = "IP_CHECK_MAP";

/**
* 请求次数
*/
private static final String VISIT_COUNT_KEY = "visit_count";

/**
* 最后的请求时间
*/
private static final String VISIT_TIME_KEY = "visit_time";

/**
* IP号码-业务模块名称的格式
*/
private static final String IP_MOUDULE_FORMAT = "%s-%s";

/**
* ip检查工具,将map 放入 ServletContext
* 1. 检测基于 ServletContext
* 2. 请附带 phoneModule: 否则校验永远为false
* map 当中:
* key: IP号码-业务
* value:
* map -> {
* key: 请求次数:value: int
* key:请求的时间:value:date
* }
*
* @param request request请求域
* @return 如果校验没有超过限制 返回 true ,否则返回false
*/
public static boolean check(HttpServletRequest request) {
String remoteIp = RequestHelper.getRemoteIp(request);
ServletContext servletContext = request.getServletContext();
LRUMap attribute = (LRUMap) servletContext.getAttribute(IP_CHECK_MAP);
if (Objects.isNull(attribute)) {
attribute = new LRUMap(LRU_MAP_INIT_SIZE);
servletContext.setAttribute(IP_CHECK_MAP, attribute);
}
Date now = new Date();
// 根据 IP + 业务模块进行绑定
// 获取请求的模块名称 同时检查是否有配置模块
String phoneMouduleFlag = CheckSendMailHelper.checkExistsAndGetModule(request);
if (phoneMouduleFlag == null) {
return false;
}
// IP号码 -业务名称
String modulePhone = String.format(IP_MOUDULE_FORMAT, remoteIp, phoneMouduleFlag);
// 获取ip对应的的当前请求次数和请求时间
Map<String, Object> ipMap = (Map<String, Object>) attribute.get(modulePhone);
// 如果当前ip没有访问过
if (MapUtils.isEmpty(ipMap)) {
ipMap = new HashMap<>();
ipMap.put(VISIT_COUNT_KEY, PHONE_REQUEST_INIT_SIZE);
ipMap.put(VISIT_TIME_KEY, now);
attribute.putIfAbsent(modulePhone, ipMap);
return true;
}
int visitCount = (int) ipMap.get(VISIT_COUNT_KEY);
Date visitDate = (Date) ipMap.get(VISIT_TIME_KEY);
// 如果长时间没有访问,重新计算
if (now.getTime() - visitDate.getTime() > MININTEVAL) {
ipMap.put(VISIT_COUNT_KEY, PHONE_REQUEST_INIT_SIZE);
ipMap.put(VISIT_TIME_KEY, now);
return true;
}
// 如果访问的次数超过了限制的次数
if (visitCount > IP_MAX_VISIST_TIME) {
// 如果已经到达限制的次数,但是访问时间超过了限制的时间,重新计时,重新计算请求次数
if (now.getTime() - visitDate.getTime() > FORBIDEN_TIME) {
ipMap.put(VISIT_COUNT_KEY, PHONE_REQUEST_INIT_SIZE);
ipMap.put(VISIT_TIME_KEY, now);
return true;
}
logger.info("当前IP: {} 请求次数超过限制", remoteIp);
return false;
} else {
// IP访问次数 + 1
visitCount++;
// 更新访问次数
ipMap.put(VISIT_COUNT_KEY, visitCount);
// 更新访问时间
ipMap.put(VISIT_TIME_KEY, now);
}
return true;
}




}
  1. 使用的是servlet-context 全局变量作为存储,依赖web的服务器空间大小,当短信号码过量会造成服务器可访问内存不够,可以考虑用redis 等中间件去存储
  2. LRUMap:使用最少使用内容作为缓存的设计,存储业务需要判断的手机号等
  3. 静态方法意味着会出现并发的问题,整个工具类是线程不安全的。

短信发送校验工具类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
/**
* 短信发送校验工具类
* map 存储的 key 为手机号码-业务
* value 为 发送对象等其他信息
* 包含
* 1. 图形验证码(不开放不做校验)
* 2. 图形验证码有效时间
* 3. 【手机号-业务】 key-name 的配置
* 4. 【手机号-业务-锁定时间】 key-date
*
*
* @Author lazytimes
* @Date 2020/09/02 10:21
**/
public class CheckSendMailHelper {

/**
* 短信验证码配置
*/
private static final Logger logger = LoggerFactory.getLogger("phoneCode");

/**
* 60 秒内不允许重复请求
*/
private static final int PHONE_REQUEST_TIME = 60 * 1000;

/**
* 60 秒 内 图形验证码有效
*/
private static final int CAPTCHA_REQUEST_TIME = 60 * 1000;

/**
* 用户模块手机号的限制时间 24 小时
*/
private static final int PHONE_REQUEST_WAIT_TIME = 60 * 1000 * 24 * 60;

/**
* 手机访问限制初始化的值
*/
private static final int PHONE_REQUEST_INIT_SIZE = 1;

/**
* 请求上下文的map key
*/
private static final String CONTEXT_MAP = "CONTEXT_MAP";

/**
* 手机号-业务模块名称的格式
*/
private static final String PHONE_MOUDULE_FORMAT = "%s-%s";


/**
* 手机号-业务模块-请求key 的格式标注用户当前模块的请求 定时器
*/
private static final String PHONE_MOUDULE_TIMER_FORMAT = "%s-%s-timer";

/**
* 短信验证码模块的通用格式
*/
private static final String SMS_MODULE_TEMPLATE = "sms.modules.%s";

/**
* 手机号-业务-图形验证码 模块名称的格式
*/
private static final String CAPTCHA_MOUDULE_FORMAT = "%s-%s-captcha";

/**
* 手机号-业务模块-图形验证码-请求key 的格式标注用户当前模块的请求 图形验证码 每个手机号对应业务一份
*/
private static final String CAPTCHA_MOUDULE_TIMER_FORMAT = "%s-%s-captcha-timer";

/**
* 业务模块名称参数Key
*/
private static final String PHONE_MOUDULE_FLAG = "phoneModule";

/**
* 图形验证码key
*/
private static final String CAPCHACODE = "capchaCode";

// /**
// * 最后发送时间key
// */
// private static final String LAST_SEND_TIME = "lastSendTime";

/**
* 图形验证码开关
*/
private static final boolean CAPTCHA_ENABLE = Setter.getBoolean("captcha.enable");


/**
* 为当前的用户手机号码绑定 图形验证码
* 图形验证码用于短信接口请求使用,超过一定时间,图形验证码失效
* 【手机号-业务-图形验证码】:key
* 【手机号-业务-图形验证码-超时时间】:key
*
* @param phoneCode 手机号
* @param code 图形验证码
* @param request 请求
*/
public static void addCapcha(String phoneCode, String code, HttpServletRequest request) {
if (!CAPTCHA_ENABLE) {
logger.info("请开启图形验证码校验之后,再配合本工具类方法使用!");
return;
}
ServletContext servletContext = request.getServletContext();
Map<String, Map<String, Object>> attribute = initServletContextMap(servletContext);
Date now = new Date();
// 获取请求的模块名称 同时检查是否有配置模块
String phoneMouduleFlag = checkExistsAndGetModule(request);
if (StringUtils.isBlank(phoneMouduleFlag)) {
return;
}
// 手机号 -业务名称
String modulePhone = String.format(PHONE_MOUDULE_FORMAT, phoneCode, phoneMouduleFlag);
// 手机号- 业务名称 - 图形验证码
String capchaModule = String.format(CAPTCHA_MOUDULE_FORMAT, phoneCode, phoneMouduleFlag);
// 手机号 - 业务名称 -图形验证码 - 定时
String capchaModuleTimer = String.format(CAPTCHA_MOUDULE_TIMER_FORMAT, phoneCode, phoneMouduleFlag);
if (!attribute.containsKey(modulePhone)) {
HashMap<String, Object> stringObjectHashMap = new HashMap<>();
stringObjectHashMap.put(capchaModule, code);
// 图片的有效期
stringObjectHashMap.put(capchaModuleTimer, now);
attribute.put(modulePhone, stringObjectHashMap);
} else {
Map<String, Object> stringObjectMap = attribute.get(modulePhone);
// 更新验证码以及有效期
stringObjectMap.put(capchaModule, code);
// 图片的有效期
stringObjectMap.put(capchaModuleTimer, now);
}
}

/**
* 手机号限制发送处理
* 1. 增加对于用户请求短信接口的限制,60秒访问一次
* 2. 增加图形验证码和用户的手机号绑定匹配
* 1. 图形校验可以灵活开放和关闭
* 3. 【手机号-业务】的key配置,短信接口当中需要对于用户的请求做限制
*
* @param result 封装了返回的状态和信息的 result
* @param request 请求request
* @param phoneCode 手机号码
* @return
*/
public static boolean checkContextMap(Map<String, Object> result, HttpServletRequest request, String phoneCode) {
// 获取当前模块配置Map集合
ServletContext servletContext = request.getServletContext();
Map<String, Map<String, Object>> attribute = initServletContextMap(servletContext);
Date now = new Date();
// 获取请求的模块名称
String phoneMouduleFlag = checkExistsAndGetModule(request);
if (phoneMouduleFlag == null) {
result.put("result", SmsRequestStatusEnum.RESULT_STATUS_9.getCode());
result.put("msg", SmsRequestStatusEnum.RESULT_STATUS_9.getName());
return false;
}
// 当前短信业务模块【手机号-业务】
String modulePhone = String.format(PHONE_MOUDULE_FORMAT, phoneCode, phoneMouduleFlag);
// 当前模块【手机号-业务-请求限制时间】
String modulePhoneTimer = String.format(PHONE_MOUDULE_TIMER_FORMAT, phoneCode, phoneMouduleFlag);
// 当前模块每个用户每天最多请求次数
int moduleCount = Setter.getInt(String.format(SMS_MODULE_TEMPLATE, phoneMouduleFlag));
if (!attribute.containsKey(modulePhone)) {
// 需要自行初始化
HashMap<String, Object> stringObjectHashMap = new HashMap<>();
// 初始化短信接口调用次数
stringObjectHashMap.put(modulePhone, PHONE_REQUEST_INIT_SIZE);
// 初始化短信接口调用时间
stringObjectHashMap.put(modulePhoneTimer, now);
attribute.put(modulePhone, stringObjectHashMap);
return true;
} else {
Map<String, Object> objectMap = attribute.get(modulePhone);
// 开启图形验证码校验才做处理
if (CAPTCHA_ENABLE) {
if (!checkCatpchaCode(result, request, phoneCode, now, phoneMouduleFlag, objectMap)) {
return true;
}
}
// 获取当前【手机号+业务】的对应 访问次数,以及最后的访问时间
Object count = objectMap.get(modulePhone);
Object timer = objectMap.get(modulePhoneTimer);
// 初始化
if (Objects.isNull(count) || Objects.isNull(timer)) {
objectMap.put(modulePhone, PHONE_REQUEST_INIT_SIZE);
objectMap.put(modulePhoneTimer, now);
return true;
}
Integer integer = Integer.valueOf(objectMap.get(modulePhone).toString());
Date time = (Date) timer;
// 检查当前短信+业务是否在60秒内访问
if(!checkLastGetTime(result, now, time)){
return false;
}
//如果长时间未访问,重置
if ((now.getTime() - time.getTime()) > PHONE_REQUEST_WAIT_TIME) {
// 刷新时间
objectMap.put(modulePhone, PHONE_REQUEST_INIT_SIZE);
objectMap.put(modulePhoneTimer, now);
return true;
}
// 当前模块超过了请求限制
if (integer > moduleCount) {
// 超过了请求时间限制,解封
if (now.getTime() - time.getTime() > PHONE_REQUEST_WAIT_TIME) {
// 刷新时间
objectMap.put(modulePhone, PHONE_REQUEST_INIT_SIZE);
objectMap.put(modulePhoneTimer, now);
return true;
}
result.put("result", SmsRequestStatusEnum.RESULT_STATUS_10.getCode());
result.put("msg", SmsRequestStatusEnum.RESULT_STATUS_10.getName());
return false;
}
// 模块请求次数 + 1
objectMap.put(modulePhone, integer + PHONE_REQUEST_INIT_SIZE);
// 刷新时间
objectMap.put(modulePhoneTimer, now);
}
return true;
}

/**
* 校验图形验证码
*
* @param result 返回处理结果
* @param request 请求
* @param phoneCode 手机号
* @param now 当前时间
* @param phoneMouduleFlag 手机号 - 业务模块 标识
* @param objectMap servletContext 对象
* @return
*/
private static boolean checkCatpchaCode(Map<String, Object> result, HttpServletRequest request, String phoneCode, Date now, String phoneMouduleFlag, Map<String, Object> objectMap) {
// 手机号- 业务名称 - 图形验证码
String capchaModule = String.format(CAPTCHA_MOUDULE_FORMAT, phoneCode, phoneMouduleFlag);
// 手机号 - 业务名称 -图形验证码 - 定时
String capchaModuleTimer = String.format(CAPTCHA_MOUDULE_TIMER_FORMAT, phoneCode, phoneMouduleFlag);
// 图形验证码超过60秒失效
Date captchaCodeValidPeriod = (Date) objectMap.get(capchaModuleTimer);
// 获取请求参数的验证码
String requestCaptchaCode = RequestHelper.getString(CAPCHACODE, request);
// 拿到map中的图形验证码
Object requestCode = objectMap.get(capchaModule);
// 是否存在图形验证码的参数,同时比对是否和请求参数一致
if (StringUtils.isBlank(requestCaptchaCode) || Objects.isNull(requestCode)) {
result.put("result", SmsRequestStatusEnum.RESULT_STATUS_8.getCode());
result.put("msg", SmsRequestStatusEnum.RESULT_STATUS_8.getName());
return false;
}
// 如果超时或者图形验证码不匹配,需要重新请求图形验证码
if (!Objects.equals(requestCaptchaCode, requestCode.toString()) || (now.getTime() - captchaCodeValidPeriod.getTime() > (CAPTCHA_REQUEST_TIME))) {
result.put("result", SmsRequestStatusEnum.RESULT_STATUS_8.getCode());
result.put("msg", SmsRequestStatusEnum.RESULT_STATUS_8.getName());
return false;
} else {
// 清空用户的图形验证码
objectMap.put(capchaModule, null);
}
return true;
}

/**
* 检查最后的访问时间是否在指定时间内容
*
* @param result 返回对象结果
* @param now 当前时间
* @return
*/
private static boolean checkLastGetTime(Map<String, Object> result, Date now, Date lastSend) {
// 60 秒内不允许再次发送
if ((now.getTime() - lastSend.getTime()) <= PHONE_REQUEST_TIME) {
result.put("result", SmsRequestStatusEnum.RESULT_STATUS_7.getCode());
result.put("msg", SmsRequestStatusEnum.RESULT_STATUS_7.getName());
return false;
}
return true;
}

/**
* 初始化全局上下文的Map容器
*
* @param servletContext 上下文
* @return 初始化之后的map参数
*/
private static Map<String, Map<String, Object>> initServletContextMap(ServletContext servletContext) {
Map<String, Map<String, Object>> attribute = (Map<String, Map<String, Object>>) servletContext.getAttribute(CONTEXT_MAP);
if (Objects.isNull(attribute)) {
attribute = new HashMap<>();
servletContext.setAttribute(CONTEXT_MAP, attribute);
}
return attribute;
}


/**
* 检查请求参数中是否存在业务模块配置
*
* @param request 请求request
* @return
*/
static String checkExistsAndGetModule(HttpServletRequest request) {
String phoneMouduleFlag = RequestHelper.getString(PHONE_MOUDULE_FLAG, request);
String moduleNo = Setter.getString(String.format(SMS_MODULE_TEMPLATE, phoneMouduleFlag));
if (StringUtils.isBlank(moduleNo)) {
logger.info("未找到对应的短信模块,请在xml配置短信模块名称,并在请求参数中加入 phoneModule: 对应模块名称之后再进行请求");
return null;
}
return phoneMouduleFlag;
}

}
  1. 使用手机号-业务模块。先获取是否存在对应的模块,然后进行校验
  2. 图形验证码的方法需要开启推行验证码的情况下,配合使用
  3. HashMap的线程是不安全的,可以考虑使用ConcurrentHashMap

图形验证码的配置:

1
2
3
4
5
6
7
8
9
10
11
<!-- =================================================================== -->
<!-- 核心:图形验证码的通用配置 -->
<!-- =================================================================== -->
<captcha description="图形验证码的通用配置">
<enable description="是否开放图形验证码" value="false" />
<length description="设置字符长度" value="5" />
<!-- 验证码图片的宽度 默认 160 -->
<width description="设置动画宽度" value="160" />
<!-- 验证码图片的高度 默认 50 -->
<height description="设置动画宽度" value="50" />
</captcha>

短信验证码的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- =================================================================== -->
<!--系统发送短信配置 -->
<!-- =================================================================== -->
<sms description="webService短信机服务配置">
<isopen description="是否开启短信发送" value="true"/>
<!-- 模块配置: 需要 name 模块名称,用于短信校验 和 value 表示每天最多的请求次数 -->
<modules>
<!-- 注册模块 -->
<registered description="注册模块" value="5"/>
<!-- 信箱请求短信验证码 -->
<mailbox description="信箱模块" value="10"/>
</modules>
<ip-size description="ip检测的限制次数" value="10"/>
<phoneMoudleCheck-enable description="手机号-业务模块校验是否开启" value="true"/>
<ip-enable description="IP检测开关" value="true"/>
</sms>

总结:

  1. 工具类基于配置进行开关配置
  2. 按照手机号+业务模块,划分同一手机号在不同的业务模块进行校验拦截
  3. 图形验证码可以配合短信接口使用,但是目前来看耦合还是有点严重

结语:

本人学艺不精,代码写的比较烂,这篇文章算是给自己留坑以后填。

如果看文章费劲头,专门另写一篇说说独立使用。

小小工具类,仅供参考

  • 本文作者: lazytime
  • 本文链接: https://whitestore.top/2020/10/29/mail/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC 许可协议。转载请注明出处!