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

2023-09-16 14:25:10

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

这篇文章就如何实现登录时的验证码的验证功能结合代码进行详细地介绍,以及介绍功能实现的思路。

目录

页面效果

实现思路

生成验证码的控制器类

前端页面代码

localStorage.js

login.html

login.js

后端登录代码

UserLoginDTO.java

UserController.java

UserServiceImpl.java

潜在问题

改进方案


页面效果

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

实现思路

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

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

由于博主也是第一次做这个功能,就随便在网上找了一个生成验证码的工具easy-captcha

<!--生成验证码工具-->
<dependency>
    <groupId>com.github.whvcse</groupId>
    <artifactId>easy-captcha</artifactId>
    <version>1.6.2</version>
</dependency>

生成验证码的控制器类

CaptchaController.java

package cn.edu.sgu.www.mhxysy.controller;

import cn.edu.sgu.www.mhxysy.annotation.AnonymityAccess;
import cn.edu.sgu.www.mhxysy.config.CaptchaConfig;
import cn.edu.sgu.www.mhxysy.exception.GlobalException;
import cn.edu.sgu.www.mhxysy.restful.ResponseCode;
import cn.edu.sgu.www.mhxysy.util.UserUtils;
import com.wf.captcha.GifCaptcha;
import com.wf.captcha.SpecCaptcha;
import com.wf.captcha.base.Captcha;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
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;

/**
 * @author heyunlin
 * @version 1.0
 */
@Slf4j
@RestController
@Api(tags = "验证码管理")
@RequestMapping(value = "/captcha", produces = "application/json;charset=utf-8")
public class CaptchaController {

    private final CaptchaConfig captchaConfig;
    private final StringRedisTemplate stringRedisTemplate;

    @Autowired
    public CaptchaController(CaptchaConfig captchaConfig, StringRedisTemplate stringRedisTemplate) {
        this.captchaConfig = captchaConfig;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /**
     * 生成验证码
     * @param type 验证码图片类型
     * @param uuid 前端生成的uuid
     */
    @AnonymityAccess
    @ApiOperation("生成验证码")
    @RequestMapping(value = "/generate", method = RequestMethod.GET)
    public void generate(@RequestParam String type, @RequestParam String uuid) throws IOException {
        // 获取HttpServletResponse对象
        HttpServletResponse response = UserUtils.getResponse();

        // 设置请求头
        response.setContentType("image/gif");
        response.setDateHeader("Expires", 0);
        response.setHeader("Pragma", "No-cache");
        response.setHeader("Cache-Control", "no-cache");

        Captcha captcha;
        Integer width = captchaConfig.getWidth();
        Integer height = captchaConfig.getHeight();

        switch (type) {
            case "png":
                captcha = new SpecCaptcha(width, height);
                break;
            case "gif":
                captcha = new GifCaptcha(width, height);
                break;
            default:
                throw new GlobalException(ResponseCode.BAD_REQUEST, "不合法的验证码类型:" + type);
        }

        captcha.setLen(4);
        captcha.setCharType(Captcha.TYPE_DEFAULT);

        String code = captcha.text();
        log.debug("生成的验证码:{}", code);

        // 保存uuid
        stringRedisTemplate.opsForValue().set("uuid", uuid);
        stringRedisTemplate.opsForValue().expire("uuid", 5, TimeUnit.MINUTES);

        // 缓存验证码
        stringRedisTemplate.opsForValue().set(uuid, code);
        stringRedisTemplate.opsForValue().expire(uuid, 5, TimeUnit.MINUTES);

        // 输出图片流
        captcha.out(response.getOutputStream());
    }

}

前端页面代码

localStorage.js

/**
 * 保存数据到localStorage
 * @param name 数据的名称
 * @param value 数据的值
 */
function storage(name, value) {
    localStorage.setItem(name, value);
}

/**
 * localStorage根据name获取value
 * @param name 数据的名称
 */
function getStorage(name) {
    return localStorage.getItem(name);
}

login.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>梦幻西游手游管理登录</title>
        <link rel="stylesheet" href="/css/login.css">
        <script src="/js/public/jquery.min.js"></script>
        <script src="/js/public/util.js"></script>
        <script src="/js/public/localStorage.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:" class="left">下载游戏</a>
                            <a href="javascript:" 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/login/mhxysy.png" />
                                        </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="login_username" />
                                        </div>

