一、开发社区首页

本文详细介绍了使用SpringBoot构建社区首页、数据库初始化、DAO层、Service层、Controller层的实现,包括分页组件、邮件发送、用户注册与激活、会话管理(Cookie与Session)、验证码生成、登录退出功能。此外,还涵盖了自定义登录状态检查、文件上传(头像设置)以及使用拦截器检查登录状态。整个过程涉及了数据库操作、模板引擎、日志配置、版本控制等多个方面。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、开发社区首页

在这里插入图片描述

代码:community-1.6

1.1、建立数据库

init.schema.sql文件

1.2、DAO数据访问层

1、编写实体类(Entity)

User.java         用户信息
DiscussPost.java		
Page.java         封装分页相关的信息

2、编写对应Mapper文件(注释:@Mapper

public interface UserMapper {}

public interface DiscussPostMapper {}

3、编写xml配置文件(放在 resources.mapper下)

user-mapper.xml       
DiscussPost-mapper.xml

4、测试 MapperTests.java

1.3、service业务层

1、编写service包下的 DiscussPostService.javaUserService.java(通过userID查询到用户的信息)

1.4、Controller视图层

1.4.1、开发社区首页,显示前10个数据

1、编写HomeController.java

2、index.html

在这里插入图片描述

1.4.2、开发分页组件

分页显示
在这里插入图片描述

1.5、项目调试技巧

响应状态码的含义

https://developer.mozilla.org/zh-CN/docs/Web/HTTP

常见状态码:

  • 200:请求成功
  • 302:重定向:请求的资源现在临时从不同的 URI 响应请求。由于这样的重定向是临时的,客户端应当继续向原有地址发送以后的请求。只有在Cache-Control或Expires中进行了指定的情况下,这个响应才是可缓存的
  • 404:客户端响应:请求失败,请求所希望得到的资源未被在服务器上发现。路径有问题。
  • 500:服务器响应:服务器遇到了不知道如何处理的情况。

服务端断点调试技巧

客户端断点调试技巧

设置日志级别,并将日志输出到不同的终端

logback配置文件:logback-spring.xml

1.6、版本控制Git

二、SpringBoot实战,开发社区登录模块

2.1、发送邮件

在这里插入图片描述

  1. 邮箱设置:启用客户端SMTP服务

  2. Spring Email:

    • pom.xml文件中导入jar包

    • 邮箱参数配置在这里插入图片描述

    • 使用JavaMailSender发送邮件

      • util包下创建MailClient.java工具类,封装发送邮件功能
      • 测试MapperTests.java
  3. 模板引擎

    • 使用Thymeleaf发送HTML邮件: resource.templates.mail.demo.html

2.2、开发注册功能

在这里插入图片描述

共涉及到了三个请求

(1) 点击注册按钮,弹出注册框

(2) 填完表单数据,点击注册,会发送一次请求

(3) 注册成功后,用户接收到激活邮件后,点击激活邮件又会发送一次请求(进行激活服务)

2.2.1、请求一:访问注册页面

controller层,显示注册网站 controller.LoginController.java

在这里插入图片描述

2.2.2、请求二:提交注册数据

  1. 通过表单提交数据
  2. 服务端验证账号是否已经存在、邮箱是否已经注册
  3. 服务端发送激活邮件
(1)注册service层模块

导入 commons lang 依赖,完成对字符串的一些基本的判断,如非空等等

配置网站相关信息

#自己定义的属性
community.path.domain=http://localhost:8080

创建一个CommunityUtil.java工具类

public class CommunityUtil {
    //生成随机的字符串,以后将用于验证码的生成以及上传文件时对文件的随机命名
    public static String generateUUID(){
        //只需要数字和字符串,因此将-替换为空字符
        return UUID.randomUUID().toString().replace("-","");
    }
    //MD5加密,在密码的后面加上一个随机字符串,进而提高密码的安全性-只能加密而不能解密
    // hello -> abc123456
    // hello + 3e4a8
    public static String md5(String key){
        if(StringUtils.isBlank(key)){
            return null;
        }
        //利用spring自带的工具类来完成加密操作
        return DigestUtils.md5DigestAsHex(key.getBytes());
    }
}

开发注册业务层:UserService.java: 注入邮件客户端,注入模板引擎,注入域名的值和项目名

    @Autowired
    private UserMapper userMapper;
 
    @Autowired
    private MailClient mailClient;//邮件客户端
 
 
    @Value("${community.path.domain}")//域名
    private String domain;
 
    @Value("${server.servlet.context-path}")//项目名
    private String contextPath;
 
    @Autowired
    private TemplateEngine templateEngine;//模板引擎

	public User findUserById(int id) {
        return userMapper.selectById(id);
    }

编写注册业务层UserService.java中注册方法的逻辑,将返回的结果封装在一个Map集合中:

  • 先对参数值非空进行判断、账号是否存在、邮箱是否已经注册过,
  • 开始注册用户,将用户信息保存至数据库中,生成一个随机码(salt:5位)追加在密码后面,对密码进行加密,覆盖user对象中的pass字段,再设置其他的字段(默认普通用户(type),默认未激活(status),默认的头像路径(head_url)),随机生成一个激活码,设置至user对象之中,默认的注册时间。
    public Map<String, Object> register(User user) {
        Map<String, Object> map = new HashMap<>();//若注册失败则map != null,返回失败原因

        // 空值处理
        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;
        }
        
        // 注册用户
        //为加密密码生成5位的随机符串:
        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());
        //insert完了以后user里面就有id了,这是数据库自动为我们生成的id
        userMapper.insertUser(user);

        //激活邮件
        Context context = new Context();
        context.setVariable("email", user.getEmail());
        
        //url由程序员自己来定,你自己希望服务器用哪个路径去处理激活的请求
        //路径中加入了userID和激活码code,为了后面激活账号的页面好获取到用户id和激活码!!!!
        // 激活路径: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;
    }

