前言

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

本项目为前后端分离开发,后端基于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等项目。

参考地址:

实现步骤

引入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 null
at 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]
......

定义实体类

为了方便后端校验,获取验证码的请求除了要返回验证码图片本身,还要返回一个验证码的唯一标识,所以笔者定义了一个实体类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;
}
}

如果要使用Session缓存验证码和使用ScheduledExecutorService实现延迟任务 ,还需要通过以下代码定义一个Bean。

/**
* 线程池配置
*/
@Configuration
public class ThreadPoolConfig {
@Bean
public ScheduledExecutorService scheduledExecutorService() {
return new ScheduledThreadPoolExecutor(50);
}
}

定义登录控制器

/**
* 登录控制器
*/
@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向后端发送请求,因代码较长,将其放在附录中,请移步至附录查看。

测试和验证

总结

本文介绍了如何基于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; @Bean
public 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);
// 从响应中获取token
const 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>

Java21 + SpringBoot3集成easy-captcha实现验证码显示和登录校验的更多相关文章

  1. day103:MoFang:用户登录部分:客户端提交登录信息&APICloud集成防水墙验证码&保存用户登录状态

    目录 bug:修复jsonrpc修改源码以后celery无法运行的问题 1.客户端提交登录信息 2.在APICloud中集成防水墙验证码 3.保存用户登录状态 bug:修复jsonrpc修改源码以后c ...

  2. dedecms后台验证码显示不正常的四种处理办法

    验证码不正确解决方法 分为两类解决方法 第一类:取消掉验证码,直接登录 第二类:修复验证码,回复验证码功能 四种常见的处理办法如下: 第一种:取消掉验证码具体方法如下 实现的方法一共分为两步来进行: ...

  3. ThinkPHP的验证码刷新显示和验证码显示不出来的原因

    1.应当这样<imp src='验证码路径' onclick="this.src='验证码路径?'+Math.random()">;如果后面不加Math.random( ...

  4. PHP验证码显示不出来

    PHP验证码显示不出来 验证码图片显示不出来,原因一般有三种(网上基本一致这几种原因): (1)php没有安装gd2模块,可以使用phpinfo()函数查看. (2)代码很可能是使用了像editpul ...

  5. web项目部署在centos 7验证码显示不出来解决方案

    今天把项目部署在centos7上,发现验证码显示不出来,看了一下tomcat日志 Exception in thread "http-nio-8080-exec-3" java.l ...

  6. 问题记录-java图片验证码显示乱码

    部署机器 操作系统:centos 7 java版本: java version "1.7.0_80" 问题症状 将一个java web的程序部署到了两台配置相同的服务器上之后(服务 ...

  7. php_mvc实现步骤九(登录验证码,退出-登录标记)

    shop34-17-登录验证码 验证码的分析 登录:防止暴力破解 论坛:防止灌水水 展示类:被抓取. 需要技术: 图片处理技术. 会话session技术. PHP图片处理技术 – GD 具体操作步骤 ...

  8. DedeCMS中实现在顶层banner中显示自定义登录信息

    一.需求描述 dedeCMS自带的模板中有互动中心模块,如下图所示: 由于会员登陆对我来说不是网站的重要模块且默认DedeCMS的会员中心模块的初始化很慢,常会显示“正在载入中,请稍候...”, 所以 ...

  9. laravel 验证码 auth方式登录 中间件判断session是否存在

    首先下载laravel的插件 composer下载  实现验证码       composer require mews/captcha 在config/app.php进行配置 'providers' ...

  10. 设置全局context变量 (显示用户登录名)

    比如在每个页面的最上面部分需要显示用户的登录名称,如果不登录则显示为Guest.这部分内容在每个页面都会出现,所以可将该部分内容作为一个公共模板(如userauth.html),如然后在其他模板中进行 ...

随机推荐

  1. HDU-2586 How far away?

    There are n houses in the village and some bidirectional roads connecting them. Every day peole alwa ...

  2. 可视化学习:CSS transform与仿射变换

    引言 在几年前,我就在一些博客中看到关于CSS中transform的分析,讲到它与线性代数中矩阵的关系,但当时由于使用transform比较少,再加上我毕竟是个数学学渣,对数学有点畏难心理,就有点看不 ...

  3. 2023年最后一个工作日,当 hr总监找上我协商赔偿

    今天是2023年最后一个工作日,hr 总监找上我协商赔偿一事,忆往昔三年前,公司刚融资1个亿,意气风发,博主入职即为公司巅峰,高级开发岗,14薪,各种福利,加班另算加班费,业务主要服务于众多500强集 ...

  4. ElasticSearch之Clone index API

    使用已有的索引,复制得到一个索引. 关闭testindex_001的写入操作,命令样例如下: curl -X PUT "https://localhost:9200/testindex_00 ...

  5. 痞子衡嵌入式:Farewell, 我的写博故事2023

    -- 题图:苏州虎丘塔 2023 年的最后一天,照旧写个年终总结.昨晚和同门师兄弟一起吃饭,有个师弟告诉痞子衡,微博上一个拥有 22.3W 粉丝的嵌入式同行今年 4 月发过一个吐槽微博,说恩智浦 MC ...

  6. 如何从零开始实现TDOA技术的 UWB 精确定位系统(1)

    前言 这是一个系列文章,将向你介绍如何从零开始实现一个使用TDOA技术的 UWB 精确定位系统. 重要提示(劝退说明): Q:做这个定位系统需要基础么? A:文章不是写给小白看的,需要有电子技术和软件 ...

  7. 记一次 .NET某收银软件 非托管泄露分析

    一:背景 1. 讲故事 在我的分析之旅中,遇到过很多程序的故障和杀毒软件扯上了关系,有杀毒软件导致的程序卡死,有杀毒软件导致的程序崩溃,这一篇又出现了一个杀毒软件导致的程序非托管内存泄露,真的是分析多 ...

  8. Llama2-Chinese项目:2.2-大语言模型词表扩充

      因为原生LLaMA对中文的支持很弱,一个中文汉子往往被切分成多个token,因此需要对其进行中文词表扩展.思路通常是在中文语料库上训练一个中文tokenizer模型,然后将中文tokenizer与 ...

  9. Java 将PPT转为OFD

    本文以Java后端程序代码展示如何实现将PPT幻灯片转成OFD格式.下面是具体步骤. 步骤1:安装PPT库-Spire.Presentation for Java 方法一.通过Maven仓库安装.在p ...

  10. 深入浅出Sqoop之迁移过程源码分析

    [摘要]Sqoop是一种用于在Apache Hadoop和结构化数据存储(如关系数据库)之间高效传输批量数据的工具 .本文将简单介绍Sqoop作业执行时相关的类及方法,并将该过程与MapReduce的 ...