仿牛客社区项目(第二章)

简介: 仿牛客社区项目(第二章)
❤ 作者主页: 欢迎来到我的技术博客😎
❀ 个人介绍:大家好,本人热衷于 Java后端开发,欢迎来交流学习哦!( ̄▽ ̄)~*
🍊 如果文章对您有帮助,记得 关注点赞收藏评论⭐️⭐️⭐️
📣 您的支持将是我创作的动力,让我们一起加油进步吧!!!🎉🎉

第二章:SpringBoot实践,开发社区登陆模块

一、发送邮件

1. 邮箱设置

首先需要启用邮箱的客户端SMTP服务,这里演示的是qq邮箱。
在这里插入图片描述

2. Spring Email

2.1 导入jia包

pom.xml 中导入一下 jar 包。

  <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
            <version>2.7.0</version>
 </dependency>

2.2 邮箱参数设置

application.properties 中配置邮箱的相关设置,不同邮箱的配置有些许不同,这里演示的是qq邮箱的相关配置。

#Mailproperties 配置发送邮箱的相关配置
#配置 SMTP 服务器地址
spring.mail.host=smtp.qq.com
#SMTP 服务器的端口
spring.mail.port=465
#配置邮箱用户名
spring.mail.username=xxxxxxx@qq.com
#配置密码,注意不是真正的密码,是申请的授权码
spring.mail.password=xxxxxxxxx
#进行SSL协议替换
spring.mail.protocol=smtps
spring.mail.properties.stmp.ssl.enable=true

2.3 使用 JavaMailSender 发送邮件

在工具类 util 包下创建 MailClient类:

@Component
public class MailClient {

    private static final Logger logger = LoggerFactory.getLogger(MailClient.class);

    @Autowired
    private JavaMailSender javaMailSender;

    @Value("${spring.mail.username}")
    private String from;

    public void sendMail(String to, String subject, String content) {
        try {
            MimeMessage message = javaMailSender.createMimeMessage();
            MimeMessageHelper helper = new MimeMessageHelper(message);

            helper.setFrom(from);
            helper.setTo(to);
            helper.setSubject(subject);
            helper.setText(content, true);

            javaMailSender.send(helper.getMimeMessage());
        } catch (Exception e) {
            logger.error(" 发送邮件失败:" + e.getMessage());
        }
    }
}

 
test 包下创建 MailTest 类:

@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class MailTest {

    @Autowired
    private MailClient mailClient;
    
    // 直接发送文字信息
    @Test
    public void testTextMail() {
        mailClient.sendMail("2869041125@qq.com", "TEST", "Welcome!");
    }
}

效果展示:
运行 testTextMail 方法,目标邮箱可以收到来自 spring.mail.host 端发来的测试文本邮件。
在这里插入图片描述

3. 模板引擎

使用 Thymeleaf发送HTML邮件

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

测试方法相关代码:

@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class MailTest {

    @Autowired
    private MailClient mailClient;

    @Autowired
    private TemplateEngine templateEngine;

    // 发送动态网页
    @Test
    public void testHtmlMail() {
        Context context = new Context();
        context.setVariable("username", "Tom");

        String content = templateEngine.process("/mail/demo", context);
        System.out.println(context);

        mailClient.sendMail("2869041125@qq.com", "HTML", content);
    }
}

 

效果展示:
在这里插入图片描述

二、开发注册功能

1. 访问注册页面

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

在这里插入图片描述
 

2. 实现注册和激活功能

2.1 添加工具类

util 包下添加 CommunityUtil工具类,这里主要是生成随机字符串和 md5 加密,相关代码如下:

public class CommunityUtil {

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

    // MD5加密
    public static String md5(String key) {
        if (StringUtils.isBlank(key)) {
            return null;
        }
        return DigestUtils.md5DigestAsHex(key.getBytes());
    }
}

 
添加 CommubityConstant 接口,返回激活的三种状态,代码如下:

