Java实现短信验证码案例

wen java案例 5

本文目录导读:

Java实现短信验证码案例

  1. 项目结构
  2. Maven依赖 (pom.xml)
  3. 配置文件 (application.yml)
  4. 启动类 (SmsApplication.java)
  5. 短信服务接口 (SmsService.java)
  6. 服务实现类 (SmsServiceImpl.java)
  7. Redis工具类 (RedisUtil.java)
  8. 短信发送类 (SmsSender.java)
  9. 控制器 (SmsController.java)
  10. 测试控制器 (SmsTestController.java)
  11. 前端示例 (index.html)
  12. 测试用例 (SmsServiceTest.java)
  13. 使用说明
  14. 注意事项

我来为您提供一个完整的Java短信验证码实现案例,包含后端服务和简单的使用示例。

项目结构

sms-verification-code/
├── pom.xml
├── src/main/java/com/example/sms/
│   ├── SmsApplication.java
│   ├── controller/
│   │   └── SmsController.java
│   ├── service/
│   │   ├── SmsService.java
│   │   └── impl/
│   │       └── SmsServiceImpl.java
│   ├── util/
│   │   ├── RedisUtil.java
│   │   └── SmsSender.java
│   └── config/
│       └── RedisConfig.java
└── src/main/resources/
    └── application.yml

Maven依赖 (pom.xml)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>sms-verification-code</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.0</version>
        <relativePath/>
    </parent>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <!-- Spring Boot Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- Redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- 阿里云短信 SDK -->
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>dysmsapi20170525</artifactId>
            <version>2.0.24</version>
        </dependency>
        <!-- 腾讯云短信 SDK(可选) -->
        <dependency>
            <groupId>com.tencentcloudapi</groupId>
            <artifactId>tencentcloud-sdk-java</artifactId>
            <version>3.1.500</version>
        </dependency>
        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- 工具类 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>
        <!-- 测试 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

配置文件 (application.yml)

server:
  port: 8080
spring:
  redis:
    host: localhost
    port: 6379
    password: 
    database: 0
    timeout: 3000ms
# 短信配置
sms:
  # 阿里云短信配置
  aliyun:
    access-key-id: your-access-key-id
    access-key-secret: your-access-key-secret
    sign-name: 您的签名
    template-code: SMS_123456789
  # 通用配置
  config:
    # 验证码有效期(分钟)
    expire-minutes: 5
    # 发送间隔(秒)
    send-interval: 60
    # 每日发送上限
    daily-limit: 10
    # 验证码尝试次数上限
    max-attempts: 3

启动类 (SmsApplication.java)

package com.example.sms;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication
@EnableCaching
public class SmsApplication {
    public static void main(String[] args) {
        SpringApplication.run(SmsApplication.class, args);
    }
}

短信服务接口 (SmsService.java)

package com.example.sms.service;
import java.util.Map;
public interface SmsService {
    /**
     * 发送验证码
     * @param phone 手机号
     * @return 是否发送成功
     */
    boolean sendVerificationCode(String phone);
    /**
     * 验证验证码
     * @param phone 手机号
     * @param code 验证码
     * @return 验证结果
     */
    boolean verifyCode(String phone, String code);
    /**
     * 获取剩余时间
     * @param phone 手机号
     * @return 剩余时间(秒)
     */
    long getRemainingTime(String phone);
}

服务实现类 (SmsServiceImpl.java)

