• 怎么实现一个登录时需要输入验证码的功能


    今天给项目换了一个登录页面,而这个登录页面设计了验证码,于是想着把这个验证码功能实现一下。

    这篇文章就如何实现登录时的验证码的验证功能结合代码进行详细地介绍,以及介绍功能实现的思路,文章不是一次写成的,中间考虑可能出现的问题,最后给出了一个比较稳定的版本。

    目录

    一、页面效果

    二、实现思路

    三、具体代码实现

    1、准备工作

    创建项目

    添加依赖

    修改配置

    2、功能实现

    后端代码

    CaptchaController.java

    UserLoginDTO.java

    UserController.java

    UserService.java

    UserServiceImpl.java

    前端代码

    login.html

    login.js


    一、页面效果

    登录的时候会把用户名、密码和验证码一起传到后端,并对验证码进行验证,只有验证码正确才能登录。

    二、实现思路

    那么,具体是如何实现的呢,首先大概介绍一下我实现这个功能的思路:

    • 验证码图片的url由后端的一个Controller生成,前端请求这个Controller接口的时候根据当前时间生成一个uuid,并把这个uuid在前端使用localStorage缓存起来,下一次还是从缓存中获取。
    • Controller生成验证码之后,把前端传过来的uuid通过redis缓存起来,这里分两次缓存
      • 缓存uuid
      • 以uuid为key,缓存验证码
    • 这样,当点击登录按钮将数据提交到后台登录接口时,会从redis中获取uuid,然后通过这个uuid去获取验证码,和前端用户输入的验证码进行比较。

    潜在问题:这样的设计可能会导致以下问题

    • 多个用户同一时间访问登录页面,导致生成的uuid一样,数据会互相覆盖;
    • uuid这个key被其他用户修改;

    改进方案

    • 前端生成随机的uuid,并缓存到localStorage,登陆的时候也把这个uuid传到后端,这样就解决了key重复和key被覆盖的问题。

    三、具体代码实现

    1、准备工作

    创建项目

    为了保存代码,在idea中新建一个springboot项目

    创建好的项目目录结构

    添加依赖

    删除多余的文件及文件夹(可选),在pom.xml中添加必要的依赖~

    1. "1.0" encoding="UTF-8"?>
    2. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    3. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    4. <modelVersion>4.0.0modelVersion>
    5. <parent>
    6. <groupId>org.springframework.bootgroupId>
    7. <artifactId>spring-boot-starter-parentartifactId>
    8. <version>2.5.9version>
    9. <relativePath />
    10. parent>
    11. <groupId>cn.edu.sgu.wwwgroupId>
    12. <artifactId>login-captchaartifactId>
    13. <version>0.0.1-SNAPSHOTversion>
    14. <name>login-captchaname>
    15. <description>Java实现登录验证码功能description>
    16. <properties>
    17. <java.version>1.8java.version>
    18. properties>
    19. <dependencies>
    20. <dependency>
    21. <groupId>org.springframework.bootgroupId>
    22. <artifactId>spring-boot-starter-webartifactId>
    23. dependency>
    24. <dependency>
    25. <groupId>org.springframework.bootgroupId>
    26. <artifactId>spring-boot-starter-validationartifactId>
    27. dependency>
    28. <dependency>
    29. <groupId>org.springframework.bootgroupId>
    30. <artifactId>spring-boot-starter-data-redisartifactId>
    31. dependency>
    32. <dependency>
    33. <groupId>org.projectlombokgroupId>
    34. <artifactId>lombokartifactId>
    35. <version>1.18.22version>
    36. dependency>
    37. <dependency>
    38. <groupId>com.github.whvcsegroupId>
    39. <artifactId>easy-captchaartifactId>
    40. <version>1.6.2version>
    41. dependency>
    42. dependencies>
    43. <build>
    44. <plugins>
    45. <plugin>
    46. <groupId>org.springframework.bootgroupId>
    47. <artifactId>spring-boot-maven-pluginartifactId>
    48. plugin>
    49. plugins>
    50. build>
    51. project>

    修改配置

    修改application.yml

    1. server:
    2. port: 8888
    3. logging:
    4. level:
    5. springfox: error
    6. cn.edu.sgu.www.logincaptcha: debug
    7. spring:
    8. application:
    9. name: login-captcha
    10. redis:
    11. port: 6379
    12. host: localhost
    13. timeout: PT5s
    14. # password: mhxy1218
    15. # 验证码图片设置
    16. captcha:
    17. image:
    18. width: 66
    19. height: 22

    最后,把必要的一些基础类复制过来。

    2、功能实现

    后端代码

    CaptchaController.java

    前端登录页面生成随机验证码的控制器类。

    1. package cn.edu.sgu.www.logincaptcha.controller;
    2. import cn.edu.sgu.www.logincaptcha.consts.RedisKeyPrefixes;
    3. import cn.edu.sgu.www.logincaptcha.exception.GlobalException;
    4. import cn.edu.sgu.www.logincaptcha.property.CaptchaImageProperties;
    5. import cn.edu.sgu.www.logincaptcha.redis.StringRedisUtils;
    6. import cn.edu.sgu.www.logincaptcha.restful.ResponseCode;
    7. import com.wf.captcha.GifCaptcha;
    8. import com.wf.captcha.SpecCaptcha;
    9. import com.wf.captcha.base.Captcha;
    10. import lombok.extern.slf4j.Slf4j;
    11. import org.springframework.beans.factory.annotation.Autowired;
    12. import org.springframework.web.bind.annotation.RequestMapping;
    13. import org.springframework.web.bind.annotation.RequestMethod;
    14. import org.springframework.web.bind.annotation.RequestParam;
    15. import org.springframework.web.bind.annotation.RestController;
    16. import org.springframework.web.context.request.RequestAttributes;
    17. import org.springframework.web.context.request.RequestContextHolder;
    18. import org.springframework.web.context.request.ServletRequestAttributes;
    19. import javax.servlet.http.HttpServletResponse;
    20. import java.io.IOException;
    21. import java.util.concurrent.TimeUnit;
    22. /**
    23. * @author heyunlin
    24. * @version 1.0
    25. */
    26. @Slf4j
    27. @RestController
    28. @RequestMapping(value = "/captcha", produces = "application/json;charset=utf-8")
    29. public class CaptchaController {
    30. private final StringRedisUtils stringRedisUtils;
    31. private final CaptchaImageProperties captchaImageProperties;
    32. @Autowired
    33. public CaptchaController(StringRedisUtils stringRedisUtils, CaptchaImageProperties captchaImageProperties) {
    34. this.stringRedisUtils = stringRedisUtils;
    35. this.captchaImageProperties = captchaImageProperties;
    36. }
    37. /**
    38. * 生成验证码
    39. * @param type 验证码图片类型
    40. * @param uuid 前端生成的uuid
    41. */
    42. @RequestMapping(value = "/generate", method = RequestMethod.GET)
    43. public void generate(@RequestParam String type, @RequestParam String uuid) throws IOException {
    44. Captcha captcha;
    45. Integer width = captchaImageProperties.getWidth();
    46. Integer height = captchaImageProperties.getHeight();
    47. switch (type) {
    48. case "png":
    49. captcha = new SpecCaptcha(width, height);
    50. break;
    51. case "gif":
    52. captcha = new GifCaptcha(width, height);
    53. break;
    54. default:
    55. throw new GlobalException(ResponseCode.BAD_REQUEST, "不合法的验证码类型:" + type);
    56. }
    57. captcha.setLen(4);
    58. captcha.setCharType(Captcha.TYPE_DEFAULT);
    59. String code = captcha.text();
    60. log.debug("生成的验证码:{}", code);
    61. // 根据uuid拼接前缀得到验证码的key
    62. String key = RedisKeyPrefixes.PREFIX_CAPTCHA + uuid;
    63. // 缓存验证码
    64. stringRedisUtils.set(key , code);
    65. // 设置验证码3分钟后过期
    66. stringRedisUtils.expire(key, 3, TimeUnit.MINUTES);
    67. // 获取HttpServletResponse对象
    68. HttpServletResponse response = getResponse();
    69. // 设置响应头
    70. response.setContentType("image/" + type);
    71. response.setDateHeader("Expires", 0);
    72. response.setHeader("Pragma", "No-cache");
    73. response.setHeader("Cache-Control", "no-cache");
    74. // 输出图片流
    75. captcha.out(response.getOutputStream());
    76. }
    77. /**
    78. * 获取HttpServletResponse对象
    79. * @return HttpServletResponse
    80. */
    81. private HttpServletResponse getResponse() {
    82. RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
    83. if (attributes != null ) {
    84. HttpServletResponse response = ((ServletRequestAttributes) attributes).getResponse();
    85. if (response != null) {
    86. // 设置内容类型为json
    87. response.setContentType("application/json;charset=utf-8");
    88. return response;
    89. }
    90. }
    91. throw new GlobalException(ResponseCode.ERROR, "获取response对象失败");
    92. }
    93. }

    UserLoginDTO.java

    在项目根目录下创建dto包,然后新建一个UserLoginDTO.java来接受前端的数据。

    1. package cn.edu.sgu.www.logincaptcha.dto;
    2. import lombok.Data;
    3. import javax.validation.constraints.NotEmpty;
    4. import java.io.Serializable;
    5. /**
    6. * @author heyunlin
    7. * @version 1.0
    8. */
    9. @Data
    10. public class UserLoginDTO implements Serializable {
    11. private static final long serialVersionUID = 18L;
    12. /**
    13. * uuid:随机字符串
    14. */
    15. @NotEmpty(message = "验证码已过期,请重新获取")
    16. private String uuid;
    17. /**
    18. * 验证码
    19. */
    20. @NotEmpty(message = "验证码不允许为空")
    21. private String code;
    22. /**
    23. * 用户名
    24. */
    25. @NotEmpty(message = "用户名不允许为空")
    26. private String username;
    27. /**
    28. * 密码
    29. */
    30. @NotEmpty(message = "密码不允许为空")
    31. private String password;
    32. }

    UserController.java
    1. package cn.edu.sgu.www.logincaptcha.controller;
    2. import cn.edu.sgu.www.logincaptcha.dto.UserLoginDTO;
    3. import cn.edu.sgu.www.logincaptcha.restful.JsonResult;
    4. import cn.edu.sgu.www.logincaptcha.service.UserService;
    5. import org.springframework.beans.factory.annotation.Autowired;
    6. import org.springframework.validation.annotation.Validated;
    7. import org.springframework.web.bind.annotation.RequestMapping;
    8. import org.springframework.web.bind.annotation.RequestMethod;
    9. import org.springframework.web.bind.annotation.RestController;
    10. /**
    11. * @author heyunlin
    12. * @version 1.0
    13. */
    14. @RestController
    15. @RequestMapping(path = "/user", produces="application/json;charset=utf-8")
    16. public class UserController {
    17. private final UserService userService;
    18. @Autowired
    19. public UserController(UserService userService) {
    20. this.userService = userService;
    21. }
    22. /**
    23. * 登录认证
    24. * @param loginDTO UserLoginDTO
    25. * @return JsonResult
    26. */
    27. @RequestMapping(value = "/login", method = RequestMethod.POST)
    28. public JsonResult login(@Validated UserLoginDTO loginDTO) {
    29. userService.login(loginDTO);
    30. return JsonResult.success();
    31. }
    32. }

    UserService.java
    1. package cn.edu.sgu.www.logincaptcha.service;
    2. import cn.edu.sgu.www.logincaptcha.dto.UserLoginDTO;
    3. /**
    4. * @author heyunlin
    5. * @version 1.0
    6. */
    7. public interface UserService {
    8. void login(UserLoginDTO loginDTO);
    9. }

    UserServiceImpl.java
    1. package cn.edu.sgu.www.logincaptcha.service.impl;
    2. import cn.edu.sgu.www.logincaptcha.consts.RedisKeyPrefixes;
    3. import cn.edu.sgu.www.logincaptcha.dto.UserLoginDTO;
    4. import cn.edu.sgu.www.logincaptcha.exception.GlobalException;
    5. import cn.edu.sgu.www.logincaptcha.redis.StringRedisUtils;
    6. import cn.edu.sgu.www.logincaptcha.restful.ResponseCode;
    7. import cn.edu.sgu.www.logincaptcha.service.UserService;
    8. import org.springframework.beans.factory.annotation.Autowired;
    9. import org.springframework.stereotype.Service;
    10. /**
    11. * @author heyunlin
    12. * @version 1.0
    13. */
    14. @Service
    15. public class UserServiceImpl implements UserService {
    16. private final StringRedisUtils stringRedisUtils;
    17. @Autowired
    18. public UserServiceImpl(StringRedisUtils stringRedisUtils) {
    19. this.stringRedisUtils = stringRedisUtils;
    20. }
    21. @Override
    22. public void login(UserLoginDTO loginDTO) {
    23. // 得到用户输入的验证码
    24. String code = loginDTO.getCode();
    25. // 获取正确的验证码
    26. String key = getCaptchaKey(loginDTO.getUuid());
    27. String realCode = stringRedisUtils.get(key);
    28. // 得到的验证码为空,则获取验证码到登录之间的时间已经过了3分钟,验证码过期已经被删除
    29. if (realCode == null) {
    30. throw new GlobalException(ResponseCode.BAD_REQUEST, "验证码已失效,请重新获取~");
    31. }
    32. // 验证码校验
    33. if (!code.equalsIgnoreCase(realCode)) {
    34. throw new GlobalException(ResponseCode.BAD_REQUEST, "验证码错误~");
    35. }
    36. /* 其它代码... */
    37. }
    38. /**
    39. * 拼接得到验证码的key
    40. * @param uuid 前端随机生成的uuid
    41. * @return String 验证码的key
    42. */
    43. private String getCaptchaKey(String uuid) {
    44. return RedisKeyPrefixes.PREFIX_CAPTCHA + uuid;
    45. }
    46. }

    前端代码

    login.html
    1. html>
    2. <html>
    3. <head>
    4. <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    5. <title>登录页面title>
    6. <link rel="stylesheet" href="/css/login.css">
    7. <script src="/js/jquery.min.js">script>
    8. <script src="/js/ajaxUtils.js">script>
    9. <script src="/js/localStorage.js">script>
    10. <script src="/js/login.js">script>
    11. head>
    12. <body style="overflow:hidden">
    13. <div class="pagewrap">
    14. <div class="main">
    15. <div class="header">div>
    16. <div class="content">
    17. <div class="con_left">div>
    18. <div class="con_right">
    19. <div class="con_r_top">
    20. <a href="javascript:" class="left">下载游戏a>
    21. <a href="javascript:" class="right">登录管理a>
    22. div>
    23. <ul>
    24. <li class="con_r_left" style="display:none;">
    25. <div class="erweima">
    26. <div class="qrcode">
    27. <div id="output">
    28. <img src="/images/mhxysy.png" />
    29. div>
    30. div>
    31. div>
    32. <div style="height:70px;">
    33. <p>扫码下载梦幻西游手游p>
    34. div>
    35. li>
    36. <li class="con_r_right" style="display:block;">
    37. <div>
    38. <div class="user">
    39. <div>
    40. <span class="user-icon">span>
    41. <input type="text" id="username" />
    42. div>
    43. <div>
    44. <span class="mima-icon">span>
    45. <input type="password" id="password" />
    46. div>
    47. <div>
    48. <span class="yzmz-icon">span>
    49. <input type="text" id="code" />  
    50. <img id="captcha" alt="看不清?点击更换" />
    51. div>
    52. div>
    53. <br>
    54. <button id="btn_Login" type="button">登 录button>
    55. div>
    56. li>
    57. ul>
    58. div>
    59. div>
    60. div>
    61. div>
    62. body>
    63. html>

    login.js
    1. /**
    2. * 禁止输入空格
    3. */
    4. function preventSpace() {
    5. let event = window.event;
    6. if(event.keyCode === 32) {
    7. event.returnValue = false;
    8. }
    9. }
    10. /**
    11. * 生成随机字符串
    12. * @param length 字符串的长度,默认11
    13. * @returns {string}
    14. */
    15. function generateRandomString(length = 11) {
    16. let charset = "abcdefghijklmnopqrstuvwxyz-_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
    17. let values = new Uint32Array(length);
    18. window.crypto.getRandomValues(values);
    19. let str = "";
    20. for (let i = 0; i < length; i++) {
    21. str += charset[values[i] % charset.length];
    22. }
    23. return str;
    24. }
    25. /**
    26. * 登录
    27. */
    28. function login() {
    29. let username = $("#username").val();
    30. let password = $("#password").val();
    31. let code = $("#code").val();
    32. if (!username) {
    33. alert("请输入用户名!");
    34. $("#username").focus();
    35. } else if (!password) {
    36. alert("请输入密码!");
    37. $("#password").focus();
    38. } else if (!code) {
    39. alert("请输入验证码!");
    40. } else {
    41. ajaxPost("/user/login", {
    42. username: username,
    43. password: password,
    44. code: code,
    45. uuid: getStorage("uuid")
    46. }, function() {
    47. location.href = "/index.html";
    48. }, function (res) {
    49. if (res && res.responseJSON) {
    50. let response = res.responseJSON;
    51. if (res.status && res.status === 404) {
    52. let message;
    53. if(response.path) {
    54. message = "路径" + response.path + "不存在。";
    55. } else {
    56. message = response.message;
    57. }
    58. alert(message);
    59. } else {
    60. alert(response.message);
    61. }
    62. } else {
    63. alert("请求无响应~");
    64. }
    65. });
    66. }
    67. }
    68. $(function() {
    69. $("#username").keydown(function() {
    70. preventSpace();
    71. }).attr("placeholder", "请输入用户名");
    72. $("#password").keydown(function() {
    73. preventSpace();
    74. }).attr("placeholder", "请输入密码");
    75. /**
    76. * 给验证码输入框绑定回车登录事件
    77. */
    78. $("#code").keydown(function() {
    79. let event = window.event;
    80. if(event.keyCode === 32) {
    81. event.returnValue = false;
    82. } else if(event.keyCode === 13) {
    83. login();
    84. }
    85. }).attr("placeholder", "验证码");
    86. /******************************************************************************************************/
    87. /*
    88. * 验证码初始化
    89. */
    90. // 从localStorage中获取uuid
    91. let uuid = getStorage("uuid");
    92. // uuid为空,则生成后保存到localStorage中
    93. if (!uuid) {
    94. // 生成uuid
    95. uuid = generateRandomString();
    96. // 保存uuid到localStorage
    97. storage("uuid", uuid);
    98. }
    99. let captcha_ = $("#captcha");
    100. // 设置验证码的图片路径
    101. captcha_.attr("src", "/captcha/generate?type=png&uuid=" + uuid);
    102. // 设置验证码的title
    103. captcha_.attr("title", "看不清?换一张");
    104. // 点击验证码刷新
    105. captcha_.click(function () {
    106. // 生成uuid
    107. uuid = generateRandomString();
    108. // 保存uuid到localStorage
    109. storage("uuid", uuid);
    110. // 重新设置验证码图片的路径
    111. $("#captcha").attr("src", "/captcha/generate?v=" + new Date().getTime() + "&type=png&uuid=" + uuid);
    112. });
    113. /******************************************************************************************************/
    114. // 点击登录按钮
    115. $("#btn_Login").click(function () {
    116. login();
    117. });
    118. $(".content .con_right .left").on("click", function () {
    119. $(this).css({
    120. "color": "#333333",
    121. "border-bottom": "2px solid #2e558e"
    122. });
    123. $(".content .con_right .right").css({
    124. "color": "#999999",
    125. "border-bottom": "2px solid #dedede"
    126. });
    127. $(".content .con_right ul .con_r_left").css("display", "block");
    128. $(".content .con_right ul .con_r_right").css("display", "none");
    129. });
    130. $(".content .con_right .right").on("click", function () {
    131. $(this).css({
    132. "color": "#333333",
    133. "border-bottom": "2px solid #2e558e"
    134. });
    135. $(".content .con_right .left").css({
    136. "color": "#999999",
    137. "border-bottom": "2px solid #dedede"
    138. });
    139. $(".content .con_right ul .con_r_right").css("display", "block");
    140. $(".content .con_right ul .con_r_left").css("display", "none");
    141. });
    142. });

    好了,这篇文章就分享到这里了,看完如果觉得对你有所帮助,不要忘了点赞+收藏哦~

    最新的代码已经保存到git仓库,可按需获取~

    Java实现登录验证码功能icon-default.png?t=N7T8https://gitee.com/he-yunlin/login-captcha.git

  • 相关阅读:
    高精度绝对角度传感器应用高速度角度监测
    3D视觉 之 线激光3D相机
    低代码搭建高效管理房屋检测系统案例分析
    小程序用vue编写,保存表单出错
    2023年中国大学生留学现状及未来发展规划分析:直接就业仍是毕业后的主流选择[图]
    10 Deployment:让应用永不宕机
    程序员眼中的中秋
    【机器学习】回归问题实例(李宏毅老师作业1)
    libxml2库使用示例
    测试环境要多少?从成本与效率说起
  • 原文地址:https://blog.csdn.net/heyl163_/article/details/132918102