public interface CommunityConstant {

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

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

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

 

3. service层

service 包下的 UserService 类中添加 registeractivation 方法分别用于提供注册和激活服务,相关代码如下:

@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) {
        Map<String, Object> map = new HashMap<>();

        // 空值处理
        if (user == null) {
            throw new IllegalArgumentException("参数不能为空!");
        }
        if (StringUtils.isBlank(user.getUsername())) {
            map.put("usernameMsg", "账号不能为空!");
            return map;
        }
        if (StringUtils.isBlank(user.getPassword())) {
            map.put("passwordMsg", "密码不能为空!");
            return map;
        }
        if (StringUtils.isBlank(user.getEmail())) {
            map.put("emailMsg", "邮箱不能为空!");
            return map;
        }

        // 验证账号
        User u = userMapper.selectByName(user.getUsername());
        if (u != null) {
            map.put("usernameMsg", "该账号已存在!");
            return map;
        }

        // 验证邮箱
        u = userMapper.selectByEmail(user.getEmail());
        if (u != null) {
            map.put("emailMsg", "该邮箱已被注册!");
            return map;
        }

        // 注册用户
        user.setSalt(CommunityUtil.generateUUID().substring(0, 5));
        user.setPassword(CommunityUtil.md5(user.getPassword() + user.getSalt()));
        user.setType(0);
        user.setStatus(0);
        user.setActivationCode(CommunityUtil.generateUUID());
        user.setHeaderUrl(String.format("http://images.nowcoder.com/head/%dt.png", new Random().nextInt(1000)));
        user.setCreateTime(new Date());
        userMapper.insertUser(user);

        // 激活邮件
        Context context = new Context();
        context.setVariable("email", user.getEmail());
        // http://localhost:8080/community/activation/101/code
        String url = domain + contextPath + "/activation/" + user.getId() + "/" + user.getActivationCode();
        context.setVariable("url", url);
        String content = templateEngine.process("/mail/activation", context);
        mailClient.sendMail(user.getEmail(), "激活账号", content);

        return map;
    }

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

    }

}

 

4. controller层

controller 包下创建 LoginController 类,用户接受前端发送过来的注册请求,代码如下:

@Controller
public class LoginController implements CommunityConstant {

    private static final Logger logger = LoggerFactory.getLogger(LoginController.class);

    @Autowired
    private UserService userService;

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

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


    @RequestMapping(path = "/register", method = RequestMethod.POST)
    public String register(Model model, User user) throws IllegalAccessException {
        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";
        }

    }

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

 

5. 注册和激活功能演示

5.1 注册功能测试

启动该项目,进入注册页面,填写相关的信息:
在这里插入图片描述
 
点击注册后,会立即向邮箱发送激活邮件,并且在5秒后会跳回首页:
在这里插入图片描述
 
同时可以在数据库 User 表中找到新注册用户的信息:
在这里插入图片描述

5.2 激活功能测试

收到激活邮件后,点击此链接即可激活成功
在这里插入图片描述

激活成功后,将会跳转到登录页面:
在这里插入图片描述
 

三、 会话管理

1. 基本知识点

  • HTTP的基本性质

    • HTTP是简单的
    • HTTP是可扩展的
    • HTTP是无状态的,有会话的
  • Cookie

    • 是服务器发送到浏览器哦,并保存在浏览器端的一小块数据。
    • 浏览器下次访问改服务器时,会自动携带该数据,将其发送给服务器。
  • Session

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

2. cookie示例

controller 包下创建 AlphaController 类,代码如下:

@Controller
@RequestMapping("alpha")
public class AlphaController {

    // cookie示例
    @RequestMapping(path = "/cookie/set", method = RequestMethod.GET)
    @ResponseBody
    public String setCookie(HttpServletResponse response) {
        // 创建cookie
        Cookie cookie = new Cookie("code", "CommunityUtil.generateUUID()");
        //  设置cookie生效的范围
        cookie.setPath("/community/alpha");
        // 设置cookie的生存时间
        cookie.setMaxAge(60 * 10);
        // 发送cookie
        response.addCookie(cookie);

        return "set cookie";
    }

    @RequestMapping(path = "/cookie/get", method = RequestMethod.GET)
    @ResponseBody
    public String getCookie(@CookieValue("code") String code) {
        System.out.println(code);

        return "get cookie";
    }
}

 
测试效果:
在这里插入图片描述
在这里插入图片描述
 

3. session示例

AlphaController 类中实现session示例,代码如下:

@Controller
@RequestMapping("alpha")
public class AlphaController {

    // session示例
    @RequestMapping(path = "/session/set", method = RequestMethod.GET)
    @ResponseBody
    public String setSession(HttpSession session) {
        session.setAttribute("id", 1);
        session.setAttribute("name", "Test");

        return "set session";
    }