package com.example.sms.service.impl;
import com.example.sms.service.SmsService;
import com.example.sms.util.RedisUtil;
import com.example.sms.util.SmsSender;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
public class SmsServiceImpl implements SmsService {
    @Autowired
    private RedisUtil redisUtil;
    @Autowired
    private SmsSender smsSender;
    @Value("${sms.config.expire-minutes}")
    private int expireMinutes;
    @Value("${sms.config.send-interval}")
    private int sendInterval;
    @Value("${sms.config.max-attempts}")
    private int maxAttempts;
    // Redis key 前缀
    private static final String CODE_KEY = "sms:code:";
    private static final String SEND_TIME_KEY = "sms:sendTime:";
    private static final String ATTEMPT_KEY = "sms:attempt:";
    private static final String DAILY_COUNT_KEY = "sms:daily:";
    @Override
    public boolean sendVerificationCode(String phone) {
        try {
            // 1. 检查发送频率
            String sendTimeKey = SEND_TIME_KEY + phone;
            Object lastSendTime = redisUtil.get(sendTimeKey);
            if (lastSendTime != null) {
                long lastTime = Long.parseLong(lastSendTime.toString());
                long currentTime = System.currentTimeMillis();
                if (currentTime - lastTime < sendInterval * 1000) {
                    log.warn("发送频率过高,手机号:{}", phone);
                    return false;
                }
            }
            // 2. 检查每日发送次数
            String dailyCountKey = DAILY_COUNT_KEY + phone;
            Object dailyCount = redisUtil.get(dailyCountKey);
            if (dailyCount != null && Integer.parseInt(dailyCount.toString()) >= 10) {
                log.warn("超出每日发送限制,手机号:{}", phone);
                return false;
            }
            // 3. 生成6位随机验证码
            String code = generateCode();
            // 4. 存储验证码到Redis
            String codeKey = CODE_KEY + phone;
            redisUtil.set(codeKey, code, expireMinutes * 60);
            // 5. 更新发送时间
            redisUtil.set(sendTimeKey, System.currentTimeMillis(), 24 * 60 * 60);
            // 6. 更新每日发送次数
            if (dailyCount != null) {
                redisUtil.increment(dailyCountKey);
            } else {
                redisUtil.set(dailyCountKey, 1, 24 * 60 * 60);
            }
            // 7. 重置验证码尝试次数
            String attemptKey = ATTEMPT_KEY + phone;
            redisUtil.set(attemptKey, 0, expireMinutes * 60);
            // 8. 调用短信发送
            log.info("发送验证码,手机号:{},验证码:{}", phone, code);
            return smsSender.sendSms(phone, code);
        } catch (Exception e) {
            log.error("发送验证码失败", e);
            return false;
        }
    }
    @Override
    public boolean verifyCode(String phone, String code) {
        try {
            String codeKey = CODE_KEY + phone;
            Object storedCode = redisUtil.get(codeKey);
            if (storedCode == null) {
                log.warn("验证码不存在或已过期,手机号:{}", phone);
                return false;
            }
            // 检查尝试次数
            String attemptKey = ATTEMPT_KEY + phone;
            Object attemptCount = redisUtil.get(attemptKey);
            int attempts = attemptCount != null ? Integer.parseInt(attemptCount.toString()) : 0;
            if (attempts >= maxAttempts) {
                log.warn("验证码尝试次数超限,手机号:{}", phone);
                redisUtil.delete(codeKey); // 删除验证码
                return false;
            }
            // 验证码比对
            if (storedCode.toString().equals(code)) {
                // 验证成功,删除验证码
                redisUtil.delete(codeKey);
                redisUtil.delete(attemptKey);
                log.info("验证码验证成功,手机号:{}", phone);
                return true;
            } else {
                // 验证失败,增加尝试次数
                redisUtil.increment(attemptKey);
                log.warn("验证码错误,手机号:{},剩余尝试次数:{}", 
                        phone, maxAttempts - attempts - 1);
                return false;
            }
        } catch (Exception e) {
            log.error("验证码验证失败", e);
            return false;
        }
    }
    @Override
    public long getRemainingTime(String phone) {
        String codeKey = CODE_KEY + phone;
        return redisUtil.getExpire(codeKey);
    }
    /**
     * 生成6位随机验证码
     */
    private String generateCode() {
        Random random = new Random();
        int code = 100000 + random.nextInt(900000);
        return String.valueOf(code);
    }
}

Redis工具类 (RedisUtil.java)

package com.example.sms.util;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
@Component
public class RedisUtil {
    @Autowired
    private StringRedisTemplate redisTemplate;
    /**
     * 设置缓存
     */
    public void set(String key, Object value, long timeout) {
        redisTemplate.opsForValue().set(key, String.valueOf(value), timeout, TimeUnit.SECONDS);
    }
    /**
     * 设置永久缓存
     */
    public void set(String key, Object value) {
        redisTemplate.opsForValue().set(key, String.valueOf(value));
    }
    /**
     * 获取缓存
     */
    public Object get(String key) {
        return redisTemplate.opsForValue().get(key);
    }
    /**
     * 删除缓存
     */
    public boolean delete(String key) {
        return redisTemplate.delete(key);
    }
    /**
     * 递增操作
     */
    public long increment(String key) {
        return redisTemplate.opsForValue().increment(key);
    }
    /**
     * 获取过期时间
     */
    public long getExpire(String key) {
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }
    /**
     * 判断key是否存在
     */
    public boolean hasKey(String key) {
        return redisTemplate.hasKey(key);
    }
}

短信发送类 (SmsSender.java)

