0
点赞
收藏
分享

微信扫一扫

2.开发社区登录模块

捡历史的小木板 2022-04-02 阅读 15

文章目录


项目源码可以在 https://gitee.com/ShayneC/community获取

2.开发社区登录模块

1. 发送邮件

  • 邮箱设置

    • 启用客户端SMTP服务
  • Spring Email

    • 导入 jar 包
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-mail</artifactId>
    </dependency>
    
    • 邮箱参数配置
    # MailProperties
    spring.mail.username=xxxxxxxxxx@qq.com
    spring.mail.password=xxxxxxxxxxxx  // 授权密码,非登录密码
    spring.mail.host=smtp.qq.com
    spring.mail.properties.mail.smtp.ssl.enable=true
    
    
    
    • 使用 JavaMailSender 发送邮件

    • 模板引擎

      • 使用 Thymeleaf 发送 HTML 邮件

    在src/main/java/com/nowcoder/community在新建一个util包,并创建MailClient类

    package com.nowcoder.community.util;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.mail.javamail.JavaMailSender;
    import org.springframework.mail.javamail.MimeMessageHelper;
    import org.springframework.stereotype.Component;
    
    import javax.mail.MessagingException;
    import javax.mail.internet.MimeMessage;
    
    @Component
    public class MailClient {
    
        private static final Logger logger = LoggerFactory.getLogger(MailClient.class);
    
        @Autowired
        private JavaMailSender mailSender;
    
        @Value("${spring.mail.username}")
        private String from;
    
        public void sendMail(String to, String subject, String content) {
            try {
                MimeMessage message = mailSender.createMimeMessage();
                MimeMessageHelper helper = new MimeMessageHelper(message);
                helper.setFrom(from);
                helper.setTo(to);
                helper.setSubject(subject);
                helper.setText(content, true);
                mailSender.send(helper.getMimeMessage());
            } catch (MessagingException e) {
                logger.error("发送邮件失败:" + e.getMessage());
            }
        }
    }
    

    在src/main/resources/templates/demo下创建testMail.html文件

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>html邮件</title>
    </head>
    <body>
        <p>欢迎你, <span style="color: red" th:text="${name}"></span></p>
    </body>
    </html>
    

    在src/test/java/com/nowcoder/community下新建MailTests类,进行测试

    package com.nowcoder.community;
    
    import com.nowcoder.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;
    
        @Test
        public void testTextMail() {
            mailClient.sendMail("1348656979@qq.com", "测试主题", "<h1>测试内容</h1>");
            System.out.println("发送成功");
        }
    
        @Test
        public void testHtmlMail() {
            Context context = new Context();
            context.setVariable("name", "ShayneC");
            String content = templateEngine.process("/demo/testMail", context);
            System.out.println(content);
            mailClient.sendMail("1348656979@qq.com", "html测试", content);
        }
    }
    

2. 开发注册功能

  • 访问注册页面
    • 点击顶部区域内的链接,打开注册页面。

在src/main/java/com/nowcoder/community/controller中创建LoginController,并修改相应的index.html和register.html文件。

package com.nowcoder.community.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class LoginController {

    @GetMapping("/register")
    public String getRegisterPage() {
        return "/site/register";
    }
}
  • 提交注册数据
    • 通过表单提交数据。

导入工具类

<!-- 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>

在src/main/java/com/nowcoder/community/util下新建CommunityUtil类

package com.nowcoder.community.util;

import org.apache.commons.lang3.StringUtils;
import org.springframework.util.DigestUtils;

import java.util.UUID;

public class CommunityUtil {

    // 生成随机字符串
    public static String generateUUID() {
        return UUID.randomUUID().toString().replaceAll("-", "");
    }

    // MD5加密
    // hello -> abc123def456
    // hello + 12d9k -> abc123def456gsd
    public static String md5(String key) {
        if (StringUtils.isBlank(key)) {
            return null;
        }
        return DigestUtils.md5DigestAsHex(key.getBytes());
    }
}

自定义域名

# community
community.path.domain=http://localhost:8888

向UserService中注入所需要的组件

package com.nowcoder.community.service;

import com.nowcoder.community.dao.UserMapper;
import com.nowcoder.community.entity.User;
import com.nowcoder.community.util.MailClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.thymeleaf.TemplateEngine;

@Service
public class UserService {

    @Autowired
    private 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);
    }
}
  • 服务端验证账号是否已存在、邮箱是否已注册。
  • 服务端发送激活邮件。

