网站搭建实操(五)后台管理-短信模块

网站搭建实操(五)后台管理-短信模块

阿里云短信

用户登陆注册可以使用短信,我这使用阿里云短信

注册阿里云短信

登录阿里云找到短信服务

进入后需要完成五步

  • 1.申请资质
  • 2.申请签名
  • 3.申请模板
  • 4.系统设置
  • 5.发送短信

    以前个人可以申请,但是现在个人需要绑定企业,所以我们申请【短信认证】产品

    选择开通

    2023年以前的版本不适配,需要升级

    可以查看操作指南和api文档

    可以看到免费赠送的签名

    短信模板

    然后绑定手机号去测试

    使用登录注册模板

    点击发起调用后,手机就会收到验证码

获取AccessKey

鼠标悬停右上角头像,点击 AccessKey

我们需要访问API

自定义名称后点击执行

认证完成后生成AccessKey ID和

AccessKey Secret,这两个在调用api会会用到

短信模块

完整流程图

pom

java 复制代码
<?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">
    <parent>
        <artifactId>forum-backend</artifactId>
        <groupId>org.example</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>forum-sms</artifactId>
    <description>微服务论坛系统 - 短信模块</description>

    <dependencies>
        <!-- 阿里云短信服务SDK -->
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>aliyun-java-sdk-core</artifactId>
            <version>4.6.0</version>
        </dependency>
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>aliyun-java-sdk-dysmsapi</artifactId>
            <version>2.2.1</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <!-- Bean Validation -->
        <dependency>
            <groupId>javax.validation</groupId>
            <artifactId>validation-api</artifactId>
        </dependency>
        <dependency>
            <groupId>org.example</groupId>
            <artifactId>forum-common</artifactId>
            <version>1.0-SNAPSHOT</version>
            <scope>compile</scope>
        </dependency>

    </dependencies>

</project>

配置文件

java 复制代码
server:
  port: 8084

spring:
  application:
    name: forum-sms

# 阿里云短信配置
aliyun:
  sms:
    access-key-id: ${ALIYUN_SMS_ACCESS_KEY_ID} 
    access-key-secret: ${ALIYUN_SMS_ACCESS_KEY_SECRET}  
    sign-name: 论坛APP
    template-code: SMS_123456789
    region-id: cn-hangzhou
    domain: dysmsapi.aliyuncs.com
    version: 2017-05-25
    # 验证码有效期(秒)
    code-expire-seconds: 300
    # 发送频率限制(秒)
    send-rate-limit-seconds: 60

accessKey和secret可以直接写在配置中,也可以配置在服务器环境变量中

在服务器上设置环境变量(推荐)

bash 复制代码
export ALIYUN_SMS_ACCESS_KEY_ID=LTAI5txxxxxxxxxxxx
export ALIYUN_SMS_ACCESS_KEY_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

读取配置

java 复制代码
package com.forum.sms.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

/**
 * 阿里云短信配置类
 */
@Data
@Configuration
@ConfigurationProperties(prefix = "aliyun.sms")
public class SmsConfig {
    private String accessKeyId;
    private String accessKeySecret;
    private String signName;
    private String templateCode;
    private String regionId = "cn-hangzhou";
    private String domain = "dysmsapi.aliyuncs.com";
    private String version = "2017-05-25";
    private Integer codeExpireSeconds = 300;
    private Integer sendRateLimitSeconds = 60;
}

工具类

java 复制代码
package com.forum.sms.config.utils;

import com.forum.common.exception.BusinessException;
import com.forum.common.utils.RedisUtils;
import com.forum.sms.config.SmsConfig;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import com.aliyuncs.CommonRequest;
import com.aliyuncs.CommonResponse;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.http.MethodType;
import com.aliyuncs.profile.DefaultProfile;

