文章目录

  • 1. 发送邮件
    • 1.1 邮箱设置
    • 1.2 Spring Email
  • 2. 注册功能
    • 2.1 访问注册页面
    • 2.2 提交注册数据
      • 2.2.1 service层开发
      • 2.2.2 Controller层开发
    • 2.3 激活注册账号
      • 2.3.1 service层开发
      • 2.3.2 controller层开发
  • 3. 会话管理
    • 3.1 HTTP基本性质
    • 3.2 Cookie
      • 3.2.1 实现服务端发送cookies
      • 3.2.2 实现服务端接收cookies
      • 3.2.3 cookie的缺点
    • 3.3 Session
      • 3.3.1 实现服务端发送seesionid
      • 3.3.2 实现服务端接收seesionid
      • 3.3.3 分布式部署中使用session的问题
  • 4. 生成验证码
    • 4.1 导入jar包
    • 4.2 编写Kaptcha配置类
      • 4.2.1 在config下实现KaptchaConfig
      • 4.2.2 在 LoginController 下添加 getKaptcha
    • 4.3 配置前端数据
      • 4.3.1 图片地址传入
      • 4.3.2 刷新验证码功能实现
  • 5. 登录和退出功能
    • 5.1 登录数据访问层实现
    • 5.2 登录业务层实现
    • 5.3 登录视图层实现
    • 5.3 退出业务层实现
    • 5.4 退出视图层实现
  • 6. 显示登录信息
    • 6.1 拦截器示例
      • 6.1.1 新建 AlphaInterceptor
      • 6.1.2 新建配置类 WebMvcConfig
    • 6.2 在页面上显示登录用户状态
      • 6.2.1 在util下新建CookieUtil工具类
      • 6.2.2 在interceptor下新建LoginRequiredInterceptor
      • 6.2.3 配置拦截器
      • 6.2.4 改写index的header部分
  • 7. 设置账号头像
    • 7.1 访问账号设置页面
    • 7.2 上传头像
      • 7.2.1 业务层
      • 7.2.3 表现层
    • 7.3 获取头像
  • 8. 检查登录状态
    • 8.1 新建注解
    • 8.2 新建拦截器

1. 发送邮件

1.1 邮箱设置

  1. 打开邮箱POP3/SMTP服务

1.2 Spring Email

  1. 导入spring mail相关依赖jar包
     <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-mail</artifactId><version>2.1.5.RELEASE</version></dependency>
  1. 邮箱参数设置
# MailProperties
spring.mail.host=smtp.sina.com
spring.mail.port=465
spring.mail.username=pingsiyuan1997@sina.com
spring.mail.password=此处配置密码
spring.mail.protocol=smtps
spring.mail.properties.mail.smtp.ssl.enable=true
  1. 使用JavaMailSender发送邮件
    ① 新建一个工具类包util,在 util 下编写 MailClient
    ② 核心组件:JavaMailSender
    ③ 使用 MimeMessageHelper 构建 MimeMessage
package com.nowcoder.community.util;import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Component;import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;@Component
public class MailClient {private static final Logger logger = LoggerFactory.getLogger(MailClient.class);@Autowiredprivate JavaMailSender mailSender;//发件人@Value("${spring.mail.username}")private String from;public void sendMail(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);mailSender.send(helper.getMimeMessage());} catch (MessagingException e) {logger.error("发送邮件失败:" + e.getMessage());}}}
  1. 创建测试类
package com.nowcoder.community;import com.nowcoder.community.util.MailClient;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class MailTests {@Autowiredprivate MailClient mailClient;@Autowiredprivate TemplateEngine templateEngine;@Testpublic void testTextMail() {mailClient.sendMail("lihonghe@nowcoder.com", "TEST", "Welcome.");}@Testpublic void testHtmlMail() {Context context = new Context();context.setVariable("username", "sunday");String content = templateEngine.process("/mail/demo", context);System.out.println(content);mailClient.sendMail("lihonghe@nowcoder.com", "HTML", content);}}


2. 注册功能

2.1 访问注册页面