在UserSerice中添加register方法,验证注册逻辑,同时修改register.html

public Map<String, Object> register(User user) {
    HashMap<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/%t.png", new Random().nextInt(1000)));
    user.setCreateTime(new Date());
    userMapper.insertUser(user);

    // 激活邮件
    Context context = new Context();
    context.setVariable("email", user.getEmail());
    // http://localhost:8888/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;
}

将UserService组件注入到LoginController中并添加在register页面提交表单的方法。

在注册成功后让页面跳转到operate-result.html页面,同时修改register.html页面。

package com.nowcoder.community.controller;

import com.nowcoder.community.entity.User;
import com.nowcoder.community.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

import java.util.Map;

@Controller
public class LoginController {

    @Autowired
    private UserService userService;

    @GetMapping("/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";
        }
    }
}

激活注册账号

  • 点击邮件中的链接,访问服务端的激活服务。

为了记录通过邮件激活的状态,在src/main/java/com/nowcoder/community/service中创建CommunityConstant接口

package com.nowcoder.community.service;

public interface CommunityConstant {

    /**
     * 激活成功
     */
    int ACTIVATION_SUCCESS = 0;

    /**
     * 重复激活
     */
    int ACTIVATION_REPEAT = 1;

    /**
     * 激活失败
     */
    int ACTIVATION_FAILURE = 2;
}

并让UserService实现CommunityConstant接口,同时添加激活的方法

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

UserService类

package com.nowcoder.community.service;

import com.nowcoder.community.dao.UserMapper;
import com.nowcoder.community.entity.User;
import com.nowcoder.community.util.CommunityUtil;
import com.nowcoder.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 implements CommunityConstant{

    @Autowired
    private 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) {
        HashMap<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:8888/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;
    }

    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中添加激活的方法

// http://localhost:8888/community/activation/101/code
@GetMapping("/activation/{userId}/{code}")
public String activation(Model model, @PathVariable("userId") int userId, @PathVariable("code") String code) {
    int result = userService.activation(userId, code);
    if (result == ACTIVATION_SUCCESS) {
        model.addAttribute("msg", "激活成功,您的账号已经可以正常使用了");
        model.addAttribute("target", "/login");
    } else if (result == ACTIVATION_REPEAT) {
        model.addAttribute("msg", "无效操作,该账号已经激活过了");
        model.addAttribute("target", "/index");
    } else {
        model.addAttribute("msg", "激活失败,您提供的激活码不正确");
        model.addAttribute("target", "/index");
    }
    return "/site/operate-result";
}

同时在LoginController中添加登录的方法,并修改login.html文件。

@GetMapping("/login")
public String getLoginPage() {
    return "/site/login";
}

此时注册功能开发完成。

3. 会话管理

  • HTTP的基本性质
    • HTTP是简单的
    • HTTP是可扩展的
    • HTTP是无状态的,有会话的
  • Cookie
    • 是服务器发送到浏览器,并保存在浏览器端的一小块数据。
    • 浏览器下次访问该服务器时,会自动携带块该数据,将其发送给服务器。

      在HelloController中添加setCookie和getCookie的方法,测试cookie的基本使用。
// cookie示例
@GetMapping("/cookie/set")
@ResponseBody
public String setCookie(HttpServletResponse response) {
    // 创建cookie
    Cookie cookie = new Cookie("code", CommunityUtil.generateUUID());
    // 设置cookie生效的范围
    cookie.setPath("/community/cookie");
    // 设置cookie的生存时间
    cookie.setMaxAge(60 * 5);
    // 发送cookie
    response.addCookie(cookie);
    return "set cookie";
}

@GetMapping("/cookie/get")
@ResponseBody
public String getCookie(@CookieValue("code") String code) {
    System.out.println(code);
    return "get cookie" + "->" + code;
}
  • Session
    • 是JavaEE的标准,用于在服务端记录客户端信息。
    • 数据存放在服务端更加安全,但是也会增加服务端的内存压力。

      在在HelloController中添加setSession和getSession的方法,测试Session的基本使用。
// session示例
@GetMapping("/session/set")
@ResponseBody
public String setSession(HttpSession session) {
    session.setAttribute("id", 1);
    session.setAttribute("name", "test");
    return "set session";
}