数据库自动为我们生成的id:在这里插入图片描述

给用户发送HTML链接,模板参数activation.html

<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<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}">此链接</a>,
			激活您的牛客账号!
		</p>
	</div>
</body>
</html>
(2)注册Controller层模块

LoginController.java:

注入userService

    @Autowired
    private UserService userService;

编写注册请求处理方法

调用userService的方法,如果注册成功,则需要跳转至网站的第三方页面上operate-result.html

 
    @RequestMapping(path="/register",method=RequestMethod.POST)
    public String register(Model model,User user){
        Map<String, Object> map = userService.register(user);
        //注册成功的话先跳转到/site/operate-result.html页面上,然后再跳转至首页
        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";
        }
    }

resource.templates.site.operate-result.html:

		<!-- 内容 -->
		<div class="main">
			<div class="container mt-5">
				<div class="jumbotron">
					<p class="lead" th:text="${msg}">您的账号已经激活成功,可以正常使用了!</p>
					<hr class="my-4">
					<p>
						系统会在 <span id="seconds" class="text-danger">8</span> 秒后自动跳转,
						您也可以点此 <a id="target" th:href="@{${target}}" class="text-primary">链接</a>, 手动跳转!
					</p>
				</div>
			</div>
		</div>

2.2.3、请求三:激活注册账号

(1)service层

用户点击激活邮件(携带参数userIdactivateCode),最终对应到数据库中就是需要改变用户的状态,在service层增加一个业务方法。

激活逻辑判断:(对应三种情况)-把这三种情况放到一个接口的常量声明(util.CommunityConstant.java)里,为了实现复用, 让UseService实现这个常量接口。

  • (1)激活成功
  • (2)重复激活
  • (3)伪造的激活码

CommunityConstant.java:

package com.newcoder.community.util;
public interface CommunityConstant {
    //激活成功
    int ACTIVATION_SUCCESS = 0;
 
    //重复激活
    int ACTIVATION_REPEAT = 1;
 
    //激活失败
    int ACTIVATION_FAILURE = 2;
}

业务层:UseService.java: 根据用户id和激活码来激活用户

@Service
public class UserService implements CommunityConstant {
    
	public int activation(int userId, String code){
        //先根据userId查询出user
        User user = userMapper.selectById(userId);
        //重复激活的情况
        if(user.getStatus() == 1){
            return ACTIVATION_REPEAT;
        }
        //可以激活成功
        else if(code.equals(user.getActivationCode())){//输入的激活码=数据库中该user激活码
            userMapper.updateStatus(userId, 1);//把用户的激活状态改为1,表示激活成功
            clearCache(userId);
            return ACTIVATION_SUCCESS;
        }
        else{
            return ACTIVATION_FAILURE;
        }
    }
}
(2)Controller层