                                        <div>
                                            <span class="mima-icon"></span>
                                            <input type="password" id="login_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;
	}
}

// 登录
function login() {
	let username = $("#login_username").val();
    let password = $("#login_password").val();
    let code = $("#code").val();

	if (!username) {
		alert("请输入用户名!");
		
		$("#login_username").focus();
	} else if (!password) {
		alert("请输入密码!");
		
		$("#login_password").focus();
	} else if (!code) {
		alert("请输入验证码!");
	} else {
		post("/user/login", {
			username: username,
			password: password,
			code: code
		}, function() {
			location.href = "/index.html";
		}, function (res) {
			if (res && res.responseJSON) {
				let response = res.responseJSON;

				if (res.status && res.status === 404) {
					let message;

					if(response.path) {
						message = "路径" + response.path + "不存在。";
					} else {
						message = response.message;
					}

					alert(message);
				} else {
					alert(response.message);
				}
			}
		});
	}
}

$(function() {
	$("#login_username").keydown(function() {
		preventSpace();
	}).attr("placeholder", "请输入用户名");

	/**
	 * 给密码输入框绑定回车登录事件
	 */
	$("#login_password").keydown(function(event) {
		if(event.keyCode === 13) {
			login();
		}
		
		preventSpace();
	}).attr("placeholder", "请输入密码");

	$("#code").keydown(function() {
		preventSpace();
	}).attr("placeholder", "验证码");

	// 获取uuid
	let uuid = getStorage("uuid");

	if (!uuid) {
		uuid = new Date().getTime();

		storage("uuid", uuid);
	}

	$("#captcha").attr("src", "/captcha/generate?type=png&uuid=" + uuid);
	
	// 点击登录按钮
	$("#btn_Login").on("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");
	});

});

后端登录代码

UserLoginDTO.java

package cn.edu.sgu.www.mhxysy.dto.system;

import lombok.Data;

import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.io.Serializable;

/**
 * @author heyunlin
 * @version 1.0
 */
@Data
public class UserLoginDTO implements Serializable {
    private static final long serialVersionUID = 18L;

    /**
     * 验证码
     */
    @NotNull(message = "验证码不允许为空")
    @NotEmpty(message = "验证码不允许为空")
    private String code;

    /**
     * 用户名
     */
    @NotNull(message = "用户名不允许为空")
    @NotEmpty(message = "用户名不允许为空")
    private String username;

    /**
     * 密码
     */
    @NotNull(message = "密码不允许为空")
    @NotEmpty(message = "密码不允许为空")
    private String password;
}

UserController.java

package cn.edu.sgu.www.mhxysy.controller.system;

import cn.edu.sgu.www.mhxysy.annotation.AnonymityAccess;
import cn.edu.sgu.www.mhxysy.annotation.Exclusion;
import cn.edu.sgu.www.mhxysy.dto.system.UserLoginDTO;
import cn.edu.sgu.www.mhxysy.restful.JsonResult;
import cn.edu.sgu.www.mhxysy.service.system.UserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
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;

/**
 * @author heyunlin
 * @version 1.0
 */
@Exclusion
@RestController
@Api(tags = "用户管理")
@RequestMapping(path = "/user", produces="application/json;charset=utf-8")
public class UserController {

	private final UserService userService;

	@Autowired
	public UserController(UserService userService) {
		this.userService = userService;
	}

	@AnonymityAccess
	@ApiOperation("登录认证")
	@RequestMapping(value = "/login", method = RequestMethod.POST)
	public JsonResult<Void> login(@Validated UserLoginDTO loginDTO) {
		userService.login(loginDTO);

		return JsonResult.success();
	}

    
    /*省略的其他代码*/

}

UserServiceImpl.java

package cn.edu.sgu.www.mhxysy.service.system.impl;