  1. 在cintroller下实现 LoginController
package com.nowcoder.community.controller;import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;@Controller
public class LoginController {@RequestMapping(path = "/register", method = RequestMethod.GET)public String getRegisterPage() {return "/sit/register";}}
  1. 调整register.html页面中的相对路径,首行加Thymeleaf声明,在之前有写过处理方法
  2. 复用头部
     <!-- 头部 --><header class="bg-dark sticky-top" th:fragment="header">
     <!-- 头部 --><header class="bg-dark sticky-top" th:replace="index::header">
  1. 鼠标悬停之后显示目标页面

  2. 点击注册之后进入注册页面

2.2 提交注册数据

  1. 导入新包
     <dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId><version>3.9</version></dependency>
  1. 配置域名
# community
community.path.domain=http://localhost:8080
  1. 在util包下新建工具类
package com.nowcoder.community.util;import org.apache.commons.lang3.StringUtils;
import org.springframework.util.DigestUtils;import java.util.UUID;public class CommunityUtil {// 生成随机字符串public static String generateUUID() {return UUID.randomUUID().toString().replaceAll("-", "");}// MD5加密// hello -> abc123def456// hello + 3e4a8 -> abc123def456abcpublic static String md5(String key) {if (StringUtils.isBlank(key)) {return null;}return DigestUtils.md5DigestAsHex(key.getBytes());}}

2.2.1 service层开发

  1. 更新UserService,添加邮件客户端,模板引擎,域名,项目名
    @Autowiredprivate MailClient mailClient;@Autowiredprivate TemplateEngine templateEngine;@Value("${community.path.domain}")private String domain;@Value("${server.servlet.context-path}")private String contextPath;
  1. 在UserService中编写注册业务。输入一个Username,返回一个map。
public Map<String, Object> register(User user) {Map<String, Object> map = new HashMap<>();// 空值处理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;}// 注册用户user.setSalt(CommunityUtil.generateUUID().substring(0, 5));user.setPassword(CommunityUtil.md5(user.getPassword() + user.getSalt()));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);// 激活邮件Context context = new Context();context.setVariable("email", user.getEmail());// http://localhost:8080/community/activation/101/codeString 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;}
  1. 开发激活邮件,activation.html

2.2.2 Controller层开发

  1. 开发loginController。注入UserService
    @Autowiredprivate UserService userService;
  1. 开发loginController。编写注册逻辑
 @RequestMapping(path = "/register", method = RequestMethod.POST)public String register(Model model, User user) {Map<String, Object> map = userService.register(user);if (map == null || map.isEmpty()) {model.addAttribute("msg", "注册成功,我们已经向您的邮箱发送了一封激活邮件,请尽快激活!");model.addAttribute("target", "/index");return "/site/operate-result";} else {model.addAttribute("usernameMsg", map.get("usernameMsg"));model.addAttribute("passwordMsg", map.get("passwordMsg"));model.addAttribute("emailMsg", map.get("emailMsg"));return "/site/register";}}
  1. 开发注册成功页面,operating.html
  2. 开发注册失败回到原页面,register.html

2.3 激活注册账号

  1. 在util下实现CommunityConstant接口定义常量
package com.nowcoder.community.util;public interface CommunityConstant {/*** 激活成功*/int ACTIVATION_SUCCESS = 0;/*** 重复激活*/int ACTIVATION_REPEAT = 1;/*** 激活失败*/int ACTIVATION_FAILURE = 2;/*** 默认状态的登录凭证的超时时间*/int DEFAULT_EXPIRED_SECONDS = 3600 * 12;/*** 记住状态的登录凭证超时时间*/int REMEMBER_EXPIRED_SECONDS = 3600 * 24 * 100;}

2.3.1 service层开发

  1. 让UserService实现常量类CommunityConstant。
  2. 在UserService编写激活方法
 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;}}

2.3.2 controller层开发