LoginController.java然该类实现CommunityConstant接口,再加一个响应登录页面/login的请求!!

@Controller
public class LoginController implements CommunityConstant {
	//....//
    
    //响应登录页面的请求
    @RequestMapping(path = "/login", method = RequestMethod.GET)
    public String getLoginPage() {
        return "/site/login";
    }
    
	//http://localhost:8080/community/activation/101/code :用户id和激活码
	//@PathVariable("userID"):从路径中取值
    @RequestMapping(path = "/activation/{userID}/{code}", method = RequestMethod.GET)
    public String activation(Model model, @PathVariable("userID")int userId, @PathVariable("code")String code){
        //成功的话直接跳转至login页面,否则直接跳转回首页
        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";
    }
}

2.3、会话管理

HTTP的基本性质:

  • -HTTP是简单的
  • -HTTP是可扩展的
  • -HTTP是无状态的、有会话的
    • HTTP是无状态的:在同一个连接中,两个执行成功的请求之间是没有关系的。

Cookie:

  • -是服务器发送到浏览器,并保存在浏览器端的一小块数据
  • -浏览器下次访问该服务器时,会自动给携带该数据,将其发送给服务器
  • -通常,用于告知服务端两个请求是否来自同一浏览器,如保持用户的登录状态。

Session:

  • -是JavaEE的标准,用于在服务端纪录客户端信息。
  • -数据存放在服务端更加安全,但是也会增加服务端的内存压力。

**会话:**用户和服务器之间的对话是连贯的,而非割裂的。由服务器创建发送到用户浏览器并保存在本地的一小块数据。它会在浏览器下次向同一服务器请求时发送这块数据给服务器。

(1)Cookie的使用案例
  • **HTTP是无状态的:**在同一个连接中,两个执行成功的请求之间是没有关系的。
  • 这就带来了一个问题,用户没有办法在同一个网站中进行连续的交互,比如在一个电商网站里,用户把某个商品加入到购物车,切换一个页面后再次添加了商品,这两次添加商品的请求之间没有关联,浏览器无法知道用户最终选择了哪些商品。
  • 而使用HTTP的头部扩展,HTTP Cookie就可以解决这个问题。把Cookies添加到头部中,创建一个会话让每次请求都能共享相同的上下文信息,达成相同的状态
  • 有些网站需要有持续性的交互,HTTP本质是无状态的,使用Cookie可以创建有状态的会话。
  • 需要把创建的cookie存放至response中,这样才能返回给客户端。
  • 由服务器创建发送到用户浏览器并保存在本地的一小块数据。它会在浏览器下次向同一服务器请求时发送这块数据给服务器。
  • **cookie只能存一个key-value,只能存很少的数据。**而且cookie是存储在response的头部中,不是在数据中。
    如果不设置生效时间,cookie存储在内存中,等浏览器一关闭,就消失了, 但如果设置了生效时间,就会存储在硬盘中。
  • 创建cookie时可以指定key的值(“code”),一个cookie只存一个key value.

在这里插入图片描述

代码实例:

	@RequestMapping(path = "/cookie/set", method = RequestMethod.GET)
    @ResponseBody
    public String setCookie(HttpServletResponse response){
        // 创建Cookie,key-value
        Cookie cookie = new Cookie("code", CommunityUtil.generateUUID());
        //设置cookie的生效范围 只针对某个功能下的访问生效,只有在这个路径下才有效
        cookie.setPath("/community/alpha");
        //设置cookie的生效时间 默认情况下浏览器一关闭cookie就失效,但一旦设置了cookie的生存时间,再次打开浏览器会依旧有效。
        cookie.setMaxAge(60 * 10);//设置10分钟
        response.addCookie(cookie); //需要把创建好的cookie存放到response之中
        return "set cookie";
    }

请求相应的url:localhost:8080/community/alpha/cookie/set

在这里插入图片描述

