第2章Spring Boot实践,开发社区登录模块【仿牛客网社区论坛项目】
- 前言
- 推荐
- 项目总结
- 第2章Spring Boot实践,开发社区登录模块
- 1.发送邮件
- 配置
- MailClient
- 测试
- 2.开发注册功能
- 访问注册页面
- 提交注册数据
- 激活注册账号
- 3.会话管理
- 体验cookie
- 体验session
- 4.生成验证码
- 配置
- KaptchaConfig
- 前端
- 5.开发登录、退出功能
- LoginTicket
- LoginTicketMapper
- 测试Dao
- UserService
- LoginController
- 前端
- 退出功能
- 忘记密码
- 6.显示登录信息
- 体验拦截器
- 配置拦截器
- 测试
- 登录拦截器
- 前端
- 7.账号设置
- UserController
- 前端
- 配置
- UserService
- UserController
- 前端
- 测试
- 修改密码
- 8.检查登录状态
- LoginRequired
- LoginRequiredInterceptor
- WebMvcConfig
- 最后
前言
2023-4-30 20:42:51
以下内容源自【Java面试项目】
仅供学习交流使用
推荐
仿牛客网项目【面试】
项目总结
第2章Spring Boot实践,开发社区登录模块
1.发送邮件
配置
pom.xml
<!-- spring 邮箱 -->
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-mail -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
<version>2.7.4</version>
</dependency>
配置
# MailProperties
spring.mail.host=smtp.qq.com
spring.mail.port=465
spring.mail.username=邮箱地址
spring.mail.password=密钥
spring.mail.protocol=smtps
spring.mail.properties.mail.smtp.ssl.enable=true
MailClient
新增:/util/MailClient
package com.jsss.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(required = true)
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());
}
}
}
新增:/templates/mail/demo.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>邮件实例</title>
</head>
<body>
<p>欢迎你,<span style="color: red" th:text="${username}"></span>!</p>
</body>
</html>
测试
新增:test:MailTests
package com.jsss.community;
import com.jsss.community.util.MailClient;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class MailTests {
@Autowired
private MailClient mailClient;
@Autowired
private TemplateEngine templateEngine;
String to="3063494684@qq.com";
@Test
public void testTextMail(){
mailClient.sendMail(to,"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(to,"HTML",content);
}
}
测试结果:
2.开发注册功能
访问注册页面
新增:controller/LoginController
package com.jsss.community.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
@Controller
public class LoginController {
@GetMapping (path = "/register")
public String getRegisterPage(){
return "site/register";
}
}
修改:index.html
修改头部尾部,用来复用
修改:register.html
修改html标签
修改静态路径
复用index头部
测试:点击顶部区域内的链接,打开注册页面。
提交注册数据
配置:
pom.xml
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
配置:
# community
community.path.domain=http://localhost:8080
community.path.upload=d:/work/data/upload
新增:/util/CommunityUtil
package com.jsss.community.util;
import org.springframework.util.DigestUtils;
import java.util.UUID;
import org.apache.commons.lang3.StringUtils;
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());
}
}
修改:/templates/mail/activation.html
<!doctype html>
<html lang="en" xmlns:th="http://www.thymleaf.com">
<head>
<meta charset="utf-8">
<link rel="icon" href="https://static.nowcoder.com/images/logo_87_87.png"/>
<title>牛客网-激活账号</title>
</head>
<body>
<div>
<p>
<b th:text="${email}">xxx@xxx.com</b>, 您好!
</p>
<p>
您正在注册牛客网, 这是一封激活邮件, 请点击
<a th:href="${url}}">http://www.nowcoder.com/activation/abcdefg123456.html</a>,
激活您的牛客账号!
</p>
</div>
</body>
</html>
新增:UserService.register()
package com.jsss.community.service;
import com.jsss.community.dao.UserMapper;
import com.jsss.community.entity.User;
import com.jsss.community.util.CommunityUtil;
import com.jsss.community.util.MailClient;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
@Service
public class UserService{
@Autowired
UserMapper userMapper;
@Autowired
private MailClient mailClient;
@Autowired
private TemplateEngine templateEngine;
@Value("${community.path.domain}")
private String domain;
@Value("${server.servlet.context-path}")
private String contextPath;
public User findUserById(int id){
return userMapper.selectById(id);
}
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;
}
}
新增:LoginController.register()
package com.jsss.community.controller;
@Controller
public class LoginController {
@Autowired
UserService userService;
@GetMapping (path = "/register")
public String getRegisterPage(){
return "site/register";
}
@PostMapping("/register")
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";
}
}
}
修改:operate-result.html
修改:register.html
修改:表单的提交
测试
- 通过表单提交数据。
- 服务端验证账号是否已存在、邮箱是否已注册。
- 服务端发送激活邮件。
激活注册账号
新增:/util/CommunityConstant
package com.jsss.community.util;
public interface CommunityConstant {
/**
* 激活成功
*/
int ACTIVATION_SUCCESS = 0;
/**
* 重复激活
*/
int ACTIVATION_REPEAT = 1;
/**
* 激活失败
*/
int ACTIVATION_FAILURE = 2;
}
新增:UserService.activation()
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;
}
}
新增:LoginController.activation()
@GetMapping (path = "/login")
public String getLoginPage(){
return "site/login";
}
// 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== CommunityConstant.ACTIVATION_SUCCESS){
model.addAttribute("msg","激活成功,您的账号已经可以正常使用了!");
model.addAttribute("target","/login");
}else if (result==CommunityConstant.ACTIVATION_REPEAT){
model.addAttribute("msg","无效操作,该账号已经激活过了!");
model.addAttribute("target","/index");
}else{
model.addAttribute("msg","激活失败,您提供的激活码不正确!");
model.addAttribute("target","/index");
}
return "site/operate-result";
}
修改:login.html
静态路径、头部
修改:index.html
头部:登录路径
测试
- 点击邮件中的链接,访问服务端的激活服务。
修改:login.html
修改验证码静态图像路径
3.会话管理
体验cookie
新增:AlphaController.setCookie()
新增:AlphaController.getCookie()
//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";
}
@RequestMapping(path = "/cookie/get",method = RequestMethod.GET)
@ResponseBody
public String getCookie(@CookieValue("code") String code){
System.out.println(code);
return "get cookie";
}
测试:
访问:http://localhost:8080/community/alpha/cookie/set
访问:http://localhost:8080/community/alpha/cookie/get
体验session
新增:AlphaController.setSession()
新增:AlphaController.getSession()
//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";
}
@RequestMapping(path = "/session/get",method = RequestMethod.GET)
@ResponseBody
public String getSession(HttpSession session){
System.out.println(session.getAttribute("id"));
System.out.println(session.getAttribute("name"));
return "get session";
}
测试:
访问:http://localhost:8080/community/alpha/session/set
访问:http://localhost:8080/community/alpha/session/get
4.生成验证码
配置
pom.xml
<!-- 验证码 -->
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>
KaptchaConfig
新建:/config/KaptchaConfig.java
package com.jsss.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;
}
}
新增:LoginController.getKaptcha()
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 {
ServletOutputStream os = response.getOutputStream();
ImageIO.write(image,"png",os);
} catch (IOException e) {
logger.error("响应验证码失败:"+e.getMessage());
}
}
测试:
访问:http://localhost:8080/community/kaptcha
前端
修改:login.html:验证码
新增:global.js:CONTEXT_PATH
var CONTEXT_PATH="/community"
5.开发登录、退出功能
LoginTicket
新增:/entity/LoginTicket.java
package com.jsss.community.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.Date;
@Component
@AllArgsConstructor
@NoArgsConstructor
@Data
public class LoginTicket {
private int id;
private int userId;
private String ticket;
private int status;
private Date expired;
}
LoginTicketMapper
新增:/dao/LoginTicketMapper.java
package com.jsss.community.dao;
import com.jsss.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);
//演示动态sql
@Update({
"<script>",
"update login_ticket set status=#{status} where ticket=#{ticket} ",
"<if test=\"ticket!=null\"> ",
"and 1=1 ",
"</if>",
"</script>"
})
int updateStatus(@Param("ticket") String ticket,@Param("status") int status);
}
测试Dao
新增:MapperTest.testInsertLoginTicket()
新增:MapperTest.testSelectLoginTicket()
@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);
}
UserService
新增:UserService.login()
@Autowired
private LoginTicketMapper loginTicketMapper;
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;
}
LoginController
新增:CommunityConstant
- DEFAULT_EXPIRED_SECONDS
- REMEMBER_EXPIRED_SECONDS
/**
* 默认状态的登录凭证的超时时间
*/
int DEFAULT_EXPIRED_SECONDS = 3600 * 12;
/**
* 记住状态的登录凭证超时时间
*/
int REMEMBER_EXPIRED_SECONDS = 3600 * 24 * 100;
新增:LoginController.login()
@Controller
public class LoginController implements CommunityConstant{
@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:表单提交
退出功能
新增:UserService.logout()
public void logout(String ticket) {
loginTicketMapper.updateStatus(ticket, 1);
}
新增:UserController.logout()
@RequestMapping(path = "/logout", method = RequestMethod.GET)
public String logout(@CookieValue("ticket") String ticket) {
userService.logout(ticket);
return "redirect:/login";
}
修改:index.html:头部
忘记密码
开发忘记密码的功能:
-
点击登录页面上的“忘记密码”链接,打开忘记密码页面。
-
在表单中输入注册的邮箱,点击获取验证码按钮,服务器为该邮箱发送一份验证码。
-
在表单中填写收到的验证码及新密码,点击重置密码,服务器对密码进行修改。
修改后端
配置
<!--fastjson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.58</version>
</dependency>
新增:CommunityUtil.getJSONString()
public static String getJSONString(int code, String msg, Map<String,Object> map){
JSONObject json=new JSONObject();
json.put("code",code);
json.put("msg",msg);
if (map!=null){
for (String key: map.keySet()) {
json.put(key,map.get(key));
}
}
return json.toJSONString();
}
public static String getJSONString(int code,String msg){
return getJSONString(code,msg,null);
}
public static String getJSONString(int code){
return getJSONString(code,null,null);
}
新增:UserService.resetPassword()
// 重置密码
public Map<String, Object> resetPassword(String email, String password) {
Map<String, Object> map = new HashMap<>();
// 空值处理
if (StringUtils.isBlank(email)) {
map.put("emailMsg", "邮箱不能为空!");
return map;
}
if (StringUtils.isBlank(password)) {
map.put("passwordMsg", "密码不能为空!");
return map;
}
// 验证邮箱
User user = userMapper.selectByEmail(email);
if (user == null) {
map.put("emailMsg", "该邮箱尚未注册!");
return map;
}
// 重置密码
password = CommunityUtil.md5(password + user.getSalt());
userMapper.updatePassword(user.getId(), password);
map.put("user", user);
return map;
}
新增:LoginController.resetPassword()
// 忘记密码页面
@RequestMapping(path = "/forget", method = RequestMethod.GET)
public String getForgetPage() {
return "/site/forget";
}
// 获取验证码
@RequestMapping(path = "/forget/code", method = RequestMethod.GET)
@ResponseBody
public String getForgetCode(String email, HttpSession session) {
if (StringUtils.isBlank(email)) {
return CommunityUtil.getJSONString(1, "邮箱不能为空!");
}
// 发送邮件
Context context = new Context();
context.setVariable("email", email);
String code = CommunityUtil.generateUUID().substring(0, 4);
context.setVariable("verifyCode", code);
String content = templateEngine.process("/mail/forget", context);
mailClient.sendMail(email, "找回密码", content);
// 保存验证码
session.setAttribute("verifyCode", code);
return CommunityUtil.getJSONString(0);
}
// 重置密码
@RequestMapping(path = "/forget/password", method = RequestMethod.POST)
public String resetPassword(String email, String verifyCode, String password, Model model, HttpSession session) {
String code = (String) session.getAttribute("verifyCode");
if (StringUtils.isBlank(verifyCode) || StringUtils.isBlank(code) || !code.equalsIgnoreCase(verifyCode)) {
model.addAttribute("codeMsg", "验证码错误!");
return "/site/forget";
}
Map<String, Object> map = userService.resetPassword(email, password);
if (map.containsKey("user")) {
return "redirect:/login";
} else {
model.addAttribute("emailMsg", map.get("emailMsg"));
model.addAttribute("passwordMsg", map.get("passwordMsg"));
return "/site/forget";
}
}
修改前端
login.html:忘记密码路径
添加:forget.js
修改:mail/forget.html
修改:site/forget.html
测试:忘记密码功能
6.显示登录信息
体验拦截器
新增:controller/interceptor/AlphaInterceptor.java
package com.jsss.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());
}
}
配置拦截器
新增:config/WebMvcConfig.java
package com.jsss.community.config;
import com.jsss.community.controller.interceptor.AlphaInterceptor;
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;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(alphaInterceptor)
.excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg")
.addPathPatterns("/register", "/login");
}
}
测试
访问:http://localhost:8080/community/login
2023-05-03 15:36:59,431 DEBUG [http-nio-8080-exec-8] c.j.c.c.i.AlphaInterceptor [AlphaInterceptor.java:19] preHandle: com.jsss.community.controller.LoginController#getLoginPage()
2023-05-03 15:36:59,433 DEBUG [http-nio-8080-exec-8] c.j.c.c.i.AlphaInterceptor [AlphaInterceptor.java:26] postHandle: com.jsss.community.controller.LoginController#getLoginPage()
2023-05-03 15:36:59,463 DEBUG [http-nio-8080-exec-8] c.j.c.c.i.AlphaInterceptor [AlphaInterceptor.java:32] afterCompletion: com.jsss.community.controller.LoginController#getLoginPage()
登录拦截器
新增:util/CookieUtil.java
package com.jsss.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();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals(name)) {
return cookie.getValue();
}
}
}
return null;
}
}
新增:UserService.findLoginTicket()
public LoginTicket findLoginTicket(String ticket) {
return loginTicketMapper.selectByTicket(ticket);
}
视频38:58左右
HostUser:
在本次请求中持有用户
响应请求是多线程的
解决存储的并发性
使用ThreadLocal解决
每一个线程,有一份数据
新增:util/HostHolder.java
package com.jsss.community.util;
import com.jsss.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();
}
}
新增:interceptor/LoginTicketInterceptor.java
package com.jsss.community.controller.interceptor;
import com.jsss.community.entity.LoginTicket;
import com.jsss.community.entity.User;
import com.jsss.community.service.UserService;
import com.jsss.community.util.CookieUtil;
import com.jsss.community.util.HostHolder;
import org.springframework.beans.factory.annotation.Autowired;
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;
import java.util.Date;
@Component
public class LoginTicketInterceptor implements HandlerInterceptor {
@Autowired
private UserService userService;
@Autowired
private HostHolder hostHolder;
//Controller执行之前,拿到登录用户
@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;
}
//Controller执行之后:把loginUser返回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);
}
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
hostHolder.clear();
}
}
新增:WebMvcConfig:登录拦截器
package com.jsss.community.config;
@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", "/**/*.png", "/**/*.jpg", "/**/*.jpeg")
.addPathPatterns("/register", "/login");
registry.addInterceptor(loginTicketInterceptor)
.excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
}
}
前端
修改:index.html:头部
7.账号设置
UserController
新建:controller/UserController.java
package com.jsss.community.controller;
import com.jsss.community.util.CommunityConstant;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Controller
@RequestMapping("/user")
public class UserController implements CommunityConstant {
@RequestMapping(path = "/setting",method = RequestMethod.GET)
public String getSettingPage(){
return "/site/setting";
}
}
前端
修改:setting.html
- html标签
- 静态路径
修改:index.html
- 头部:账号设置路径
测试
- 点击:账号设置
配置
community.path.upload=d:/work/data/upload
UserService
新增:UserService.updateHeader()
public int updateHeader(int userId,String headUrl){
return userMapper.updateHeader(userId,headUrl);
}
UserController
新增:UserController.uploadHeader()
新增:UserController.getHeader()
package com.jsss.community.controller;
@Controller
@RequestMapping("/user")
public class UserController implements CommunityConstant {
private static 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 = "/setting",method = RequestMethod.GET)
public String getSettingPage(){
return "/site/setting";
}
@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";
}
@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:修改头像
测试
访问:账户设置
测试:上传头像
图片下载到:D:\work\data\upload
修改密码
新增:UserService.updatePassword()
// 修改密码
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.md5(oldPassword + user.getSalt());
if (!user.getPassword().equals(oldPassword)) {
map.put("oldPasswordMsg", "原密码输入有误!");
return map;
}
// 更新密码
newPassword = CommunityUtil.md5(newPassword + user.getSalt());
userMapper.updatePassword(userId, newPassword);
return map;
}
新增:UserController.updatePassword()
@RequestMapping(path = "/update_password", method = RequestMethod.POST)
public String updatePassword(@CookieValue("ticket") String ticket, String oldPassword, String password, Model model) {
User user=hostHolder.getUser();
int userId=user.getId();
oldPassword=CommunityUtil.md5(oldPassword+user.getSalt());
if (user.getPassword().equals(oldPassword)&&!StringUtils.isBlank(password)){
password=CommunityUtil.md5(password+user.getSalt());
if (userService.updatePassword(userId,password)>0){
model.addAttribute("msg","修改密码成功");
model.addAttribute("target","/login");
//取消登录状态
userService.logout(ticket);
return "site/operate-result";
}
}
model.addAttribute("msg","修改密码失败");
model.addAttribute("target","/user/setting");
return "site/operate-result";
}
修改:前端
<!-- 修改密码 -->
<h6 class="text-left text-info border-bottom pb-2 mt-5">修改密码</h6>
<form class="mt-5" th:action="@{/user/updatePassword}" method="post">
<div class="form-group row mt-4">
<label for="old-password" class="col-sm-2 col-form-label text-right">原密码:</label>
<div class="col-sm-10">
<input type="password" th:class="|form-control ${oldPasswordMsg!=null?'is-invalid':''}|"
name="oldPassword" th:value="${param.oldPassword}" id="old-password" placeholder="请输入原始密码!" required>
<div class="invalid-feedback" th:text="${oldPasswordMsg}">
密码长度不能小于8位!
</div>
</div>
</div>
<div class="form-group row mt-4">
<label for="new-password" class="col-sm-2 col-form-label text-right">新密码:</label>
<div class="col-sm-10">
<input type="password" th:class="|form-control ${newPasswordMsg!=null?'is-invalid':''}|"
name="newPassword" th:value="${param.newPassword}" id="new-password" placeholder="请输入新的密码!" required>
<div class="invalid-feedback" th:text="${newPasswordMsg}">
密码长度不能小于8位!
</div>
</div>
</div>
<div class="form-group row mt-4">
<label for="confirm-password" class="col-sm-2 col-form-label text-right">确认密码:</label>
<div class="col-sm-10">
<input type="password" class="form-control" th:value="${param.newPassword}" id="confirm-password" placeholder="再次输入新密码!" required>
<div class="invalid-feedback">
两次输入的密码不一致!
</div>
</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>
8.检查登录状态
LoginRequired
新增:/annotation/LoginRequired.java
package com.jsss.community.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginRequired {
}
修改:UserController
给需要登录的功能添加注解
- 账号设置功能
LoginRequiredInterceptor
新增:interceptor/LoginRequiredInterceptor.java
package com.jsss.community.controller.interceptor;
import com.jsss.community.annotation.LoginRequired;
import com.jsss.community.util.HostHolder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
@Component
public class LoginRequiredInterceptor implements HandlerInterceptor {
@Autowired
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
修改:WebMvcConfig
添加登录拦截
@Autowired
private LoginRequiredInterceptor loginRequiredInterceptor;
registry.addInterceptor(loginRequiredInterceptor)
.excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
测试
访问:http://localhost:8080/community/user/setting
会跳到登录页面
最后
2023-7-30 16:26:24
这篇博客能写好的原因是:站在巨人的肩膀上
这篇博客要写好的目的是:做别人的肩膀
开源:为爱发电
学习:为我而行