Java21 + SpringBoot3集成easy-captcha实现验证码显示和登录校验

文章目录

    • 前言
    • 相关技术简介
      • easy-captcha
    • 实现步骤
      • 引入maven依赖
      • 定义实体类
      • 定义登录服务类
      • 定义登录控制器
      • 前端登录页面实现
      • 测试和验证
    • 总结
    • 附录
      • 使用`Session`缓存验证码
      • 前端登录页面实现代码

前言

近日心血来潮想做一个开源项目,目标是做一款可以适配多端、功能完备的模板工程,包含后台管理系统和前台系统,开发者基于此项目进行裁剪和扩展来完成自己的功能开发。

本项目为前后端分离开发,后端基于Java21SpringBoot3开发,后端使用Spring SecurityJWTSpring Data JPA等技术栈,前端提供了vueangularreactuniapp微信小程序等多种脚手架工程。

本文主要介绍在SpringBoot3项目中如何集成easy-captcha生成验证码,JDK版本是Java21,前端使用Vue3开发。

项目地址:https://gitee.com/breezefaith/fast-alden

相关技术简介

easy-captcha

easy-captcha是生成图形验证码的Java类库,支持gif、中文、算术等类型,可用于Java Web、JavaSE等项目。

参考地址:

  • Github:https://github.com/whvcse/EasyCaptcha

实现步骤

引入maven依赖

pom.xml中添加easy-captcha以及相关依赖,并引入Lombok用于简化代码。

<dependencies><!-- easy-captcha --><dependency><groupId>com.github.whvcse</groupId><artifactId>easy-captcha</artifactId><version>1.6.2</version></dependency><!--    解决easy-captcha算术验证码报错问题    --><dependency><groupId>org.openjdk.nashorn</groupId><artifactId>nashorn-core</artifactId><version>15.4</version></dependency><!-- Lombok --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.30</version><optional>true</optional></dependency>
</dependencies>

笔者使用的JDK版本是Java21SpringBoot版本是3.2.0,如果不引入nashorn-core,生成验证码时会报错java.lang.NullPointerException: Cannot invoke "javax.script.ScriptEngine.eval(String)" because "engine" is null。有开发者反馈使用Java 17时也遇到了同样的问题,手动引入nashorn-core后即可解决该问题。

详细堆栈和截图如下:

java.lang.NullPointerException: Cannot invoke "javax.script.ScriptEngine.eval(String)" because "engine" is nullat com.wf.captcha.base.ArithmeticCaptchaAbstract.alphas(ArithmeticCaptchaAbstract.java:42) ~[easy-captcha-1.6.2.jar:na]at com.wf.captcha.base.Captcha.checkAlpha(Captcha.java:156) ~[easy-captcha-1.6.2.jar:na]at com.wf.captcha.base.Captcha.text(Captcha.java:137) ~[easy-captcha-1.6.2.jar:na]at com.fast.alden.admin.service.impl.AuthServiceImpl.generateVerifyCode(AuthServiceImpl.java:72) ~[classes/:na]......

image.png

定义实体类

为了方便后端校验,获取验证码的请求除了要返回验证码图片本身,还要返回一个验证码的唯一标识,所以笔者定义了一个实体类VerifyCodeEntity

/*** 验证码实体*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class VerifyCodeEntity implements Serializable {/*** 验证码Key*/private String key;/*** 验证码图片,base64压缩后的字符串*/private String image;/*** 验证码文本值*/@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)private String text;
}

使用@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)注解可以使text属性不会被序列化后返回给前端。

为实现登录功能,还要定义一个登录参数类LoginParam

@Data
public class LoginParam {/*** 用户名*/private String username;/*** 密码*/private String password;/*** 验证码Key*/private String verifyCodeKey;/*** 验证码*/private String verifyCode;
}

定义登录服务类

在登录服务类中,我们需要定义以下方法:

  1. 生成验证码

    在该方法中使用easy-captcha生成一个验证码,生成的验证码除了要返回给前端,还需要在后端进行缓存,这样才能实现前后端的验证码校验。本文中给出了两种缓存验证码的方式,一种是基于RedisTemplate缓存至Redis,一种是缓存至Session,读者可根据需要选择性使用,推荐使用**Redis**。在本文附录中给出了缓存至Session的实现方式。

  2. 登录

    在登录方法中首先校验验证码是否正确,然后再校验用户名和密码是否正确,校验通过后生成Token返回给前端。本文中该方法仅给出验证码校验相关的逻辑,其他逻辑请自行实现。