    @RequestMapping(path = "/session/get", method = RequestMethod.GET)
    @ResponseBody
    public String getSession(HttpSession session) {
        System.out.println(session.getAttribute("id"));
        System.out.println(session.getAttribute("name"));
        return "get session";

    }
}

 
测试效果:
在这里插入图片描述
在这里插入图片描述
 

四、生成验证码

1. 导入 jar 包

在项目的 pom.xml 中添加 Kaptcha 需要的jar 包:

 <dependency>
            <groupId>com.github.penggle</groupId>
            <artifactId>kaptcha</artifactId>
            <version>2.3.2</version>
</dependency>

2. 编写Kaptcha配置类

config 包下创建 Kaptcha 类,编写 Kaptcha 所需的配置信息,代码如下:

@Configuration
public class KaptchaConfig {

   @Bean
   public Producer kaptchaProducer() {
       Properties properties = new Properties();
       properties.setProperty("kaptcha.image.width", "100");
       properties.setProperty("kaptcha.image.height", "40");
       properties.setProperty("kaptcha.textproducer.font.size", "32");
       properties.setProperty("kaptcha.textproducer.font.color", "0,0,0");
       // 在哪个字符串中生成验证码
       properties.setProperty("kaptcha.textproducer.char.string", "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYAZ");
       properties.setProperty("kaptcha.textproducer.char.length", "4");
       properties.setProperty("kaptcha.noise.impl", "com.google.code.kaptcha.impl.NoNoise");


       DefaultKaptcha kaptcha = new DefaultKaptcha();
       Config config = new Config(properties);
       kaptcha.setConfig(config);
       return kaptcha;
   }
}

 

3. controller层

LoginController 类中添加 getKaptcha 方法,代码如下:

@Controller
public class LoginController implements CommunityConstant {

    private static final Logger logger = LoggerFactory.getLogger(LoginController.class);

    @Autowired
    private Producer kaptchaProducer;

    @RequestMapping(path = "/kaptcha", method = RequestMethod.GET)
    public void getKaptcha(HttpServletResponse response, HttpSession session) {
        // 生成验证码
        String text = kaptchaProducer.createText();
        BufferedImage image = kaptchaProducer.createImage(text);

        // 将验证码存入session
        session.setAttribute("kaptcha", text);

        // 将图片输出给浏览器
        response.setContentType("image/png");
        try {
            OutputStream os = response.getOutputStream();
            ImageIO.write(image, "png", os);
        } catch (IOException e) {
            logger.error("响应验证码失败:" + e.getMessage());
        }
    }
}

 

4. View层

前端对生成验证码以及刷新验证码进行修改,代码如下:

 <div class="form-group row mt-4">
                    <label for="verifycode" class="col-sm-2 col-form-label text-right">验证码:</label>
                    <div class="col-sm-6">
                        <input type="text" class="form-control is-invalid" id="verifycode" placeholder="请输入验证码">
                        <div class="invalid-feedback">
                            验证码不正确!
                        </div>
                    </div>
                    <div class="col-sm-4">
                        <img th:src="@{/kaptcha}" id="kaptcha" style="width:100px;height:40px;" class="mr-2"/>
                        <a href="javascript:refresh_kaptcha();" class="font-size-12 align-bottom">刷新验证码</a>
                    </div>
 </div>

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

 

5. 功能测试

在这里插入图片描述

点击刷新验证码,可以变化验证码:
在这里插入图片描述

 

五、开发登陆、退出功能

  • 访问登录页面

    • 点击顶部区域内的链接,打开登录页面。
  • 登录

    • 验证账号、密码、验证码。
    • 成功时,生成登录凭证,发放给客户端。
    • 失败时,跳转回登录页。
  • 退出

    • 将登录凭证修改为失效状态。
    • 跳转至网站首页。

1. 实体类

用户完成注册或者用户登录后,系统会给每个用户一个对应的登录凭证,用 LoginTicket 封装,在enity 包下创建 LoginTicket 类,代码如下:

public class LoginTicket {

    private  int id;
    private int userId;
    private String ticket;
    private int status; // 0:有效  1:无效
    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 +
                '}';
    }
}

 

2. 工具类

CommunityConstant 类中添加默认状态的登录凭证超时时间和记住状态的登录凭证超时时间,代码如下:

public interface CommunityConstant {

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

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

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

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

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

 

3. dao层

dao 包下创建 LoginTicketMapper 类,实现对 login_ticket 表的增删改查,这里以注解的形式进行编码,就省去了在 mapper 包下创建 LoginTicket-Mapper.xml,代码如下:

@Mapper
public interface LoginTicketMapper {

