• 仿牛客网项目---用户注册登录功能的实现


    从今天开始我们来写一个新项目,这个项目是一个完整的校园论坛的项目。主要功能模块:用户登录注册,帖子发布和热帖排行,点赞关注,发送私信,消息通知,社区搜索等。这篇文章我们先试着写一下用户的登录注册功能。

    我们做web项目,一般web项目是主要解决浏览器和服务器之间交互的问题。而浏览器和服务器是由一次一次的请求交互的。因此,任何功能都可拆解成若干次请求,其实只要掌握好每次请求的执行过程,按照步骤开发每一次请求,基本上项目就可以逐步完善起来。

    一次请求的执行过程:

    其实最好是可以把功能做拆解,第一步先实现什么效果,第二步再完善这个效果。因为你不可能一下子就一步到位写出完整的代码的,是吧?

    所以这就是我的思路,我是想着一个功能一个功能的搞定。这篇文章来搞定用户登录注册。

    用户登录注册功能,说实话,这个功能是真的比较复杂的,所以我们必须把这个功能拆解成若干部分,使得每部分都简单点,易于开发。对于web项目来说,每个功能都可以按照请求去拆解,因为每个功能都是由若干个浏览器和服务器的请求交互所构成的。比如注册功能,怎么访问和使用呢?比如你首页点击“注册”这个链接,就能打开注册的页面,那这个很显然就是访问服务器,就有一次请求,只不过这次请求就是打开页面而已,比较简单。第二个请求就是填写数据(填账号密码邮箱这些),然后点击“注册”按钮,那么这个数据就会提交给服务器,服务器就把这些数据存起来。之后,服务器就会给你一个邮件,邮件里面是链接,你点击那个链接就可以激活账号了,事实上,你点链接也是访问服务器,进行激活的服务。我们在写代码时还是按照DAO->Service->Controller来开发,但是有些功能可能没有DAO层,有些功能可能没有Service层,这都很正常。比如第一次请求:访问注册页面。你就访问个注册页面而已,哪有什么业务,什么访问数据库这些。

    OK!接下来就开始写了!

    用户注册登录模块大致结构

    entity文件夹:User.java LoginTicket.java

    DAO层:UserMapper   LoginMapper

    Service层:UserService

    Controller层:LoginController

    entity文件夹---User.java

    下面是entity文件夹下一个名为User.java的实体类文件。该文件定义了一个名为User的实体类,用于表示用户的信息。

    1. public class User {
    2. private int id; //用户的唯一标识符
    3. private String username; //用户的用户名,用于登录和显示
    4. private String password; //用户的密码,用于登录验证
    5. private String salt; //密码加密时使用的盐值,增加密码的安全性
    6. private String email; //用户的电子邮件地址
    7. private int type; //用户类型,用于区分不同类型的用户,例如普通用户、管理员等
    8. private int status; //用户的状态,表示用户的激活状态或禁用状态等
    9. private String activationCode; //用户的激活码,用于激活帐户或进行身份验证
    10. private String headerUrl; //用户头像的URL地址
    11. private Date createTime; //用户创建的时间戳,表示用户的注册时间或创建时间
    12. // 省略了getter和setter方法
    13. @Override
    14. public String toString() {
    15. return "User{" +
    16. "id=" + id +
    17. ", username='" + username + '\'' +
    18. ", password='" + password + '\'' +
    19. ", salt='" + salt + '\'' +
    20. ", email='" + email + '\'' +
    21. ", type=" + type +
    22. ", status=" + status +
    23. ", activationCode='" + activationCode + '\'' +
    24. ", headerUrl='" + headerUrl + '\'' +
    25. ", createTime=" + createTime +
    26. '}';
    27. }
    28. }

    这个实体类主要用于封装用户相关的信息,并在系统中进行传递和处理。它可以作为数据库表的映射实体类,也可以用于数据传输和业务逻辑操作。

    entity文件夹---LoginTicket.java

    下面是entity文件夹下一个名为LoginTicket.java的实体类文件。该文件定义了一个名为LoginTicket的实体类,用于表示登录凭证的信息。

    对于用户登录凭证,我之前一直搞不懂,所以就查百度,感觉有一个例子很好的解释了什么是吧用户登录凭证,我分享给大家:

    假设有一个在线购物网站,用户在该网站上注册账号并进行购物。当用户想要登录到他们的账号时,他们需要提供用户名和密码。在成功验证用户名和密码后,网站会颁发给用户一个登录凭证。

    在这个例子中,登录凭证可以是一个唯一的令牌或会话ID。这个凭证会与用户的账号进行关联,并在用户的浏览器中存储为一个Cookie或其他形式的标识。

    用户在登录凭证的帮助下,在一段时间内可以访问网站的受限资源,如个人购物车、订单历史记录等。每当用户访问需要身份验证的页面时,他们的凭证将被检查以验证其身份和权限。如果凭证有效且与用户账号匹配,用户将被授权访问相应的资源。

    在此过程中,登录凭证充当了用户的身份标识,用于验证用户的身份并授权其访问网站的特定功能和信息。凭证的生成和验证是网站实现用户认证和会话管理的重要部分。

    需要注意的是,每次用户登录时,会生成一个新的登录凭证,并在一段时间后过期。这样可以提高系统的安全性,并确保用户的身份唯一性。

    1. package com.nowcoder.community.entity;
    2. import java.util.Date;
    3. public class LoginTicket {
    4. private int id; //登录凭证的唯一标识符
    5. private int userId; //与登录凭证相关联的用户的ID
    6. private String ticket; //登录凭证的字符串值
    7. private int status; //登录凭证的状态,表示凭证的有效性或失效状态
    8. private Date expired; //登录凭证的过期时间,表示凭证的有效期限
    9. // 省略了getter和setter方法
    10. @Override
    11. public String toString() {
    12. return "LoginTicket{" +
    13. "id=" + id +
    14. ", userId=" + userId +
    15. ", ticket='" + ticket + '\'' +
    16. ", status=" + status +
    17. ", expired=" + expired +
    18. '}';
    19. }
    20. }

    这些属性太晦涩了,我举个例子帮助大家理解。

    • id:假设用户A在登录后生成了一个登录凭证,系统为该凭证分配了一个唯一的ID,比如1。
    • userId:用户A的ID是123,这个属性将与用户A的ID关联起来,表示登录凭证与用户A的关联。
    • ticket:系统生成了一个字符串作为登录凭证,比如"abc123",这个字符串将在用户登录时颁发给用户A。
    • status:登录凭证的状态属性,用于标识凭证的有效性。比如,0表示凭证失效,1表示凭证有效。
    • expired:登录凭证的过期时间,比如2024-03-01 00:00:00,表示凭证在这个时间之后将失效。

    DAO层---UserMapper

    下面的代码是一个名为UserMapper的数据访问对象(DAO)接口,用于与用户数据进行交互。该接口使用MyBatis注解进行映射,用于定义与用户数据相关的数据库操作方法。

    1. @Mapper
    2. public interface UserMapper {
    3. User selectById(int id);
    4. //根据用户ID查询用户信息,并返回一个User对象
    5. User selectByName(String username);
    6. //根据用户名查询用户信息,并返回一个User对象
    7. User selectByEmail(String email);
    8. //根据电子邮件地址查询用户信息,并返回一个User对象
    9. int insertUser(User user);
    10. //插入一个新用户的信息,并返回插入的行数
    11. int updateStatus(int id, int status);
    12. //更新用户的状态信息(例如激活状态),并返回更新的行数
    13. int updateHeader(int id, String headerUrl);
    14. //更新用户的头像URL信息,并返回更新的行数
    15. int updatePassword(int id, String password);
    16. //更新用户的密码信息,并返回更新的行数
    17. }

    "返回更新的行数"是指在执行数据库更新操作后,影响到的数据库行数。例如,如果执行更新操作后返回的值为1,则表示成功更新了一行数据。

    DAO层---LoginTicketMapper

    下面的代码是一个名为LoginTicketMapper的数据访问对象(DAO)接口,用于与登录凭证数据进行交互。该接口使用了MyBatis的注解来定义与数据库的交互操作。

    1. @Mapper
    2. @Deprecated
    3. public interface LoginTicketMapper {
    4. @Insert({
    5. "insert into login_ticket(user_id,ticket,status,expired) ",
    6. "values(#{userId},#{ticket},#{status},#{expired})"
    7. })
    8. @Options(useGeneratedKeys = true, keyProperty = "id")
    9. int insertLoginTicket(LoginTicket loginTicket);
    10. //向数据库中插入一条登录凭证的信息,并返回插入的行数。使用了@Insert注解定义了插入操作的SQL语句
    11. @Select({
    12. "select id,user_id,ticket,status,expired ",
    13. "from login_ticket where ticket=#{ticket}"
    14. })
    15. LoginTicket selectByTicket(String ticket);
    16. //根据登录凭证的字符串ticket从数据库中查询对应的登录凭证信息,并返回一个LoginTicket对象。使用了@Select注解定义了查询操作的SQL语句。
    17. @Update({
    18. ""
    19. })
    20. int updateStatus(String ticket, int status);
    21. //根据登录凭证的字符串ticket更新对应登录凭证的状态信息,并返回更新的行数。使用了@Update注解定义了更新操作的SQL语句。
    22. }

    Service层---UserService

    下面的代码名叫UserService,其中包含了用户注册、登录、激活、注销等功能的实现。

    1. @Service
    2. public class UserService implements CommunityConstant {
    3. //省略了一些导入包和导入依赖项的代码,直接写方法
    4. public User findUserById(int id) {
    5. // return userMapper.selectById(id);
    6. User user = getCache(id);
    7. if (user == null) {
    8. user = initCache(id);
    9. }
    10. return user;
    11. }
    12. public Map register(User user) {
    13. Map map = new HashMap<>();
    14. // 空值处理
    15. if (user == null) {
    16. throw new IllegalArgumentException("参数不能为空!");
    17. }
    18. if (StringUtils.isBlank(user.getUsername())) {
    19. map.put("usernameMsg", "账号不能为空!");
    20. return map;
    21. }
    22. if (StringUtils.isBlank(user.getPassword())) {
    23. map.put("passwordMsg", "密码不能为空!");
    24. return map;
    25. }
    26. if (StringUtils.isBlank(user.getEmail())) {
    27. map.put("emailMsg", "邮箱不能为空!");
    28. return map;
    29. }
    30. // 验证账号
    31. User u = userMapper.selectByName(user.getUsername());
    32. if (u != null) {
    33. map.put("usernameMsg", "该账号已存在!");
    34. return map;
    35. }
    36. // 验证邮箱
    37. u = userMapper.selectByEmail(user.getEmail());
    38. if (u != null) {
    39. map.put("emailMsg", "该邮箱已被注册!");
    40. return map;
    41. }
    42. // 注册用户
    43. user.setSalt(CommunityUtil.generateUUID().substring(0, 5));
    44. user.setPassword(CommunityUtil.md5(user.getPassword() + user.getSalt()));
    45. user.setType(0);
    46. user.setStatus(0);
    47. user.setActivationCode(CommunityUtil.generateUUID());
    48. user.setHeaderUrl(String.format("http://images.nowcoder.com/head/%dt.png", new Random().nextInt(1000)));
    49. user.setCreateTime(new Date());
    50. userMapper.insertUser(user);
    51. // 激活邮件
    52. Context context = new Context();
    53. context.setVariable("email", user.getEmail());
    54. // http://localhost:8080/community/activation/101/code
    55. String url = domain + contextPath + "/activation/" + user.getId() + "/" + user.getActivationCode();
    56. context.setVariable("url", url);
    57. String content = templateEngine.process("/mail/activation", context);
    58. mailClient.sendMail(user.getEmail(), "激活账号", content);
    59. return map;
    60. }
    61. public int activation(int userId, String code) {
    62. User user = userMapper.selectById(userId);
    63. if (user.getStatus() == 1) {
    64. return ACTIVATION_REPEAT;
    65. } else if (user.getActivationCode().equals(code)) {
    66. userMapper.updateStatus(userId, 1);
    67. clearCache(userId);
    68. return ACTIVATION_SUCCESS;
    69. } else {
    70. return ACTIVATION_FAILURE;
    71. }
    72. }
    73. public Map login(String username, String password, long expiredSeconds) {
    74. Map map = new HashMap<>();
    75. // 空值处理
    76. if (StringUtils.isBlank(username)) {
    77. map.put("usernameMsg", "账号不能为空!");
    78. return map;
    79. }
    80. if (StringUtils.isBlank(password)) {
    81. map.put("passwordMsg", "密码不能为空!");
    82. return map;
    83. }
    84. // 验证账号
    85. User user = userMapper.selectByName(username);
    86. if (user == null) {
    87. map.put("usernameMsg", "该账号不存在!");
    88. return map;
    89. }
    90. // 验证状态
    91. if (user.getStatus() == 0) {
    92. map.put("usernameMsg", "该账号未激活!");
    93. return map;
    94. }
    95. // 验证密码
    96. password = CommunityUtil.md5(password + user.getSalt());
    97. if (!user.getPassword().equals(password)) {
    98. map.put("passwordMsg", "密码不正确!");
    99. return map;
    100. }
    101. // 生成登录凭证
    102. LoginTicket loginTicket = new LoginTicket();
    103. loginTicket.setUserId(user.getId());
    104. loginTicket.setTicket(CommunityUtil.generateUUID());
    105. loginTicket.setStatus(0);
    106. loginTicket.setExpired(new Date(System.currentTimeMillis() + expiredSeconds * 1000));
    107. // loginTicketMapper.insertLoginTicket(loginTicket);
    108. String redisKey = RedisKeyUtil.getTicketKey(loginTicket.getTicket());
    109. redisTemplate.opsForValue().set(redisKey, loginTicket);
    110. map.put("ticket", loginTicket.getTicket());
    111. return map;
    112. }
    113. public void logout(String ticket) {
    114. // loginTicketMapper.updateStatus(ticket, 1);
    115. String redisKey = RedisKeyUtil.getTicketKey(ticket);
    116. LoginTicket loginTicket = (LoginTicket) redisTemplate.opsForValue().get(redisKey);
    117. loginTicket.setStatus(1);
    118. redisTemplate.opsForValue().set(redisKey, loginTicket);
    119. }
    120. public LoginTicket findLoginTicket(String ticket) {
    121. // return loginTicketMapper.selectByTicket(ticket);
    122. String redisKey = RedisKeyUtil.getTicketKey(ticket);
    123. return (LoginTicket) redisTemplate.opsForValue().get(redisKey);
    124. }
    125. public int updateHeader(int userId, String headerUrl) {
    126. // return userMapper.updateHeader(userId, headerUrl);
    127. int rows = userMapper.updateHeader(userId, headerUrl);
    128. clearCache(userId);
    129. return rows;
    130. }
    131. public User findUserByName(String username) {
    132. return userMapper.selectByName(username);
    133. }
    134. // 1.优先从缓存中取值
    135. private User getCache(int userId) {
    136. String redisKey = RedisKeyUtil.getUserKey(userId);
    137. return (User) redisTemplate.opsForValue().get(redisKey);
    138. }
    139. // 2.取不到时初始化缓存数据
    140. private User initCache(int userId) {
    141. User user = userMapper.selectById(userId);
    142. String redisKey = RedisKeyUtil.getUserKey(userId);
    143. redisTemplate.opsForValue().set(redisKey, user, 3600, TimeUnit.SECONDS);
    144. return user;
    145. }
    146. // 3.数据变更时清除缓存数据
    147. private void clearCache(int userId) {
    148. String redisKey = RedisKeyUtil.getUserKey(userId);
    149. redisTemplate.delete(redisKey);
    150. }
    151. public Collectionextends GrantedAuthority> getAuthorities(int userId) {
    152. User user = this.findUserById(userId);
    153. List list = new ArrayList<>();
    154. list.add(new GrantedAuthority() {
    155. @Override
    156. public String getAuthority() {
    157. switch (user.getType()) {
    158. case 1:
    159. return AUTHORITY_ADMIN;
    160. case 2:
    161. return AUTHORITY_MODERATOR;
    162. default:
    163. return AUTHORITY_USER;
    164. }
    165. }
    166. });
    167. return list;
    168. }
    169. }
    1. 实现了findUserById方法,根据用户ID查找用户信息。在这个方法中,首先尝试从缓存中获取用户信息,如果缓存中没有,则从数据库中查询,并将结果存入缓存中。
    2. 实现了register方法,用于用户注册。在这个方法中,首先进行参数的空值处理和账号、邮箱的验证。然后设置用户的一些属性,如盐值、密码加密、激活码等,并将用户信息插入数据库中。最后发送激活邮件给用户。
    3. 实现了activation方法,用于用户账号激活。根据传入的用户ID和激活码,验证用户的激活状态和激活码是否匹配,如果匹配则更新用户状态为已激活,并清除缓存中的用户信息。
    4. 实现了login方法,用户用户登录。在这个方法中,首先进行参数的空值处理和账号、密码的验证。然后验证账号的状态和密码的正确性。最后生成登录凭证(LoginTicket),将凭证信息存入Redis中,并返回给客户端。
    5. 实现了logout方法,用户用户注销。根据传入的登录凭证,将凭证的状态更新为已注销,并在Redis中更新相应的信息。
    6. 实现了findLoginTicket方法,根据登录凭证查找登录凭证的信息。
    7. 实现了updateHeader方法,用于更新用户的头像信息。在更新用户头像后,清除缓存中的用户信息。
    8. 实现了findUserByName方法,根据用户名查找用户信息。
    9. 实现了getCache方法,用于从缓存中获取用户信息。
    10. 实现了initCache方法,用于初始化缓存中的用户信息。
    11. 实现了clearCache方法,用于清除缓存中的用户信息。
    12. 实现了getAuthorities方法,用于获取用户的权限信息这就是展示了一个用户登录注册功能中,用户服务类的实现,涵盖了用户注册、登录、激活、注销等功能,并使用了缓存和Redis存储登录凭证的信息。

    Controller层---LoginController

    1. @Controller
    2. public class LoginController implements CommunityConstant {
    3. //省略了一些配置依赖关系的的代码
    4. @RequestMapping(path = "/register", method = RequestMethod.GET)
    5. public String getRegisterPage() {
    6. return "/site/register";
    7. }
    8. @RequestMapping(path = "/login", method = RequestMethod.GET)
    9. public String getLoginPage() {
    10. return "/site/login";
    11. }
    12. @RequestMapping(path = "/register", method = RequestMethod.POST)
    13. public String register(Model model, User user) {
    14. Map map = userService.register(user);
    15. if (map == null || map.isEmpty()) {
    16. model.addAttribute("msg", "注册成功,我们已经向您的邮箱发送了一封激活邮件,请尽快激活!");
    17. model.addAttribute("target", "/index");
    18. return "/site/operate-result";
    19. } else {
    20. model.addAttribute("usernameMsg", map.get("usernameMsg"));
    21. model.addAttribute("passwordMsg", map.get("passwordMsg"));
    22. model.addAttribute("emailMsg", map.get("emailMsg"));
    23. return "/site/register";
    24. }
    25. }
    26. // http://localhost:8080/community/activation/101/code
    27. @RequestMapping(path = "/activation/{userId}/{code}", method = RequestMethod.GET)
    28. public String activation(Model model, @PathVariable("userId") int userId, @PathVariable("code") String code) {
    29. int result = userService.activation(userId, code);
    30. if (result == ACTIVATION_SUCCESS) {
    31. model.addAttribute("msg", "激活成功,您的账号已经可以正常使用了!");
    32. model.addAttribute("target", "/login");
    33. } else if (result == ACTIVATION_REPEAT) {
    34. model.addAttribute("msg", "无效操作,该账号已经激活过了!");
    35. model.addAttribute("target", "/index");
    36. } else {
    37. model.addAttribute("msg", "激活失败,您提供的激活码不正确!");
    38. model.addAttribute("target", "/index");
    39. }
    40. return "/site/operate-result";
    41. }
    42. @RequestMapping(path = "/kaptcha", method = RequestMethod.GET)
    43. public void getKaptcha(HttpServletResponse response/*, HttpSession session*/) {
    44. // 生成验证码
    45. String text = kaptchaProducer.createText();
    46. BufferedImage image = kaptchaProducer.createImage(text);
    47. // 将验证码存入session
    48. // session.setAttribute("kaptcha", text);
    49. // 验证码的归属
    50. String kaptchaOwner = CommunityUtil.generateUUID();
    51. Cookie cookie = new Cookie("kaptchaOwner", kaptchaOwner);
    52. cookie.setMaxAge(60);
    53. cookie.setPath(contextPath);
    54. response.addCookie(cookie);
    55. // 将验证码存入Redis
    56. String redisKey = RedisKeyUtil.getKaptchaKey(kaptchaOwner);
    57. redisTemplate.opsForValue().set(redisKey, text, 60, TimeUnit.SECONDS);
    58. // 将突图片输出给浏览器
    59. response.setContentType("image/png");
    60. try {
    61. OutputStream os = response.getOutputStream();
    62. ImageIO.write(image, "png", os);
    63. } catch (IOException e) {
    64. logger.error("响应验证码失败:" + e.getMessage());
    65. }
    66. }
    67. @RequestMapping(path = "/login", method = RequestMethod.POST)
    68. public String login(String username, String password, String code, boolean rememberme,
    69. Model model, /*HttpSession session, */HttpServletResponse response,
    70. @CookieValue("kaptchaOwner") String kaptchaOwner) {
    71. // 检查验证码
    72. // String kaptcha = (String) session.getAttribute("kaptcha");
    73. String kaptcha = null;
    74. if (StringUtils.isNotBlank(kaptchaOwner)) {
    75. String redisKey = RedisKeyUtil.getKaptchaKey(kaptchaOwner);
    76. kaptcha = (String) redisTemplate.opsForValue().get(redisKey);
    77. }
    78. if (StringUtils.isBlank(kaptcha) || StringUtils.isBlank(code) || !kaptcha.equalsIgnoreCase(code)) {
    79. model.addAttribute("codeMsg", "验证码不正确!");
    80. return "/site/login";
    81. }
    82. // 检查账号,密码
    83. int expiredSeconds = rememberme ? REMEMBER_EXPIRED_SECONDS : DEFAULT_EXPIRED_SECONDS;
    84. Map map = userService.login(username, password, expiredSeconds);
    85. if (map.containsKey("ticket")) {
    86. Cookie cookie = new Cookie("ticket", map.get("ticket").toString());
    87. cookie.setPath(contextPath);
    88. cookie.setMaxAge(expiredSeconds);
    89. response.addCookie(cookie);
    90. return "redirect:/index";
    91. } else {
    92. model.addAttribute("usernameMsg", map.get("usernameMsg"));
    93. model.addAttribute("passwordMsg", map.get("passwordMsg"));
    94. return "/site/login";
    95. }
    96. }
    97. @RequestMapping(path = "/logout", method = RequestMethod.GET)
    98. public String logout(@CookieValue("ticket") String ticket) {
    99. userService.logout(ticket);
    100. SecurityContextHolder.clearContext();
    101. return "redirect:/login";
    102. }
    103. }

    这段代码也是很长,狗屎一样,我就简单说说这段代码大概的意思吧。

    这段代码中使用了UserService来处理用户相关的业务逻辑,使用了Producer来生成验证码图片,以及使用了RedisTemplate来操作Redis数据库。

    1. /register和/login:这两个GET请求的处理方法返回注册页面和登录页面的视图。

    2. /register(POST请求):该方法处理用户注册的请求。根据用户提供的信息进行注册,并返回相应的结果。如果注册成功,会向用户的邮箱发送一封激活邮件。

    3. /activation/{userId}/{code}(GET请求):该方法处理用户账号激活的请求。通过传入的userId和code参数进行账号激活,并返回相应的结果。

    4. /kaptcha(GET请求):该方法生成验证码图片,并将验证码存入Redis和浏览器的Cookie中,以便后续验证。

    5. /login(POST请求):该方法处理用户登录的请求。首先检查验证码的正确性,然后根据用户提供的用户名和密码进行登录。如果登录成功,会生成一个ticket凭证,并将凭证存入浏览器的Cookie中,并重定向到首页。

    6. /logout(GET请求):该方法处理用户退出登录的请求。清除用户的登录凭证(ticket)并重定向到登录页面。

  • 相关阅读:
    跨语言调用C#代码的新方式-DllExport
    2020 ICPC银川 个人题解
    UiPath实战(10) - 往数据表(DataTable)中插入数据
    Python之文件处理-JSON文件
    Centos配置samba文件共享服务器
    Spring5学习笔记05--BeanFactory后处理器
    Docker
    美创数据安全管理平台获信通院“数据安全产品能力验证计划”评测证书
    Kafka消费组无法消费问题排查实战
    莫慌!Java 多商户外贸版系统这不就来了么
  • 原文地址:https://blog.csdn.net/qq_54432917/article/details/136345354