第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 {@Autowiredprivate MailClient mailClient;@Autowiredprivate TemplateEngine templateEngine;String to="3063494684@qq.com";@Testpublic void testTextMail(){mailClient.sendMail(to,"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(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 -> abc123def456abcpublic 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{@AutowiredUserMapper userMapper;@Autowiredprivate MailClient mailClient;@Autowiredprivate 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/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;}}
新增:LoginController.register()
package com.jsss.community.controller;@Controller
public class LoginController {@AutowiredUserService 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)@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";}@RequestMapping(path = "/cookie/get",method = RequestMethod.GET)@ResponseBodypublic 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)@ResponseBodypublic String setSession(HttpSession session){session.setAttribute("id",1);session.setAttribute("name","Test");return "set session";}@RequestMapping(path = "/session/get",method = RequestMethod.GET)@ResponseBodypublic 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 {@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;}
}
新增:LoginController.getKaptcha()
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 {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()
@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);}
UserService
新增:UserService.login()
@Autowiredprivate 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)@ResponseBodypublic 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之前执行@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());}
}
配置拦截器
新增: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 {@Autowiredprivate AlphaInterceptor alphaInterceptor;@Overridepublic 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 {@Autowiredprivate UserService userService;@Autowiredprivate HostHolder hostHolder;//Controller执行之前,拿到登录用户@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;}//Controller执行之后:把loginUser返回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);}}@Overridepublic 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 {@Autowiredprivate AlphaInterceptor alphaInterceptor;@Autowiredprivate LoginTicketInterceptor loginTicketInterceptor;@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");}}
前端
修改: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;@Autowiredprivate UserService userService;@Autowiredprivate 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.pngUser 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 {@AutowiredHostHolder 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;}
}
WebMvcConfig
修改:WebMvcConfig
添加登录拦截
@Autowiredprivate LoginRequiredInterceptor loginRequiredInterceptor;registry.addInterceptor(loginRequiredInterceptor).excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
测试
访问:http://localhost:8080/community/user/setting
会跳到登录页面
最后
2023-7-30 16:26:24
这篇博客能写好的原因是:站在巨人的肩膀上
这篇博客要写好的目的是:做别人的肩膀
开源:为爱发电
学习:为我而行