  1. 在loginController中增加激活逻辑
    // http://localhost:8080/community/activation/101/code@RequestMapping(path = "/activation/{userId}/{code}", method = RequestMethod.GET)public String activation(Model model, @PathVariable("userId") int userId, @PathVariable("code") String code) {int result = userService.activation(userId, code);if (result == ACTIVATION_SUCCESS) {model.addAttribute("msg", "激活成功,您的账号已经可以正常使用了!");model.addAttribute("target", "/login");} else if (result == ACTIVATION_REPEAT) {model.addAttribute("msg", "无效操作,该账号已经激活过了!");model.addAttribute("target", "/index");} else {model.addAttribute("msg", "激活失败,您提供的激活码不正确!");model.addAttribute("target", "/index");}return "/site/operate-result";}
  1. 在LoginController中增加登录页面。注意配置其他前端信息。(登录功能尚未实现)
    @RequestMapping(path = "/login", method = RequestMethod.GET)public String getLoginPage() {return "/site/login";}

3. 会话管理

3.1 HTTP基本性质

3.2 Cookie

HTTP Cookie(也叫 Web Cookie 或浏览器 Cookie)是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。通常,它用于告知服务端两个请求是否来自同一浏览器,如保持用户的登录状态。Cookie 使基于无状态的HTTP协议记录稳定的状态信息成为了可能。

3.2.1 实现服务端发送cookies

在 AlphaConctroller 中增加 setCookie 方法
cookies 不设置生存时间关浏览器就没了

    // cookie示例@RequestMapping(path = "/cookie/set", method = RequestMethod.GET)@ResponseBodypublic String setCookie(HttpServletResponse response) {// 创建cookieCookie cookie = new Cookie("code", CommunityUtil.generateUUID());// 设置cookie生效的范围cookie.setPath("/community/alpha");// 设置cookie的生存时间cookie.setMaxAge(60 * 10);// 发送cookieresponse.addCookie(cookie);return "set cookie";}

发送请求前:

回车之后:

点进set请求,显示code,过期时间,path等

访问其他地址,如index,此时没有cookies内容,因为我们cookies的有效路径是alpha,index不匹配,无效:

3.2.2 实现服务端接收cookies

注解 @CookieValue ,获取指定名称的cookie的值

    @RequestMapping(path = "/cookie/get", method = RequestMethod.GET)@ResponseBodypublic String getCookie(@CookieValue("code") String code) {System.out.println(code);return "get cookie";}

set发送的cookie为:f1733d4c94044c87b2cce513187a6b84

get得到的cookie为:
f1733d4c94044c87b2cce513187a6b84

结果相同

3.2.3 cookie的缺点

  • 优点:
    ① 弥补http无状态的缺点,让业务得以延续
    ② 使用简单
  • 缺点:
    ① cookie存在客户端(浏览器),不安全,不能存密码等隐私数据
    ② cookie会主动发送给服务器,对流量和性能产生影响

3.3 Session

Session本质上依赖Cookie。在服务端记录客户端信息。安全,但服务器内存压力大。

3.3.1 实现服务端发送seesionid

 // session示例@RequestMapping(path = "/session/set", method = RequestMethod.GET)@ResponseBodypublic String setSession(HttpSession session) {session.setAttribute("id", 1);session.setAttribute("name", "Test");return "set session";}

JSESSIONID=324283446903F68D40D20E42DF6A7402

3.3.2 实现服务端接收seesionid

JSESSIONID=324283446903F68D40D20E42DF6A7402

3.3.3 分布式部署中使用session的问题

niginx进行负载均衡,sessionid不一定传回创建他的第一个服务器,新的服务器得不到数据

  1. 黏性session。固定ip发给同个服务器。但是很难保证负载均衡
  2. 同步session。每个服务器创建seesion后同步给其他所有服务器。对服务器性能产生影响,服务器产生耦合,不独立。
  3. 共享session。单独一台服务器,专门处理session。但是是单体服务器,万一他挂了,都挂了。

