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. 最佳实践
-
验证码安全
- 设置验证码过期时间(通常5分钟)
- 限制发送频率,避免短信轰炸
- 使用Redis存储验证码,设置TTL
-
手机号验证
- 正则验证手机号格式
- 检查手机号是否已注册
-
短信内容
- 遵守短信平台规范
- 内容简洁明了
- 包含必要的安全提示
-
防护措施
- IP限制
- 图形验证码预校验
- 短信发送频率限制
6. 总结
短信验证码注册登录是移动互联网应用的标准功能,不仅提高了用户体验,也增强了账户安全性。本文介绍了实现这一功能的完整流程,从后端API到前端UI。
在实际应用中,可以根据需求进一步优化:
- 添加图形验证码预校验
- 使用Redis存储验证码
- 实现短信模板多样化
- 增加短信登录的安全策略