在这里插入图片描述

   //服务器获取cookie
	@RequestMapping(path = "/cookie/get", method = RequestMethod.GET)
    @ReusponseBody
    public String getCookie(@CookieValue("code") String code){
        System.out.println(code);
        return "get cooke";
    }

如何获取浏览器中指定的cookie值?

  • 可以从request对象中获得Cookie的数组(包含了所有的cookie)
  • @CookieValue表示需要从cookie中获取code的值
(2)Session的使用案例
  • cookie的缺点:不安全,因为cookie是存在客户端的;每次请求都要携带cookie数据,会造成性能低的后果
  • 利用一种思路:session:在服务端存放用户数据:session使用依赖cookie
  • session不是http的标准,而是JavaEE的标准,用于在服务端记录客户端信息。

在这里插入图片描述

  • 浏览器第一次访问服务器:服务器先创建一个session对象(每个浏览器访问服务器都会创建一个session)
  • 服务器自动创建一个存放sessionId的cookie, 用来维护浏览器和服务器之间的对应关系。
  • 浏览器保存cookie,下次访问服务器,携带上cookie。
  • 服务器再从cookie中根据sessionId找到响应的session,再找到对应的session里存放的数据。
  • 和手动创建cookie不同的是,HttpSession SpringMVC会自动给我们创建session创建并注入进来。
  • session的使用和request和response的使用一样。
    //在服务器端创建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";
    }
 	
	//服务器获取浏览器请求中的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";
    }

浏览器发起在服务器端创建session的请求:

在这里插入图片描述

浏览器发起让服务器获取session的请求:

在这里插入图片描述

sesson存储在服务端,大小和类型不受限制,但cookie需要来回传,大小受限制,而且只能保存字符串类型。

一般情况下,如果数据不是特别隐私,还是使用cookie会比较好。

(3)共享Session

session和cookie都是为了解决会话的技术,各有各自的优缺点,使用应该视具体情况而定,能用cookie尽量用cookie,减少服务端的压力。但是使用session会存在一个session共享(分布式部署的时候)的问题。

在这里插入图片描述

解决方案:

  • 黏性session,不完美,将请求和IP固定起来了,每个IP发送过来的请求都由固定个服务器来进行处理,会造成负载不均衡。
  • 同步session,所有的服务器中的session数据都是相同的,每个服务器都存储了所有的session,服务器的压力会比较大,另外一个,服务器和服务器之间会产生关联,不再像以前那么独立了,产生一定的耦合,对部署有一定的影响, 非理想!
  • 共享session: 所有的session都由一台服务器进行专门进行处理,当其他服务器需要利用session时,直接从此服务器中获取。缺点:容易出现单点故障问题。分布式部署的目的:解决性能瓶颈
  • 主流的解决方案:
    • 数据能存放在cookie就存放在cookie里,而不存放至session中去。
    • 将session存放至**数据库中,所有服务器都可以访问数据库集群**来得到客户端的会话的数据。
    • 缺点:从数据库硬盘中获取数据显然和从内存中获取数据的性能差别甚大,在并发度大的时候也容易成为一个瓶颈,但现在非关系型数据库(内存) redis能弥补以上问题。
img

2.4、生成验证码(存于session)

(1)导入Kaptcha的jar包

(2)编写Kaptcha配置类

  • 加入至spring的bean容器管理之中。
  • 核心是一个Producer接口,创建一个Properties ,用Config装配,然后再传递给DefaultKaptcha创建一个实例,然后再调用这个实例的方法来生成图片。
    在这里插入图片描述

config.KaptchaConfig.java:

import com.google.code.kaptcha.Producer;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;

@Configuration
public class KaptchaConfig {
    @Bean
    public Producer kaptchaProducer() {//Producer接口
        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);//config依赖于properties
        kaptcha.setConfig(config);
        return kaptcha;
    }
}

(3)生成随机字符、生成图片(Contoller层)

  • 方法返回值为void,因为需要利用response对象向浏览器输出数据
  • 服务端需要记住生成的验证码,因为客户端输入验证码后,服务器需要验证是否正确(是敏感数据,不能存在浏览器中,而存倒服务器端,而且是需要跨请求的,选择**session**).
  • 注入生成验证码的配置类

