Spring手机验证码

0. 前言

在注册各种网站的时候,我们经常需要使用到短信验证码,可是个人开发的时候,在各大平台申请短信验证码的模板的时,往往需要已经上线的项目,这里找到一个容联云的网站,能够免费133天短信验证码测试。容联云,全球智能通讯云服务商 (yuntongxun.com)

1. api申请

注册号账号后,我们需要创建一个应用

image-20241123214142478

点击创建好的应用中的应用管理,获取其中的appID

image-20241123214217380

在控制台首页保存好开发者账号id和token,外泄会导致账号金额被盗取,由于没有进行认证,所以我只能使用默认的3个测试号码

image-20241123214236051

2. 创建spring项目

创建一个普通的spring项目,点击最上方的开发文档SDK文档,参考其中的文档编写测试项目:Java SDK (yuntongxun.com)

image-20241123214320059

2.1 导入依赖

根据上方内容可以得知,我们需要在pom.xml中导入这个项目提供的一些依赖包,这里列出生成验证码用的hutool各种使用到的依赖包:

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
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.cloopen</groupId>
<artifactId>java-sms-sdk</artifactId>
<version>1.0.4</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.19</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

2.2 添加api

image-20241123214338475

根据官方提供的参考文档我们可以得知,我们需要定义其中的

accountSId ;accountToken ;appId 这三个变量,就是上述提到的三个值

2.3 application.yml

在这里配置全局常量,就是上述的token值,

image-20241123203713487

2.4 SMSModel

由于每次使用到 CCPRestSmsSDK sdk = new CCPRestSmsSDK();这个对象,其中由需要设置id等等属性,我们将他封装成为一个对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@Data
@AllArgsConstructor
@NoArgsConstructor
@ConfigurationProperties("yuntongxun.sms")
public class SMSModel {
private String accountId;
private String authToken;
private String appId;
private String serverIp;
private String serverPort;
}

2.5 JSON封装

统一传递方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

@AllArgsConstructor
@NoArgsConstructor
@Data
public class ResponseModel<T> implements Serializable{
private Integer code;
private String msg;
private T data;
}

2.6 SMSUtil

这里主要做的就是生成验证码和发送的请求了,由于并发量比较小,可以使用map存储也可以使用更加规范的redis存储,这里两种方式都会给出,redis需要自行配置环境

redis版本

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
package com.kd_13.ssmcode.util;

import cn.hutool.captcha.generator.RandomGenerator;
import com.cloopen.rest.sdk.BodyType;
import com.cloopen.rest.sdk.CCPRestSmsSDK;
import com.kd_13.ssmcode.model.SMSModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.concurrent.TimeUnit;

@Component
public class SMSUtil {

@Autowired
private SMSModel smsModel;
@Autowired
private RedisTemplate redisTemplate;

/**
* 发送验证码
* @param phone
* @return
*/
public String sendSMS(String phone) {
String ServerIp = smsModel.getServerIp();
String serverPort = smsModel.getServerPort();
String accountSId = smsModel.getAccountId();
String accountToken = smsModel.getAuthToken();
String appId = smsModel.getAppId();
CCPRestSmsSDK sdk = new CCPRestSmsSDK();
sdk.init(ServerIp, serverPort);
sdk.setAccount(accountSId, accountToken);
sdk.setAppId(appId);
sdk.setBodyType(BodyType.Type_JSON);
String to = phone;
String templateId = "1";
RandomGenerator randomGenerator = new RandomGenerator("0123456789", 4);
String randomNum = randomGenerator.generate();
Long expire = 2l;
String[] datas = {
randomNum, expire.toString()
};
HashMap<String, Object> result = sdk.sendTemplateSMS(to, templateId, datas);
if ("000000".equals(result.get("statusCode"))) {
redisTemplate.opsForValue().set(phone,randomNum);
redisTemplate.expire(phone,10L, TimeUnit.SECONDS);
return randomNum;
} else {
return null;
}
}

/**
* 验证码校验
* @param phone
* @param code
* @return
*/
public boolean checkSMS(String phone,String code){
Object obj = redisTemplate.opsForValue().get(phone);
if(obj == null){
throw new RuntimeException("验证码过期");
}
String redis_code = (String)obj;
return redis_code.equals(code);
}
}

map版本

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
package com.kd_13.ssmcode.util;

import cn.hutool.captcha.generator.RandomGenerator;
import com.cloopen.rest.sdk.BodyType;
import com.cloopen.rest.sdk.CCPRestSmsSDK;
import com.kd_13.ssmcode.model.SMSModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