/**
 * 阿里云短信工具类
 *
 * 功能:封装阿里云短信SDK调用
 * 流程:生成验证码 → 存入Redis → 调用阿里云API发送短信
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class SmsUtils {
    private final SmsConfig smsConfig;
    private final RedisUtils redisUtils;

    private static final String ACTION_SEND_SMS = "SendSms";
    private static final String REDIS_CODE_PREFIX = "sms:code:";
    private static final String REDIS_RATE_LIMIT_PREFIX = "sms:rate:";

    /**
     * 发送短信验证码
     *
     * @param phone 手机号
     * @return 验证码
     */
    public String sendSmsCode(String phone) {
        // 1. 频率限制检查
        checkRateLimit(phone);

        // 2. 生成6位随机验证码
        String code = generateCode();

        // 3. 调用阿里云发送短信
        boolean success = sendSms(phone, code);

        if (success) {
            // 4. 将验证码存入Redis,设置过期时间
            String redisKey = REDIS_CODE_PREFIX + phone;
            redisUtils.set(redisKey, code, smsConfig.getCodeExpireSeconds(), java.util.concurrent.TimeUnit.SECONDS);

            // 5. 记录发送频率限制
            String rateLimitKey = REDIS_RATE_LIMIT_PREFIX + phone;
            redisUtils.set(rateLimitKey, "1", smsConfig.getSendRateLimitSeconds(), java.util.concurrent.TimeUnit.SECONDS);

            log.info("短信验证码发送成功: phone={}, code={}", phone, code);
            return code;
        }

        throw new BusinessException("短信发送失败,请稍后重试");
    }

    /**
     * 校验验证码
     */
    public boolean verifyCode(String phone, String code) {
        String redisKey = REDIS_CODE_PREFIX + phone;
        String cachedCode = (String) redisUtils.get(redisKey);

        if (cachedCode == null) {
            throw new BusinessException("验证码已过期,请重新获取");
        }

        if (!cachedCode.equals(code)) {
            throw new BusinessException("验证码错误");
        }

        // 验证成功后删除验证码,防止重复使用
        redisUtils.delete(redisKey);
        return true;
    }

    /**
     * 调用阿里云API发送短信
     */
    private boolean sendSms(String phone, String code) {
        try {
            // 创建客户端
            DefaultProfile profile = DefaultProfile.getProfile(
                    smsConfig.getRegionId(),
                    smsConfig.getAccessKeyId(),
                    smsConfig.getAccessKeySecret()
            );
            IAcsClient client = new DefaultAcsClient(profile);

            // 构建请求
            CommonRequest request = new CommonRequest();
            request.setSysMethod(MethodType.POST);
            request.setSysDomain(smsConfig.getDomain());
            request.setSysVersion(smsConfig.getVersion());
            request.setSysAction(ACTION_SEND_SMS);

            // 设置参数
            request.putQueryParameter("RegionId", smsConfig.getRegionId());
            request.putQueryParameter("PhoneNumbers", phone);
            request.putQueryParameter("SignName", smsConfig.getSignName());
            request.putQueryParameter("TemplateCode", smsConfig.getTemplateCode());
            request.putQueryParameter("TemplateParam", "{\"code\":\"" + code + "\"}");

            // 发送请求
            CommonResponse response = client.getCommonResponse(request);
            String data = response.getData();
            log.debug("阿里云短信响应: {}", data);

            // 解析响应
            return data != null && data.contains("\"Code\":\"OK\"");

        } catch (ClientException e) {
            log.error("阿里云短信发送失败: {}", e.getMessage(), e);
            return false;
        }
    }

    /**
     * 频率限制检查
     */
    private void checkRateLimit(String phone) {
        String rateLimitKey = REDIS_RATE_LIMIT_PREFIX + phone;
        Object rateLimit = redisUtils.get(rateLimitKey);
        if (rateLimit != null) {
            throw new BusinessException("发送过于频繁,请" + smsConfig.getSendRateLimitSeconds() + "秒后再试");
        }
    }

    /**
     * 生成6位随机验证码
     */
    private String generateCode() {
        int code = (int) ((Math.random() * 9 + 1) * 100000);
        return String.valueOf(code);
    }
}

dto

java 复制代码
@Data
public class SmsSendDTO {
    @NotBlank(message = "手机号不能为空")
    @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
    private String phone;

    private String type = "login"; // login:登录注册, reset:找回密码
}

package com.forum.sms.config.dto;

import lombok.Data;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;

@Data
public class SmsVerifyDTO {
    @NotBlank(message = "手机号不能为空")
    @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
    private String phone;

    @NotBlank(message = "验证码不能为空")
    @Pattern(regexp = "^\\d{6}$", message = "验证码必须是6位数字")
    private String code;
}

controller

java 复制代码
package com.forum.sms.config.controller;

import com.forum.common.result.Result;
import com.forum.sms.config.dto.SmsSendDTO;
import com.forum.sms.config.dto.SmsVerifyDTO;
import com.forum.sms.config.service.SmsService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;

import static com.forum.common.result.Result.success;

@Slf4j
@Api(tags = "短信服务")
@RestController
@RequestMapping("/api/sms")
@RequiredArgsConstructor
public class SmsController {
    private final SmsService smsService;

    @ApiOperation("发送验证码")
    @PostMapping("/send")
    public Result<Void> sendCode(@Valid @RequestBody SmsSendDTO dto) {
        smsService.sendCode(dto);
        return success();
    }

    @ApiOperation("校验验证码")
    @PostMapping("/verify")
    public Result<Void> verifyCode(@Valid @RequestBody SmsVerifyDTO dto) {
        smsService.verifyCode(dto);
        return success();
    }
}

实现类

java 复制代码
package com.forum.sms.config.service.impl;

import com.forum.sms.config.dto.SmsSendDTO;
import com.forum.sms.config.dto.SmsVerifyDTO;
import com.forum.sms.config.service.SmsService;
import com.forum.sms.config.utils.SmsUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Slf4j
@Service
@RequiredArgsConstructor
public class SmsServiceImpl implements SmsService {
    private final SmsUtils smsUtils;

    @Override
    public void sendCode(SmsSendDTO dto) {
        log.info("发送短信验证码: phone={}, type={}", dto.getPhone(), dto.getType());
        smsUtils.sendSmsCode(dto.getPhone());
    }

    @Override
    public void verifyCode(SmsVerifyDTO dto) {
        log.info("校验短信验证码: phone={}", dto.getPhone());
        smsUtils.verifyCode(dto.getPhone(), dto.getCode());
    }
}

源码

https://gitee.com/qfp17393120407/forum

相关推荐
极创信息2 小时前
信创软件安全加固指南,信创软件的纵深防御体系
java·大数据·数据库·金融·php·mvc·软件工程
蜘蛛侠..2 小时前
什么是 Plan-and-Execute 模式?与ReAct模式区别?
java·ai·大模型·llm·agent·react·plan模式
untE EADO3 小时前
SpringBoot:几种常用的接口日期格式化方法
java·spring boot·后端
一个人说晚安3 小时前
Docker 部署 OpenClaw 并接入第三方大模型 (MiniMax) 完整排坑指南
java·开发语言·dubbo
迷藏4943 小时前
**雾计算中的边缘智能:基于Python的轻量级任务调度系统设计与实现**在物联网(IoT)飞速发展的今天,传统云
java·开发语言·python·物联网
LSL666_3 小时前
云服务上安装nginx
java·运维·nginx
biubiubiu07063 小时前
从 Python 和 Node.js 的流行看 Java 的真实位置
java·python·node.js
我是大猴子3 小时前
队列的一些场景题以及处理方式
java
ictI CABL4 小时前
MySQL数据库的数据文件保存在哪?MySQL数据存在哪里
java