package com.example.sms.util;
import com.aliyun.dysmsapi20170525.Client;
import com.aliyun.dysmsapi20170525.models.SendSmsRequest;
import com.aliyun.dysmsapi20170525.models.SendSmsResponse;
import com.aliyun.teaopenapi.models.Config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class SmsSender {
    @Value("${sms.aliyun.access-key-id}")
    private String accessKeyId;
    @Value("${sms.aliyun.access-key-secret}")
    private String accessKeySecret;
    @Value("${sms.aliyun.sign-name}")
    private String signName;
    @Value("${sms.aliyun.template-code}")
    private String templateCode;
    /**
     * 发送短信
     */
    public boolean sendSms(String phone, String code) {
        try {
            // 创建阿里云客户端
            Config config = new Config()
                    .setAccessKeyId(accessKeyId)
                    .setAccessKeySecret(accessKeySecret);
            config.endpoint = "dysmsapi.aliyuncs.com";
            Client client = new Client(config);
            // 构建请求
            SendSmsRequest request = new SendSmsRequest()
                    .setPhoneNumbers(phone)
                    .setSignName(signName)
                    .setTemplateCode(templateCode)
                    .setTemplateParam("{\"code\":\"" + code + "\"}");
            // 发送短信
            SendSmsResponse response = client.sendSms(request);
            // 判断是否发送成功
            if ("OK".equals(response.body.code)) {
                log.info("短信发送成功,手机号:{}", phone);
                return true;
            } else {
                log.error("短信发送失败,错误代码:{},错误信息:{}", 
                        response.body.code, response.body.message);
                return false;
            }
        } catch (Exception e) {
            log.error("短信发送异常", e);
            return false;
        }
    }
    /**
     * 模拟发送(没有短信服务时使用)
     */
    public boolean sendSmsMock(String phone, String code) {
        log.info("【模拟短信】手机号:{},验证码:{}", phone, code);
        return true;
    }
}

控制器 (SmsController.java)

package com.example.sms.controller;
import com.example.sms.service.SmsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api/sms")
public class SmsController {
    @Autowired
    private SmsService smsService;
    /**
     * 发送验证码
     */
    @PostMapping("/send")
    public Map<String, Object> sendCode(@RequestParam String phone) {
        Map<String, Object> result = new HashMap<>();
        // 参数校验
        if (!isValidPhone(phone)) {
            result.put("success", false);
            result.put("message", "手机号格式不正确");
            return result;
        }
        boolean success = smsService.sendVerificationCode(phone);
        result.put("success", success);
        if (success) {
            result.put("message", "验证码发送成功");
            result.put("remainingTime", smsService.getRemainingTime(phone));
        } else {
            result.put("message", "验证码发送失败,请稍后重试");
        }
        return result;
    }
    /**
     * 验证验证码
     */
    @PostMapping("/verify")
    public Map<String, Object> verifyCode(@RequestParam String phone, 
                                          @RequestParam String code) {
        Map<String, Object> result = new HashMap<>();
        boolean valid = smsService.verifyCode(phone, code);
        result.put("success", valid);
        result.put("message", valid ? "验证成功" : "验证码错误或已过期");
        return result;
    }
    /**
     * 获取剩余时间
     */
    @GetMapping("/remaining-time")
    public Map<String, Object> getRemainingTime(@RequestParam String phone) {
        Map<String, Object> result = new HashMap<>();
        long remainingTime = smsService.getRemainingTime(phone);
        result.put("success", true);
        result.put("remainingTime", remainingTime);
        return result;
    }
    /**
     * 校验手机号格式
     */
    private boolean isValidPhone(String phone) {
        return phone != null && phone.matches("^1[3-9]\\d{9}$");
    }
}

测试控制器 (SmsTestController.java)

package com.example.sms.controller;
import com.example.sms.util.RedisUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/test")
public class SmsTestController {
    @Autowired
    private RedisUtil redisUtil;
    @GetMapping("/redis")
    public String testRedis() {
        try {
            redisUtil.set("test:key", "hello", 60);
            Object value = redisUtil.get("test:key");
            return "Redis测试成功,获取到的值:" + value;
        } catch (Exception e) {
            return "Redis测试失败:" + e.getMessage();
        }
    }
}

前端示例 (index.html)

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">短信验证码示例</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 400px;
            margin: 50px auto;
            padding: 20px;
        }
        .form-group {
            margin-bottom: 20px;
        }
        input {
            width: 100%;
            padding: 10px;
            font-size: 16px;
            border: 1px solid #ddd;
            border-radius: 5px;
            box-sizing: border-box;
        }
        button {
            padding: 10px 20px;
            font-size: 16px;
            border: none;
            border-radius: 5px;
            cursor: pointer;
        }
        .btn-primary {
            background-color: #007bff;
            color: white;
        }
        .btn-success {
            background-color: #28a745;
            color: white;
        }
        #message {
            margin-top: 20px;
            padding: 10px;
            border-radius: 5px;
            display: none;
        }
        .success {
            background-color: #d4edda;
            color: #155724;
            border: 1px solid #c3e6cb;
        }
        .error {
            background-color: #f8d7da;
            color: #721c24;
            border: 1px solid #f5c6cb;
        }
        #countdown {
            color: #6c757d;
            font-size: 14px;
            margin-top: 5px;
        }
    </style>
