今天给项目换了一个登录页面,而这个登录页面设计了验证码,于是想着把这个验证码功能实现一下。
这篇文章就如何实现登录时的验证码的验证功能结合代码进行详细地介绍,以及介绍功能实现的思路,文章不是一次写成的,中间考虑可能出现的问题,最后给出了一个比较稳定的版本。
目录
一、页面效果
登录的时候会把用户名、密码和验证码一起传到后端,并对验证码进行验证,只有验证码正确才能登录。
二、实现思路
那么,具体是如何实现的呢,首先大概介绍一下我实现这个功能的思路:
- 验证码图片的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中添加必要的依赖~
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.9</version>
<relativePath />
</parent>
<groupId>cn.edu.sgu.www</groupId>
<artifactId>login-captcha</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>login-captcha</name>
<description>Java实现登录验证码功能</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--validation-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
</dependency>
<!--生成验证码工具-->
<dependency>
<groupId>com.github.whvcse</groupId>
<artifactId>easy-captcha</artifactId>
<version>1.6.2</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
修改配置
修改application.yml
server:
port: 8888
logging:
level:
springfox: error
cn.edu.sgu.www.logincaptcha: debug
spring:
application:
name: login-captcha
redis:
port: 6379
host: localhost
timeout: PT5s
# 验证码图片设置
captcha:
image:
length: 4
width: 66
height: 22
2、功能实现
后端代码
RedisKeyPrefixes
创建consts.RedisKeyPrefixes接口,保存项目中用到的redis的key前缀。
为了防止本项目的key和其他项目的key重复,在所有key前面加上项目名~
package cn.edu.sgu.www.logincaptcha.consts;
/**
* redis的key前缀的常量接口
* @author heyunlin
* @version 1.0
*/
public interface RedisKeyPrefixes {
/**
* 所有key前面加上服务名
*/
String PREFIX_BASE = "login-captcha:";
/**
* 用户登录的验证码
*/
String PREFIX_CAPTCHA = PREFIX_BASE + "login_captcha:";
}
CaptchaImageProperties
创建config.CaptchaImageProperties类,用于读取验证码的配置
package cn.edu.sgu.www.logincaptcha.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* @author heyunlin
* @version 1.0
*/
@Data
@Component
@ConfigurationProperties(prefix = "captcha.image")
public class CaptchaImageProperties {
/**
* 验证码宽度
*/
private Integer width;
/**
* 验证码高度
*/
private Integer height;
/**
* 验证码的字符长度
*/
private Integer length;
}
CaptchaController.java
给前端登录页面生成随机验证码的控制器类。
package cn.edu.sgu.www.logincaptcha.controller;
import cn.edu.sgu.www.logincaptcha.config.CaptchaImageProperties;
import cn.edu.sgu.www.logincaptcha.consts.RedisKeyPrefixes;
import com.wf.captcha.SpecCaptcha;
import com.wf.captcha.base.Captcha;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
/**
* @author heyunlin
* @version 1.0
*/
@Slf4j
@RestController
@RequestMapping(value = "/captcha", produces = "application/json;charset=utf-8")
public class CaptchaController {
private final StringRedisTemplate redisTemplate;
private final CaptchaImageProperties captchaImageProperties;
@Autowired
public CaptchaController(StringRedisTemplate redisTemplate, CaptchaImageProperties captchaImageProperties) {
this.redisTemplate = redisTemplate;
this.captchaImageProperties = captchaImageProperties;
}
/**
* 生成验证码
* @param uuid 前端生成的uuid
* @param response HttpServletResponse对象
*/
@RequestMapping(value = "/generate", method = RequestMethod.GET)
public void generate(@RequestParam String uuid, HttpServletResponse response) throws IOException {
Integer width = captchaImageProperties.getWidth();
Integer height = captchaImageProperties.getHeight();
Integer length = captchaImageProperties.getLength();
Captcha captcha = new SpecCaptcha(width, height);
captcha.setLen(length);
captcha.setCharType(Captcha.TYPE_DEFAULT);
String code = captcha.text();
log.debug("生成的验证码:{}", code);
// 根据uuid拼接前缀得到验证码的key
String key = RedisKeyPrefixes.PREFIX_CAPTCHA + uuid;
// 缓存验证码
redisTemplate.opsForValue().set(key , code);
// 设置验证码3分钟后过期
redisTemplate.expire(key, 3, TimeUnit.MINUTES);
// 设置响应头
response.setContentType("image/png");
response.setDateHeader("Expires", 0);
response.setHeader("Pragma", "No-cache");
response.setHeader("Cache-Control", "no-cache");
// 输出图片流
captcha.out(response.getOutputStream());
}
}
UserLoginDTO.java
在项目根目录下创建dto.UserLoginDTO接受前端登录提交的数据。
package cn.edu.sgu.www.logincaptcha.dto;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import java.io.Serializable;
/**
* @author heyunlin
* @version 1.0
*/
@Data
public class UserLoginDTO implements Serializable {
private static final long serialVersionUID = 18L;
/**
* uuid:随机字符串
*/
@NotBlank(message = "验证码已过期,请重新获取")
private String uuid;
/**
* 验证码
*/
@NotBlank(message = "验证码不允许为空")
private String code;
/**
* 用户名
*/
@NotBlank(message = "用户名不允许为空")
private String username;
/**
* 密码
*/
@NotBlank(message = "密码不允许为空")
private String password;
}
UserService.java
创建业务层接口service.UserService
package cn.edu.sgu.www.logincaptcha.service;
import cn.edu.sgu.www.logincaptcha.dto.UserLoginDTO;
/**
* @author heyunlin
* @version 1.0
*/
public interface UserService {
/**
* 用户登录
* @param loginDTO 登录信息
*/
void login(UserLoginDTO loginDTO);
}
UserController.java
新建一个控制器类controller.UserController,创建登录接口/user/login
package cn.edu.sgu.www.logincaptcha.controller;
import cn.edu.sgu.www.logincaptcha.dto.UserLoginDTO;
import cn.edu.sgu.www.logincaptcha.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
/**
* @author heyunlin
* @version 1.0
*/
@RestController
@RequestMapping(path = "/user", produces="application/json;charset=utf-8")
public class UserController {
private final UserService userService;
@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
/**
* 登录认证
* @param loginDTO UserLoginDTO
* @return JsonResult<Void>
*/
@RequestMapping(value = "/login", method = RequestMethod.POST)
public Map<String, Object> login(@Validated UserLoginDTO loginDTO) {
userService.login(loginDTO);
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("code", 200);
resultMap.put("success", true);
resultMap.put("message", "登录成功");
return resultMap;
}
}
UserServiceImpl.java
在service包下创建impl.UserServiceImpl类,实现UserService接口,完成用户登录的业务代码。
package cn.edu.sgu.www.logincaptcha.service.impl;
import cn.edu.sgu.www.logincaptcha.consts.RedisKeyPrefixes;
import cn.edu.sgu.www.logincaptcha.dto.UserLoginDTO;
import cn.edu.sgu.www.logincaptcha.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
/**
* @author heyunlin
* @version 1.0
*/
@Service
public class UserServiceImpl implements UserService {
private final StringRedisTemplate redisTemplate;
@Autowired
public UserServiceImpl(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
public void login(UserLoginDTO loginDTO) {
// 得到用户输入的验证码
String code = loginDTO.getCode();
// 获取正确的验证码
String key = RedisKeyPrefixes.PREFIX_CAPTCHA + loginDTO.getUuid();
String realCode = redisTemplate.opsForValue().get(key);
// 得到的验证码为空,则获取验证码到登录之间的时间已经过了3分钟,验证码过期已经被删除
if (realCode == null) {
throw new RuntimeException("验证码已失效,请重新获取~");
}
// 验证码校验
if (!code.equalsIgnoreCase(realCode)) {
throw new RuntimeException("验证码错误~");
}
/*
其它登录代码...
*/
}
}
前端代码
ajaxUtils.js
封装的ajax工具类
/**
* 封装的ajax get请求
* @param url 请求url
* @param params 请求参数
* @param success 成功回调函数
* @param error 失败回调函数
* @param async 是否异步
*/
function ajaxGet(url, params, success, error, async = true) {
$.ajax({
type: "GET",
url: url,
data: params,
cache: false,
async: async,
dataType: "json",
processData: true,
success: success,
error: error
});
}
/**
* 封装的ajax post请求
* @param url 请求url
* @param params 请求参数
* @param success 成功回调函数
* @param error 失败回调函数
* @param async 是否异步
*/
function ajaxPost(url, params, success, error, async = true) {
$.ajax({
type: "POST",
url: url,
data: params,
async: async,
cache: false,
dataType: "json",
processData: true,
success: success,
error: error
});
}
/**
* 错误回调函数
* @param res
*/
let error = (res) => {
let response = res.responseJSON;
// 请求有响应
if (res && response) {
let status = res.status;
if (status) {
let message;
if (status === 404 && response.path) {
message = "路径" + response.path + "不存在。";
} else {
message = response.message;
}
alert(message);
console.log("响应状态码:" + status + ", 响应消息:" + message);
} else {
console.log("请求没有响应状态码~");
}
} else {
console.log("请求无响应~");
}
}
login.html
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>登录页面</title>
<link rel="stylesheet" href="/css/login.css" />
<script src="/js/jquery.min.js"></script>
<script src="/js/ajaxUtils.js"></script>
<script src="/js/login.js"></script>
</head>
<body style="overflow:hidden">
<div class="pagewrap">
<div class="main">
<div class="header"></div>
<div class="content">
<div class="con_left"></div>
<div class="con_right">
<div class="con_r_top">
<a href="javascript:void(0)" class="left">关注博客</a>
<a href="javascript:void(0)" class="right">登录管理</a>
</div>
<ul>
<li class="con_r_left" style="display:none;">
<div class="erweima">
<div class="qrcode">
<div id="output">
<img src="/images/blog.jpg" />
</div>
</div>
</div>
<div style="height:70px;">
<p>扫码下载梦幻西游手游</p>
</div>
</li>
<li class="con_r_right" style="display:block;">
<div>
<div class="user">
<div>
<span class="user-icon"></span>
<input type="text" id="username" />
</div>
<div>
<span class="mima-icon"></span>
<input type="password" id="password" />
</div>
<div>
<span class="yzmz-icon"></span>
<input type="text" id="code" />
<img id="captcha" alt="看不清?点击更换" />
</div>
</div>
<br>
<button id="btn_Login" type="button">登 录</button>
</div>
</li>
</ul>
</div>
</div>
</div>
</div>
</body>
</html>
login.js
/**
* 禁止输入空格
*/
function preventSpace() {
let event = window.event;
if(event.keyCode === 32) {
event.returnValue = false;
}
}
/**
* 生成随机字符串
* @param length 字符串的长度,默认11
* @returns {string}
*/
function generateRandomString(length = 11) {
let charset = "abcdefghijklmnopqrstuvwxyz-_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
let values = new Uint32Array(length);
window.crypto.getRandomValues(values);
let str = "";
for (let i = 0; i < length; i++) {
str += charset[values[i] % charset.length];
}
return str;
}
/**
* 登录
*/
function login() {
let username = $("#username").val();
let password = $("#password").val();
let code = $("#code").val();
if (!username) {
alert("请输入用户名!");
$("#username").focus();
} else if (!password) {
alert("请输入密码!");
$("#password").focus();
} else if (!code) {
alert("请输入验证码!");
} else {
ajaxPost("/user/login", {
username: username,
password: password,
code: code,
uuid: localStorage.getItem("login-captcha:uuid")
}, function() {
location.href = "/index.html";
}, error);
}
}
$(function() {
$("#username").keydown(function() {
preventSpace();
}).attr("placeholder", "请输入用户名");
$("#password").keydown(function() {
preventSpace();
}).attr("placeholder", "请输入密码");
/**
* 给验证码输入框绑定回车登录事件
*/
$("#code").keydown(function() {
let event = window.event;
if(event.keyCode === 32) {
event.returnValue = false;
} else if(event.keyCode === 13) {
login();
}
}).attr("placeholder", "验证码");
/******************************************************************************************************/
/*
* 验证码初始化
*/
// 从localStorage中获取uuid
let uuid = localStorage.getItem("login-captcha:uuid");
// uuid为空,则生成后保存到localStorage中
if (!uuid) {
// 生成uuid
uuid = generateRandomString();
// 保存uuid到localStorage
localStorage.setItem("login-captcha:uuid", uuid);
}
$("#captcha").attr({
"title": "看不清?换一张",
"src": "/captcha/generate?type=png&uuid=" + uuid
}).click(function () {
// 生成uuid
uuid = generateRandomString();
// 保存uuid到localStorage
localStorage.setItem("login-captcha:uuid", uuid);
// 重新设置验证码图片的路径
$("#captcha").attr("src", "/captcha/generate?v=" + new Date().getTime() + "&uuid=" + uuid);
});
/******************************************************************************************************/
// 点击登录按钮
$("#btn_Login").click(function () {
login();
});
$(".content .con_right .left").on("click", function () {
$(this).css({
"color": "#333333",
"border-bottom": "2px solid #2e558e"
});
$(".content .con_right .right").css({
"color": "#999999",
"border-bottom": "2px solid #dedede"
});
$(".content .con_right ul .con_r_left").css("display", "block");
$(".content .con_right ul .con_r_right").css("display", "none");
});
$(".content .con_right .right").on("click", function () {
$(this).css({
"color": "#333333",
"border-bottom": "2px solid #2e558e"
});
$(".content .con_right .left").css({
"color": "#999999",
"border-bottom": "2px solid #dedede"
});
$(".content .con_right ul .con_r_right").css("display", "block");
$(".content .con_right ul .con_r_left").css("display", "none");
});
});
好了,这篇文章就分享到这里了,看完如果觉得对你有所帮助,不要忘了点赞+收藏哦~
最新的代码已经保存到git仓库,可按需获取~