AI摘要

本文详细介绍了在Spring Boot应用中实现短信验证码注册和登录功能的方法。技术栈包括Spring Boot、互亿无线短信平台、MyBatis Flex和JWT。实现步骤包括集成短信服务、创建短信工具类、验证码生成和存储、控制器实现以及服务层实现。前端实现部分包括API层和手机号注册页面。文章还提供了一些最佳实践,如验证码安全、手机号验证、短信内容和防护措施。通过这些步骤,可以为用户提供便捷的移动端认证体验,同时增强账户安全性。

本文将介绍如何在Spring Boot应用中实现短信验证码注册和登录功能,为用户提供便捷的移动端认证体验。

1. 功能概述

我们将实现以下功能:

  • 手机号+验证码注册
  • 手机号+验证码登录

2. 技术栈

  • Spring Boot 3.2.0
  • 互亿无线短信平台
  • MyBatis Flex
  • JWT

3. 实现步骤

3.1 短信服务集成

首先,需要在application.properties中配置短信平台信息:

# 短信配置
sms.account=C09251523
sms.apikey=826efdf7322e3c49355428a523c10eec

3.2 创建短信工具类

创建SmsUtil工具类处理短信发送:

@Component
public class SmsUtil {
    private static final Logger logger = LoggerFactory.getLogger(SmsUtil.class);

    // 短信发送API地址
    private static final String SMS_API_URL = "http://106.ihuyi.com/webservice/sms.php?method=Submit";

    // API账号和密码
    @Value("${sms.account:C09***523}")
    private String account;

    @Value("${sms.apikey:826efdf7322e3c49355***a523c10eec}")
    private String apiKey;

    /**
     * 发送短信验证码
     * @param phone 手机号
     * @param code 验证码
     * @return 发送结果,包含code和msg
     */
    public Map<String, String> sendVerificationCode(String phone, String code) {
        Map<String, String> result = new HashMap<>();

        try {
            // 短信内容
            String content = "您的验证码是:" + code + "。请不要把验证码泄露给其他人。";

            // 发送请求
            String response = sendSmsRequest(phone, content);

            // 解析结果
            result = parseXmlResponse(response);

            // 记录日志
            if ("2".equals(result.get("code"))) {
                logger.info("短信发送成功,手机号: {}, 验证码: {}", phone, code);
            } else {
                logger.error("短信发送失败,手机号: {}, 错误信息: {}", phone, result.get("msg"));
            }

        } catch (Exception e) {
            logger.error("发送短信验证码异常", e);
            result.put("code", "0");
            result.put("msg", "系统异常,短信发送失败");
        }

        return result;
    }

    // 省略发送请求和解析响应的方法...
}

3.3 验证码生成和存储

UserServiceImpl中实现验证码管理:

// 在UserServiceImpl中
private final Map<String, String> smsCodeCache = new HashMap<>();

@Override
public boolean sendSmsCode(String phone, String type) {
    try {
        // 校验手机号格式
        if (!isValidPhoneNumber(phone)) {
            return false;
        }

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

        // 将验证码存入缓存,实际项目中应使用Redis等缓存服务
        smsCodeCache.put(phone + ":" + type, code);

        // 发送短信
        Map<String, String> result = smsUtil.sendVerificationCode(phone, code);

        // 检查发送结果
        return "2".equals(result.get("code"));
    } catch (Exception e) {
        logger.error("发送短信验证码失败", e);
        return false;
    }
}

@Override
public String getSmsCode(String phone, String type) {
    return smsCodeCache.get(phone + ":" + type);
}

@Override
public void removeSmsCode(String phone, String type) {
    smsCodeCache.remove(phone + ":" + type);
}

3.4 控制器实现

发送短信验证码

@PostMapping("/send/sms")
public ResponseEntity<?> sendSmsCode(
        @RequestParam @NotBlank @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") String phone,
        @RequestParam @NotBlank String type) {
    logger.info("发送短信验证码,手机号: {}, 类型: {}", phone, type);
    boolean result = userService.sendSmsCode(phone, type);

    if (result) {
        return ResponseEntity.ok().body(Map.of("code", 200, "message", "验证码发送成功"));
    } else {
        return ResponseEntity.badRequest().body(Map.of("code", 400, "message", "验证码发送失败"));
    }
}

手机号+验证码注册