LoginController.java:

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();//OS输出流
            ImageIO.write(image, "png", os);//将image以.png的格式向OS输出流输出
        } catch (IOException e) {
            logger.error("响应验证码失败:" + e.getMessage());
        }
    }

login.html:

在前台中通过js来实现每次点击都刷新验证码的功能。

在这里插入图片描述

	<script>
		function refresh_kaptcha() {
			var path = CONTEXT_PATH + "/kaptcha?p=" + Math.random();
			$("#kaptcha").attr("src", path);
		}
	</script>

2.5、开发登录和退出功能

点击登录后,转到登录页面,服务器在这里就存了验证码session,用户填完登录信息提交后,
在这里插入图片描述

建立登录凭证的数据库:

在这里插入图片描述

(1)dao数据访问层

  1. Entity:LoginTicket.java

  2. dao: LoginTicketMapper.java,通过注解代替Mapper文件

    @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);//ticket是唯一标识符
    
        @Update({//演示注解的动态sql
                "<script>",
                "update login_ticket set status=#{status} where ticket=#{ticket} ",
                "<if test=\"ticket!=null\"> ",
                "and 1=1 ",
                "</if>",
                "</script>"
        })
        int updateStatus(String ticket, int status);
    }
    
  3. 测试:MapperTest.java

(2)service层

UserService.java:

    @Autowired
    private LoginTicketMapper loginTicketMapper;
    //map:多种情况的返回结果;password需要加密;expiredSeconds过期时限
	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 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);//0-有效状态
        loginTicket.setExpired(new Date(System.currentTimeMillis() + expiredSeconds * 1000));
        loginTicketMapper.insertLoginTicket(loginTicket);

        map.put("ticket", loginTicket.getTicket());//ticket放入map中作为返回值
        return map;
    }

退出:

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

(3)Controller层

在这里插入图片描述

CommunityConstant.java:

    /**
     * 默认状态的登录凭证的超时时间
     */
    int DEFAULT_EXPIRED_SECONDS = 3600 * 12;

    /**
     * 记住状态的登录凭证超时时间
     */
    int REMEMBER_EXPIRED_SECONDS = 3600 * 24 * 100;

LoginController.java:

response中的cookie:若登录成功,服务端需将凭证放在cookie中发送给客户端

    @Value("${server.servlet.context-path}")
    private String contextPath;
	//和点进等录按钮的请求路径一致,不要紧,因为请求方式不同,这里是post,提交数据
	//code:客户端提交的验证码;session中保存了服务器向客户端发送的验证码;
	//response中的cookie:若登录成功,服务端需将凭证放在cookie中发送给客户端
	@RequestMapping(path = "/login", method = RequestMethod.POST)
    public String login(String username, String password, String code, boolean rememberme, Model model, HttpSession session, HttpServletResponse response) {
        // 检查验证码,在2.4节中服务器给客户端的响应中存了session(验证码)
        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 cookie = new Cookie("ticket", map.get("ticket").toString());
            cookie.setPath(contextPath);//整个项目的路径都有效
            cookie.setMaxAge(expiredSeconds);//cookie的有效时间
            response.addCookie(cookie);
            return "redirect:/index";
        } else {
            model.addAttribute("usernameMsg", map.get("usernameMsg"));
            model.addAttribute("passwordMsg", map.get("passwordMsg"));
            return "/site/login";
        }
    }

login.html

退出登录:通过ticket退出登录

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

配置退出登录的链接index.html

2.6、显示登录信息

在这里插入图片描述

根据登录与否显示相应的内容

(1)拦截器实例

AlphaInterceptor.java :

@Component
public class AlphaInterceptor implements HandlerInterceptor {
    private static final Logger logger = LoggerFactory.getLogger(AlphaInterceptor.class);
    //在Controller处理请求之前执行,如果返回为false的话,请求就不会被处理
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        logger.debug("preHandle:" + handler.toString());
        return true;
    }
    //在controller之后执行,主要的请求已经处理完了,下一步就是要去调用模板引擎了,所以有ModelAndView
    //在模板引擎执行之前执行
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        logger.debug("postHandle:" + handler.toString());
    }
    //在模板引擎执行之后执行
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        logger.debug("afterCompletion:" + handler.toString());
    }
}

