重置密码:两种场景三种方式
前言
注册、登录、用户名片功能完成之后,与用户信息相关的操作还剩一个重置密码。本篇我们分析一下重置密码的不同场景和方法,并通过代码实现。
知识准备
本篇主要介绍,如何通过发送一封带有链接的邮件的方式完成重置密码功能。涉及到了 Vue 的动态路由机制。
Vue Router 动态路由匹配
动态路由匹配就像是将路由声明成了一个带有参数的函数,带有不同后缀路由都能访问到统一个页面,并且能在页面中获取到要传递参数。和函数一样,也支持多个参数的传递。
在页面中使用 this.$route.params 就可以获取到传递的参数,声明和使用方法如下:
| 模式 | 匹配路径 | $route.params |
|---|---|---|
| /user/:username | /user/evan | { username: 'evan' } |
| /user/:username/post/:post_id | /user/evan/post/123 | { username: 'evan', post_id: '123' } |
功能分析
根据用户的使用场景来说,在登录状态下和未登录状态下都有可能有重置密码的需求:
- 登录状态下:想更换密码,Keller 云笔记项目中使用 JWT 完成用户身份验证,重设密码就可以和修改昵称、头像一样简单。
- 重设密码:用户填写新的密码即可。
- 未登录状态下:因为忘记了密码而需要重置密码,这个时候需要通过某种方式验证用户的身份。因为 Keller 云笔记项目在注册时选用邮件作为用户身份唯一性验证,在这里就可以通过邮件的方式验证。
- 通过邮件验证码重置密码:发送注册、登录验证码的思路一样,前后端都可以复用原来的方法。
- 通过链接重置密码:给用户发送一封包含重置密码链接的邮件,用户点击后打开重置密码页面,输入新密码即可。
总的来说前两种方式都比价容易实现,没有新的知识点,大家可以有空的时候自己实现一下,本篇主要讲解第三种方式的实现。目前这是多数 App、网站在验证邮箱、重置密码时会选择使用的方式,如:一个背单词的 App 邮箱验证邮件的效果图:
流程设计
通过链接重置密码,对用户来说操作比较简单:
- 用户忘记密码后,在登录页面可以点击“忘记密码”按钮,进入 ForgetPassword 页面;
- 用户在 ForgetPassword 页面填写注册时的邮箱,然后,将收到一封重置密码的邮件;
- 用户点击邮件中的链接,可以到 ResetPassword 页面;
- 用户在 ResetPassword 页面填写新密码,即可完成密码重置。
其中:发送重置密码邮件(第 1、2 步)流程图如下:
根据邮件重置密码(第 3、4 步)流程图如下:
总体方案
根据流程图,设计出以下实现方案。
接口 1006 发送重置密码邮件:
- 修改 JWT 结构,添加新的类型,用于重置密码时的用户身份验证;
- 复用邮件验证码机制,在 JWT 中嵌入加密后的验证码;
- 通过邮件将重置密码链接及 JWT 发送给用户,并记录邮件发送日志。
接口 1007 通过邮件重置密码:
- 解析请求中的 JWT,取出注册邮箱地址及重置密码的验证码;
- 通过比对邮件发送日志,对用户操作进行二次校验;
- 校验成功后重置用户密码。
服务端代码实现
代码结构
本篇涉及到的代码结构如下:
- controlelr
- BaseController.java:基础请求接口,添加 1006 发送重置密码邮件接口、1007 根据邮件重置密码接口
- service
- UserService.java:用户信息逻辑处理层,添加重置密码的方法
- EmailService:邮件逻辑处理层,修改发送邮件的方法
- util
- SendEmailUtil.java:邮件发送工具类,添加发送重置密码邮件的内容模板
- JwtUtils.java:JWT 工具类,添加用于重置密码的 JWT
本篇服务端代码的思路是:
- 先修改工具类,使其支持所需功能;
- 然后实现 Controller 层,将相关参数转换成适合系统处理的数据;
- 最后实现 Service 层,接收 Controller 层传入的参数,使用工具类完成逻辑处理。
重设密码 JWT 的生成与解析
首先准备好重置密码的 JWT,对原来的 JWT 生成机制进行修改,使其支持不同的类型。在 JwtUtils.java 中对 getJwtString 方法中进行修改:
- JWT 中放入注册邮件地址、类型(类型分为:登录 JWT、重置密码 JWT)
- 如果是重置密码的 JWT,在内容荷载中放入随机验证码
具体代码如下:
private static String getJwtString(UserInfo userInfo,int type,String code){long now=System.currentTimeMillis();JwtBuilder jwtBuilder = Jwts.builder().setId(userInfo.getId() + "")//设置应用名.setSubject(PublicConstant.appName)//签发时间.setIssuedAt( new Date() )//过期时间.setExpiration( new Date( now + PublicConstant.JWT_EXP_TIME ) )//自定义内容.claim(userNameKey,userInfo.getEmail()).claim(userTypeKey,userInfo.getType()).claim(typeKey,type)//签名 密钥.signWith( SignatureAlgorithm.HS256, PublicConstant.JWT_SIGN_KEY);//如果是重置密码类型的 JWT,放入随机验证码if(type == PublicConstant.RESET_PASSWORD_TYPE){jwtBuilder.claim(codeKey,code);}return jwtBuilder.compact();}
然后对外提供从 JWT 中读取注册邮件地址、随机验证码等方法,如下:
public static String getEmailForResetPassword(String jwtString){//校验 JWT,并获取内容荷载Claims claims = getClaims(jwtString,PublicConstant.RESET_PASSWORD_TYPE);if(claims == null){return null;}// 读取指定内容return claims.get(userNameKey,String.class);}
至此,JWT 的支持已经就绪。
重置密码邮件的保存与验证
有了 JWT 的支持,接下来就定义好邮件发送的格式。在 SendEmailUtil.java 中定义邮件内容格式,如下:
//邮件标题
public static final String TITLE ="【From】" + PublicConstant.appName;//邮件内容
public static final String ResetPasswordLinkBody ="您好!我们已收到您的账号: %1$s 重置密码的申请," +"请点击链接重置密码: " + PublicConstant.webUrl +"/ResetPassword/%2$s,该链接使用一次后失效";
然后添加发送重置密码邮件的方法:
记录邮件类型为重置密码类型,并设置邮件标题和内容,发送邮件。如果发送成功,设置邮件状态为发送成功;否则,设置邮件状态为发送失败,并记录失败原因。
具体代码如下:
public static EmailLog sendResetPasswordEmail(String email,String code,String token){EmailLog entity = new EmailLog();entity.setEmail(email);// 设置邮件类型为重置密码类型entity.setType(PublicConstant.RESET_PASSWORD_TYPE );entity.setTitle(TITLE);String body = String.format(ResetPasswordLinkBody,email,token);// 设置邮件内容entity.setContent(body);// 单独记录邮件验证码entity.setCode(code);try {// 发送邮件sendSimpleMail(entity.getEmail(),entity.getTitle(),entity.getContent());}catch (Exception e){Console.error("send sendResetPasswordEmail error :",e.getMessage());// 记录失败原因entity.setResult(e.getMessage());// 记录失败状态entity.setStatusCode(PublicConstant.FAILED);}return entity;}
至此,邮件发送的支持就准备完毕了。
声明接口
完成工具类的基础支持工作后,接下来按照接口文档实现 Controller 层的接口,来回顾一下 1006 接口。
该接口用于用户忘记密码时发送重置密码邮件:
POST /base/sendResetPasswordEmail
Content-Type:application/json
Body:{"email":"2221167890@qq.com","type":0}
请求参数说明:
- email:邮箱账号
- type:用户类型,非必填,默认为普通用户
成功应答:
- success:0
失败应答:
- success:1
- message:账号尚未注册
在 BaseController.java 中添加方法 sendResetPasswordEmail 用于实现 1006 接口:
- 校验参数中 email 的格式是否是邮箱地址
- 判断参数中是否包含 type 字段,若不包含,则使用普通用户类型
- 判断完毕后,调用 Server 层的方法完成业务处理
具体代码如下:
@PostMapping("/sendResetPasswordEmail")public ResponseEntity sendResetPasswordEmail(@RequestBody Map params){Console.info("sendResetPasswordEmail",params);String email = params.get("email");String typeStr = params.get("type");int type ;if(typeStr == null){type = PublicConstant.DEFAULT_USER_TYPE;}else {type= Integer.parseInt(typeStr);}if(StringUtils.isEmail(email)){return Response.ok(userService.sendResetPasswordEmail(email,type));}return Response.badRequest();}
在这里要注意一点:Spring Boot 接收 json 格式的请求参数时,可以使用 @RequestBody 注解,将参数转换为指定的对象。但是当参数为空时,会返回 400 错误,提示请求参数异常,如图:
因此,当你的请求参数可以全部为空时,不能使用 @RequestBody 注解,可以使用本项目 common/util/RequestUtil.java 中提供的解析 Request 的方法:
public static Map getBodyParams(HttpServletRequest request)
获取到 Body 中的全部参数。
接口 1007 的声明同样,在这里就不赘述了。
发送重置密码邮件
完成 Controller 层的声明后,就可以实现业务处理逻辑了。发送重置密码邮件需要 UserService 和 EmailService 合作实现,在该功能中它们分别负责的任务如下:
- UserService:负责用户信息处理
- 查询用户注册状态
- 生成随机验证码
- 根据用户信息和验证码生成重置密码专用的 JWT
- EmailService:负责邮件信息处理
- 根据指定的内容发送邮件
- 将邮件发送记录保存到数据库
UserService.java 代码如下:
public ResultData sendResetPasswordEmail(String email,int type){// 查询用户信息UserInfo userInfo = getByEmailAndType(type,email);if(userInfo == null){return ResultData.error("该邮箱尚未注册");}// 生成随机验证码String code = StringUtils.getAllCharString(PublicConstant.EMAIL_CODE_LENGTH);// 生成 JWTString token = JwtUtils.getJwtForResetPassword(userInfo,code);return emailService.sendResetPasswordEmail(email,code,token);}
EmailService.java 代码如下:
public ResultData sendResetPasswordEmail(String email,String code,String token){// 发送邮件 EmailLog emailLog = SendEmailUtils.sendResetPasswordEmail(email,code,token);if(emailLog == null){return ResultData.error("邮件发送失败");}// 保存邮件发送记录mapper.baseInsertAndReturnKey(emailLog);return ResultData.success();}
根据邮件重置密码
根据密码重置邮件同样需要 UserService 和 EmailService 合作实现:
- UserService:负责用户信息处理
- 从 JWT 中解析出所需要的信息
- 查询用户注册信息
- 重置用户密码
- EmailService:负责邮件信息处理
- 根据指定的条件查询邮件发送记录
- 校验验证码的有效性
UserService.java 代码如下:
public ResultData resetPasswordByEmail(String token,String password){// 解析注册邮箱String userEmail = JwtUtils.getEmailForResetPassword(token);// 解析随机验证码String userCode = JwtUtils.getCodeForResetPassword(token);// 解析用户类型Integer userType = JwtUtils.getUserTypeForResetPassword(token);// 查询用户信息UserInfo result = getByEmailAndType(userType,userEmail);if(result == null){return ResultData.error("该邮箱尚未注册");}if(StringUtils.isEmpty(userCode,userEmail)){return ResultData.error("身份验证失败");}// 校验验证码if(emailService.checkCode(userEmail,userCode,PublicConstant.RESET_PASSWORD_TYPE)){result.setPassword(password);result.setUpdateTime(new Date());result.setUpdateUserId(result.getId());// 重置用户密码userMapper.baseUpdateById(result);return ResultData.success();}return ResultData.error("密码重置失败,请重试");}
EmailService.java 代码如下:
public boolean checkCode(String email,String code,Integer type){// 设置查询条件EmailLog result = new EmailLog();result.setType(type);result.setEmail(email);result.setBaseKyleUseASC(false);result.setBaseKyleUseAnd(true);// 查询邮件发送记录List list = mapper.baseSelectByCondition(result);if(list == null || list.size() <= 0){return false;}result = list.get(0);// 校验验证码if(result.isEfficientVerificationCode() &&result.getCode().equals(code)){setCodeUsed(result);return true;}return false;}
至此,服务端已完成重置密码功能,可以使用 Postman 对接口进行测试,在测试中遇到问题,可以根据错误提示或异常堆栈信息进行分析。
Web 端代码实现
重置密码功能在 Web 端的工作比较简单,只需要两个简单页面即可。
忘记密码页面
在登录页面(Login.vue)中点击“忘记密码”按钮,跳转到忘记密码页面,页面布局如下:
这里就不再啰嗦该页面的布局代码了,点击“发送重置密码邮件”按钮后发起的请求接口在 api.js 中如下定义:
export const req_sendResetPasswordEmail = (email) => { return axios.post(base + '/sendResetPasswordEmail', {email:email}).then(res => res.data).catch(err => err);
};
点击按钮后,将用户填写的邮件地址传入请求接口,并获取到接口应答即可。大家可以尝试自己实现,感觉实现又困难的话再参考源码。
重置密码页面
重置密码页面布局和忘记密码页面一样简单,效果如下:
点击“重置密码”按钮后调用的接口如下:
export const req_resetPasswordByEmail = (password,token) => { return axios.post(base + '/resetPasswordByEmail', {password:password,token:token}).then(res => res.data).catch(err => err);
};
其中,token 的获取用到了 Vue Router 的动态路由匹配机制,ResetPassword.vue 页面的路由声明如下。
routes.js:
{ path: '/ResetPassword/:token',component: ResetPassword,name:"ResetPassword"},
在 ResetPassword.vue 中,mounted 钩子方法中使用 this.$route.params.token 即可获取到传递的参数。具体代码实现同样希望大家自己完成,在此不再赘述。
效果测试
实际收到的邮件:
源码地址
本篇完整的源码地址如下:
https://github.com/tianlanlandelan/KellerNotes/tree/master/18.修改密码
小结
本篇带领大家分析了重置密码的不同方式,并选择其中比较典型的一种方式实现。在实现过程中,有选择性地修改之前的方法,以期达到代码的复用,这也是在实际项目开发过程中需要注意的一点。随着项目开发进度的推进,要抽出一定的时间对以前写的代码进行审视,及时进行总结和优化,才能使项目代码的易用性、健壮性持续提升,才能让自己不断成长进步。
至此,项目核心功能开发完毕,这些功能是所有项目的基础功能,可以直接使用在绝大多数项目中去。希望大家能认真学习、重点掌握。也希望大家能对此深入思考,发现其中不足之处,对其进行优化分析,形成一套属于自己的风格。
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
