1. 发送邮件
1.1 邮箱设置
- 打开邮箱POP3/SMTP服务
1.2 Spring Email
- 导入spring mail相关依赖jar包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
<version>2.1.5.RELEASE</version>
</dependency>
- 邮箱参数设置
# 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
- 使用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);
@Autowired
private 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());
}
}
}
- 创建测试类
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 {
@Autowired
private MailClient mailClient;
@Autowired
private TemplateEngine templateEngine;
@Test
public void testTextMail() {
mailClient.sendMail("lihonghe@nowcoder.com", "TEST", "Welcome.");
}
@Test
public 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 访问注册页面
- 在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";
}
}
- 调整register.html页面中的相对路径,首行加Thymeleaf声明,在之前有写过处理方法
- 复用头部
<!-- 头部 -->
<header class="bg-dark sticky-top" th:fragment="header">
<!-- 头部 -->
<header class="bg-dark sticky-top" th:replace="index::header">
- 鼠标悬停之后显示目标页面
- 点击注册之后进入注册页面
2.2 提交注册数据
- 导入新包
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.9</version>
</dependency>
- 配置域名
# community
community.path.domain=http://localhost:8080
- 在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 -> abc123def456abc
public static String md5(String key) {
if (StringUtils.isBlank(key)) {
return null;
}
return DigestUtils.md5DigestAsHex(key.getBytes());
}
}
2.2.1 service层开发
- 更新UserService,添加邮件客户端,模板引擎,域名,项目名
@Autowired
private MailClient mailClient;
@Autowired
private TemplateEngine templateEngine;
@Value("${community.path.domain}")
private String domain;
@Value("${server.servlet.context-path}")
private String contextPath;
- 在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/code
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;
}
- 开发激活邮件,activation.html
2.2.2 Controller层开发
- 开发loginController。注入UserService
@Autowired
private UserService userService;
- 开发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";
}
}
- 开发注册成功页面,operating.html
- 开发注册失败回到原页面,register.html
2.3 激活注册账号
- 在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层开发
- 让UserService实现常量类CommunityConstant。
- 在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层开发
- 在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";
}
- 在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)
@ResponseBody
public String setCookie(HttpServletResponse response) {
// 创建cookie
Cookie cookie = new Cookie("code", CommunityUtil.generateUUID());
// 设置cookie生效的范围
cookie.setPath("/community/alpha");
// 设置cookie的生存时间
cookie.setMaxAge(60 * 10);
// 发送cookie
response.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)
@ResponseBody
public 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)
@ResponseBody
public 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不一定传回创建他的第一个服务器,新的服务器得不到数据
- 黏性session。固定ip发给同个服务器。但是很难保证负载均衡
- 同步session。每个服务器创建seesion后同步给其他所有服务器。对服务器性能产生影响,服务器产生耦合,不独立。
- 共享session。单独一台服务器,专门处理session。但是是单体服务器,万一他挂了,都挂了。
- 尽量存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 {
@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;
}
}
4.2.2 在 LoginController 下添加 getKaptcha
- 返回方法是 void
- 敏感数据,存入session
- 图片直接输出给浏览器
private static final Logger logger = LoggerFactory.getLogger(LoginController.class);
@Autowired
private Producer kaptchaProducer;
@RequestMapping(path = "/kaptcha", method = RequestMethod.GET)
public void getKaptcha(HttpServletResponse response, HttpSession session) {
// 生成验证码
String text = kaptchaProducer.createText();
BufferedImage image = kaptchaProducer.createImage(text);
// 将验证码存入session
session.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 登录数据访问层实现
- 数据库中表,ticket是关键数据
- 在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;
}
@Override
public String toString() {
return "LoginTicket{" +
"id=" + id +
", userId=" + userId +
", ticket='" + ticket + '\'' +
", status=" + status +
", expired=" + expired +
'}';
}
}
- 在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);
}
- 测试类测试
@Autowired
private LoginTicketMapper loginTicketMapper;
@Test
public 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);
}
@Test
public 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 登录视图层实现
- path 一样没事,method不一样ok
- 从 seesion 中取出验证码对比,需要session
- 最终如果登陆成功 cookie 发送给客户端保存
- 先判断验证码对不对,不对直接拉倒
- 如果没有勾上“记住我”,存的时间短;勾上了记的时间长,在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
- preHandle:在Controller之前执行
- postHandle:在Controller之后前执行
- 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之前执行
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
logger.debug("preHandle: " + handler.toString());
return true;
}
// 在Controller之后执行
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
logger.debug("postHandle: " + handler.toString());
}
// 在TemplateEngine之后执行
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
logger.debug("afterCompletion: " + handler.toString());
}
}
6.1.2 新建配置类 WebMvcConfig
在 config 下新建实现 WebMvcConfig
- 需要实现接口WebMvcConfigurer
- 实现方法addInterceptors
- 通过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 {
@Autowired
private AlphaInterceptor alphaInterceptor;
@Autowired
private LoginTicketInterceptor loginTicketInterceptor;
@Autowired
private LoginRequiredInterceptor loginRequiredInterceptor;
@Override
public 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();//得到所有cookie
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals(name)) {
return cookie.getValue();
}
}
}
return null;
}
}
6.2.2 在interceptor下新建LoginRequiredInterceptor
- 实现HandlerInterceptor接口
- 根据上图逻辑,首先通过上面的工具类得到cookie,再得到ticket
- 在userservice里增加查询凭证的代码,传入的参数就是ticket
public LoginTicket findLoginTicket(String ticket) {
return loginTicketMapper.selectByTicket(ticket);
}
public int updateHeader(int userId, String headerUrl) {
return userMapper.updateHeader(userId, headerUrl);
}
- 根据凭证查询用户,在本次请求持有用户。需要工具类实现多线程隔离。在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();
}
}
- 如果ticket不等null,判断凭证是否有效:不为空,状态0,没超时。通过上面实现的工具类持有用户。为什么能持有?我们把当前线程数据存入map中,这个请求没有处理完线程就一直还在,请求处理完线程才被销毁。
@Autowired
private UserService userService;
@Autowired
private HostHolder hostHolder;
@Override
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);
// 检查凭证是否有效
if (loginTicket != null && loginTicket.getStatus() == 0 && loginTicket.getExpired().after(new Date())) {
// 根据凭证查询用户
User user = userService.findUserById(loginTicket.getUserId());
// 在本次请求中持有用户
hostHolder.setUser(user);
}
}
return true;
}
- 应该在模板引擎之前就调用,所以使用postHandle。先从hostHolder得到user,user不为空且modelandview不为空,就把user存如mav
@Override
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);
}
}
- 模板都执行完之后,调用clear方法清空hostHolser
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
hostHolder.clear();
}
6.2.3 配置拦截器
在WebMvcConfig中注册拦截器
@Autowired
private LoginTicketInterceptor loginTicketInterceptor;
@Override
public 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 访问账号设置页面
- 在controller下新建usercontroller
@Controller
@RequestMapping("/user")
public class UserController {
@RequestMapping(path = "/setting", method = RequestMethod.GET)
public String getSettingPage() {
return "/site/setting";
}
}
- 配置setting.html 模板
7.2 上传头像
# 配置上传资源存放路径
# community
community.path.upload=d:/work/data/upload
7.2.1 业务层
上传完文件后,更新用户头像url
- 在UserService中增加更新头像方法
public int updateHeader(int userId, String headerUrl) {
return userMapper.updateHeader(userId, headerUrl);
}
7.2.3 表现层
- 在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;
@Autowired
private UserService userService;
@Autowired
private HostHolder hostHolder;
- 增加上传头像方法
@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.png
User 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. 检查登录状态
没登陆之前,不能让用户通过输入路径访问不该访问的页面。
众多请求都有同样的逻辑,使用拦截器。
通过添加注解,自定义注解。
元注解:
- @Target:自定义的注解可以声明在哪些地方
- @Retention:保留时间或有效时间
- @Document:生成文档时是否加入文档
- @Inherited:子类继承时是否也有效
读取注解(通过反射):
- Method.getDeclearedAnnotations:获取方法上所有注解
- Method.getAnnotation(Class annotationClass):获取指定类型的注解
8.1 新建注解
- 新建包annotation,下面新建LoginRequired
@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) {
……
}
8.2 新建拦截器
- 判断拦截的是不是方法
- 如果是方法,得到方法的注解
- 如果有我们上面自定义的方法,说明这个方法登录了才能访问
- 如果当前没有登录,return 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 false;
}
}
return true;
}
}
- 把拦截器注入WebMvcConfig
@Autowired
private LoginRequiredInterceptor loginRequiredInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginRequiredInterceptor)
.excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
}