第一次写博客就拿出纯干货,手把手教你解决如何通过邮箱发送链接修改密码,这个是我在工作中实际遇到的情况,经过多次调试,可靠好用。
目录
需求分析
最终目标:
实现用户点击找回密码,选择邮箱方式后,向用户邮箱中发送一个修改链接,用户点击链接跳转到修改密码页面,提交后修改密码成功。
细节:
1:需要保证链接的唯一性,同时因为无登陆状态,链接应该能准确指向当前修改用户。
2:(可选)安全考量,设置链接在某个时间段内有效,一般为10分钟;
3:(可选)安全考量,设置链接仅可点击一次,退出再进,或者重复进入,均应被拒绝。
4:(可选)安全考量,当用户点击多次,发送多个链接时,仅最后一次发送的链接有效。
为什么说2和3是可选的呢,因为笔者在考察其他网站业务时,发现有一些大型知名网站并没有此类限制,可能目前手机验证码改密才是主流,而邮箱改密则被轻视,随便做了。这个可以根据自己业务需求设置专业程度。(手机号验证码改密,有需要的话,后面也会出)。考虑到以上三点基本就可以做出一个可用性和安全性强的邮箱改密功能。
ps:做这个模块时候,经理曾说,你这个链接时谁点都能改吗,那不是很危险了。众所周知,产品经理是一种奇特的生物,他要求我在链接上加上身份验证,同学们,修改密码链接加上身份验证就意味着需要登录操作,用户密码都忘了怎么个登录法?最后在我据理力争下,经理放弃了他的想法和要求!
好了进入正题:
一、准备工作:
这个业务可以是登录也的忘记密码进入,也可以是个人中心的修改密码进入,可以根据自己需求加入对邮箱的验证,这个自己拿捏;
二、发送链接:
我们先参考一个其他公司的修改链接,做出分析:
无非就是一个固定链接后面加上一个或多个“随机”参数,固定地址指向跳转的页面,参数负责确定安全校验和提交后的身份确定。
于是我就大胆地开始了随机参数的准备,采用生成uuid作为随机参数,以该随机参数作为key,以用户信息和安全验证信息作为value,存入redis;代码如下:
//业务前缀,可以自己定义,我这里是提前定义的常量,可替换
String key = KEY_RESET_PWD_REDIS;
//对邮箱进行加密,作为拼接的key,保证用一个邮箱只存储一份信息
String code = MD5Utils.md5(keyChecker);
//uuid作为随参数,保证链接的唯一性,同时确保了多个链接仅最后一个有效,也可采用其他策略
String checkCode = UUID.randomUUID().toString(true);
//构建value的值
HashMap<String, String> map = new HashMap<>();
//用户id,用于指向修改用户
map.put("id",studentId.toString());
//随机验证码,保证链接的唯一性
map.put("checkCode",checkCode);
//邮箱也存储上去,可多重验证
map.put("email",keyChecker);
//字段限制链接只能访问一次
//map.put(KEY_LINK_VISIT,LINK_CANUSE_TRUE);
map.put("visit","0");
//存入redis,设置过期时间
redisTemplate.opsForHash().putAll(key+code,map);
redisTemplate.expire(key+code,REDIS_LINK_TTL,TimeUnit.MINUTES);
上面为核心代码,下面我将发送链接业务全部代码贴下:
public Result<?> sendLinkWithEmail(String keyChecker) {
//获取当前登陆用户id
Long studentId = UserUtil.getUserDetails().getUserAuths().getUserId();
//通过用户id去查询用户邮箱
UserAuths userAuths = userAuthsService
.getOne(new LambdaQueryWrapper<UserAuths>()
.eq(UserAuths::getUserId, studentId)
.eq(UserAuths::getIdentityType, IDENTITYTYPE_EMAIL));
//验证keyChecker是否是用户绑定的邮箱
if (Objects.isNull(userAuths) || !userAuths.getIdentifier().equals(keyChecker)){
return Result.failed("邮箱非绑定邮箱");
}
/*
准备发送前,验证用户请求频率
*/
if (!checkedRequestFrequency(keyChecker)){
return Result.failed("请求过于频繁");
}
String key = KEY_RESET_PWD_REDIS;
//在链接中加入随机uuid作为验证,只保证最后一次链接有效
String code = MD5Utils.md5(keyChecker);
String checkCode = UUID.randomUUID().toString(true);
HashMap<String, String> map = new HashMap<>();
map.put("id",studentId.toString());
map.put("checkCode",checkCode);
map.put("email",keyChecker);
//字段限制链接只能访问一次
map.put(KEY_LINK_VISIT,LINK_CANUSE_TRUE);
redisTemplate.opsForHash().putAll(key+code,map);
redisTemplate.expire(key+code,REDIS_LINK_TTL,TimeUnit.MINUTES);
String link = RESET_PWD_LINK_PREFIX+"?code="+code+"&checkCode="+checkCode;
//调用提前写好的发送邮件的工具类,将链接发送到用户邮箱内
//因发送邮件不是核心业务,这里不再赘述,不会的可以参考其他博主资料,也可留言我会再出教程。
SendEmailUtil.sendHtmlMail(keyChecker,RESET_PWD_SUBJECT,SEND_LINK_RESET_PWD(link),EMAIL_FROM);
return Result.succeed("email","修改链接已发送至邮箱");
}
(需要邮件发送教程的请留言,或关注后续文章,比较简单,可能会出。)
三、点击链接(不做234条的可以跳过)
要保证链接只能点击一次,10分钟内有效等功能,需要在用户点击链接跳转的进入页面,页面尚未渲染前,发送一个验证请求,参数就是我们链接上的连个参数,验证通过,成功跳转,验证不通过则直接跳转到错误页面。
public String getInfoAndCheckLink(String code,String checkCode){
String key = KEY_RESET_PWD_REDIS;
//获取到上一步存储的信息
Map entries = redisTemplate.opsForHash().entries(key + code);
//验证存储的visit是否已访问,如果访问过一次则验证不通过
if (entries.get(KEY_LINK_VISIT).toString().equals(LINK_CANUSE_FALSE)){
return null;
}
//是第一次访问,则修改该值为 已访问 状态
redisTemplate.opsForHash().put(key+code,KEY_LINK_VISIT,LINK_CANUSE_FALSE);
//验证uuid随机字符串,保证链接是最后一次发送的链接(多次点击发送的话)
if (checkCode.equals(entries.get("checkCode").toString())){
return entries.get("email").toString();
}
return null;
}
四、最终修改密码
所有验证都通过后,用户在前台提交新密码,进行修改操作。
public Result<?> resetPassWordByEmail(String password, String code, String checkCode) {
String key = KEY_RESET_PWD_REDIS;
//同样取出保存在redis的用户信息
Map entries = redisTemplate.opsForHash().entries(key + code);
//验证链接时间是否在有效期内
if (entries.size()==0 || !entries.get("checkCode").equals(checkCode)){
return Result.failed("链接已失效");
}
//新密码加密策略,根据自己业务需要决定
// 重新生成盐值
String salt = UUID.randomUUID().toString(true);
// 加密
password = SHA1Utils.sha1(password, salt);
//修改密码
boolean isSuccess = userAuthsService
.update(new LambdaUpdateWrapper<UserAuths>()
.set(UserAuths::getPassword, password)
.set(UserAuths::getSalt,salt)
.eq(UserAuths::getUserId, entries.get("id")));
if (isSuccess){
redisTemplate.delete(key + code);
return Result.succeed("修改成功");
}
return Result.failed("修改失败,请重试");
}
至此,邮箱改密全部业务逻辑和业务代码搞定,你学会了吗?