@GetMapping("/session/get")
@ResponseBody
public String getSession(HttpSession session) {
    System.out.println(session.getAttribute("id"));
    System.out.println(session.getAttribute("name"));
    return "get session";
}

4. 生成验证码

  • Kaptcha

    • 导入 jar 包
    <!-- https://mvnrepository.com/artifact/com.github.penggle/kaptcha -->
    <dependency>
        <groupId>com.github.penggle</groupId>
        <artifactId>kaptcha</artifactId>
        <version>2.3.2</version>
    </dependency>
    
    • 编写 Kaptcha 配置类

    在src/main/java/com/nowcoder/community在新建config包,并创建KaptchaConfig

    package com.nowcoder.community.config;
    
    import com.google.code.kaptcha.Producer;
    import com.google.code.kaptcha.impl.DefaultKaptcha;
    import com.google.code.kaptcha.util.Config;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    import java.util.Properties;
    
    @Configuration
    public class KaptchaConfig {
    
        @Bean
        public Producer kaptchaProducer() {
            Properties properties = new Properties();
            properties.setProperty("kaptcha.image.width", "100");
            properties.setProperty("kaptcha.image.height", "40");
            properties.setProperty("kaptcha.textproducer.font.size", "32");
            properties.setProperty("kaptcha.textproducer.font.color", "black");
            properties.setProperty("kaptcha.textproducer.char.string", "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ");
            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中注入KaptchaConfig中的Producer组件,并添加生成验证码的Controller方法,同时修改login.html。

@Autowired
private Producer kaptchaProducer;
@GetMapping("/kaptcha")
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());
    }
}

此时通过点击刷新验证码可以不断刷新验证码

5. 开发登录、退出功能

  • 访问登录页面
    • 点击顶部区域内的链接,打开登录页面。
  • 登录
    • 验证账号、密码、验证码。
    • 成功时,生成登录凭证,发放给客户端。
    • 失败时,跳转回登录页。
  • 退出
    • 将登录凭证修改为失效状态。
    • 跳转至网站首页。

新建LoginTicket实体类

package com.nowcoder.community.entity;

import java.util.Date;

public class LoginTicket {

    private int id;
    private int userId;
    private String ticket;
    private int status;
    private Date expired;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public int getUserId() {
        return userId;
    }

    public void setUserId(int userId) {
        this.userId = userId;
    }

    public String getTicket() {
        return ticket;
    }

    public void setTicket(String ticket) {
        this.ticket = ticket;
    }

    public int getStatus() {
        return status;
    }

    public void setStatus(int status) {
        this.status = status;
    }

    public Date getExpired() {
        return expired;
    }

    public void setExpired(Date expired) {
        this.expired = expired;
    }

    @Override
    public String toString() {
        return "LoginTicket{" +
                "id=" + id +
                ", userId=" + userId +
                ", ticket='" + ticket + '\'' +
                ", status=" + status +
                ", expired=" + expired +
                '}';
    }
}

并创建对应的LoginTicketMapper

package com.nowcoder.community.dao;

import com.nowcoder.community.entity.LoginTicket;
import org.apache.ibatis.annotations.*;

@Mapper
public interface LoginTicketMapper {

    @Insert({
            "insert into login_ticket(user_id, ticket, status, expired) ",
            "values(#{userId}, #{ticket}, #{status}, #{expired})"
    })
    @Options(useGeneratedKeys = true, keyProperty = "id")
    int insertLoginTicket(LoginTicket loginTicket);

    @Select({
            "select id, user_id, ticket, status, expired from login_ticket ",
            "where ticket=#{ticket}"
    })
    LoginTicket selectByTicket(String ticket);

    @Update({
            "update login_ticket set status = #{status} where ticket = #{ticket}"
    })
    int updateStatus(String ticket, int status);

}

在UserService中添加登录的业务方法

public Map<String, Object> login(String username, String password, int expiredSeconds) {
    HashMap<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.setStatus(0);
    loginTicket.setTicket(CommunityUtil.generateUUID());
    loginTicket.setExpired(new Date(System.currentTimeMillis() + expiredSeconds * 1000));
    loginTicketMapper.insertLoginTicket(loginTicket);

    map.put("ticket", loginTicket.getTicket());
    return map;
}

在CommunityConstant中设置登录状态凭证超时时间的常量

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

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

在LoginController中添加登录页面表单验证的方法, 同时修改login.html

@PostMapping("/login")
public String login(String username, String password, String code, boolean remember, 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 = remember ? 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";
    }
}