@PostMapping("/register/sms")
public ResponseEntity<?> registerBySms(
        @RequestBody(required = false) Map<String, String> registerRequest,
        @RequestParam(required = false) String phone,
        @RequestParam(required = false) String password,
        @RequestParam(required = false) String code,
        @RequestParam(required = false) String nickname) {

    // 优先使用URL参数,如果没有则使用请求体
    if (registerRequest != null) {
        if (phone == null) phone = registerRequest.get("phone");
        if (password == null) password = registerRequest.get("password");
        if (code == null) code = registerRequest.get("code");
        if (nickname == null) nickname = registerRequest.get("nickname");
    }

    if (phone == null || password == null || code == null) {
        return ResponseEntity.badRequest().body(Map.of(
            "code", 400, 
            "message", "手机号、密码和验证码不能为空"));
    }

    // 验证手机号格式
    if (!phone.matches("^1[3-9]\\d{9}$")) {
        return ResponseEntity.badRequest().body(Map.of(
            "code", 400, 
            "message", "手机号格式不正确"));
    }

    logger.info("短信验证码一步式注册,手机号: {}", phone);

    // 验证验证码
    String cachedCode = userService.getSmsCode(phone, "register");
    if (cachedCode == null || !cachedCode.equals(code)) {
        return ResponseEntity.status(400).body(Map.of(
            "code", 400, 
            "message", "验证码错误或已过期"));
    }

    // 创建用户
    User user = new User();
    user.setPhone(phone);
    user.setUsername(phone); // 使用手机号作为用户名
    user.setPassword(password); // 未加密的密码,在service层会加密
    user.setNickname(nickname != null ? nickname : "用户" + phone.substring(phone.length() - 4));

    // 注册用户
    Map<String, Object> registerResult = userService.register(user, true);
    int registerCode = (int) registerResult.get("code");

    // 验证码验证成功后,删除缓存中的验证码
    if (registerCode == 200) {
        userService.removeSmsCode(phone, "register");
    }

    return ResponseEntity.status(registerCode == 200 ? 200 : 400).body(registerResult);
}

短信验证码登录

@PostMapping("/login/sms")
public ResponseEntity<?> loginBySms(
        @RequestParam @NotBlank @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") String phone,
        @RequestParam @NotBlank String code) {
    logger.info("短信验证码登录,手机号: {}", phone);
    Map<String, Object> result = userService.smsLogin(phone, code);

    int code2 = (int) result.get("code");
    return ResponseEntity.status(code2 == 200 ? 200 : 400).body(result);
}

3.5 服务层实现

实现短信验证码登录的业务逻辑:

@Override
public Map<String, Object> smsLogin(String phone, String code) {
    Map<String, Object> result = new HashMap<>();

    // 验证验证码
    String cachedCode = getSmsCode(phone, "login");
    if (cachedCode == null || !cachedCode.equals(code)) {
        result.put("code", 400);
        result.put("message", "验证码错误或已过期");
        return result;
    }

    // 清除验证码缓存
    removeSmsCode(phone, "login");

    // 根据手机号查询用户
    User user = getOneOrNull(QueryWrapper.create().where("phone = ?", phone));

    if (user == null) {
        result.put("code", 400);
        result.put("message", "用户不存在,请先注册");
        return result;
    }

    // 检查用户状态
    if (user.getStatus() != 1) {
        result.put("code", 403);
        result.put("message", "账号已被禁用");
        return result;
    }

    // 更新登录信息
    user.setLastLoginTime(LocalDateTime.now());
    updateById(user);

    // 生成token
    String token = jwtUtil.generateToken(user.getUsername());

    // 准备返回数据
    Map<String, Object> userData = new HashMap<>();
    userData.put("id", user.getId());
    userData.put("username", user.getUsername());
    userData.put("nickname", user.getNickname());
    userData.put("avatar", user.getAvatar());
    userData.put("phone", user.getPhone());

    result.put("code", 200);
    result.put("message", "登录成功");
    result.put("data", new HashMap<String, Object>() {{
        put("token", token);
        put("user", userData);
    }});

    return result;
}

4. 前端实现

4.1 API层