为了使得拦截器生效,还需要写一个配置类:WebMvcConfig.java

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Autowired
    private AlphaInterceptor alphaInterceptor;
    @Autowired
    private LoginTicketInterceptor loginTicketInterceptor;
    /*@Autowired
    private LoginRequiredInterceptor loginRequiredInterceptor;*/
    @Autowired
    private DataInterceptor dataInterceptor;
    @Autowired
    private MessageInterceptor messageInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //排除所有目录下的所有文件夹,将拦截器注册进去
        //排除掉静态资源不进行处理
        registry.addInterceptor(alphaInterceptor)
            .excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.jpg", "/**/*.jpeg", "/**/*.png").addPathPatterns("/register", "/login");//表示只拦截register下的内容
        registry.addInterceptor(loginTicketInterceptor)
                .excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.jpg", "/**/*.jpeg", "/**/*.png");
        //registry.addInterceptor(loginRequiredInterceptor)
        // .excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.jpg", "/**/*.jpeg", "/**/*.png");
        registry.addInterceptor(messageInterceptor)
                .excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.jpg", "/**/*.jpeg", "/**/*.png");
        registry.addInterceptor(dataInterceptor)
                .excludePathPatterns("/**/*.css", "/**/*.jpg", "/**/*.jpeg", "/**/*.png");
    }
}

需要根据是否登录来调整头部显示的信息。可以利用拦截器来降低模块之间的耦合度。服务器得到了凭证就能得到用户对象。

handler为拦截的目标。

(2)拦截器应用-显示用户登录信息

  • 类似这种显示用户信息的在多个页面上都要显示,如果每打开一个页面都重新发送一次请求获取数据的话,耦合度比较高.
  • 可以利用统一集中的低耦合度方式实现(拦截器,可以拦截请求,在请求开始和结束的位置插入一些代码,从而批量解决多个请求共有的业务,以低耦度的方式解决通用的问题)
img
  • 浏览器携带cookie访问服务器,服务器获得cookie中的ticket凭证,从数据中查询关联到用户(userid),进而查询到User对象
  • 在接下来的每次调用模板引擎之前将用户信息装入Model中。
  • 关键在于(用户登陆的时候,存入了ticket,再下次访问的时候可获取到ticket值)。这套逻辑应被复用,利用拦截器来实现。

只在浏览器中存一个ticket,而不存储用户的数据,这避免了敏感数据所造成的不安全问题。这套逻辑不应该实现多次,因为每次请求都需要在页面上显示用户的数据,可以让拦截器来实现。

1)创建LoginInterceptor(涉及多线程!)
1、preHandle方法之中,保存用户数据
  • 封装从request中获取cookie的工具类CookieUtil。
  • 在请求开始之前就需要保存数据,因为在之后的可能随时随地都需要使用。

CookieUtil.java 从requeset中获取cookie:

public class CookieUtil {
    //name:我们想得到的cookie的名字
    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.java中添加查询ticket方法:

    public LoginTicket findLoginTicket(String ticket) {
        return loginTicketMapper.selectByTicket(ticket);
    }

HostHolder.java

  • 服务器在处理请求的时候会创建一个线程来处理用户的请求,请求响应了数据,有了返回值之后,线程才会被销毁。
  • 在**多并发的情况下,需要考虑线程的隔离,利用ThreadLocal**工具(以线程为key存取值)来实现。
    • ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本
  • 创建一个HostHolder工具类,代替session对象
/**
 * 持有用户信息,用于代替session对象.
 */
@Component
public class HostHolder {

    private ThreadLocal<User> users = new ThreadLocal<>();
	//设置当前线程的user
    public void setUser(User user) {
        users.set(user);
    }
	//得到当前线程的user
    public User getUser() {
        return users.get();
    }
    //清理当前线程的user
    public void clear() {
        users.remove();
    }

}

LoginTicketInterceptor.java中preHandle方法:

@Component
public class LoginTicketInterceptor implements HandlerInterceptor {

    @Autowired
    private UserService userService;

    @Autowired
    private HostHolder hostHolder;
	
    //一开始就拦截请求获得里面的ticket再去查询有没有这个用户
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 从cookie中获取凭证
        String ticket = CookieUtil.getValue(request, "ticket");