  1. 尽量存cookie,敏感数据存入数据库。但是问题是传统关系型数据库是数据存入硬盘中,不是在内存,并发量大时产生瓶颈。

当前主流解决办法就是存入nosql数据库Redis。

4. 生成验证码


使用vpn才能打开

4.1 导入jar包

访问Maven找到这个包,加入pom.xml

     <dependency><groupId>com.github.penggle</groupId><artifactId>kaptcha</artifactId><version>2.3.2</version></dependency>

4.2 编写Kaptcha配置类

4.2.1 在config下实现KaptchaConfig

package com.nowcoder.community.config;import com.google.code.kaptcha.Producer;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import java.util.Properties;@Configuration
public class KaptchaConfig {@Beanpublic 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;}}

4.2.2 在 LoginController 下添加 getKaptcha

  1. 返回方法是 void
  2. 敏感数据,存入session
  3. 图片直接输出给浏览器
 private static final Logger logger = LoggerFactory.getLogger(LoginController.class);@Autowiredprivate Producer kaptchaProducer;@RequestMapping(path = "/kaptcha", method = RequestMethod.GET)public void getKaptcha(HttpServletResponse response, HttpSession session) {// 生成验证码String text = kaptchaProducer.createText();BufferedImage image = kaptchaProducer.createImage(text);// 将验证码存入sessionsession.setAttribute("kaptcha", text);// 将突图片输出给浏览器response.setContentType("image/png");try {OutputStream os = response.getOutputStream();ImageIO.write(image, "png", os);} catch (IOException e) {logger.error("响应验证码失败:" + e.getMessage());}}


4.3 配置前端数据

4.3.1 图片地址传入

原始代码:

<img th:src="@{/img/kaptcha.png}" style="width:100px;height:40px;" class="mr-2"/>

修改为:

<img th:src="@{/kaptcha}" id="kaptcha" style="width:100px;height:40px;" class="mr-2"/>

每次刷新效果:

4.3.2 刷新验证码功能实现

原始代码:

<a href="javascript:;" class="font-size-12 align-bottom">刷新验证码</a>

修改为:

<a href="javascript:refresh_kaptcha();" class="font-size-12 align-bottom">刷新验证码</a><script>function refresh_kaptcha() {var path = CONTEXT_PATH + "/kaptcha?p=" + Math.random();$("#kaptcha").attr("src", path);}
</script>

在 global.js 下新加:

var CONTEXT_PATH = "/community";

5. 登录和退出功能

5.1 登录数据访问层实现

  1. 数据库中表,ticket是关键数据
  2. 在entity下实现LoginTicket实体类
package com.nowcoder.community.entity;import java.util.Date;public class LoginTicket {private int id;private int userId;private String ticket;private int status;private Date expired;public int getId() {return id;}public void setId(int id) {this.id = id;}public int getUserId() {return userId;}public void setUserId(int userId) {this.userId = userId;}public String getTicket() {return ticket;}public void setTicket(String ticket) {this.ticket = ticket;}public int getStatus() {return status;}public void setStatus(int status) {this.status = status;}public Date getExpired() {return expired;}public void setExpired(Date expired) {this.expired = expired;}@Overridepublic String toString() {return "LoginTicket{" +"id=" + id +", userId=" + userId +", ticket='" + ticket + '\'' +", status=" + status +", expired=" + expired +'}';}
}
  1. 在dao包下新建 LoginTicketMapper 接口。不写配置类,使用注解的方式写sql
package com.nowcoder.community.dao;import com.nowcoder.community.entity.LoginTicket;
import org.apache.ibatis.annotations.*;@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);}
  1. 测试类测试
    @Autowiredprivate LoginTicketMapper loginTicketMapper;@Testpublic void testInsertLoginTicket() {LoginTicket loginTicket = new LoginTicket();loginTicket.setUserId(101);loginTicket.setTicket("abc");loginTicket.setStatus(0);loginTicket.setExpired(new Date(System.currentTimeMillis() + 1000 * 60 * 10));loginTicketMapper.insertLoginTicket(loginTicket);}@Testpublic void testSelectLoginTicket() {LoginTicket loginTicket = loginTicketMapper.selectByTicket("abc");System.out.println(loginTicket);loginTicketMapper.updateStatus("abc", 1);loginTicket = loginTicketMapper.selectByTicket("abc");System.out.println(loginTicket);}