在UserService中添加退出登录的业务方法

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

在LoginController中添加退出登录的请求

@GetMapping("/logout")
public String logout(@CookieValue("ticket") String ticket) {
    userService.logout(ticket);
    return "redirect:/login";
}

6. 显示登录信息

  • 拦截器示例
    • 定义拦截器,实现HandlerInterceptor
    • 配置拦截器,为它指定拦截、排除的路径

在src/main/java/com/nowcoder/community/controller下新建一个interceptor包并创建TestInterceptor类

package com.nowcoder.community.controller.interceptor;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class TestInterceptor implements HandlerInterceptor {

    private static final Logger logger = LoggerFactory.getLogger(TestInterceptor.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());
    }
}

在src/main/java/com/nowcoder/community/config新建WebMvcConfig

package com.nowcoder.community.config;

import com.nowcoder.community.controller.interceptor.TestInterceptor;
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 TestInterceptor testInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registr y.addInterceptor(testInterceptor)
                .excludePathPatterns("/**/*.css", "/**/*.css", "/**/*.png", "/**/*.jpg", "/**/*.jpeg")
                .addPathPatterns("/register", "/login");
    }
}
  • 拦截器应用
    • 在请求开始时查询登录用户
    • 在本次请求中持有用户数据
    • 在模板视图上显示用户数据
    • 在请求结束时清理用户数据

在src/main/java/com/nowcoder/community/util中新建CookieUtil的工具类,用于获取cookie

package com.nowcoder.community.util;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;

public class CookieUtil {

    public static String getValue(HttpServletRequest request, String name) {
        if (request == null || name == null) {
            throw new IllegalArgumentException("参数为空");
        }

        Cookie[] cookies = request.getCookies();
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if (cookie.getName().equals(name)) {
                    return cookie.getValue();
                }
            }
        }
        return null;
    }
}

在src/main/java/com/nowcoder/community/util中新建HostHolder工具类用于存储用户信息

package com.nowcoder.community.util;

import com.nowcoder.community.entity.User;
import org.springframework.stereotype.Component;

/**
 * 持有用户信息,用于代替session对象
 */
@Component
public class HostHolder {

    private ThreadLocal<User> users= new ThreadLocal<>();

    public void setUser(User user) {
        users.set(user);
    }

    public User getUser() {
        return users.get();
    }

    public void clear() {
        users.remove();
    }
}

在UserService中新建查询用户登录凭证的方法

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

在src/main/java/com/nowcoder/community/controller/interceptor中新建LoginTicketInterceptor拦截器

package com.nowcoder.community.controller.interceptor;

import com.nowcoder.community.entity.LoginTicket;
import com.nowcoder.community.entity.User;
import com.nowcoder.community.service.UserService;
import com.nowcoder.community.util.CookieUtil;
import com.nowcoder.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;

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

    @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中配置loginTicketInterceptor,同时修改index.html

package com.nowcoder.community.config;

import com.nowcoder.community.controller.interceptor.LoginTicketInterceptor;
import com.nowcoder.community.controller.interceptor.TestInterceptor;
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 TestInterceptor testInterceptor;

    @Autowired
    private LoginTicketInterceptor loginTicketInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(testInterceptor)
                .excludePathPatterns("/**/*.css", "/**/*.css", "/**/*.png", "/**/*.jpg", "/**/*.jpeg")
                .addPathPatterns("/register", "/login");

        registry.addInterceptor(loginTicketInterceptor)
                .excludePathPatterns("/**/*.css", "/**/*.css", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
    }
}

此时拦截器功能开发完成。

7. 账号设置

  • 上传文件
    • 请求:必须是POST请求
    • 表单:enctype=“multipart/form-data”
    • Spring MVC:通过 MultipartFile 处理上传文件
  • 开发步骤
    • 访问账号设置页面

添加UserController,并修改setting.html

package com.nowcoder.community.controller;

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 {

    @RequestMapping(path = "/setting", method = RequestMethod.GET)
    public String getSettingPage() {
        return "/site/setting";
    }
}
  • 上传头像
  • 获取头像

在UserService中添加方法

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

同时配置图像上传路径

community.path.upload=d:/upload

然后再UserController中分别添加上传图像和访问图像的请求

package com.nowcoder.community.controller;