import cn.edu.sgu.www.mhxysy.dto.system.UserLoginDTO;
import cn.edu.sgu.www.mhxysy.entity.system.User;
import cn.edu.sgu.www.mhxysy.entity.system.UserLoginLog;
import cn.edu.sgu.www.mhxysy.exception.GlobalException;
import cn.edu.sgu.www.mhxysy.feign.FeignService;
import cn.edu.sgu.www.mhxysy.redis.RedisRepository;
import cn.edu.sgu.www.mhxysy.restful.ResponseCode;
import cn.edu.sgu.www.mhxysy.service.system.UserService;
import cn.edu.sgu.www.mhxysy.util.IpUtils;
import cn.edu.sgu.www.mhxysy.util.StringUtils;
import cn.edu.sgu.www.mhxysy.util.UserUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;

/**
 * @author heyunlin
 * @version 1.0
 */
@Slf4j
@Service
public class UserServiceImpl implements UserService {

	private final FeignService feignService;
	private final RedisRepository redisRepository;
	private final StringRedisTemplate stringRedisTemplate;

	@Value("${syslog.enable}")
	private boolean enable;

	@Autowired
	public UserServiceImpl(
			FeignService feignService,
			RedisRepository redisRepository,
			StringRedisTemplate stringRedisTemplate) {
		this.feignService = feignService;
		this.redisRepository = redisRepository;
		this.stringRedisTemplate = stringRedisTemplate;
	}

	@Override
	public void login(UserLoginDTO loginDTO) {
		String code = loginDTO.getCode();
		String uuid = stringRedisTemplate.opsForValue().get("uuid");

		// 得到的uuid为空,则获取验证码到登录之间的时间已经过了5分钟,uuid已经过期
		if (uuid == null) {
			throw new GlobalException(ResponseCode.BAD_REQUEST, "验证码已失效,请刷新页面重新获取~");
		}

		if (!code.equalsIgnoreCase(stringRedisTemplate.opsForValue().get(uuid))) {
			throw new GlobalException(ResponseCode.BAD_REQUEST, "验证码错误~");
		}

		// 得到用户名
		String username = loginDTO.getUsername();
		log.debug("用户{}正在登录...", username);

		// 查询用户信息,如果用户被锁定,提前退出
		User user = feignService.selectByUsername(username);

		if (user != null) {
			if (user.getEnable()) {
				// shiro登录认证
				UsernamePasswordToken token = new UsernamePasswordToken(username, loginDTO.getPassword());
				Subject subject = UserUtils.getSubject();

				subject.login(token);
				// 设置session失效时间:永不超时
				subject.getSession().setTimeout(-1001);

				// 修改管理员上一次登录时间
				User usr = new User();

				usr.setId(user.getId());
				usr.setLastLoginTime(LocalDateTime.now());

				feignService.updateById(usr);

				// 如果开启了系统日志
				if (enable) {
					// 添加管理员登录历史
					UserLoginLog loginLog = new UserLoginLog();

					loginLog.setId(StringUtils.uuid());
					loginLog.setUserId(user.getId());
					loginLog.setLoginTime(LocalDateTime.now());
					loginLog.setLoginIp(IpUtils.getLocalHostAddress());
					loginLog.setLoginHostName(IpUtils.getLocalHostName());

					feignService.saveLoginLog(loginLog);
				}

				// 从redis中删除用户权限
				redisRepository.remove(username);

				// 查询用户的权限信息,并保存到redis
				redisRepository.save(username);
			} else {
				throw new GlobalException(ResponseCode.FORBIDDEN, "账号已被锁定,禁止登录!");
			}
		} else {
			throw new GlobalException(ResponseCode.NOT_FOUND, "用户名不存在~");
		}
	}

}

潜在问题

这样的设计会有一个问题,uuid这个key有可能会被其他用户修改,但是验证码并不会被修改。

改进方案

目前正在找解决方案~

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

更多推荐

SpringBoot【SpringBoot介绍、SpringBoot入门、SpringBoot原理分析、SpringBoot原理分析】(一)-全面详解(学习总结---从入门到深化)

目录SpringBoot介绍_Spring缺点分析SpringBoot介绍_什么是SpringBootSpringBoot介绍_SpringBoot核心功能SpringBoot入门_通过官网搭建项目SpringBoot入门_通过IDEA脚手架搭建项目SpringBoot入门_SpringBoot项目结构SpringBo

超硬核的Move Dev Meetup上海线下交流会圆满结束

