村级网站建设 不断增强,七牛云对象存储,上海有哪些网络公司,企业网站设计模板文章目录 SSM个人博客系统实现项目介绍 一、准备工作0. 创建项目添加对应依赖1. 数据库设计2. 定时实体类 二、功能实现1.统一功能处理统一返回格式统一异常处理定义登录拦截器 2. 注册登录实现生成获取验证码密码加盐实现注册功能登录功能注销功能 3.登录用户博客列表获取登录… 文章目录 SSM个人博客系统实现项目介绍 一、准备工作0. 创建项目添加对应依赖1. 数据库设计2. 定时实体类 二、功能实现1.统一功能处理统一返回格式统一异常处理定义登录拦截器 2. 注册登录实现生成获取验证码密码加盐实现注册功能登录功能注销功能 3.登录用户博客列表获取登录用户信息获取登录用户文章列表 4.文章相关操作发布文章删除文章修改文章定时发布文章分页获取所有用户的文章文章详情 5.文章草稿箱实现从草稿箱发布文章修改草稿 6.个人信息修改头像修改基本信息修改 7. 其它密码相关功能实现修改密码设置密保问题找回密码 SSM个人博客系统实现 项目介绍
本项目是一个前后端分离的个人博客系统实现的主要功能有用户注册、用户登录、找回密码、验证码、文章的发布和删除、定时发布文章功能、草稿箱功能、文章列表分页功能、用户信息修改包括上传头像。利用SpingAOP实现了统一的登录验证、异常处理、统一返回格式。
一、准备工作
0. 创建项目添加对应依赖 从Maven仓库引入SprinAOP依赖和第三方Hutool依赖
dependencygroupIdcn.hutool/groupIdartifactIdhutool-all/artifactIdversion5.8.16/version
/dependency
dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-aop/artifactId
/dependency1. 数据库设计
数据库一共有6张表分别是用户表、文章表、文章草稿表、定时发布文章表、密保问题表 2. 定时实体类
实体类分别对应数据表
用户类
Data
public class User {private int id;private String username;private String password;private String netName;private String photo;private String git;private Integer articleCount;private Date createTime;private Date updateTime;// 用户是否第一次登录private int state;
}文章类
Data
public class Article {private Integer id;private String title;private String content;private Date createTime;private Date updateTime;private Integer userId;private Integer visits;
}文章草稿
Data
public class Drafts {private Integer id;private String title;private String content;private Date createTime;private Date updateTime;private Integer userId;
}定时发布的文章
Data
public class TimingArticle {private Integer id;private String title;private String content;private Date postTime;private Integer userId;
}密保问题
Data
public class QuestionPassword {private Integer id;private String question1;private String question2;private String question3;private String answer1;private String answer2;private String answer3;private Integer userId;
}二、功能实现
1.统一功能处理
使用SpringAOP可以实现统一功能的处理统一的功能处理可以避免代码的冗余。
统一返回格式
先定义一个响应类重载一些方法success表示执行成功(正确的查询数据、登录成功等)fail表示执行失败(密码错误、参数非法的)重载可以非常灵活的让我们给前端返回数据。
Getter
Setter
public class Response {private int code;private String message;private Object data;public static Response success(Object data) {Response response new Response();response.code 200;response.message ;response.data data;return response;}public static Response success(String message) {Response response new Response();response.message message;response.code 200;return response;}public static Response success(Object data,String message) {Response response new Response();response.message message;response.code 200;response.data data;return response;}public static Response fail(String message) {Response response new Response();response.code -1;response.message message;return response;}public static Response fail(String message,int code) {Response response new Response();response.code code;response.message message;return response;}
}实现统一响应格式实现统一的返回格式有利于后端统一标准规范降低前后端的沟通成本。定义ResponseAdvice类实现ResponseBodyAdvice接口并用ControllerAdvice注解修饰表示该类为一个增强类
ControllerAdvice
ResponseBody
public class ResponseAdvice implements ResponseBodyAdvice {Resourceprivate ObjectMapper mapper;Overridepublic boolean supports(MethodParameter returnType, Class converterType) {return true;}SneakyThrowsOverridepublic Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType,ServerHttpRequest request, ServerHttpResponse response) {// 如果是定义的返回格式就直接返回if (body instanceof Response) {return body;}if (body instanceof String) {// 转换成json根式的返回格式return mapper.writeValueAsString(Response.success(body));}return Response.success(body);}
}最后的响应返回格式后面的所有响应都是如此
正确执行的响应
{code:200data: 自定义的返回数据message: 自定义的返回消息
}错误执行的响应
{code:-1data: message: 自定义的返回消息
}统一异常处理
统一的异常处理服务器发送异常后我们可以通过增强类做统一处理后给前端返回响应可以添加一些预计会发送的异常分别进行处理。
ControllerAdvice
ResponseBody
public class ExceptionAdvice {ExceptionHandler(Exception.class)public Object handler(Exception e) {return Response.fail(服务器异常,500);}
}定义登录拦截器
定义登录拦截器可以避免大量登录验证的代码冗余让指定的接口统一验证。
/*** 自定义登录拦截器*/
public class LoginInterceptor implements HandlerInterceptor {Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {HttpSession session request.getSession(false);if (session ! null session.getAttribute(Constant.SESSION) ! null) {response.setStatus(200);return true;}// 重定向到登录页面response.sendRedirect(/login.html);return false;}
}
添加自定义拦截器如果某些接口被拦截器拦截就需要经过拦截器验证后才能去执行对应的Controller方法也就是需要登录后才能使用。
Configuration
public class AppConfig implements WebMvcConfigurer {Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new LoginInterceptor()) // 添加自定义拦截器.addPathPatterns(/**) //拦截所有接口.excludePathPatterns(/user/login) //排除的接口.excludePathPatterns(/user/reg).excludePathPatterns(/user/getVerify).excludePathPatterns(/user/byIdUser).excludePathPatterns(/questionPass/getQuestion).excludePathPatterns(/questionPass/findPass).excludePathPatterns(/questionPass/setNewPass).excludePathPatterns(/article/byIdArticle).excludePathPatterns(/article/pagingGetArticle).excludePathPatterns(/login.html).excludePathPatterns(/blog_detailed.html).excludePathPatterns(/blog_list.html).excludePathPatterns(/find_pass.html).excludePathPatterns(/register.html).excludePathPatterns(/img/**).excludePathPatterns(/js/**).excludePathPatterns(/photo/**).excludePathPatterns(/css/**);}
}2. 注册登录实现
生成获取验证码
使用第三方工具Hutool来绘制验证码把生成的验证码保存到session中通过响应把图片传递个前端
GetMapping(/getVerify)
public Response getVerify(HttpSession session,HttpServletResponse response) {VerificationCodeUtil.getCode(session,response);return Response.success(ok);
}密码加盐实现
通过密码加盐来保证用户密码的一定安全性
加盐的实现思路为用户注册把生成一个UUID拼接上用户前端传递的明文密码进行MD5以#号分割再把生成的UUID拼接到#后面生成最终密码存入数据库。解密思路把数据库加密的代码查询出来以#把UUID分割出来把用户输入的明文密码以同样的方式拼接上分割出来的UUID加盐后再和数据库查询的密码进行比对
public class PasswordUtil {/*** 密码加盐* param password 明文密码* return*/public static String passwordAddSalt(String password) {String uuid UUID.randomUUID().toString().replace(-,);String finalPass (DigestUtils.md5DigestAsHex((uuidpassword).getBytes())#uuid);return finalPass;}/*** 验证密码* param password 明文密码* param finalPass 加密密码* return*/public static boolean check(String password,String finalPass) {String uuid finalPass.split(#)[1];String pass (DigestUtils.md5DigestAsHex((uuidpassword).getBytes())#uuid);return pass.equals(finalPass);}
}注册功能 约定前后端交互
请求:
{username : 用户名,password : 密码,confirmPass :确认密码,verifyCode : 验证码
}响应:
{code:200,message:注册成功,data:null
}先简单的数据校验密码加盐再生成随机的网名注意网名和用户名要区分考虑到用户名已经存在问题。
PostMapping(/reg)
public Response reg(String username,String password,String confirmPass,HttpSession session,String verifyCode) {if (verifyCode null || .equals(verifyCode.trim()) || !VerificationCodeUtil.check(session,verifyCode)) {return Response.fail(验证码错误);}if (username null || password null || confirmPass null || .equals(username.trim())|| .equals(password.trim()) || .equals(confirmPass.trim())) {return Response.fail(用户名密码非法);}if (!password.equals(confirmPass)) {return Response.fail(两次密码输入不一致);}User user userService.byNameUser(username);if (user ! null) {return Response.fail(用户已经被注册);}// 密码加盐String finalPass PasswordUtil.passwordAddSalt(password);String netName 用户System.currentTimeMillis()%1000000;int ret userService.reg(username,finalPass,netName);if (ret 1) {return Response.success(注册成功);}return Response.fail(用户名密码非法);
}登录功能 约定前后端交互
请求
{username:用户名,password:密码,inputVerify验证码
}响应:
{code:200,message:登录成功/用户名密码错误/验证码错误,data:null
}登录成功后把用户信息保存session会话,前端跳转到博客列表页
PostMapping(/login)
public Response login(String username, String password,String inputVerify ,HttpSession httpSession,HttpServletRequest request) {if (inputVerify null || .equals(inputVerify.trim()) || !VerificationCodeUtil.check(httpSession,inputVerify)) {return Response.fail(验证码错误);}if (username null || password null || .equals(username.trim()) || .equals(password.trim())) {return Response.fail(用户名密码错误);}User user userService.byNameUser(username);if (user ! null PasswordUtil.check(password,user.getPassword())) {// 把密码置为空user.setPassword();user.setUsername();HttpSession session request.getSession(true);session.setAttribute(Constant.SESSION,user);return Response.success(登录成功);}return Response.fail(用户名密码错误);
}注销功能
删除session会话即可
PostMapping(/logout)
public Response login(HttpServletRequest request) {User user LogInUserInfo.getUserInfo(request);if (user null) {return Response.fail(当前未登录);}request.removeAttribute(Constant.SESSION);return Response.success(注销成功);
}3.登录用户博客列表
登录成功后跳转到博客列表页面需要获取当前用户的所有博客信息和当前用户的信息。
获取登录用户信息
用户登录后通过用户的id来查询到用户的信息查询到后要把用户名和密码一些敏感信息给影响。
请求
http://127.0.0.17070/usre/getUserInfo响应
{code:200,message:,data:{id:1,username:,password:,netName:用户11326,photo:../img/logo.jpg,git:https://gitee.com/he-hanyu,articleCount:0,createTime:null,updateTime:null,state:0}
}主意通过用户的state字段判断用户是否第一次登录如果是第一次登录就在前端提示用户设置密保问题。
GetMapping(/getUserInfo)
public Response getUserInfo(HttpServletRequest request) {User user LogInUserInfo.getUserInfo(request);User myUser userService.byIdUser(user.getId());myUser.setPassword();myUser.setUsername();// 判断是否是第一次登录if (myUser.getState() 1) {//如果是第一登录就修改状态userService.updateState(user.getId());}return Response.success(myUser);
}获取登录用户文章列表
请求
http://127.0.0.1:7070/user/getUserArticle响应
{code:200,message:,data:[{id:1,title:测试,content:#Hh宇的个人博客,createTime:2023-08-06,updateTime:null,userId:1,visits:0}]
}通过用户id来查看当前用户的博客显示博客是博客列表要注意把文章内荣进行一个截取
GetMapping(/getUserArticle)
public Response getUserArticle(HttpServletRequest request) {User user LogInUserInfo.getUserInfo(request);ListArticle articleList articleService.getUserArticle(user.getId());for (Article article : articleList) {if (article.getContent().length() 100) {article.setContent(article.getContent().substring(0,100)......);}}return Response.success(articleList);
}博客列表有对应的查看文章响应修改文章删除文章。
4.文章相关操作 发布文章
发布文章校验标题和正文是否为空校验完毕后给数据表添加信息。
约定前后端交互
请求
{title: 文章标题,content: 文章正文
}响应
{code:200,message:发布成功,data:null
}PostMapping(/addArticle)
public Response addArticle(String title,String content,HttpServletRequest request) {if (title null || content null || .equals(title.trim()) || .equals(content.trim())) {return Response.fail(发布失败);}User user LogInUserInfo.getUserInfo(request);int row articleService.addArticle(title,content,user.getId());if (row 1) {userService.articleCountAuto(user.getId(),1);return Response.success(发布成功);}return Response.fail(发布失败);
}删除文章
点击登录用户文章列表后通过博客id来删除文章文章id由前端在生成链接时拼接在querystr中。点击删除链接就会获取到文章Id给后端发送删除请求。且删除时验证该文章是否属于当前登录用户。
请求:
POST http://127.0.0.1:7070/article/idDeleteArticle
{articleId : 文章Id
}PostMapping(/idDeleteArticle)
public Response idDeleteArticle(Integer articleId,HttpServletRequest request) {if (articleId null || articleId 0) {return Response.fail(删除失败);}User user LogInUserInfo.getUserInfo(request);int ret articleService.idDeleteArticle(articleId,user.getId());if (ret 1) {userService.articleCountAuto(user.getId(),-1);return Response.success(删除成功);}return Response.fail(删除失败);
}修改文章
点击修改文章后会在url里拼接上一个文章id进入博客编辑页面再次点击发布博客后会判断url中的querystr中是否有文章Id如果有说明是修改博客。
获取博客请求也就是通过querystr中的id查询博客
GET http://127.0.0.1:7070/article/byIdArticle?articleId2 修改博客请求
POST http://127.0.0.1:7070/article/updateArticle HTTP/1.1
{title: 修改后的标题,content: 修改后的正文
}响应
{code:200,message:修改成功,data:null
}PostMapping(/updateArticle)
public Response updateArticle(String title,String content,Integer articleId,HttpServletRequest request) {if (title null || content null || .equals(title.trim()) || .equals(content.trim())|| articleId null || articleId 0) {return Response.fail(内容非法);}User user LogInUserInfo.getUserInfo(request);int ret articleService.updateArticle(title,content,articleId,user.getId());if (ret 1) {return Response.success(修改成功);}return Response.fail(修改失败);
}定时发布文章 定时发布文章前端给后端传递格式化的时间后端再装换成和数据库对应的Date时间存把待发布文章存入数据库通过Spring的定时任务每5秒扫描一下定时任务数据表如果当前的大于发布时间就从数据库中获取到文章信息并进行发布且删除对应的文章信息。
请求
{title: 文章标题,content: 文章正文,postTime : 2023-08-06 16:28:26
}响应
{code:200,message:定时博客任务发布成功,data:null
}需要通过正则判断前端传递的时间格式是否正确且发布时间没有大于预期值。
RestController
RequestMapping(/timed)
public class TimedArticleController {Autowiredprivate TimedArticleService timedArticleService;SneakyThrowsPostMapping(/timingPost)public Response addTimedPost(String title, String content, String postTime, HttpServletRequest request) {if (title null || content null || postTime null ||.equals(title.trim()) || .equals(content.trim()) || .equals(postTime.trim())) {return Response.fail(内容非法);}// 校验时间格式是否正确if (DateTimeUtil.isValidDateTimeFormat(postTime)) {System.out.println(postTime);// 获取当前时间String time DateUtil.now();// 判断当前时间和发布时间是否合法if (DateTimeUtil.getTimeDifferenceDay(time,postTime) 7) {return Response.fail(发布时间不合法,请输入小于7天的发布时间);}SimpleDateFormat format new SimpleDateFormat(yyyy-MM-dd HH:mm:ss);Date date format.parse(postTime);User user LogInUserInfo.getUserInfo(request);int ret timedArticleService.addTimedPost(title,content,date,user.getId());if (ret 1) {return Response.success(定时博客任务发布成功);}}return Response.fail(发布失败);}
}
在SpingBoot的启动类上添加注解开启定时任务
SpringBootApplication
EnableScheduling
public class BlogApplication {public static void main(String[] args) {SpringApplication.run(BlogApplication.class, args);}}再创建一个定时任务类每5秒扫描一下数据库
Component
public class ScheduledRelease {Autowiredprivate TimedArticleService timedArticleService;Autowiredprivate ArticleService articleService;// 每5秒执行一次Scheduled(fixedRate 5000)Transactionalpublic void scheduledTak() {Date date new Date();ListTimingArticle timingArticles timedArticleService.getPostArticle(date);for(TimingArticle article : timingArticles) {articleService.addArticle(article.getTitle(),article.getContent(),article.getUserId());// 发布后立马删除记录timedArticleService.idDelete(article.getId());}}
}需要注意的是查询时间对比sql要先把时间转换为时间戳
select idgetPostArticle resultMapTimingArticleInfoselect * from timed_article where unix_timestamp(#{time}) unix_timestamp(post_time);/select分页获取所有用户的文章
所有用户的文章数量比较多为了减小服务器压力所以采用分页。一页显示5篇文章。每次进入博客列表页后端会先查询数据库中的文章数量通过公式计算出一共有多少页再把最大页数和当前页的文章传递给前端前端默认获取第一页的文章。 分页公式为文章总数*1.0/每页显示数量向上取整算出页数前端传递的(页数-1)*每页显示数量求出从数据表中哪一条数据开始查询。
请求:
GET http://127.0.0.1:7070/article/pagingGetArticle?pageNumber1size5 HTTP/1.1响应:
{
code:200,
message:,
data:{pageCount:2,articles:[{id:6,title:再来一篇文章,content:#Hh宇的个人博客,createTime:2023-08- 08,updateTime:null,userId:2,visits:0},]}
}后端代码
/*** 分页获取所有用户博客* param pageNumber 页数* param size 每页显示多少篇文章* return*/GetMapping(/pagingGetArticle)public Response pagingGetArticle(Integer pageNumber,Integer size) {if (pageNumber 1) {pageNumber 1;}// 获取所有文章数量int articleCount articleService.getArticleCount();// 计算出每页size个最多有多少页int pageCount (int)(Math.ceil(articleCount*1.0/size));// 公式计算步长int offset (pageNumber-1)*size;ListArticle articles articleService.pagingGetArticle(size,offset);System.out.println(articles);HashMapString,Object result new HashMap();result.put(pageCount,pageCount);result.put(articles,articles);return Response.success(result);} 文章详情
点击查看全文即可查看文章详情文章详情里展示了文章标题、文章发布时间、文章的浏览量和文章正文。左边显示的是当前文章的作者信息。先通过querystr中的的文章id查询到文章再同过文章里返回的当前用户id来查询到当前文章作者对应的信息。 请求
GET http://120.25.124.200:7070/article/byIdArticle?articleId5 HTTP/1.1响应
{
code:200,
message:,
data:{id:5,title:文章分页,content:#Hh宇的个人博客,createTime:2023-08-08,updateTime:null,userId:2,visits:0}
}5.文章草稿箱实现 文章草稿包存草稿箱和发布文章类似点击保存草稿后就把文章保存到草稿箱当中并更新数据表信息可以从草稿箱中发布博客也可以修改草稿箱里的草稿文章或者删除草稿。
需要注意的是修改草稿点击修改草稿后通过url中的querystr来查询到对应的草稿
GET http://127.0.0.1:7070/blog_editor.html?draftId1 HTTP/1.1响应
{code:200,message:,data:{id:1,title:这是一篇草稿,content:#Hh宇的个人博客,createTime:null,updateTime:null,userId:1}
}从草稿箱发布文章
因为发布文章和草稿共用的是一个页面所以发布文章的时候需要通过url中的querystr的id来判断是普通发布文章还是从草稿箱中发布文章。从草稿箱发布完文章后,就删除草稿数据报中的文章。
请求
POST http://127.0.0.1:7070/articleDraft/postArticle HTTP/1.1
{title:草稿标题content:草稿正文,draftId : 1
}响应
{code:200,message:发布成功,data:null
}PostMapping(/postArticle)
public Response postArticle(String title,String content,Integer draftId,HttpServletRequest request) {if (title null || content null || .equals(title.trim()) || .equals(content.trim()) || draftId 0) {return Response.fail(发布失败);}User user LogInUserInfo.getUserInfo(request);int ret articleService.addArticle(title,content,user.getId());// 发布成功就删除草稿if (ret 1) {int row articleDraftService.idDeleteDrafts(draftId,user.getId());if (row 1) {return Response.success(发布成功);}}return Response.fail(发布失败);
}修改草稿
如果是修改草稿也就是点击编辑草稿后再次点击保存草稿此时就是修改草稿了而不是保存草稿同样是在前端通过querystr区分。
请求
POST http://127.0.0.1:7070/articleDraft/updateDraft HTTP/1.1
{draftId : 2,title : 标题,content : 正文
}响应
{code:200,message:保存成功,data:null
}/*** 修改草稿* param title* param content* param draftId* return*/
PostMapping(/updateDraft)
public Response updateDraft(String title,String content,Integer draftId,HttpServletRequest request) {if (title null || content null || .equals(title.trim()) || .equals(content.trim())|| draftId null ||draftId 1) {return Response.fail(内容非法);}User user LogInUserInfo.getUserInfo(request);int ret articleDraftService.updateDraft(title,content,draftId,user.getId());if (ret 1) {return Response.success(保存成功);}return Response.success(保存失败);
}6.个人信息修改 头像修改
前端传递头像后端校验一下文件格式,生成唯一的文件名把头像路径更新到对应的用户数据库。SpingBoot对文件上传大小有默认限制我们只需处理对应的异常即可。
SneakyThrows
PostMapping(/updatePhoto)
public Response updatePhoto(RequestPart(photo)MultipartFile imgFile, HttpServletRequest request,HttpServletResponse response) {// 设置重定向response.setStatus(302);response.sendRedirect(/user_blog_list.html);if (imgFile ! null !imgFile.isEmpty()) {String fileName imgFile.getOriginalFilename();if (fileName.contains(.jpg) || fileName.contains(.png)) {// 1.生成唯一文件名String newFileName UUID.randomUUID().toString().replaceAll(-,)fileName.substring(fileName.length()-4);// 路径文件名File file new File(photoPath,newFileName);//保存文件imgFile.transferTo(file);User user LogInUserInfo.getUserInfo(request);int ret userService.updatePhoto(user.getId(),Constant.PHOTO_UPLOAD_PATHnewFileName);if (ret 1) {return Response.success(修改成功);}}}return Response.fail(修改失败);
}基本信息修改
修改网名和gitee连接
请求
POST http://127.0.0.1:7070/user/updateUserInfo HTTP/1.1
{netName:网名,gitee: gitee链接
}响应
{code:200,message:修改成功,data:null
}PostMapping(/updateUserInfo)
public Response updateUserInfo(String netName,String git,HttpServletRequest request) {if (netName null || git null || .equals(netName.trim()) || .equals(git.trim())|| (!git.contains(https://))) {return Response.fail(参数非法或git链接非法);}User user LogInUserInfo.getUserInfo(request);int ret userService.updateUserInfo(netName,git,user.getId());if (ret 1) {return Response.success(修改成功);}return Response.fail(修改失败);
}7. 其它密码相关功能实现 修改密码
修改密码比较简单用户登录后输入原密码和新密码后端通过解密方式验证。验证通过即可修改密码。
PostMapping(/updatePassword)
public Response updatePassword(String password,String newPassword,String confirmPassword,HttpServletRequest request) {if (password null || newPassword null || confirmPassword null ||.equals(password.trim()) || .equals(newPassword.trim()) || .equals(confirmPassword.trim())) {return Response.fail(修改失败);}User user LogInUserInfo.getUserInfo(request);User myUser userService.byIdUser(user.getId());if (PasswordUtil.check(password,myUser.getPassword())) {String finalPass PasswordUtil.passwordAddSalt(newPassword);int ret userService.updatePassword(finalPass,user.getId());if (ret 1) {HttpSession session request.getSession(false);session.removeAttribute(Constant.SESSION);return Response.success(修改成功);}}return Response.fail(修改失败密码错误);
}设置密保问题 在用户第一登录的时候提示用户设置密保问题通过密保问题即可找回密码。
给定3个问题和指定的问题选项让用户输入答案和用户密码进行设置密码问题。再对答案进行md5加密存入数据库。
请求
POST http://127.0.0.1:7070/questionPass/addQuestionPassword HTTP/1.1
{password:hhy,question1你最相信的人的姓名是,question2你的出生地是,question3你最喜欢吃的水果是,answer1某某,answer2湖南,answer3葡萄
}响应
{code:200,message:密保问题设置成功,data:null
}找回密码
通过用户设置的密保问题来找回密码用户输入查找的用户名来获取对应的密保问题再输入答案后对答案进行md5同问题一起比对验证端验证正确后再给用户输入新密码输入新密码后再次提交。修改密码前再次验证一下密保问题如果验证通过即可修改密码。