import com.nowcoder.community.entity.User;
import com.nowcoder.community.service.UserService;
import com.nowcoder.community.util.CommunityUtil;
import com.nowcoder.community.util.HostHolder;
import org.apache.commons.lang3.StringUtils;
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.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;

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

    @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 dir = new File(uploadPath);
        if (!dir.exists()) {
            dir.mkdir();
        }

        // 确定文件存放的路径
        File dest = new File(uploadPath + "/" + filename);
        try {
            // 存储文件
            headerImage.transferTo(dest);
        } catch (IOException e) {
            logger.error("上传文件失败:" + e.getMessage());
            throw new RuntimeException("上传文件失败,服务器发生异常!", e);
        }

        // 更新当前用户的图像的路径(web访问路径)
        // http://localhost:8888/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(".") + 1);
        // 响应图片
        response.setContentType("image/" + suffix);
        try(
            OutputStream os = response.getOutputStream();
            FileInputStream fis = new FileInputStream(filename);
            ) {
            byte[] buffer = new byte[1024];
            int b = 0;
            while ((b = fis.read(buffer)) != -1) {
                os.write(buffer, 0, b);
            }
        } catch (IOException e) {
            logger.error("读取图像失败" + e.getMessage());
        }
    }
}

在修改setting.html后即可重新启动项目进行测试。

8. 检查登录状态

  • 使用拦截器
    • 在方法前标注自定义注解
    • 拦截所有请求,只处理带有该注解的方法
  • 自定义注解
    • 常用的元注解:
      @Target、@Retention、@Document、@Inherited
  • 如何读取注解:
    Method.getDeclaredAnnotations ()
    Method.getAnnotation (Class annotationClass)

在src/main/java/com/nowcoder/community下新建一个annotation包,创建一个注解

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 {
}

新建LoginRequiredInterceptor拦截器,并在WebMvcConfig中进行配置

package com.nowcoder.community.controller.interceptor;

import com.nowcoder.community.annotation.LoginRequired;
import com.nowcoder.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
    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;
    }
}

最后在/setting和/upload上添加@LoginRequired注解。

  • 修改密码

在UserController新增如下方法

@LoginRequired
@RequestMapping(path = "/updatePassword", method = RequestMethod.POST)
public String updatePassword(String password, String newPassword, String confirmPassword, Model model) {

    // 首先验证原密码
    User user = hostHolder.getUser();
    if (StringUtils.isBlank(password) || !user.getPassword().equals(CommunityUtil.md5(password + user.getSalt()))) {
        model.addAttribute("passwordMsg", "密码不正确");
        return "/site/setting";
    }

    // 修改密码
    if (newPassword.equals(confirmPassword)) {
        newPassword = CommunityUtil.md5(newPassword + user.getSalt());
        userService.updatePassword(user.getId(), newPassword);
    } else {
        model.addAttribute("confirmPasswordMsg", "两次输入的密码不一致!");
        return "/site/setting";
    }

    return "redirect:/index";

}

在UserService中新增如下方法并修改setting.html

public int updatePassword(int userId, String newPassword) {
    return userMapper.updatePassword(userId, newPassword);
}

thod();
LoginRequired loginRequired = method.getAnnotation(LoginRequired.class);
if (loginRequired != null && hostHolder.getUser() == null) {
response.sendRedirect(request.getContextPath() + “/login”);
return false;
}
}
return true;
}
}


最后在/setting和/upload上添加@LoginRequired注解。

- 修改密码

在UserController新增如下方法

```java
@LoginRequired
@RequestMapping(path = "/updatePassword", method = RequestMethod.POST)
public String updatePassword(String password, String newPassword, String confirmPassword, Model model) {

    // 首先验证原密码
    User user = hostHolder.getUser();
    if (StringUtils.isBlank(password) || !user.getPassword().equals(CommunityUtil.md5(password + user.getSalt()))) {
        model.addAttribute("passwordMsg", "密码不正确");
        return "/site/setting";
    }

    // 修改密码
    if (newPassword.equals(confirmPassword)) {
        newPassword = CommunityUtil.md5(newPassword + user.getSalt());
        userService.updatePassword(user.getId(), newPassword);
    } else {
        model.addAttribute("confirmPasswordMsg", "两次输入的密码不一致!");
        return "/site/setting";
    }

    return "redirect:/index";

}

在UserService中新增如下方法并修改setting.html

public int updatePassword(int userId, String newPassword) {
    return userMapper.updatePassword(userId, newPassword);
}
举报

相关推荐

0 条评论