北京时间9月16日下午2–6点,由MoveFunsDAO联合其他组织举办的Move开发者线下交流会在上海悦达国际大厦圆满完成。此次活动也是上海区块链周的周边活动,受到了Web3从业者的广泛关注。本场交流会邀请了OpenBuild技术社区主理人Ian主持,50余位参会者来到现场参与此次交流。以下是嘉宾分享:Jolesta

基础算法--双指针算法

双指针算法1.基本介绍严格的来说,双指针只能说是是算法中的一种技巧。双指针指的是在遍历对象的过程中,不是普通的使用单个指针进行访问,而是使用两个相同方向(快慢指针)或者相反方向(对撞指针)的指针进行扫描,从而达到相应的目的。最常见的双指针算法有两种:一种是,在一个序列里边,用两个指针维护一段区间;另一种是,在两个序列里

药品咨询报告合集整理平台打包(一共36597份)【专题推荐】

<医药行业从业者必看>笔者今天分享高价值医药行业报告36500余份的获取/下载方法,报告涵盖了医药细分领域研究报告+药品报告(所有上市药品)+医药行业分析报告+医药环境观察报告+药品市场调研报告+药品靶点研究报告+医药白皮书;数据来源于药融云自产报告&药品报告自动生成系统(最新日期)。①报告下载途径药品报告:药融云医药

Linux文件操作基础:快速入门指南和实用技巧

文章目录linux文件操作基础I.查看文件和目录1.`ls`命令用法`ls`命令详细介绍`pwd`命令用法:`pwd`命令详细介绍`cd`命令用法:`cd`命令详细介绍:II.创建文件和目录`touch`命令用法`touch`命令详细介绍III.复制、移动和重命名`cp`命令用法`cp`命令详细介绍IV.删除文件和目录

无人机(UAV)隐蔽通信(covert communication)的联合功率分配和轨迹设计

文章目录摘要Introduction本文是JointPowerAllocationandTrajectoryDesignforUAV-EnabledCovertCommunication一文的阅读笔记摘要在本文中,我们研究了无人机(UAV)网络中的隐蔽通信,其中无人机将信息传输给多个地面用户(GU),而不会被隐藏探测器

计算机竞赛 深度学习 机器视觉 车位识别车道线检测 - python opencv

0前言🔥优质竞赛项目系列,今天要分享的是🚩深度学习机器视觉车位识别车道线检测该项目较为新颖,适合作为竞赛课题方向,学长非常推荐!🥇学长这里给一个题目综合评分(每项满分5分)难度系数:3分工作量:3分创新点:4分🧿更多资料,项目分享:https://gitee.com/dancheng-senior/postgr

走进人工智能|自主无人系统 从概念到现实的飞跃

前言:自主无人系统是具备自主感知、决策和执行能力的智能系统,无需人类干预即可完成任务的技术体系。文章目录序言AUS的现有应用从概念到现实的飞跃`技术发展历程`目前形式领跑人困难和挑战总结自主无人系统(AutonomousUnmannedSystems,简称AUS)是当代科技领域的重要发展方向之一。它代表了人工智能、机器

云原生之深入解析Kubernetes Pod的网络状态监控

一、前言在Kubernetes系统里,由kubelet内置的cadvisor组件收集每个容器资源监控信息,但官方基于性能相关的考虑,如果抓取这些每个容器中网络相关的指标,将会耗费大量的CPU内存资源,cadvisor中默认给关掉了网络等相关指标的收集。https://github.com/google/cadvisor

vue中使用vue-property-decorator

一、前言Vue.js是一个非常受欢迎的前端框架,它能够快速构建交互性强的单页面应用。而vue-property-decorator是一个用于Vue.js的装饰器库,可以帮助我们更方便地编写Vue.js组件。下面来详细讲解vue-property-decorator的用法。vue-class-component是vue的

更快更强更稳定:腾讯向量数据库测评

向量数据库:AI时代的新基座人工智能在无处不在影响着我们的生活,而人工智能飞速发展的背后是需要对越来越多的海量数据处理,传统数据库已经难以支撑大规模的复杂数据处理。特别是大模型的出现,向量数据库横空出世。NVIDIACEO黄仁勋在NVIDIAGTCKeynote演讲中首次提到了向量数据库,并强调它在构建专有大型语言模型

热文推荐