</head>
<body>
    <h2>短信验证码示例</h2>
    <div class="form-group">
        <input type="text" id="phone" placeholder="请输入手机号" maxlength="11">
    </div>
    <div class="form-group">
        <input type="text" id="code" placeholder="请输入验证码" maxlength="6">
    </div>
    <div class="form-group">
        <button class="btn-primary" id="sendBtn" onclick="sendCode()">发送验证码</button>
        <button class="btn-success" id="verifyBtn" onclick="verifyCode()">验证</button>
    </div>
    <div id="countdown"></div>
    <div id="message"></div>
    <script>
        let countdown = 0;
        let timerId = null;
        // 发送验证码
        async function sendCode() {
            const phone = document.getElementById('phone').value;
            if (!/^1[3-9]\d{9}$/.test(phone)) {
                showMessage('请输入正确的手机号', false);
                return;
            }
            try {
                const response = await fetch(`/api/sms/send?phone=${phone}`, {
                    method: 'POST'
                });
                const data = await response.json();
                if (data.success) {
                    showMessage('验证码发送成功', true);
                    startCountdown(60);
                    document.getElementById('sendBtn').disabled = true;
                } else {
                    showMessage(data.message, false);
                }
            } catch (error) {
                showMessage('请求失败,请稍后重试', false);
            }
        }
        // 验证验证码
        async function verifyCode() {
            const phone = document.getElementById('phone').value;
            const code = document.getElementById('code').value;
            if (!phone || !code) {
                showMessage('请填写手机号和验证码', false);
                return;
            }
            try {
                const response = await fetch(`/api/sms/verify?phone=${phone}&code=${code}`, {
                    method: 'POST'
                });
                const data = await response.json();
                showMessage(data.message, data.success);
            } catch (error) {
                showMessage('请求失败,请稍后重试', false);
            }
        }
        // 倒计时
        function startCountdown(seconds) {
            countdown = seconds;
            timerId = setInterval(() => {
                countdown--;
                document.getElementById('countdown').textContent = 
                    `${countdown}秒后可重新发送`;
                if (countdown <= 0) {
                    clearInterval(timerId);
                    document.getElementById('sendBtn').disabled = false;
                    document.getElementById('countdown').textContent = '';
                }
            }, 1000);
        }
        // 显示消息
        function showMessage(message, isSuccess) {
            const msgDiv = document.getElementById('message');
            msgDiv.textContent = message;
            msgDiv.style.display = 'block';
            msgDiv.className = isSuccess ? 'success' : 'error';
            setTimeout(() => {
                msgDiv.style.display = 'none';
            }, 3000);
        }
    </script>
</body>
</html>

测试用例 (SmsServiceTest.java)

package com.example.sms;
import com.example.sms.service.SmsService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
public class SmsServiceTest {
    @Autowired
    private SmsService smsService;
    @Test
    public void testSendCode() {
        String phone = "13800138000";
        boolean success = smsService.sendVerificationCode(phone);
        assertTrue(success);
    }
    @Test
    public void testVerifyCode() {
        String phone = "13800138000";
        // 先发送验证码
        smsService.sendVerificationCode(phone);
        // 获取验证码(测试用)
        String code = "123456";
        boolean valid = smsService.verifyCode(phone, code);
        assertTrue(valid);
    }
    @Test
    public void testVerifyWrongCode() {
        String phone = "13800138000";
        // 先发送验证码
        smsService.sendVerificationCode(phone);
        // 错误的验证码
        String code = "000000";
        boolean valid = smsService.verifyCode(phone, code);
        assertFalse(valid);
    }
}

使用说明

  1. 配置短信服务:根据您使用的短信服务商(阿里云、腾讯云等),配置相应的API密钥和模板

  2. 启动Redis:确保Redis已启动,并修改application.yml中的Redis配置

  3. 运行项目:启动SmsApplication主类

  4. 测试接口

    • 发送验证码:POST /api/sms/send?phone=13800138000
    • 验证验证码:POST /api/sms/verify?phone=13800138000&code=123456

注意事项

  1. 生产环境:生产环境中应使用真实的短信服务商,并做好安全防护
  2. 安全考虑
    • 添加图形验证码防止机器人
    • 使用HTTPS加密传输
    • 对手机号做频率限制
    • 敏感操作需要二次验证
  3. 优化建议
    • 使用消息队列异步发送短信
    • 添加短信发送日志记录
    • 支持多短信服务商切换
    • 添加监控告警功能

这个案例提供了一个完整的短信验证码实现,您可以根据实际需求进行调整和扩展。

抱歉,评论功能暂时关闭!