5.2 登录业务层实现

在 UserService 下增加

    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;}// 验证密码password = CommunityUtil.md5(password + user.getSalt());if (!user.getPassword().equals(password)) {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;}

5.3 登录视图层实现

  1. path 一样没事,method不一样ok
  2. 从 seesion 中取出验证码对比,需要session
  3. 最终如果登陆成功 cookie 发送给客户端保存
  4. 先判断验证码对不对,不对直接拉倒
  5. 如果没有勾上“记住我”,存的时间短;勾上了记的时间长,在Constant常量类添加
    @Value("${server.servlet.context-path}")private String contextPath;@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);if (map.containsKey("ticket")) {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";}}

配置 login.html 页面表单参数

5.3 退出业务层实现

在 UserService 下添加

    public void logout(String ticket) {loginTicketMapper.updateStatus(ticket, 1);}

5.4 退出视图层实现

在 LoginController 下添加

    @RequestMapping(path = "/logout", method = RequestMethod.GET)public String logout(@CookieValue("ticket") String ticket) {userService.logout(ticket);return "redirect:/login";}

配置index退出选项按钮链接

<a class="dropdown-item text-center" href="login.html">退出登录</a>

6. 显示登录信息

6.1 拦截器示例

拦截器可以说相当于是个过滤器:就是把不想要的或不想显示的内容给过滤掉。拦截器可以抽象出一部分代码可以用来完善原来的方法。同时可以减轻代码冗余,提高重用率,降低耦合度。

6.1.1 新建 AlphaInterceptor

在 controller 下新建interceptor 下新建 AlphaInterceptor

  1. preHandle:在Controller之前执行
  2. postHandle:在Controller之后前执行
  3. afterCompletion:在TemplateEngine之后执行
package com.nowcoder.community.controller.interceptor;import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;@Component
public class AlphaInterceptor implements HandlerInterceptor {private static final Logger logger = LoggerFactory.getLogger(AlphaInterceptor.class);// 在Controller之前执行@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {logger.debug("preHandle: " + handler.toString());return true;}// 在Controller之后执行@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {logger.debug("postHandle: " + handler.toString());}// 在TemplateEngine之后执行@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {logger.debug("afterCompletion: " + handler.toString());}
}

6.1.2 新建配置类 WebMvcConfig

在 config 下新建实现 WebMvcConfig

  1. 需要实现接口WebMvcConfigurer
  2. 实现方法addInterceptors
  3. 通过registry添加拦截器
package com.nowcoder.community.config;import com.nowcoder.community.controller.interceptor.AlphaInterceptor;
import com.nowcoder.community.controller.interceptor.LoginRequiredInterceptor;
import com.nowcoder.community.controller.interceptor.LoginTicketInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration
public class WebMvcConfig implements WebMvcConfigurer {@Autowiredprivate AlphaInterceptor alphaInterceptor;@Autowiredprivate LoginTicketInterceptor loginTicketInterceptor;@Autowiredprivate LoginRequiredInterceptor loginRequiredInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(alphaInterceptor).excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg").addPathPatterns("/register", "/login");registry.addInterceptor(loginTicketInterceptor).excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");registry.addInterceptor(loginRequiredInterceptor).excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");}}

当访问指定页面后,在控制台打印:

6.2 在页面上显示登录用户状态

下面这套逻辑,每次请求都要实现,所以应该使用拦截器

6.2.1 在util下新建CookieUtil工具类

package com.nowcoder.community.util;import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;public class CookieUtil {public static String getValue(HttpServletRequest request, String name) {if (request == null || name == null) {throw new IllegalArgumentException("参数为空!");}Cookie[] cookies = request.getCookies();//得到所有cookieif (cookies != null) {for (Cookie cookie : cookies) {if (cookie.getName().equals(name)) {return cookie.getValue();}}}return null;}}

6.2.2 在interceptor下新建LoginRequiredInterceptor

  1. 实现HandlerInterceptor接口
  2. 根据上图逻辑,首先通过上面的工具类得到cookie,再得到ticket
  3. 在userservice里增加查询凭证的代码,传入的参数就是ticket
    public LoginTicket findLoginTicket(String ticket) {return loginTicketMapper.selectByTicket(ticket);}public int updateHeader(int userId, String headerUrl) {return userMapper.updateHeader(userId, headerUrl);}
  1. 根据凭证查询用户,在本次请求持有用户。需要工具类实现多线程隔离。在util包下实现HostHolder,持有用户信息代替session对象。需要使用ThreadLocal,通过set存值,通过get取值。首先获取当前线程,根据当前线程获取map对象,把这个值存进map,根据map存值,每个线程map对象不一样。逻辑就是以线程存取值。源码如下
   /*** Sets the current thread's copy of this thread-local variable* to the specified value.  Most subclasses will have no need to* override this method, relying solely on the {@link #initialValue}* method to set the values of thread-locals.** @param value the value to be stored in the current thread's copy of*        this thread-local.*/public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null) {map.set(this, value);} else {createMap(t, value);}}/*** Returns the value in the current thread's copy of this* thread-local variable.  If the variable has no value for the* current thread, it is first initialized to the value returned* by an invocation of the {@link #initialValue} method.** @return the current thread's value of this thread-local*/public T get() {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}return setInitialValue();}
