4.开发社区登录模块
application.properties中配置邮箱信息(发送方)
#MailProperties # 使用的邮箱对应的smtp服务器地址 spring.mail.host=smtp.163.com # 邮箱信息 spring.mail.username=cjhtxdy@163.com spring.mail.password=WSSFFVTWDBLLJDDZ # smtp协议相关配置 spring.mail.properties.mail.smtl.auth=true spring.mail.properties.mail.smtp.starttls.enable=true spring.mail.properties.mail.smtp.starttls.required=true
写一个工具类MailClient并测试
@Component public class MailClient { private static final Logger logger= (Logger) LoggerFactory.getLogger(MailClient.class); @Autowired private JavaMailSender mailSender; @Value("$spring.mail.username")//从application里注入 private String from; //邮件发送人 //to:发送目标 subject:邮件主题 content:邮件内容 public void setMailSender(String to,String subject,String content){ try { MimeMessage message=mailSender.createMimeMessage(); MimeMessageHelper helper=new MimeMessageHelper(message); helper.setFrom(from); helper.setTo(to); helper.setSubject(subject); helper.setText(content,true); } catch (MessagingException e) { logger.info("发送邮件失败:"+e.getMessage()); } } } @Test public void testTextMail(){ mailClient.sendMail("接收方@qq.com","TEST","welcome."); }
(1)首页跳转注册页面
首页头部【注册】按钮,点击跳转 <li class="nav-item ml-3 btn-group-vertical"> <a class="nav-link" th:href="@{/register}">注册</a> </li>
(2)LoginController
@Controller public class LoginController { @RequestMapping(path = "/register",method = RequestMethod.GET) public String getRegisterPage(){//获取首页 return "/site/register"; } }
(3)一些准备
commons lang依赖:对字符串、集合等做一些数据检查 #community链接可能在开发、上线等地址不同,要做成可配置的 community.path.domain=http://localhost:8080 新建工具类:CommunityUtil 生成随机字符串、MD5加密(只能加密不能解密)+随机盐
//生成随机字符串
public static String generateUUID(){
return UUID.randomUUID().toString().replaceAll("-","");
}
//MD5加密(安全性不如SHA1,都不如SHA2,sha1输出160位消息摘要 sha2输出256位消息摘要。sha2的碰撞的概率比sha1要低,因为sha2有2^256种组合sha1有2^160种组合。)
//hello---->abc123456
//helld + 随机盐(随机字符串)--->abc123456ghk
public static String md5(String key){
if(StringUtils.isBlank(key)){
return null;
}
return DigestUtils.md5DigestAsHex(key.getBytes());
}
}
存储用户密码应该使用什么加密算法: 算法需不可逆,这样才能有效防止密码泄露。 算法需相对慢,可以动态调整计算成本,缓慢是应对暴力破解有效方式。 旧的加密: 过去密码加密常用MD5或者SHA。MD5是早期设计的加密哈希,它生成哈希速度很快,随着计算机能力的增强,出现了被破解的情况,所以又有了一些长度增大的哈希函数,如:SHA-1,SHA-256等。下面是它们的一些比较: MD5:速度快生成短哈希(16 字节)。意外碰撞的概率约为:1.47*10^(-29)。 SHA1:比 md5 慢 20%,生成的哈希比 MD5 长一点(20 字节)。意外碰撞的概率约为:1*10^(-45)。 SHA256:最慢,通常比 md5 慢 60%,并且生成的哈希长(32 字节)。意外碰撞的概率约为:4.3*10^(-60)。 为了确保安全你可能会选择目前长度最长的哈希SHA-512,但硬件能力在增强,或许有一天又会发现新的漏洞,研究人员又推出较新的版本,新版本的长度也会越来越长,而且他们也可能会发布底层算法,所以我们应该另外寻找更合适的算法。 加盐操作: 密码安全,除了要选择足够可靠的加密算法外,输入数据的强度也要提升,因为密码是人设置的,其字符长度组合强度不可能一致,如果直接进行哈希存储往往会提升爆破的概率,这时我们需要加盐。加盐是密码学中经常提到的概念,其实就是随机数据。 目前来看有这么几个算法 PBKDF2、 BCrypt 和 SCrypt 可以满足。 Bcrypt 是基于 eksblowfish 算法设计的加密哈希函数,它最大的特点是:可以动态调整工作因子(迭代次数)来调整计算速度,因此就算以后计算机能力不断增加,它仍然可以抵抗暴力攻击。(spring security BCryptPasswordEncoder) 密码存储这种场景下,将密码哈希处理是最好的方式,第一它本身就是加密哈希函数,其次按照摩尔定律的定义,集成系统上每平方英寸的晶体管数量大约每 18 个月翻一番。在 2 年内,我们可以增加它的工作因子以适应任何变化。
https://learn.skyofit.com/archives/2487
MD5加密 改成 bCrypt加密(虽然BCrypt也是输入的字符串+盐,但是与MD5+盐的主要区别是:每次加的盐不同,导致每次生成的结果也不相同。)
public static String bCrypt(String password){
if(StringUtils.isBlank(password)){
return null;
}
return BCrypt.hashpw(password,BCrypt.gensalt());
}
(4)UserService:注册逻辑
//注册: //返回错误信息:账号不存在等 public Map<String,Object> register(User user){ Map<String,Object> map=new HashMap<>(); //1.参数判断 //空值处理(这里其实前端会做处理,这里基本走不到,但是一些无效行为可以到此,比如username为" ") if(user==null){ throw new IllegalArgumentException("参数不能为空!"); } if(StringUtils.isBlank(user.getUsername())){ map.put("usernameMsg","账号不能为空"); return map; } if(StringUtils.isBlank(user.getPassword())){ map.put("passwordMsg","密码不能为空"); return map; } if(StringUtils.isBlank(user.getEmail())){ map.put("emailMsg","邮箱不能为空"); return map; } //验证账号 User u=userMapper.selectByName(user.getUsername());//数据库里查到的用户 if(u!=null){ map.put("usernameMsg","该账号已存在!"); return map; } //验证邮箱 u=userMapper.selectByEmail(user.getEmail());//数据库里查到的用户 if(u!=null){ map.put("emailMsg","该邮箱已被注册!"); return map; } //2.注册用户 user.setPassword(CommunityUtil.bCrypt(user.getPassword()));//加密密码 user.setType(0);//默认普通用户 user.setStatus(0);//账号未激活 user.setActivationCode(CommunityUtil.generateUUID());//激活码 user.setHeaderUrl(String.format("http://images.nowcoder.com/head/%dt.png",new Random().nextInt(1000)));//随机头像 user.setCreateTime(new Date()); userMapper.insertUser(user); //3.激活邮件 Context context=new Context(); context.setVariable("email",user.getEmail()); //激活链接 String url=domain+contextPath+"/activation/"+user.getId()+"/"+user.getActivationCode(); context.setVariable("url",url); String content=templateEngine.process("/mail/activation",context); mailClient.sendMail(user.getEmail(),"激活账号",content); return map; }
(5)注册后与前端交互
@RequestMapping(path = "/register",method = RequestMethod.POST)//注意这里是POST,提交表单 public String register(Model model, User user){ Map<String,Object> map=userService.register(user);//拿到service返回的错误信息 if(map==null&&map.isEmpty()){//错误信息为空,发激活邮件 model.addAttribute("msg","注册成功,我们已经向您的邮箱发送了一封激活邮件,请尽快激活!"); model.addAttribute("target","/index");//跳转至首页 return "/site/operate-result";//model+此模板=返回view }else{//携带错误信息,直接返回到注册页面上显示 model.addAttribute("usernameMsg",map.get("usernameMsg")); model.addAttribute("usernameMsg",map.get("usernameMsg")); model.addAttribute("usernameMsg",map.get("usernameMsg")); return "/site/register"; } }
//前端部分需要注意:
页面跳转时,用户名和密码都要带着
页面显示错误信息(获取变量)
确认密码部分直接前端判断即可
问题:
激活邮件发送失败后,这时候再注册,用户会显示已注册
而且注册时,如果是在邮件发送时期出现问题,就会重复插入,下次再查询,数据库直接报错(select one失败)
所以在注册时,需要先看status是否是0
0则未激活,还可以进行注册,删除原记录,重新注册
1已激活,可以使用其他功能
//则删除原记录,重新注册
userMapper.deleteUser(user.getUsername());
}
(6)激活账号:重复激活、status改变为1激活成功、激活码伪造则失败
public int activation(int userId,String code){ User user=userMapper.selectById(userId); if(user.getStatus()==1){//重复激活 return ACTIVATION_REPEAT; }else if(user.getActivationCode().equals(code)){//激活码正确,激活成功 userMapper.updateStatus(userId,1); return ACTIVATION_SUCCESS; }else{//激活失败 return ACTIVATION_FAILURE; } }
分布式session共享方案: 1、粘性session:在nginx中提供一致性哈希策略,可以保持用户ip进行hash值计算固定分配到某台服务器上,负载也比较均衡,其问题是假如有一台服务器挂了,session也丢失了。 2、同步session:当某一台服务器存了session后,同步到其他服务器中,其问题是同步session到其他服务器会对服务器性能产生影响,服务器之间耦合性较强。 3、共享session:单独搞一台服务器用来存session,其他服务器都向这台服务器获取session,其问题是这台服务器挂了,session就全部丢失。 4、redis集中管理session(主流方法):redis为内存数据库,读写效率高,并可在集群环境下做高可用。
Spring Boot没有为Kaptcha提供自动配置,但是可以通过配置类和其他配置手动集成Kaptcha到Spring Boot应用中。Spring Framework本身并不为Kaptcha提供自动配置。
@Configuration public class KaptchaConfig {//验证码配置类 @Bean public Producer kaptchaProducer() { Properties properties = new Properties(); properties.setProperty("kaptcha.image.width", "100"); properties.setProperty("kaptcha.image.height", "40"); properties.setProperty("kaptcha.textproducer.font.size", "32"); properties.setProperty("kaptcha.textproducer.font.color", "0,0,0"); properties.setProperty("kaptcha.textproducer.char.string", "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYAZ"); properties.setProperty("kaptcha.textproducer.char.length", "4"); properties.setProperty("kaptcha.noise.impl", "com.google.code.kaptcha.impl.NoNoise"); DefaultKaptcha kaptcha = new DefaultKaptcha(); Config config = new Config(properties); kaptcha.setConfig(config); return kaptcha; } } <div class="col-sm-4"> <img th:src="@{/kaptcha}" id="kaptcha" style="width:100px;height:40px;" class="mr-2"/> <a href="javascript:refresh_kaptcha();" class="font-size-12 align-bottom">刷新验证码</a> </div> <script> function refresh_kaptcha() { var path = CONTEXT_PATH + "/kaptcha?p=" + Math.random(); $("#kaptcha").attr("src", path); } </script>
5.登录和退出
(1)登录凭证
@Data //登陆凭证 public class LoginTicket { private int id; private int userId; private String ticket; private int status; private Date expired; }
(2)LoginTicketMapper
@Mapper public interface LoginTicketMapper { @Insert({"insert into login-ticket(user_id,ticket,status,expired) " + "values(#{userId},#{ticket},#{status},#{expired})"}) @Options(useGeneratedKeys = true,keyProperty = "id") int insertLoginTicket(LoginTicket loginTicket); @Select({ "select id,user_id,ticket,status,expired ", "from login_ticket where ticket=#{ticket}"}) LoginTicket selectByTicket(String ticket); @Update({"<script>", "update login_ticket set status=#{status} where ticket=#{ticket}", "<if test=\"ticket!=null\">", "and 1=1", "</if>", "</script>"}) int updateStatus(String ticket, int status); }
(3)service 登录逻辑:验证是否有该用户,密码是否正确
//5.登录逻辑 public Map<String,Object> login(String username,String password,int expiredSeconds){ Map<String,Object> map=new HashMap<>(); //空值处理 if(StringUtils.isBlank(username)){ map.put("usernameMsg","账号不能为空"); return map; } if(StringUtils.isBlank(password)){ map.put("passwordMsg","密码不能为空"); return map; } //验证账号 User user=userMapper.selectByName(username);//数据库里查到的用户 if(user==null){ map.put("usernameMsg","该账号不存在!"); return map; } if(user.getStatus()==0){//未激活 map.put("usernameMsg","该账号未激活!"); return map; } //验证密码 boolean flag=CommunityUtil.checkPassword(password,user.getPassword()); if(!flag){ map.put("passwordMsg","密码不正确"); return map; } //生成登陆凭证 LoginTicket loginTicket=new LoginTicket(); loginTicket.setUserId(user.getId()); loginTicket.setTicket(CommunityUtil.generateUUID());//凭证 loginTicket.setStatus(0); loginTicket.setExpired(new Date(System.currentTimeMillis()+expiredSeconds*1000)); loginTicketMapper.insertLoginTicket(loginTicket); map.put("ticket",loginTicket.getTicket()); return map; }
(4)编写表现层(Controller)层的逻辑,成功时给客户端发送一个cookie,失败时跳转回登录页
@RequestMapping(path = "/login",method = RequestMethod.POST) public String login(String username,String password,String code,boolean rememberme, Model model,HttpSession session,HttpServletResponse response){ //检查验证码 String kaptcha=(String)session.getAttribute("kaptcha"); if(StringUtils.isBlank(kaptcha) ||StringUtils.isBlank(code) ||!kaptcha.equalsIgnoreCase(code) ){ model.addAttribute("codeMsg","验证码不正确"); return "/site/login"; } int expiredSeconds=rememberme?REMEMBER_EXPIRED_SECONDS:DEFAULT_EXPIRED_SECONDS;//凭证失效时间 Map<String,Object> map=userService.login(username,password,expiredSeconds);//拿到service返回的错误信息 if(map.containsKey("ticket")){//成功有登录凭证,放至cookie Cookie cookie=new Cookie("ticket",map.get("ticket").toString()); cookie.setPath(contextPath); cookie.setMaxAge(expiredSeconds); response.addCookie(cookie); return "redirect:/index"; }else{//携带错误信息,直接返回到登录页面上显示 model.addAttribute("usernameMsg",map.get("usernameMsg")); model.addAttribute("passwordMsg",map.get("passwordMsg")); return "/site/login"; } }
(5)修改对应html
<div class="main"> <div class="container pl-5 pr-5 pt-3 pb-3 mt-3 mb-3"> <h3 class="text-center text-info border-bottom pb-3">登 录</h3> <form class="mt-5" method="post" th:action="@{/login}"> <div class="form-group row"> <label for="username" class="col-sm-2 col-form-label text-right">账号:</label> <div class="col-sm-10"> <input type="text" th:class="|form-control ${usernameMsg!=null?'is-invalid':''}|" th:value="${param.username}" id="username" name="username" placeholder="请输入您的账号!" required> <div class="invalid-feedback" th:text="${usernameMsg}"> 该账号不存在! </div> </div> </div> <div class="form-group row mt-4"> <label for="password" class="col-sm-2 col-form-label text-right">密码:</label> <div class="col-sm-10"> <input type="password" th:class="|form-control ${passwordMsg!=null?'is-invalid':''}|" th:value="${param.password}" id="password" name="password" placeholder="请输入您的密码!" required> <div class="invalid-feedback" th:text="${passwordMsg}"> 密码长度不能小于8位! </div> </div> </div> <div class="form-group row mt-4"> <label for="verifycode" class="col-sm-2 col-form-label text-right">验证码:</label> <div class="col-sm-6"> <input type="text" th:class="|form-control ${codeMsg!=null?'is-invalid':''}|" id="verifycode" name="code" placeholder="请输入验证码!"> <div class="invalid-feedback" th:text="${codeMsg}"> 验证码不正确! </div> </div> <div class="col-sm-4"> <img th:src="@{/kaptcha}" id="kaptcha" style="width:100px;height:40px;" class="mr-2"/> <a href="javascript:refresh_kaptcha();" class="font-size-12 align-bottom">刷新验证码</a> </div> </div> <div class="form-group row mt-4"> <div class="col-sm-2"></div> <div class="col-sm-10"> <!--name="rememberme" 和后端对应--> <input type="checkbox" id="remember-me" name="rememberme" th:checked="${param.rememberme}"> <label class="form-check-label" for="remember-me">记住我</label> <a href="forget.html" class="text-danger float-right">忘记密码?</a> </div> </div> <div class="form-group row mt-4"> <div class="col-sm-2"></div> <div class="col-sm-10 text-center"> <button type="submit" class="btn btn-info text-white form-control">立即登录</button> </div> </div> </form> </div> </div>
(6)登录退出:拿到cookie中ticket信息,修改数据库中该ticket的状态status,重定向到登陆页面
@RequestMapping(path="/logout",method = RequestMethod.GET) public String logout(@CookieValue("ticket")String ticket){ userService.logout(ticket); return "redirect:/login"; } //登录退出 public void logout(String ticket){ loginTicketMapper.updateStatus(ticket,1);//修改该凭证的登陆状态 }
(7)login_ticket表中很多ticket超时时要清除
5.显示登录信息
1. 拦截器概念 动态拦截Actioon调用的对象,使开发者在一个Actioon执行的前后执行一段代码,也可以在Action执行前阻止其执行,同时也提供了一种可以提取Action中可重用部分代码的方式。 2. 作用 动态拦截Action调用的对象(也就是实际项目中的controller层的接口) 3. 适用场景 3.1. 用户登录校验 3.2. 资源拦截,阻止访问 4. 实现拦截器 实现拦截器需要实现HandlerInterceptor接口,HandlerInterceptor接口中有三个方法preHandle、postHandle、afterCompletion。 4.1. preHandle 在Action执行前调用 4.2. postHandle 在Action执行后调用,生成视图前调用 4.3. afterCompletion 在DispatcherServlet完全处理完请求之后被调用,可用于清理资源
(1)需要实现的功能:
在请求开始时查询登录用户,在本次请求中持有用户数据,在模板视图上显示用户数据,在请求结束时清理用户数据。登录后,显示用户的逻辑如下图所示
(2)以定义拦截器,实现HandlerInterceptor
页面拦截器处理的是请求,属于表现层的逻辑,在Controller包下新建包Interceptor,新建LoginTicketInterceptor类实现HandleInterceptor接口
@Component public class LoginTicketInterceptor implements HandlerInterceptor { @Autowired private UserService userService; @Autowired private HostHolder hostHolder; //在请求开始时查询登录用户 //在本次请求中持有用户数据 @Override //在Action执行前调用 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //从cookie中取出凭证,查看是否过期,未过期可以直接放行,过期需要重新登录 String ticket= CookieUtil.getValue(request,"ticket"); if(ticket!=null){ LoginTicket loginTicket=userService.findLoginTicket(ticket); //检查该凭证是否有效:状态是否是登录状态(0),是否超时 if(loginTicket!=null&&loginTicket.getStatus()==0&&loginTicket.getExpired().after(new Date())){//如果登出了,就再也进不去(问题!)//如果Cookie清了,就再也进不去(问题!)
//有效,则在本次请求中持有用户数据 User user=userService.findUserById(loginTicket.getUserId()); hostHolder.setUsers(user);//user被持有 } } return true; } //在模板视图上显示用户数据 @Override//在Action执行后调用,生成视图前调用 public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { User user=hostHolder.getUser(); if(user!=null&&modelAndView!=null){ modelAndView.addObject("loginUser",user); } } //在请求结束时清理用户数据 @Override//在DispatcherServlet完全处理完请求之后被调用,可用于清理资源 public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex){ hostHolder.clear(); } }
这里有两个问题:
如果登出了,就再也进不去
如果Cookie清了,就再也进不去
<li class="nav-item ml-3 btn-group-vertical" th:if="${loginUser!=null}"> <a class="nav-link position-relative" href="site/letter.html">消息<span class="badge badge-danger">12</span></a> </li> <li class="nav-item ml-3 btn-group-vertical" th:if="${loginUser==null}"> <a class="nav-link" th:href="@{/register}">注册</a> </li> <li class="nav-item ml-3 btn-group-vertical" th:if="${loginUser==null}"> <a class="nav-link" th:href="@{/login}">登录</a> </li>
(3)配置拦截器,指定拦截路径
写一个配置类,在之前写配置类主要是想在配置类中声明一个第三方Bean,将其装配到容器中,拦截器的逻辑和别的不太一样,要求在配置类中实现一个接口而不是简单装配一个Bean。通过以下代码reqistry实现对拦截器的注入。
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Autowired private AlphaInterceptor alphaInterceptor; @Autowired private LoginTicketInterceptor loginTicketInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(alphaInterceptor) .excludePathPatterns("/css/*","/js/*","/img/*") .addPathPatterns("/register","/login"); registry.addInterceptor(loginTicketInterceptor) .excludePathPatterns("/css/*","/js/*","/img/*"); } }
先在util包下建一个在工具类,封装从request中获取cookie的方法
public class CookieUtil { public static String getValue(HttpServletRequest request,String name){ if(request==null||name==null){ throw new IllegalArgumentException("参数为空!"); } Cookie[] cookies=request.getCookies(); if(cookies!=null){ for(Cookie cookie:cookies){ if(cookie.getName().equals(name)){//cookie中该用户的信息 return cookie.getValue(); } } } return null; } }
通过获取的cookie中的信息,查询LoginTicket,并从中获取用户信息,在这之前需要在服务层先编写通过cookie中的信息获取LoginTicket的方法
public LoginTicket findLoginTicket(String ticket){ return loginTicketMapper.selectByTicket(ticket); }
在存储用户信息时需要考虑多线程的情况,不然会在并发的时候产生冲突,必须考虑线程隔离。java中的ThreadLocal工具可以解决该问题。新建一个工具类HostHolder,该类的起到一个容器的作用
//持有用户信息,用于代替session对象 @Component public class HostHolder { private ThreadLocal<User> users = new ThreadLocal<>(); public void setUsers(User user) { users.set(user); } public User getUser(){ return users.get(); } public void clear(){ users.remove(); } }
6.账号设置:
首先请求必须是一个 POST 请求,其次表单的属性 enctype = “multipart/form-data”
然后就是利用 MultipartFile 处理上传文件。
然后就是访问账号设置页面,上传头像,获取头像。
(1)头像上传之后是存放到我们的服务器硬盘之上,所以需要在 application.properties配置一下资源上传之后的存放路径。
community.path.upload=f:/nowcoder/data/upload
上传完文件最终是需要更新用户的 HeaderUrl,所以 Service 就需要提供一个方法改变这个 URL,然后上传文件的事情就在 Controller 里面解决掉,业务层只解决更新路径的这个业务就可以了。
(2)在 UserService 里面追加一个方法更新用户的 URL:
public int updateHeader(int userId,String headerUrl){ return userMapper.updateHeader(userId,headerUrl); }
(3)上传头像:
MultipartFile是SpringMVC提供简化上传操作的工具类。
//上传头像 @RequestMapping(path="/upload",method = RequestMethod.POST) public String uploadHeader(MultipartFile headerImage, Model model){//MultipartFile是SpringMVC提供简化上传操作的工具类。 if(headerImage==null){ model.addAttribute("error","您还未选择图片!"); return "/site/setting"; } String fileName=headerImage.getOriginalFilename(); String suffix=fileName.substring(fileName.lastIndexOf(".")); if(StringUtils.isBlank(suffix)){ model.addAttribute("error","文件格式不正确"); return "/site/setting"; } //生成随机文件名 fileName = CommunityUtil.generateUUID() + suffix; //确定文件的存放路径 File dest = new File(uploadPath + "/" +fileName); try { //存储文件 headerImage.transferTo(dest); } catch (IOException e) { logger.error("上传文件失败" + e.getMessage()); throw new RuntimeException("上传文件失败,服务器发生异常!",e); } //更新当前用户的头像的网址 User user = hostHolder.getUser(); String headerUrl = domain + contextPath +"/user/header/" + fileName; userService.updateHeader(user.getId(),headerUrl); return "redirect:/index"; } //获取头像 @RequestMapping(path="/header/{fileName}",method = RequestMethod.GET) public void getHeader(@PathVariable("fileName") String fileName, HttpServletResponse response){ //服务器存放路径 fileName=uploadPath+"/"+fileName; //文件后缀 String suffix = fileName.substring(fileName.lastIndexOf(".")); response.setContentType("image/" + suffix); try ( FileInputStream fis = new FileInputStream(fileName); OutputStream os = response.getOutputStream(); ){ byte[] buffer = new byte[1024]; int b = 0; while((b = fis.read(buffer)) != -1){ os.write(buffer,0, b); } } catch (IOException e) { logger.error("读取头像失败" + e.getMessage()); } }
(4)修改密码:(有问题,待修正)
验证原始密码:给用户输入的旧密码加密,验证是否和数据库中密码一致
更新密码:给新密码加密,入库
// 修改密码 Controller @LoginRequired @RequestMapping(path = "/updatePassword", method = RequestMethod.POST) public String updatePassword(String oldPassword, String newPassword, Model model) { User user = hostHolder.getUser(); Map<String, Object> map = userService.updatePassword(user.getId(), oldPassword, newPassword); if (map == null || map.isEmpty()) { return "redirect:/logout"; } else { model.addAttribute("oldPasswordMsg", map.get("oldPasswordMsg")); model.addAttribute("newPasswordMsg", map.get("newPasswordMsg")); return "/site/setting"; } } // 修改密码 Service public Map<String, Object> updatePassword(int userId, String oldPassword, String newPassword) { Map<String, Object> map = new HashMap<>(); // 空值处理 if (StringUtils.isBlank(oldPassword)) { map.put("oldPasswordMsg", "原密码不能为空!"); return map; } if (StringUtils.isBlank(newPassword)) { map.put("newPasswordMsg", "新密码不能为空!"); return map; } // 验证原始密码 User user = userMapper.selectById(userId); oldPassword = CommunityUtil.bCrypt(oldPassword); if (!user.getPassword().equals(oldPassword)) { map.put("oldPasswordMsg", "原密码输入有误!"); return map; } // 更新密码 newPassword = CommunityUtil.bCrypt(newPassword); userMapper.updatePassword(userId, newPassword); return map; }
7.检查登录状态
需要解决的问题:防止直接在浏览器输入网址进入“登录信息设置”的网页。
解决方案:设置拦截器
,拦截所有请求,且在指定的方法上进行拦截。那么可以自定义注解,让拦截器只拦截带有这个自定义注解的方法。
使用拦截器
- 在方法前标注自定义注解
- 拦截所有请求,只处理带有该注解的方法
- 自定义注解
- 常用的元注解:
@Target、@Retention、@Document、@Inherited
- 常用的元注解:
- 如何读取注解:
Method.getDeclaredAnnotations ()
Method.getAnnotation (Class annotationClass)
(1)自定义注解,是否登录:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface LoginRequired {//打了这个标记,登录时才能访问 }
使用注解:算是一个权限控制,这两个请求必须登录
@LoginRequired @RequestMapping(path = "/setting", method = RequestMethod.GET) public String getSettingPage() { return "/site/setting"; } @LoginRequired @RequestMapping(path = "/upload", method = RequestMethod.POST) public String uploadHeader(MultipartFile headerImage, Model model) { if (headerImage == null) { model.addAttribute("error", "您还没有选择图片!"); return "/site/setting"; } }
(2)拦截器
看拦截目标的类型是不是方法HandlerMethod,如果是,转型成handlerMethod,获取method对象,
获取注解,注解不为空loginRequired != null
即带有这个注解(LoginRequired.class
),即需要登录,
若此时hostHolder.getUser()==null
没登录,就要重定向到登录页面(即项目路径+/login
),返回false,不能继续执行该方法。
@Component public class LoginRequiredInterceptor implements HandlerInterceptor { @Autowired private HostHolder hostHolder; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if(handler instanceof HandlerMethod){ HandlerMethod handlerMethod=(HandlerMethod) handler; Method method=handlerMethod.getMethod(); LoginRequired loginRequired=method.getAnnotation(LoginRequired.class); if(loginRequired!=null&&hostHolder.getUser()==null){ response.sendRedirect(request.getContextPath()+"/login");//return ”redirect“底层其实也是这个,可以通过配置文件注入路径参数,也可以从请求中直接取到路径 return false; } } return true; } }
拦截器配置和之前一样,要注入拦截器、排除静态路径。
WebMvcConfig
@Autowired private LoginRequiredInterceptor loginRequiredInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginRequiredInterceptor) .excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg"); }