// 用户相关接口
export const userApi = {
    // 手机号验证码注册
    registerBySms: (data) => {
        return request({
            url: '/user/register/sms',
            method: 'post',
            data
        })
    },
    // 手机号验证码登录
    smsLogin: (data) => {
        return request({
            url: '/user/login/sms',
            method: 'post',
            data
        })
    },
    // 发送短信验证码
    sendSmsCode: (phone, type) => {
        return request({
            url: '/user/send/sms',
            method: 'post',
            params: {
                phone,
                type // login-登录, register-注册, reset-重置密码
            }
        })
    }
}

4.2 手机号注册页面

<template>
  <div class="form-container">
    <div class="form-item">
      <label class="label">手机号</label>
      <input type="number" class="input" placeholder="请输入手机号" v-model="form.phone" />
    </div>

    <div class="form-item code-group">
      <label class="label">验证码</label>
      <div class="code-input-wrapper">
        <input type="number" class="input code-input" placeholder="请输入验证码" v-model="form.code" />
        <button class="code-btn" :disabled="countdown > 0" @click="sendCode">
          {{ countdown > 0 ? `${countdown}s后重发` : '获取验证码' }}
        </button>
      </div>
    </div>

    <div class="form-item">
      <label class="label">密码</label>
      <input type="password" class="input" placeholder="请输入密码" v-model="form.password" />
    </div>

    <div class="form-item">
      <label class="label">昵称</label>
      <input type="text" class="input" placeholder="请输入昵称" v-model="form.nickname" />
    </div>

    <button class="submit-btn" @click="register">注册</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      form: {
        phone: '',
        code: '',
        password: '',
        nickname: ''
      },
      countdown: 0
    }
  },
  methods: {
    sendCode() {
      // 验证手机号格式
      if (!this.form.phone || !/^1[3-9]\d{9}$/.test(this.form.phone)) {
        uni.showToast({
          title: '请输入正确的手机号',
          icon: 'none'
        })
        return
      }

      userApi.sendSmsCode(this.form.phone, 'register').then(res => {
        if (res.code === 200) {
          uni.showToast({
            title: '验证码发送成功',
            icon: 'success'
          })
          this.countdown = 60
          const timer = setInterval(() => {
            this.countdown--
            if (this.countdown <= 0) {
              clearInterval(timer)
            }
          }, 1000)
        } else {
          uni.showToast({
            title: res.message || '验证码发送失败',
            icon: 'none'
          })
        }
      })
    },
    register() {
      // 表单验证
      if (!this.form.phone) {
        uni.showToast({ title: '请输入手机号', icon: 'none' })
        return
      }
      if (!this.form.code) {
        uni.showToast({ title: '请输入验证码', icon: 'none' })
        return
      }
      if (!this.form.password) {
        uni.showToast({ title: '请输入密码', icon: 'none' })
        return
      }

      userApi.registerBySms(this.form).then(res => {
        if (res.code === 200) {
          uni.showToast({ title: '注册成功', icon: 'success' })
          setTimeout(() => {
            uni.navigateTo({ url: '/pages/login/index' })
          }, 1500)
        } else {
          uni.showToast({ title: res.message || '注册失败', icon: 'none' })
        }
      }).catch(err => {
        uni.showToast({ title: '注册失败,请稍后再试', icon: 'none' })
      })
    }
  }
}
</script>

5. 最佳实践

  1. 验证码安全

    • 设置验证码过期时间(通常5分钟)
    • 限制发送频率,避免短信轰炸
    • 使用Redis存储验证码,设置TTL
  2. 手机号验证

    • 正则验证手机号格式
    • 检查手机号是否已注册
  3. 短信内容

    • 遵守短信平台规范
    • 内容简洁明了
    • 包含必要的安全提示
  4. 防护措施

    • IP限制
    • 图形验证码预校验
    • 短信发送频率限制

6. 总结

短信验证码注册登录是移动互联网应用的标准功能,不仅提高了用户体验,也增强了账户安全性。本文介绍了实现这一功能的完整流程,从后端API到前端UI。

在实际应用中,可以根据需求进一步优化:

  • 添加图形验证码预校验
  • 使用Redis存储验证码
  • 实现短信模板多样化
  • 增加短信登录的安全策略
如果觉得我的文章对你有用,请随意赞赏
END
本文作者:
文章标题:Springboot 手机号短信验证码注册登录实现
本文地址:https://blog.ybyq.wang/archives/647.html
版权说明:若无注明,本文皆Xuan's blog原创,转载请保留文章出处。