@Component
public class SMSUtil_map {

@Autowired
private SMSModel smsModel;

// 用于存储验证码的内存 Map
private final Map<String, ExpiringCode> codeStore = new HashMap<>();

/**
* 发送验证码
* @param phone
* @return
*/
public String sendSMS(String phone) {
String ServerIp = smsModel.getServerIp();
String serverPort = smsModel.getServerPort();
String accountSId = smsModel.getAccountId();
String accountToken = smsModel.getAuthToken();
String appId = smsModel.getAppId();
CCPRestSmsSDK sdk = new CCPRestSmsSDK();
sdk.init(ServerIp, serverPort);
sdk.setAccount(accountSId, accountToken);
sdk.setAppId(appId);
sdk.setBodyType(BodyType.Type_JSON);
String to = phone;
String templateId = "1";
RandomGenerator randomGenerator = new RandomGenerator("0123456789", 4);
String randomNum = randomGenerator.generate();

// 发送短信
HashMap<String, Object> result = sdk.sendTemplateSMS(to, templateId, new String[]{randomNum, "2"});
if ("000000".equals(result.get("statusCode"))) {
// 存储验证码及其过期时间
codeStore.put(phone, new ExpiringCode(randomNum, System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(5)));
return randomNum;
} else {
return null;
}
}

/**
* 验证码校验
* @param phone
* @param code
* @return
*/
public boolean checkSMS(String phone, String code) {
ExpiringCode expiringCode = codeStore.get(phone);
System.out.println("当前时间"+System.currentTimeMillis());
System.out.println("数据库时间"+expiringCode.expiryTime);
if (expiringCode == null || System.currentTimeMillis() > expiringCode.expiryTime) {
throw new RuntimeException("验证码过期");
}
return expiringCode.code.equals(code);
}

// 内部类,用于存储验证码及其过期时间
private static class ExpiringCode {
private final String code;
private final long expiryTime;

public ExpiringCode(String code, long expiryTime) {
this.code = code;
this.expiryTime = expiryTime;
}
}
}

2.7 SMSController

控制层,和接口打交道的,不做过多赘述

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
package com.kd_13.ssmcode.controller;

import com.kd_13.ssmcode.model.ResponseModel;
import com.kd_13.ssmcode.util.SMSUtil;
import com.kd_13.ssmcode.util.SMSUtil_map;
import lombok.val;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.*;

import java.util.Map;
import java.util.Objects;

@RestController
public class SMSController {

@Autowired
private SMSUtil_map smsUtil;

/**
* @description: 访问发送验证码
* @author: 康弟弟
* @date: 2024/11/19 17:51
* @param: [map]
* @return: com.kd_13.ssmcode.model.ResponseModel
**/
@PostMapping("/sendSMS")
public ResponseModel sendSMS(@RequestBody Map<String, String> map) {
String phone = map.get("phone");
String sms = smsUtil.sendSMS(phone);
Integer code = 500;
String msg = "error";
Object data = null;
if (sms != null && !sms.isEmpty()) {
code = 200;
msg = "success";
data = sms;
}
return new ResponseModel(code, msg, data);
}

/**
* @description: 核对验证
* @author: 康弟弟
* @date: 2024/11/19 17:51
* @param: [phone, codeSMS]
* @return: com.kd_13.ssmcode.model.ResponseModel
**/
@GetMapping("/checkSMS")
public ResponseModel checkSMS(@RequestHeader String phone, @RequestHeader String codeSMS) {
boolean resule = smsUtil.checkSMS(phone, codeSMS);
Integer code = 500;
String msg = "error";
Object data = null;
if (resule) {
code = 200;
msg = "success";
}
return new ResponseModel(code, msg, data);
}

}

3. 接口测试

发送验证码

1
http://localhost:8080/sendSMS

校验验证码

1
http://localhost:8080/checkSMS?

4. 网页端测试

依旧是使用同一个端口号进行测试,避免跨域问题

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
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>验证码验证</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
padding: 20px;
background-color: #f4f4f4;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
input {
padding: 10px;
margin: 10px 0;
width: calc(100% - 22px);
border: 1px solid #ccc;
border-radius: 4px;
}
button {
padding: 10px;
background-color: #28a745;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #218838;
}
.message {
margin-top: 10px;
color: red;
}
</style>
</head>
<body>

<h2>验证码验证</h2>
<input type="text" id="phone" placeholder="请输入手机号" />
<button id="sendSMS">发送验证码</button>
<div class="message" id="sendMessage"></div>

<input type="text" id="codeSMS" placeholder="请输入验证码" />
<button id="checkSMS">核对验证码</button>
<div class="message" id="checkMessage"></div>

<script>
document.getElementById('sendSMS').onclick = function() {
const phone = document.getElementById('phone').value;
fetch('sendSMS', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ phone: phone })
})
.then(response => response.json())
.then(data => {
document.getElementById('sendMessage').innerText = data.msg;
})
.catch(error => {
document.getElementById('sendMessage').innerText = '发送失败,请重试!';
});
};

document.getElementById('checkSMS').onclick = function() {
const phone = document.getElementById('phone').value;
const codeSMS = document.getElementById('codeSMS').value;
fetch('/checkSMS', {
method: 'GET',
headers: {
'Phone': phone,
'CodeSMS': codeSMS
}
})
.then(response => response.json())
.then(data => {
document.getElementById('checkMessage').innerText = data.msg;
})
.catch(error => {
document.getElementById('checkMessage').innerText = '验证失败,请重试!';
});
};
</script>

</body>
</html>

5. 目录结构

image-20241123214422312