    // 这里是用注解的方式进行编码

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

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

    @Update({
            "<script>",
            "update login_ticket set status=#{status} where ticket=#{ticket} ",
            "<if test=\"ticket!=null\"> ",
            "and 1=1 ",
            "</if>",
            "</script>"
    })
    int updateStatus(String ticket, int status);
}

 

4. service层

UserService 类中添加 loginlogout方法来分别表示提供登录和退出登录的功能,代码如下:

@Service
public class UserService implements CommunityConstant {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private MailClient mailClient;

    @Autowired
    private TemplateEngine templateEngine;

    @Autowired
    private LoginTicketMapper loginTicketMapper;

    @Value("${community.path.domain}")
    private String domain;

    @Value("${server.servlet.context-path}")
    private String contextPath;

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

    public Map<String, Object> register(User user) {
        Map<String, Object> map = new HashMap<>();

        // 空值处理
        if (user == null) {
            throw new IllegalArgumentException("参数不能为空!");
        }
        if (StringUtils.isBlank(user.getUsername())) {
            map.put("usernameMsg", "账号不能为空!");
            return map;
        }
        if (StringUtils.isBlank(user.getPassword())) {
            map.put("passwordMsg", "密码不能为空!");
            return map;
        }
        if (StringUtils.isBlank(user.getEmail())) {
            map.put("emailMsg", "邮箱不能为空!");
            return map;
        }

        // 验证账号
        User u = userMapper.selectByName(user.getUsername());
        if (u != null) {
            map.put("usernameMsg", "该账号已存在!");
            return map;
        }

        // 验证邮箱
        u = userMapper.selectByEmail(user.getEmail());
        if (u != null) {
            map.put("emailMsg", "该邮箱已被注册!");
            return map;
        }

        // 注册用户
        user.setSalt(CommunityUtil.generateUUID().substring(0, 5));
        user.setPassword(CommunityUtil.md5(user.getPassword() + user.getSalt()));
        user.setType(0);
        user.setStatus(0);
        user.setActivationCode(CommunityUtil.generateUUID());
        user.setHeaderUrl(String.format("http://images.nowcoder.com/head/%dt.png", new Random().nextInt(1000)));
        user.setCreateTime(new Date());
        userMapper.insertUser(user);

        // 激活邮件
        Context context = new Context();
        context.setVariable("email", user.getEmail());
        // http://localhost:8080/community/activation/101/code
        String url = domain + contextPath + "/activation/" + user.getId() + "/" + user.getActivationCode();
        context.setVariable("url", url);
        String content = templateEngine.process("/mail/activation", context);
        mailClient.sendMail(user.getEmail(), "激活账号", content);

        return map;
    }

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

    // 登录
    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;

    }

    // 退出登录
    public void logout(String ticket) {
        loginTicketMapper.updateStatus(ticket, 1);
    }

}

 

5. controller层

LoginController 类中添加 loginlogout 方法来捕获用户的发出的登录和退出登录的请求,代码如下:

@Controller
public class LoginController implements CommunityConstant {

    private static final Logger logger = LoggerFactory.getLogger(LoginController.class);

    @Autowired
    private UserService userService;

    @Autowired
    private Producer kaptchaProducer;

    @Value("${server.servlet.context-path}")
    private String  contextPath;

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

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


    @RequestMapping(path = "/register", method = RequestMethod.POST)
    public String register(Model model, User user) throws IllegalAccessException {
        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";
        }

    }




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

        return "/site/operate-result";
    }




    @RequestMapping(path = "/kaptcha", method = RequestMethod.GET)
    public void getKaptcha(HttpServletResponse response, HttpSession session) {
        // 生成验证码
        String text = kaptchaProducer.createText();
        BufferedImage image = kaptchaProducer.createImage(text);

        // 将验证码存入session
        session.setAttribute("kaptcha", text);

        // 将图片输出给浏览器
        response.setContentType("image/png");
        try {
            OutputStream os = response.getOutputStream();
            ImageIO.write(image, "png", os);
        } catch (IOException e) {
            logger.error("响应验证码失败:" + e.getMessage());
        }
    }


    @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生效时间
            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";
        }
    }

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

}

 

6. View层

index.htmllogin.html 中部分代码进行修改,相关修改如下:

login.html 页面的 修改如下:

    <!-- 内容 -->
    <div class="main">
        <div class="container pl-5 pr-5 pt-3 pb-3 mt-3 mb-3">
            <h3 class="text-center text-info border-bottom pb-3">登&nbsp;&nbsp;录</h3>
            <form class="mt-5" method="post" th:action="@{/login}">
                <div class="form-group row">
                    <label for="username" class="col-sm-2 col-form-label text-right">账号:</label>
                    <div class="col-sm-10">
                        <input type="text" th:class="|form-control ${usernameMsg!=null?'is-invalid':''}|"
                               th:value="${param.username}"
                               id="username" name="username" placeholder="请输入您的账号!" required>
                        <div class="invalid-feedback" th:text="${usernameMsg}">
                            该账号不存在!
                        </div>
                    </div>
                </div>
                <div class="form-group row mt-4">
                    <label for="password" class="col-sm-2 col-form-label text-right">密码:</label>
                    <div class="col-sm-10">
                        <input type="password" th:class="|form-control ${passwordMsg!=null?'is-invalid':''}|"
                               th:value="${param.password}"
                               id="password" name="password" placeholder="请输入您的密码!" required>
                        <div class="invalid-feedback" th:text="${passwordMsg}">
                            密码长度不能小于8位!
                        </div>
                    </div>
                </div>
                <div class="form-group row mt-4">
                    <label for="verifycode" class="col-sm-2 col-form-label text-right">验证码:</label>
                    <div class="col-sm-6">
                        <input type="text" th:class="|form-control ${codeMsg!=null?'is-invalid':''}|"
                               id="verifycode" name="code" placeholder="请输入验证码!">
                        <div class="invalid-feedback" th:text="${codeMsg}">
                            验证码不正确!
                        </div>
                    </div>
                    <div class="col-sm-4">
                        <img th:src="@{/kaptcha}" id="kaptcha" style="width:100px;height:40px;" class="mr-2"/>
                        <a href="javascript:refresh_kaptcha();" class="font-size-12 align-bottom">刷新验证码</a>
                    </div>
                </div>
                <div class="form-group row mt-4">
                    <div class="col-sm-2"></div>
                    <div class="col-sm-10">
                        <input type="checkbox" id="remember-me" name="rememberme"
                               th:checked="${param.rememberme}">
                        <label class="form-check-label" for="remember-me">记住我</label>
                        <a href="forget.html" class="text-danger float-right">忘记密码?</a>
                    </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>
        </div>
    </div>

 

index.html页面的修改如下:
在这里插入图片描述
 

7. 功能测试

启动项目后,点击进入登录页面,输入注册完成的账号、密码以及验证码进行登录:
在这里插入图片描述
 

点击立即登录后将会跳转到首页:
在这里插入图片描述
 
也可以点击退出登录来退出当前的登录状态,点击后将返回登录页面:
在这里插入图片描述
在这里插入图片描述
 

六、显示登陆信息

  • 拦截器示例

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

    
    - 在请求开始时查询登录用户
    - 在本次请求中持有用户数据
    - 在模板视图上显示用户数据
    - 在请求结束时清理用户数据
    

    1. 工具类

    util 包下创建 CookieUtil 类用于获取 Cookie 的方法,代码如下:

    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;

}

&nbsp;