        if (ticket != null) {
            // 查询凭证,得到loginTicket对象
            LoginTicket loginTicket = userService.findLoginTicket(ticket);
            // 检查凭证是否有效
            if (loginTicket != null && loginTicket.getStatus() == 0 && loginTicket.getExpired().after(new Date())) {
                // 根据凭证查询用户
                User user = userService.findUserById(loginTicket.getUserId());
                //在本次请求中持有用户
                //考虑多线程的情况,要隔离的存当前用户信息
                //需要把User进行暂存,表示在本次请求中持有用户,把对象存储在当前线程ThreadLocal之中
                hostHolder.setUser(user);
            }
        }

        return true;
    }
}
2、 在postHandle方法之中使用保存的User对象信息
    @Override //在模板引擎调用之前把数据存储在hostHolder之中的数据放入Model之中
    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);
        }
    }
3、 在afterCompletion(),模板引擎渲染完之后清除掉用户的数据
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response,Object handler, Exception ex) throws Exception {
        //在模板执行完之后将数据清理掉
        hostHolder.clear();
    }
2) 配置LoginInterceptor
  • 在WebMvcConfig类之中
    @Autowired
    private LoginTicketInterceptor loginTicketInterceptor;

	@Override
    public void addInterceptors(InterceptorRegistry registry) {

        registry.addInterceptor(loginTicketInterceptor)
                .excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");//所有路径的请求都要此拦截!!!
        
    }
3) 页面显示用户信息
  • 处理情况:未登录不显示用户的信息,登录才显示(根据Model里面是否有loginUser来判断)
<div class="collapse navbar-collapse" id="navbarSupportedContent">
	<ul class="navbar-nav mr-auto">
		<li class="nav-item ml-3 btn-group-vertical">
			<a class="nav-link" th:href="@{/index}">首页</a>
		</li>
		<li class="nav-item ml-3 btn-group-vertical" th:if="${loginUser!=null}">
			<a class="nav-link position-relative" th:href="@{/letter/list}">消息
				<span class="badge badge-danger" th:text="${allUnreadCount!=0?allUnreadCount:''}">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"
				   th:href="@{|/user/profile/${loginUser.id}|}">个人主页</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>
	</ul>
	<!-- 搜索 -->
	<form class="form-inline my-2 my-lg-0" th:action="@{/search}" method="get">
		<input class="form-control mr-sm-2" type="search" aria-label="Search" name="keyword" th:value="${keyword}"/>
		<button class="btn btn-outline-light my-2 my-sm-0" type="submit">搜索</button>
	</form>
</div>

2.7、账号设置

在这里插入图片描述

上传文件-这里不需要数据访问层的代码,上传文件的话只需要在表现层实现就可以

  • 请求:必须是POST请求
  • 表单:enctype=”multipart/form-data”
  • Spring MVC:通过MultipartFile处理上传文件 MultipartFile:表现层的对象,不应该放到业务层来处理,这会带来较高的耦合度。业务层值只负责更新路径即可

配置文件:上传的头像图片的保存路径

community.path.upload=d:/project/upload

(1)service层

UserService.java

    public int updateHeader(int userId, String headerUrl) {
        return userMapper.updateHeader(userId, headerUrl);
    }

(2)Controller层

UserController:

@Controller
@RequestMapping("/user")
public class 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;//从hostHolder里取当前用户

    @LoginRequired
    @RequestMapping(path = "/setting", method = RequestMethod.GET)
    public String getSettingPage() {
        return "/site/setting";
    }

    @LoginRequired
    @RequestMapping(path = "/upload", method = RequestMethod.POST)
    public String uploadHeader(MultipartFile headerImage, Model model) {
        if (headerImage == null) {
            model.addAttribute("error", "您还没有选择图片!");
            return "/site/setting";
        }
		
        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());
        }
    }

}

hostHolder到底什么时候清除数据???整个网页都关掉的时候?所有路径的请求都要被拦截!!!

2.8、检查登录状态(自定义注解)

在这里插入图片描述

自定义注解:LoginRequired.java

package com.nowcoder.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 {

}

给UserService.java中需要被拦截的方法加上自定义注释:

在这里插入图片描述

LoginRequiredInterceptor.java:

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

配置拦截器:WebMvcConfigurer

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值