package com.nowcoder.community.util;import com.nowcoder.community.entity.User;
import org.springframework.stereotype.Component;/*** 持有用户信息,用于代替session对象.*/
@Component
public class HostHolder {private ThreadLocal<User> users = new ThreadLocal<>();public void setUser(User user) {users.set(user);}public User getUser() {return users.get();}public void clear() {users.remove();}}
  1. 如果ticket不等null,判断凭证是否有效:不为空,状态0,没超时。通过上面实现的工具类持有用户。为什么能持有?我们把当前线程数据存入map中,这个请求没有处理完线程就一直还在,请求处理完线程才被销毁。
 @Autowiredprivate UserService userService;@Autowiredprivate HostHolder hostHolder;@Overridepublic 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);// 检查凭证是否有效if (loginTicket != null && loginTicket.getStatus() == 0 && loginTicket.getExpired().after(new Date())) {// 根据凭证查询用户User user = userService.findUserById(loginTicket.getUserId());// 在本次请求中持有用户hostHolder.setUser(user);}}return true;}
  1. 应该在模板引擎之前就调用,所以使用postHandle。先从hostHolder得到user,user不为空且modelandview不为空,就把user存如mav
    @Overridepublic 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);}}
  1. 模板都执行完之后,调用clear方法清空hostHolser
    @Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {hostHolder.clear();}

6.2.3 配置拦截器

在WebMvcConfig中注册拦截器

    @Autowiredprivate LoginTicketInterceptor loginTicketInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(loginTicketInterceptor).excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");}

6.2.4 改写index的header部分

没登陆,不显示,登陆了才显示

<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>
<li class="nav-item ml-3 btn-group-vertical dropdown" th:if="${loginUser!=null}"><a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><img th:src="${loginUser.headerUrl}" class="rounded-circle" style="width:30px;"/></a><div class="dropdown-menu" aria-labelledby="navbarDropdown"><a class="dropdown-item text-center" href="site/profile.html">个人主页</a><a class="dropdown-item text-center" th:href="@{/user/setting}">账号设置</a><a class="dropdown-item text-center" th:href="@{/logout}">退出登录</a><div class="dropdown-divider"></div><span class="dropdown-item text-center text-secondary" th:utext="${loginUser.username}">nowcoder</span></div>
</li>