在 `util` 包下创建 `HostHolder` 类,该方法持有用户信息,用户代替 `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();
}

}

&nbsp;

## 2. 添加拦截器
创建 `interceptor` 包,在该包下创建 `LoginTicketInterceptor` 拦截器。
在 `LoginTicketInterceptor` 拦截器作用的影响下,浏览器端每发送一次请求,`LoginTicketInterceptor`都会拦截请求,先执行 `preHandler` 方法,再执行 `postHandler` 方法,再执行请求对应的控制器方法,最后执行 `afterCompletion `方法。

- `preHandle` 方法:从 `cookie` 中获取凭证 `ticket`,再去查验凭证,如果凭证合法的话就根据 `userId`在数据库中查到 `user` 实体类,将 `user`实体类存到 `hostHolder` 中。
- `postHandle`方法: 从 `hostHolder` 获取 `user` 实体类,再将 `user` 实体类添加到 `modelAndview`中,这样整个请求都持有用户信息。
- `afterCompletion` 方法:浏览器端一次请求结束后,将 `hostHolder` 中的用户信息清除。

代码如下:

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

}

&nbsp;

## 3. 配置拦截器
创建 `config` 包,在该报下创建 `WebMvcConfig` 类,代码如下:

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

@Autowired
private AlphaInterceptor alphaInterceptor;

@Autowired
private LoginTicketInterceptor loginTicketInterceptor;

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

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

}

}

&nbsp;

## 4. service层
在 `UserService` 中添加 `findLoginTicket`方法,核心代码如下:

public LoginTicket findLoginTicket(String ticket) {

    return loginTicketMapper.selectByTicket(ticket);

}

&nbsp;

                               <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" action="site/search.html">
                       <input class="form-control mr-sm-2" type="search" aria-label="Search" />
                       <button class="btn btn-outline-light my-2 my-sm-0" type="submit">搜索</button>
                   </form>
</div>

 

6. 功能测试

登录前的页面如下:
在这里插入图片描述

 
登陆后的页面如下:
在这里插入图片描述
 

七、检查登录状态

  • 使用拦截器

    • 在方法前标注自定义注解
    • 拦截所有请求,只处理带有该注解打方法
  • 自定义注解

    • 常用的元注解:

    @Target@Retention@Document@Inherited

    • 如何读取注解:

    Method.getDeclaredAnnotations()
    Method.getAnnotation(Class<T> annotationClass)

1. 创建自定义注解

在工程中创建 annoatation 包,在此包下创建 LoginRequired注解,代码如下:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginRequired {

}

 

2. 编写登录拦截器

interceptor 包中创建LoginRequiredInterceptor 类,代码如下:

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

 

3. 配置拦截器

WebMvcConfig 类中配置 LoginRequiredInterceptor 拦截器,代码如下:

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private AlphaInterceptor alphaInterceptor;

    @Autowired
    private LoginTicketInterceptor loginTicketInterceptor;

    @Autowired
    private LoginRequiredInterceptor loginRequiredInterceptor;

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

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

        registry.addInterceptor(loginRequiredInterceptor)
                .excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");

    }

 

4. controller层中加入注解

UserContoller 中的 getSettingPageuploadHeader添加LoginRequired注解,代码如下:

@Controller
@RequestMapping("/user")
public class UserController {

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

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

        // 更新当前用户头像的路径
        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());
        }

    }

 

5. 功能测试

在未登录的情况下访问 http://localhost:8080/community/user/setting,回车后返回登录页面:
在这里插入图片描述
在这里插入图片描述

 
 

创作不易,如果有帮助到你,请给文章==点个赞和收藏==,让更多的人看到!!!
==关注博主==不迷路,内容持续更新中。

目录
相关文章
|
6月前
|
移动开发 小程序 JavaScript
(一)、项目介绍及知识点概述【uniapp+uinicloud多用户社区博客实战项目(完整开发文档-从零到完整项目)】
(一)、项目介绍及知识点概述【uniapp+uinicloud多用户社区博客实战项目(完整开发文档-从零到完整项目)】
99 0
|
5月前
|
JSON 程序员 Swift
技术好文:Swit项目
技术好文:Swit项目
24 0
|
6月前
|
设计模式 存储 算法
C++从入门到精通:3.7阅读和理解开源项目——在实际项目中应用C++编程技巧和规范
C++从入门到精通:3.7阅读和理解开源项目——在实际项目中应用C++编程技巧和规范
125 0
|
前端开发 Java 关系型数据库
仿牛客社区项目(第一章)
仿牛客社区项目(第一章)
349 0
仿牛客社区项目(第一章)
|
XML 前端开发 JavaScript
仿牛客社区项目(第三章)(总)
仿牛客社区项目(第三章)(总)
231 0
仿牛客社区项目(第三章)(总)
|
Java 测试技术 编译器
仿牛客社区项目(第三章)(下)
仿牛客社区项目(第三章)(下)
139 0
仿牛客社区项目(第三章)(下)
|
XML JSON 前端开发
仿牛客社区项目(第三章)(上)
仿牛客社区项目(第三章)(上)
112 0
仿牛客社区项目(第三章)(上)
|
XML Java 测试技术
仿牛客社区项目(第三章)(中)
仿牛客社区项目(第三章)(中)
101 0
仿牛客社区项目(第三章)(中)
|
存储 缓存 NoSQL
仿牛客社区项目(第四章)(总)
仿牛客社区项目(第四章)(总)
192 0
仿牛客社区项目(第四章)(总)
|
消息中间件 存储 缓存
仿牛客社区项目(第四章)(上)
仿牛客社区项目(第四章)(上)
74 0
仿牛客社区项目(第四章)(上)