前言
近日心血來潮想做一個開源項目,目標是做一款可以適配多端、功能完備的模板工程,包含后臺管理系統(tǒng)和前臺系統(tǒng),開發(fā)者基于此項目進行裁剪和擴展來完成自己的功能開發(fā)。
本項目為前后端分離開發(fā),后端基于Java21
和SpringBoot3
開發(fā),后端使用Spring Security
、JWT
、Spring Data JPA
等技術棧,前端提供了vue
、angular
、react
、uniapp
、微信小程序
等多種腳手架工程。
本文主要介紹在SpringBoot3
項目中如何集成easy-captcha
生成驗證碼,JDK版本是Java21
,前端使用Vue3
開發(fā)。
項目地址:https://gitee.com/breezefaith/fast-alden
相關技術簡介
easy-captcha
easy-captcha是生成圖形驗證碼的Java類庫,支持gif、中文、算術等類型,可用于Java Web、JavaSE等項目。
參考地址:
- Github:https://github.com/whvcse/EasyCaptcha
實現(xiàn)步驟
引入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版本是Java21
,SpringBoot
版本是3.2.0
,如果不引入nashorn-core
,生成驗證碼時會報錯java.lang.NullPointerException: Cannot invoke "javax.script.ScriptEngine.eval(String)" because "engine" is null
。有開發(fā)者反饋使用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
屬性不會被序列化后返回給前端。
為實現(xiàn)登錄功能,還要定義一個登錄參數類LoginParam
。
@Data
public class LoginParam {
/**
* 用戶名
*/
private String username;
/**
* 密碼
*/
private String password;
/**
* 驗證碼Key
*/
private String verifyCodeKey;
/**
* 驗證碼
*/
private String verifyCode;
}
定義登錄服務類
在登錄服務類中,我們需要定義以下方法:
-
生成驗證碼
在該方法中使用easy-captcha生成一個驗證碼,生成的驗證碼除了要返回給前端,還需要在后端進行緩存,這樣才能實現(xiàn)前后端的驗證碼校驗。本文中給出了兩種緩存驗證碼的方式,一種是基于
RedisTemplate
緩存至Redis
,一種是緩存至Session
,讀者可根據需要選擇性使用,推薦使用**Redis**
。在本文附錄中給出了緩存至Session
的實現(xiàn)方式。 -
登錄
在登錄方法中首先校驗驗證碼是否正確,然后再校驗用戶名和密碼是否正確,校驗通過后生成Token返回給前端。本文中該方法僅給出驗證碼校驗相關的邏輯,其他邏輯請自行實現(xiàn)。
@Service
public class AuthService {
private final RedisTemplate<String, Object> redisTemplate;
public AuthService(
RedisTemplate<String, Object> redisTemplate
) {
this.redisTemplate = redisTemplate;
}
public VerifyCodeEntity generateVerifyCode() throws IOException {
// 創(chuàng)建驗證碼對象
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);
}
}
前端登錄頁面實現(xiàn)
此前端頁面基于Vue3
的組合式API和Element Plus
開發(fā),使用Axios
向后端發(fā)送請求,因代碼較長,將其放在附錄中,請移步至附錄查看。
測試和驗證
總結
本文介紹了如何基于Java21
和SpringBoot3
集成easy-captcha
實現(xiàn)驗證碼顯示和登錄校驗,給出了詳細的實現(xiàn)代碼,如有錯誤,還望批評指正。
在后續(xù)實踐中我也是及時更新自己的學習心得和經驗總結,希望與諸位看官一起進步。
附錄
使用Session緩存驗證碼
使用Session
緩存驗證碼時還需要借助ScheduledExecutorService
、Timer
、Quartz
等實現(xiàn)一個延遲任務,用于從Session
中刪除超時的驗證碼。文章來源:http://www.zghlxwxcb.cn/news/detail-817352.html
@Service
public class AuthService {
private final ScheduledExecutorService scheduledExecutorService;
public AuthService(
ScheduledExecutorService scheduledExecutorService
) {
this.scheduledExecutorService = scheduledExecutorService;
}
public VerifyCodeEntity generateVerifyCode() throws IOException {
// 創(chuàng)建驗證碼對象
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實現(xiàn)
scheduledExecutorService.schedule(() -> {
session.removeAttribute(verifyCode);
}, 120, TimeUnit.SECONDS);
// // 以下是使用Timer實現(xiàn)超時后刪除驗證碼
// 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。文章來源地址http://www.zghlxwxcb.cn/news/detail-817352.html
/**
* 線程池配置
*/
@Configuration
public class ThreadPoolConfig {
/**
* 核心線程池大小
*/
private final int corePoolSize = 50;
@Bean
public ScheduledExecutorService scheduledExecutorService() {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
}
前端登錄頁面實現(xiàn)代碼
<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:尋找更合理方式實現(xiàn)“記住我”
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">后臺管理系統(tǒng)</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實現(xiàn)驗證碼顯示和登錄校驗的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!