7. 设置账号头像

7.1 访问账号设置页面

  1. 在controller下新建usercontroller
@Controller
@RequestMapping("/user")
public class UserController {@RequestMapping(path = "/setting", method = RequestMethod.GET)public String getSettingPage() {return "/site/setting";}
}
  1. 配置setting.html 模板

7.2 上传头像

# 配置上传资源存放路径
# community
community.path.upload=d:/work/data/upload

7.2.1 业务层

上传完文件后,更新用户头像url

  1. 在UserService中增加更新头像方法
    public int updateHeader(int userId, String headerUrl) {return userMapper.updateHeader(userId, headerUrl);}

7.2.3 表现层

  1. 在usercontroller下增加日志及属性
    private static final Logger logger = LoggerFactory.getLogger(UserController.class);@Value("${community.path.upload}")private String uploadPath;@Value("${community.path.domain}")private String domain;@Value("${server.servlet.context-path}")private String contextPath;@Autowiredprivate UserService userService;@Autowiredprivate HostHolder hostHolder;
  1. 增加上传头像方法
 @RequestMapping(path = "/upload", method = RequestMethod.POST)public String uploadHeader(MultipartFile headerImage, Model model) {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);}// 更新当前用户的头像的路径(web访问路径)// http://localhost:8080/community/user/header/xxx.pngUser user = hostHolder.getUser();String headerUrl = domain + contextPath + "/user/header/" + fileName;userService.updateHeader(user.getId(), headerUrl);return "redirect:/index";}

7.3 获取头像

 @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());}}

最后处理页面表单 setting.html

8. 检查登录状态


没登陆之前,不能让用户通过输入路径访问不该访问的页面。
众多请求都有同样的逻辑,使用拦截器。
通过添加注解,自定义注解。

元注解:

  1. @Target:自定义的注解可以声明在哪些地方
  2. @Retention:保留时间或有效时间
  3. @Document:生成文档时是否加入文档
  4. @Inherited:子类继承时是否也有效

读取注解(通过反射):

  1. Method.getDeclearedAnnotations:获取方法上所有注解
  2. Method.getAnnotation(Class annotationClass):获取指定类型的注解

8.1 新建注解

  1. 新建包annotation,下面新建LoginRequired
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginRequired {}
  1. 在需要的方法上打上标记
     @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) {……}

8.2 新建拦截器

  1. 判断拦截的是不是方法
  2. 如果是方法,得到方法的注解
  3. 如果有我们上面自定义的方法,说明这个方法登录了才能访问
  4. 如果当前没有登录,return false,拒绝请求,返回登陆页面
@Component
public class LoginRequiredInterceptor implements HandlerInterceptor {@Autowiredprivate HostHolder hostHolder;@Overridepublic 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 false;}}return true;}
}
  1. 把拦截器注入WebMvcConfig
    @Autowiredprivate LoginRequiredInterceptor loginRequiredInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(loginRequiredInterceptor).excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");}