@Service
public class AuthService {private final RedisTemplate<String, Object> redisTemplate;public AuthService(RedisTemplate<String, Object> redisTemplate) {this.redisTemplate = redisTemplate;}public VerifyCodeEntity generateVerifyCode() throws IOException {// 创建验证码对象Captcha captcha = new ArithmeticCaptcha();// 生成验证码编号String verifyCodeKey = UUID.randomUUID().toString();String verifyCode = captcha.text();// 获取验证码图片,构造响应结果VerifyCodeEntity verifyCodeEntity = new VerifyCodeEntity(verifyCodeKey, captcha.toBase64(), verifyCode);// 存入Redis,设置120s过期redisTemplate.opsForValue().set(verifyCodeKey, verifyCode, 120, TimeUnit.SECONDS);return verifyCodeEntity;}public String login(LoginParam param) {// 校验验证码// 获取用户输入的验证码String actual = param.getVerifyCode();// 判断验证码是否过期if (redisTemplate.getExpire(param.getVerifyCodeKey(), TimeUnit.SECONDS) < 0) {throw new RuntimeException("验证码过期");}// 从redis读取验证码并删除缓存String expect = (String) redisTemplate.opsForValue().get(param.getVerifyCodeKey());redisTemplate.delete(param.getVerifyCodeKey());// 比较用户输入的验证码和缓存中的验证码是否一致,不一致则抛错if (!StringUtils.hasText(expect) || !StringUtils.hasText(actual) || !actual.equalsIgnoreCase(expect)) {throw new RuntimeException("验证码错误");}// 校验用户名和密码,校验成功后生成token返回给前端,具体逻辑省略String token = "";return token;}
}

定义登录控制器

/*** 登录控制器*/
@RestController("/auth")
public class AuthController {private final AuthService authService;public AuthController(AuthService authService) {this.authService = authService;}/*** 获取验证码*/@GetMapping("/verify-code")public VerifyCodeEntity generateVerifyCode() throws IOException {return authService.generateVerifyCode();}/*** 登录*/@PostMapping("/login")public String login(@RequestBody @Validated LoginParam param) {return authService.login(param);}
}

前端登录页面实现

此前端页面基于Vue3的组合式API和Element Plus开发,使用Axios向后端发送请求,因代码较长,将其放在附录中,请移步至附录查看。

测试和验证

image.png

总结

本文介绍了如何基于Java21SpringBoot3集成easy-captcha实现验证码显示和登录校验,给出了详细的实现代码,如有错误,还望批评指正。

在后续实践中我也是及时更新自己的学习心得和经验总结,希望与诸位看官一起进步。

附录

使用Session缓存验证码

使用Session缓存验证码时还需要借助ScheduledExecutorServiceTimerQuartz等实现一个延迟任务,用于从Session中删除超时的验证码。

@Service
public class AuthService {private final ScheduledExecutorService scheduledExecutorService;public AuthService(ScheduledExecutorService scheduledExecutorService) {this.scheduledExecutorService = scheduledExecutorService;}public VerifyCodeEntity generateVerifyCode() throws IOException {// 创建验证码对象Captcha captcha = new ArithmeticCaptcha();// 生成验证码编号String verifyCodeKey = UUID.randomUUID().toString();String verifyCode = captcha.text();// 获取验证码图片,构造响应结果VerifyCodeEntity verifyCodeEntity = new VerifyCodeEntity(verifyCodeKey, captcha.toBase64(), verifyCode);// 存入session,设置120s过期ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();HttpSession session = attributes.getRequest().getSession();session.setAttribute(verifyCodeKey, verifyCode);// 超时后删除验证码缓存// 以下是使用ScheduledExecutorService实现scheduledExecutorService.schedule(() -> {session.removeAttribute(verifyCode);}, 120, TimeUnit.SECONDS);// // 以下是使用Timer实现超时后删除验证码// Timer timer = new Timer();// timer.schedule(new TimerTask() {//     @Override//     public void run() {//         session.removeAttribute(verifyCode);//     }// }, 120 * 1000L);return verifyCodeEntity;}public String login(LoginParam param) {// 校验验证码// 获取用户输入的验证码String actual = param.getVerifyCode();// 从Session读取验证码并删除缓存ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();HttpSession session = attributes.getRequest().getSession();String expect = (String) session.getAttribute(param.getVerifyCodeKey());session.removeAttribute(param.getVerifyCodeKey());// 比较用户输入的验证码和缓存中的验证码是否一致,不一致则抛错if (!StringUtils.hasText(expect) || !StringUtils.hasText(actual) || !actual.equalsIgnoreCase(expect)) {throw new RuntimeException("验证码错误");}// 校验用户名和密码,校验成功后生成token返回给前端,具体逻辑省略String token = "";return token;}
}

以上代码中使用ScheduledExecutorService设置了一个延迟任务,120s后从Session中删除验证码,还需要声明一个ScheduledExecutorService的Bean。

/*** 线程池配置*/
@Configuration
public class ThreadPoolConfig {/*** 核心线程池大小*/private final int corePoolSize = 50;@Beanpublic ScheduledExecutorService scheduledExecutorService() {return new ScheduledThreadPoolExecutor(corePoolSize);}
}

前端登录页面实现代码

<script setup>
import { onBeforeUnmount, onMounted, reactive, ref } from 'vue';
import { useRouter } from 'vue-router';
import { ElMessage, ElForm, ElFormItem, ElInput, ElButton, ElCheckbox } from 'element-plus';
import { CircleCheck, Lock, User, Search, Refresh, Plus, Edit, Delete, View, Upload, Download, Share, Close } from "@element-plus/icons-vue";
import axios, { AxiosError } from 'axios';
import bg from "@/assets/login/bg.png";const router = useRouter();const entity = ref({});
const rememberMe = ref(true);
const REMEMBER_ME_KEY = "remember_me";
const formRef = ref();
const loading = ref(false);
const verifyCodeUrl = ref("");const rules = reactive({username: [{required: true,message: '请输入用户名',trigger: 'blur'}],password: [{validator: (rule, value, callback) => {if (!value) {callback(new Error("请输入密码"));} else {callback();}},trigger: "blur"}],verifyCode: [{required: true,message: '请输入验证码',trigger: 'blur'},],
});// 点击登录按钮
const login = async () => {const formEl = formRef.value;loading.value = true;if (!formEl) {loading.value = false;return;}await formEl.validate(async (valid, fields) => {if (valid) {try {const res = await login$(entity.value);// 从响应中获取tokenconst token = res.data.data;if (token) {// 将token存入Pinia,authStore请自行定义// authStore.authenticate({ token });// warning: 此方式直接将用户名密码明文存入localStorage,并不安全// todo:寻找更合理方式实现“记住我”if (rememberMe.value) {localStorage.setItem(REMEMBER_ME_KEY, JSON.stringify({username: entity.value.username,password: entity.value.password,}));} else {localStorage.removeItem(REMEMBER_ME_KEY);}ElMessage({ message: "登录成功", type: "success" });router.push("/");}else{ElMessage({ message: "登录失败", type: "error" });}} catch (err) {if (err instanceof AxiosError) {const msg = err.response?.data?.message || err.message;ElMessage({ message: msg, type: "error" });}updateVerifyCode();throw err;} finally {loading.value = false;}} else {loading.value = false;return fields;}});
};// 获取验证码请求
const getVerifyCode$ = async () => {return axios.get(`/api/v1.0/admin/auth/verify-code?timestamp=${new Date().getTime()}`, false);
}// 登录请求
const login$ = async (param) => {return axios.post(`/api/v1.0/admin/auth/login`, {...param,});
}// 更新验证码图片
const updateVerifyCode = async () => {const res = await getVerifyCode$();verifyCodeUrl.value = `${res.data.data?.image}`;entity.value.verifyCodeKey = res.data.data?.key;
}/** 使用公共函数,避免`removeEventListener`失效 */
function onkeypress({ code }) {if (code === "Enter" || code === "NumpadEnter") {login();}
}// 页面加载时读取localStorage,如果有记住的用户名密码则加载至界面
const load = async () => {const tmp = localStorage.getItem(REMEMBER_ME_KEY);if (tmp) {const e = JSON.parse(tmp);entity.value.username = e.username;entity.value.password = e.password;}
}onMounted(async () => {window.document.addEventListener("keypress", onkeypress);updateVerifyCode();load();
});onBeforeUnmount(() => {window.document.removeEventListener("keypress", onkeypress);
});</script><template><img class="login-bg" :src="bg" /><div class="login-container"><div class="login-box"><ElForm class="login-form" ref="formRef" :model="entity" :rules="rules" size="large"><h3 class="title">后台管理系统</h3><ElFormItem prop="username"><ElInput clearable v-model="entity.username" placeholder="用户名/手机号/邮箱" :prefix-icon="User" /></ElFormItem><ElFormItem prop="password"><ElInput clearable show-password v-model="entity.password" placeholder="密码" :prefix-icon="Lock" /></ElFormItem><ElFormItem class="verify-code-row" prop="verifyCode"><ElInput clearable v-model="entity.verifyCode" placeholder="验证码" :prefix-icon="CircleCheck"><template #append><img :src="verifyCodeUrl" class="verify-code" @click="updateVerifyCode()" /></template></ElInput></ElFormItem><ElFormItem><ElCheckbox v-model="rememberMe" label="记住我"></ElCheckbox></ElFormItem><ElFormItem><ElButton class="w-full" style="width: 100%" size="default" type="primary" :loading="loading" @click="login()">登录</ElButton></ElFormItem></ElForm></div></div>
</template><style lang="scss">
.login-bg {position: fixed;height: 100%;left: 0;bottom: 0;z-index: -1;
}.login-container {top: 0;left: 0;width: 100%;height: 100%;position: absolute;display: flex;justify-items: center;justify-content: center;.login-box {display: flex;align-items: center;text-align: center;.login-form {width: 360px;.verify-code-row {.el-input-group__append {padding: 0;}.verify-code {height: 40px;}}}}
}
</style>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/642133.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

虚拟机下载docker

一&#xff0c;Docker简介 百科说&#xff1a;Docker 是一个开源的应用容器引擎&#xff0c;让开发者可以打包他们的应用以及依赖包到一个可移植的容器中&#xff0c;然后发布到任何流行的Linux机器上&#xff0c;也可以实现虚拟化&#xff0c;容器是完全使用沙箱机制&#xff…

CentOS 7安装全解析

目录 一.centos安装1.1 下载镜像文件1.2 安装 二.远程连接&#xff0c;换源2.1 下载并且使用MobaXterm2.2 远程连接2.3 换源 一.centos安装 1.1 下载镜像文件 https://mirrors.aliyun.com/centos/7/isos/x86_64/ 下载即可 1.2 安装 二.远程连接&#xff0c;换源 2.1 下载并…

租幻兽帕鲁Palworld服务器多少钱?

使用腾讯云服务器搭建搭建幻兽帕鲁Palworld如何选择服务器配置&#xff1f;腾讯云百科txybk.com建议幻兽帕鲁选择腾讯云轻量应用服务器4核16G14M带宽&#xff0c;Ubuntu/Debian系统。如何收费&#xff1f; 腾讯云幻兽帕鲁服务器活动 https://curl.qcloud.com/oRMoSucP 轻量应用…

C#,入门教程(28)——文件夹(目录)、文件读(Read)与写(Write)的基础知识

上一篇&#xff1a; C#&#xff0c;入门教程(27)——应用程序&#xff08;Application&#xff09;的基础知识https://blog.csdn.net/beijinghorn/article/details/125094837 C#知识比你的预期简单的多&#xff0c;但也远远超乎你的想象&#xff01; 与文件相关的知识&#xf…

记一次低级且重大的Presto运维事故

本文纯属虚构&#xff0c;旨在提醒各位别犯类似低级错误。 如有雷同&#xff0c;说的就是你&#xff01; 文章目录 前言事件回顾后续总结 前言 首先&#xff0c;要重视运维工作和离职人员的交接工作&#xff0c;这个不必多说。一将无能&#xff0c;累死三军&#xff01; 接下来…

目标检测难题 | 小目标检测策略汇总

大家好&#xff0c;在计算机视觉中&#xff0c;检测小目标是最有挑战的问题之一&#xff0c;本文给出了一些有效的策略。 从无人机上看到的小目标 为了提高模型在小目标上的性能&#xff0c;本文推荐以下技术&#xff1a; 提高图像采集的分辨率 增加模型的输入分辨率 tile你…

3DMAX初级小白班第一课:菜单栏介绍

基本介绍 这里不可能一个一个选项全部教给大家&#xff08;毕竟之后靠实操慢慢就记住了&#xff09;&#xff0c;只说一些相对需要注意的设置。 自定义-热键编辑器-热键设置 这里有你所需要的全部快捷键 自定义-自定义UI启动布局 将UI布局还原到启动的位置 自定义-通用单…

【Linux配置yum源以及基本yum指令】

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言 一、yum是什么&#xff1f; 二、什么是软件包&#xff1f; 三、三种安装软件包的方式 四、yum的相关操作 4.1、搜索软件 4.2、安装软件 4.3、卸载软件 4.4、那…

操作系统-进程的概念,组成,特征(PCB 程序如何运行)

文章目录 总览进程的概念进程的组成-PCBPCB中存放的内容程序是如何运行的进程的组成-程序段&#xff0c;数据段进程的特征小结 总览 进程的概念 任务管理器&#xff1a;显示运行的进程 打开qq前后 打开三个qq&#xff0c;有三个进程 进程的组成-PCB PCB包含进程的很多信息 …

AI搜索引擎Perplexity来了,谷歌等老牌搜索引擎或许会有新的威胁?

Perplexity AI 是一家 AI 搜索初创公司&#xff0c;它通过结合内容索引技术和大型语言模型的推理能力&#xff0c;提供更便捷和高效的搜索体验。另外&#xff0c;最近很火的小兔子Rabbit R1硬件AI设备中的搜索功能正是这家公司的杰作。在短短一年半的时间里&#xff0c;一个企业…

51单片机1-6

目录 单片机介绍 点亮一个LED 流水灯参考代码 点亮流水LEDplus版本 独立按键 独立按键控制LED亮灭 静态数码管 静态数码管显示 动态数码管显示 模块化编程 调试工具 矩阵键盘 矩阵键盘显示数据 矩阵键盘密码锁 学习B站江协科技课程笔记。 安装keil&#xff0c;下…

Qt配置OpenCV

首先安装好Qt Createor&#xff0c;CMake&#xff0c;OpenCV,我本次使用的是Qt6.3.4和OpenCV4.6.0 Qt Creator清华镜像源:https://mirrors.tuna.tsinghua.edu.cn/qt/official_releases/qtcreator/OpenCV官网下载: https://opencv.org/releases/ 一. 编译OpenCV 首先使用Qt C…

three.js从入门到精通系列教程004 - three.js透视相机(PerspectiveCamera)滚动浏览全景大图

<!DOCTYPE html> <html><head><meta charset"UTF-8"><title>three.js从入门到精通系列教程004 - three.js透视相机&#xff08;PerspectiveCamera&#xff09;滚动浏览全景大图</title><script src"js/three.js"&g…

ThinkPHP5.0.0~5.0.23路由控制不严谨导致的RCE

本次我们继续以漏洞挖掘者的视角&#xff0c;来分析thinkphp的RCE 敏感函数发现 在调用入口函数&#xff1a;/ThinkPHP_full_v5.0.22/public/index.php 时 发现了框架底层调用了\thinkphp\library\think\App.php的app类中的incokeMethod方法 注意传递的参数&#xff0c;Refle…

shopee最新选品:Shopee平台上的最新选品策略和方法

在Shopee平台上进行选品是卖家们必须经历的重要步骤。通过精心选择和定位产品&#xff0c;卖家可以提高产品的市场接受度和销售业绩。然而&#xff0c;要在竞争激烈的电商市场中脱颖而出&#xff0c;并不是一件容易的事情。本文将介绍一些在Shopee平台上进行最新选品时可以采用…

打造专业级ChatGPT风格聊天界面:SpringBoot与Vue实现动态打字机效果,附完整前后端源码

大家好&#xff0c;今天用SpringBoot、vue写了一个仿ChatGPT官网聊天的打字机效果。 所有代码地址:gitee代码地址 &#xff0c;包含前端和后端&#xff0c;可以直接运行 使用本技术实现的项目&#xff1a;aicnn.cn&#xff0c;欢迎大家体验 如果文章知识点有错误的地方&#xf…

【源码】医院绩效管理系统,针对医、护、技、药、管不同岗位,可设置不同的核算方法、核算参数

医院绩效管理系统源码 医院绩效管理系统以国家医院绩效管理考核政策法规为依据&#xff0c;结合医院管理实践&#xff0c;以经济管理指标为核心&#xff0c;医疗质量、安全、效率、效益管理为重点&#xff0c;特别强调持续改进&#xff08;PDCA&#xff09;管理理念。实现医院绩…

《WebKit 技术内幕》学习之九(1): JavaScript引擎

1 概述 1.1 JavaScript语言 说起JavaScript语言&#xff0c;又要讲一个典型的从弱小到壮大的奋斗史。起初&#xff0c;它只是一个非常不起眼的语言&#xff0c;用来处理非常小众的问题。所以&#xff0c;从设计之初&#xff0c;它的目标就是解决一些脚本语言的问题&#xff…

[Android] Android架构体系(2)

文章目录 Bionic精简对系统调用的支持:不支持 System V IPC:有限的 Pthread 功能:有限支持C:不再支持本地化和/或宽字符:Bionic新增的特性系统属性硬编码写死的UID/GID内置了DNS解析硬编码写死的服务和协议 硬件抽象层Linux内核匿名共享内存(ASHMem)BinderLoggerION 内存管理内…

ZXing开源库生成二维码

引言 二维码&#xff08;QR Code&#xff09;作为一种快速、高容量、高密度的矩阵条码&#xff0c;已经在各行各业得到广泛应用。ZXing&#xff08;Zebra Crossing&#xff09;是一款由Google开源的Java二维码生成和解析库&#xff0c;提供了丰富的功能和易于使用的API。本篇博…