本课程开发软件与项目初始代码可以通过云盘下载,答疑服务可以关注视频片头的二维码获取。
https://www.123912.com/s/A75eVv-27mod,提取码:yton
教学视频
在华夏OA系统中密码是加密混淆之后存储在数据表中的,倘若用户忘记了自己的密码是无法找回的,只能重新设置密码。这节课我们用Redis缓存实现验证码机制,然后模拟发送验证码短信。如果你想要真实发送验证码短信,则可以对接阿里云或者腾讯云的相关接口,但是必须要先提供企业资质才可以。个人身份是无法使用运营商的短信接口的,以防有人群发诈骗短信。
视频中有代码讲解,大家认真观看视频,不要快进。有些操作只在视频中演示,只对着手册操作并不能完成案例,切记!
一、Refresh与Code缓存
在发送短信验证码的流程中,我们需要用到Refresh与Code两种缓存。Code缓存决定了短信验证码的有效期是多久,就是往Redis里面创建Code缓存的时候设置一个过期时间即可;Refresh缓存决定了在多长时间内系统不会再次发送验证码短信。比如用户每隔5秒钟(绕过后端防抖)点击一次发送验证码按钮,后端系统每次就真要发送短信吗?显而易见,肯定不行。毕竟发送短信是需要花钱的,我们不能无限制的为用户发送验证码短信,我们必须要降低发送短信的频率。其实只要加长发送短信的间隔时间,频率自然就降低了。假如我希望为用户发送验证码短信的间隔时间最少为两分钟,也就是说在两分钟内,即便用户没有收到验证码短信,系统也不会再为他发送短信。用户必须等待两分钟,在页面上点击发送验证码按钮,系统才会发送信息的验证码短信。这样做可以大幅降低发送短信的频率,短信开销也就节省下来了。
二、编写hxoa-oms
子系统
1. 编写持久层
在UserMapper.xml
文件中,声明SQL语句根据手机号码查询用户ID。
<select id="searchIdByTel" parameterType="String" resultType="Long">
SELECT id
FROM tb_user
WHERE tel = #{tel}
</select>
在com.example.hxoa.cloud.oms.db.dao
包UserMapper.java
接口中,声明抽象方法。
public interface UserMapper {
……
public Long searchIdByTel(String tel);
}
2. 编写Dubbo层
在com.example.hxoa.cloud.dubbo
包UserApi.java
接口中,声明抽象方法。
public interface UserApi {
……
public R sendSmsCaptcha(String tel);
public R verifySmsCaptcha(String tel, String captcha);
}
在com.example.hxoa.cloud.oms.api
包UserApiHandler.java
类中,实现抽象方法。
@DubboService
@Slf4j
public class UserApiHandler implements UserApi {
@Resource
private RedissonClient redissonClient;
……
@Override
@SentinelResource("UserApiHandler.sendSmsCaptcha")
public R sendSmsCaptcha(String tel) {
Long userId = userMapper.searchIdByTel(tel);
Map result;
if (userId != null) {
String key = "sms_captcha_refresh_" + tel;
//检查该手机号码是否被暂时禁止刷新验证码
RBucket rBucket = redissonClient.getBucket(key);
if (rBucket.isExists()) {
//如果存在Refresh缓存就不发送验证码短信
result = new HashMap() {{
put("status", "FORBIDDEN");
put("message", "该手机号码暂时被禁止刷新验证码");
}};
}
else {
//生成6位数字验证码
String captcha = RandomUtil.randomNumbers(6);
System.out.println("验证码:" + captcha);
//向Redis中写入Code缓存
key = "sms_code_" + tel;
RMap rmap = redissonClient.getMap(key);
rmap.putAll(new HashMap() {{
put("captcha", captcha);
put("userId", userId);
}});
rmap.expire(Duration.of(5, ChronoUnit.MINUTES));
// TODO 发送短信验证码
//向Redis中写入Refresh缓存,2分钟内不再向该用户发送验证码短信
key = "sms_captcha_refresh_" + tel;
rBucket = redissonClient.getBucket(key);
rBucket.set(captcha, Duration.of(2, ChronoUnit.MINUTES));
result = new HashMap() {{
put("status", "SUCCESS");
put("message", "验证码发送成功");
}};
}
}
else {
result = new HashMap() {{
put("status", "FAILED");
put("message", "手机号码不存在");
}};
}
return R.ok().setAttributeAll(result);
}
@Override
@SentinelResource("UserApiHandler.verifySmsCaptcha")
public R verifySmsCaptcha(String tel, String captcha) {
String key = "sms_code_" + tel;
RMap rmap = redissonClient.getMap(key);
//判断验证码是否已经过期
if (!rmap.isExists()) {
return R.ok().setAttribute("status", "OVERDUE");
}
//从Code缓存中取出验证码和用户ID
String cacheCaptcha = rmap.get("captcha").toString();
String cacheUserId = rmap.get("userId").toString();
//比较用户提交的验证码和缓存的验证码是否一致
if (cacheCaptcha.equals(captcha)) {
//两个验证码一致就删除Code缓存和Refresh缓存
rmap.delete();
redissonClient.getMap("sms_captcha_refresh_" + tel).delete();
return R.ok().setAttributeAll(new HashMap() {{
put("status", "SUCCESS");
put("userId", cacheUserId);
}});
}
else {
return R.ok().setAttribute("status", "DIFFERENT");
}
}
}
三、编写hxoa-mis
子系统
1. 编写Dubbo层
在com.example.hxoa.cloud.dubbo
包UserApi.java
接口中,声明抽象方法。
public interface UserApi {
……
public R sendSmsCaptcha(String tel);
public R verifySmsCaptcha(String tel, String captcha);
}
2. 编写业务层
在com.example.hxoa.cloud.mis.service
包UserService.java
接口中,声明抽象方法。
public interface UserService {
……
public Map sendSmsCaptcha(String tel);
public Map verifySmsCaptcha(String tel, String captcha);
}
在com.example.hxoa.cloud.mis.service.impl
包UserServiceImpl.java
类中,实现抽象方法。
@Service
public class UserServiceImpl implements UserService {
……
@Override
public Map sendSmsCaptcha(String tel) {
R r = userApi.sendSmsCaptcha(tel);
Map result = r.getAttributeAll();
return result;
}
@Override
public Map verifySmsCaptcha(String tel, String captcha) {
R r = userApi.verifySmsCaptcha(tel, captcha);
Map result = r.getAttributeAll();
return result;
}
}
3. 编写Web层
在com.example.hxoa.cloud.mis.controller.form
包中,创建SendSmsCaptchaForm.java
类。
@Data
public class SendSmsCaptchaForm {
@NotBlank(message = "手机号不能为空")
@Pattern(regexp = "^1[34578]\\d{9}$", message = "手机号格式不正确")
private String tel;
}
在com.example.hxoa.cloud.mis.controller.form
包中,创建VerifySmsCaptchaForm.java
类。
@Data
public class VerifySmsCaptchaForm {
@NotBlank(message = "手机号不能为空")
@Pattern(regexp = "^1[34578]\\d{9}$", message = "手机号格式不正确")
private String tel;
@NotBlank(message = "验证码不能为空")
@Pattern(regexp = "^\\d{6}$", message = "验证码格式不正确")
private String captcha;
}
在com.example.hxoa.cloud.mis.controller
包UserController.java
类中,声明Web方法。
@RestController
@RequestMapping("/user")
public class UserController {
……
@PostMapping("/send-sms-captcha")
@SentinelResource("UserController.sendSmsCaptcha")
public R sendSmsCaptcha(@RequestBody @Valid SendSmsCaptchaForm form) {
Map result = userService.sendSmsCaptcha(form.getTel());
return R.ok().setAttributeAll(result);
}
@PostMapping("/verify-sms-captcha")
@SentinelResource("UserController.verifySmsCaptcha")
public R verifySmsCaptcha(@RequestBody @Valid VerifySmsCaptchaForm form) {
Map result = userService.verifySmsCaptcha(form.getTel(), form.getCaptcha());
R r = R.ok();
String status = MapUtil.getStr(result, "status");
if ("SUCCESS".equals(status)) {
Long userId = MapUtil.getLong(result, "userId");
result.remove("userId");
//重置密码的过程中,把用户先踢下线销毁现有令牌,防止他执行其他请求
StpUtil.logout(userId, "Web");
//因为重置密码的后端Web方法需要登录才能调用,所以重新执行登录。
StpUtil.login(userId, "Web");
String token = StpUtil.getTokenValueByLoginId(userId, "Web");
//把新令牌返回给前端页面
r.setAttribute("token", token);
}
r.setAttributeAll(result);
return r;
}
}
上面的Web方法并没有添加后端防抖@NoDuplicateSubmit
注解,这是因为后端处理防抖的时候需要从请求头中提取Token然后解析出UserId。现在由于用户忘记密码而无法登录华夏OA系统,也就说明浏览器并没有缓存Token,所以我们不能给上面两个方法添加防抖注解,否则后端执行防抖的时候会因为拿不到Token而报错。