牛客网项目——项目开发(三):开发登录模块相关推荐

  1. 仿牛客网社区项目 全栈总结

    学习仿牛客网社区项目 代码&资源 各章节总结 第一章 第二章 第三章 第四章 第五章 第六章 第七章 第八章 争取让每个知识点都有链接可点 项目总结 网站架构图 常见面试题 MySQL Red ...

  2. Java牛客网社区项目——知识点面试题

    Java牛客网社区项目--知识点&面试题 持续更新中(ง •̀_•́)ง 文章目录 Java牛客网社区项目--知识点&面试题 请简要介绍一下你的项目? 什么是Spring框架? 对Sp ...

  3. 2019牛客网高级项目

    本项目是一个基于SpringBoot的社区平台,实现了牛客网讨论区的功能.实现了邮箱注册.验证码登录.发帖.评论.私信.点赞.关注.统计网站访问次数等功能,数据库使用Mybatis.Redis,使用K ...

  4. 牛客网中级项目学习笔记(一)

    牛客中级项目学习: Controller 解析web请求 Service 业务层 DAO(data access object)数据处理层 database 底层数据库 重定向 代码如下: @Requ ...

  5. 2022-1-13牛客网C++项目—— 第二章 Linux 多进程开发(一)

    复习用的问题 进程和程序之间的关系是什么? 进程包含了哪些信息? 一.程序当中包含了一系列的信息,这些信息用于描述如何创建一个进程. 1)二进制格式标识:描述文件的格式,内核根据这个信息来解释文件中的 ...

  6. 牛客网实战项目详细到每一步(更新中)

    一技术架构 Spring Boot Spring Spring MVC MyBatics Redis Kafka Elasticsearch重点的提高性能的技术 Spring Security, Sp ...

  7. [牛客网中级项目]第四章用户注册登陆管理

    目录 1. 预习 1.1 拦截器: 1.2 MD5加密算法: 2. 内容: 3. 注册: 3.1 注册要实现的功能: 3.2 代码实现: 3.2.1 建立LoginCotroller.class 3. ...

  8. 牛客网社区项目——p3.4事务管理

    数据库保障事务的机制如下 spring事务管理 在业务层模拟某个业务,注册用户和自动发送新人报到帖这两个业务视作一个事务 代码如下(两种方法): @Transactional(isolation = ...

  9. 2021-12-11牛客网C++项目——Linux编程介绍入门(二)

    1.15 目录操作函数 mkdir 函数 /*#include <sys/stat.h>#include <sys/types.h>int mkdir(const char * ...

  10. 仿牛客网项目第五,六章:异步消息系统和分布式搜索引擎(详细步骤和思路)

    目录 1. Kafka:构建TB级异步消息系统 1.0 同步/异步消息的区别 1.1 项目的目的 1. 2 阻塞队列实现异步消息系统 1.4 Kafka入门 1.5 Spring整合Kafka 1.6 ...

最新文章

  1. qt 二维数组初始化_第十九章、C语言学习之数组3
  2. 多少行数_经验丰富的程序员和其每日代码行数
  3. SQL Server游标
  4. 和dump文件什么区别_将java进程转移到“解剖台”之前,法医都干了什么?
  5. 强烈推荐!入门大数据分析必看的知识点总结,适合零基础学习
  6. c语言内循环外循环怎么使用,开高速, 用内循环还是外循环? 教你正确使用内外循环!...
  7. 使用es6制作简单数独游戏
  8. 浏览器地址栏和标题栏显示的小图标
  9. python应用学习(五)——requests爬取网页图片
  10. Linux下使用nohup部署java 后台程序
  11. 清零软件解决连供打印机喷嘴断墨和堵塞
  12. 如何提高项目管理效率
  13. 调焦后焦实现不同距离成像_照片要清晰、对焦必须深入理解!对焦模式、对焦区域模式等对焦知识...
  14. java怎么读取docx文件_java – 如何显示或读取docx文件
  15. java long类型溢出误区
  16. 如何在Debian系统下搭建SVN
  17. 【绝对详细!不好使你顺着网线敲我!】Django3.1在Ubuntu16.04上的部署
  18. jmeter中控制器的使用
  19. C++基础(1)- 声明(前向声明 Forward Declaration)与定义
  20. 总是封群怎么解决_我的群被封了怎么办

热门文章

  1. 你还在为找素材发愁吗?自媒体高手都知道的免费自媒体素材网
  2. 小说里的编程 【连载之二十四】元宇宙里月亮弯弯
  3. NMPA已注册肿瘤小Panel试剂盒生物信息学分析内容对比
  4. 查看Win10是否永久激活
  5. Python 操作谷歌浏览器
  6. 项目经理应对需求变更的策略
  7. 状语从句不是简单句_简单句、并列句、复合句
  8. Andriod --- JetPack :LiveData setValue 和 postValue 的区别
  9. PS软件Photoshop设置使用鼠标进行放大缩小设置
  10. PC微信逆向之发送消息