在springboot加vue项目中加入图形验证码
后端
首先先要创建一个CaptchaController的类,可以在下面的代码中看到
在getCaptcha的方法里面写好了生成随机的4位小写字母或数字的验证码,然后通过BufferedImage类变为图片,顺便加上了干扰线。之后把图片转为Base64编码方便传给前端
为了安全我写了encrypt方法把4位验证码加密了一下,和图片放在了Mapli传给了前端,后面的verifyCaptcha是对前端输入的内容和我们生成的验证码进行比较,并返回状态码。
package cn.kmbeast.controller;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/captcha")
public class CaptchaController {
private static final String ALGORITHM = "AES";
private static final String SECRET_KEY = "1234567890123456"; // 16字节的密钥
@GetMapping("/get")
public Map<String, Object> getCaptcha(HttpServletResponse response) throws Exception {
System.out.println("验证码已生成");
response.setContentType("image/png");
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Expires", "0");
// 生成随机4位验证码(字母+数字)
String code = RandomStringUtils.randomAlphanumeric(4).toLowerCase();
// 加密验证码
String encryptedCode = encrypt(code);
// 生成图片
int width = 100, height = 40;
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics2D g = image.createGraphics();
// 设置背景
g.setColor(Color.WHITE);
g.fillRect(0, 0, width, height);
// 绘制干扰线
g.setColor(Color.GRAY);
for (int i = 0; i < 10; i++) {
int x1 = (int) (Math.random() * width);
int y1 = (int) (Math.random() * height);
int x2 = (int) (Math.random() * width);
int y2 = (int) (Math.random() * height);
g.drawLine(x1, y1, x2, y2);
}
// 绘制验证码
g.setColor(Color.BLACK);
g.setFont(new Font("Arial", Font.BOLD, 30));
g.drawString(code, 15, 30);
g.dispose();
// 将图片转换为Base64编码
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
ImageIO.write(image, "PNG", baos);
byte[] imageBytes = baos.toByteArray();
String base64Image = Base64.getEncoder().encodeToString(imageBytes);
Map<String, Object> result = new HashMap<>();
result.put("image", base64Image);
result.put("encryptedCode", encryptedCode);
return result;
}
@PostMapping("/verify")
public Map<String, Object> verifyCaptcha(@RequestBody Map<String, String> requestBody) {
Map<String, Object> result = new HashMap<>();
String inputCode = requestBody.get("code");
String encryptedCode = requestBody.get("encryptedCode");
try {
// 解密验证码
String decryptedCode = decrypt(encryptedCode);
if (!decryptedCode.equalsIgnoreCase(inputCode)) {
result.put("code", 500);
result.put("msg", "验证码错误");
} else {
result.put("code", 200);
result.put("msg", "验证码验证通过");
}
} catch (Exception e) {
result.put("code", 500);
result.put("msg", "验证码验证出错");
}
return result;
}
// 加密方法
private String encrypt(String data) throws Exception {
SecretKeySpec secretKey = new SecretKeySpec(SECRET_KEY.getBytes(), ALGORITHM);
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
byte[] encryptedBytes = cipher.doFinal(data.getBytes());
return Base64.getEncoder().encodeToString(encryptedBytes);
}
// 解密方法
private String decrypt(String encryptedData) throws Exception {
SecretKeySpec secretKey = new SecretKeySpec(SECRET_KEY.getBytes(), ALGORITHM);
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, secretKey);
byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedData));
return new String(decryptedBytes);
}
}
前端
在网页要渲染的样式
<!-- 添加验证码输入框和显示验证码的图片 -->
<div class="text">
<input v-model="code" class="act" placeholder="验证码" />
<img :src="captchaImage" @click="refreshCaptcha" alt="验证码" style="cursor: pointer; vertical-align: middle; margin-left: 10px;">
</div>
逻辑处理
//在data里加入
data() {
return {
captchaImage: '', // 新增:用于存储验证码图片的 Base64 编码
encryptedCode: '' // 新增:用于存储加密的验证码
}
},methods: {
// 刷新验证码
async refreshCaptcha() {
try {
const { data } = await request.get(`http://localhost:8088/api/online-travel-sys/v1.0/captcha/get`);
this.captchaImage = `data:image/png;base64,${data.image}`;
this.encryptedCode = data.encryptedCode;
this.code = ''; // 刷新验证码时清空输入框
} catch (error) {
console.error('获取验证码出错:', error);
}
},
async login() {
if (!this.act || !this.pwd) {
this.$swal.fire({
title: '填写校验',
text: '账号或密码不能为空',
icon: 'error',
showConfirmButton: false,
timer: DELAY_TIME,
});
return;
}
if (!this.code) {
this.$swal.fire({
title: '填写校验',
text: '验证码不能为空',
icon: 'error',
showConfirmButton: false,
timer: DELAY_TIME,
});
return;
}
// 先验证验证码是否正确
try {
const { data } = await request.post(`http://localhost:8088/api/online-travel-sys/v1.0/captcha/verify`, { code: this.code, encryptedCode: this.encryptedCode });
if (data.code !== 200) {
this.$swal.fire({
title: '验证码错误',
text: data.msg,
icon: 'error',
showConfirmButton: false,
timer: DELAY_TIME,
});
this.refreshCaptcha(); // 刷新验证码
return;
}
} catch (error) {
console.error('验证码验证请求错误:', error);
this.$message.error('验证码验证出错,请重试!');
this.refreshCaptcha(); // 刷新验证码
return;
}
}
完整的前端代码
<template>
<div class="login-container">
<div class="login-panel">
<div class="logo">
<Logo :bag="colorLogo" sysName="旅友请上车"/>
</div>
<div class="text">
<input v-model="act" class="act" placeholder="账号" />
</div>
<div class="text">
<input v-model="pwd" class="pwd" type="password" placeholder="密码" />
</div>
<!-- 添加验证码输入框和显示验证码的图片 -->
<div class="text">
<input v-model="code" class="act" placeholder="验证码" />
<img :src="captchaImage" @click="refreshCaptcha" alt="验证码" style="cursor: pointer; vertical-align: middle; margin-left: 10px;">
</div>
<div>
<span class="login-btn" @click="login">立即登录</span>
</div>
<div class="tip">
<p>没有账号?<span class="no-act" @click="toDoRegister">点此注册</span></p>
</div>
</div>
</div>
</template>
<script>
const ADMIN_ROLE = 1;
const USER_ROLE = 2;
const DELAY_TIME = 1300;
import request from "@/utils/request.js";
import { setToken } from "@/utils/storage.js";
import md5 from 'js-md5';
import Logo from '@/components/Logo.vue';
export default {
name: "Login",
components: { Logo },
data() {
return {
act: '',
pwd: '',
code: '', // 新增:用于存储用户输入的验证码
colorLogo: 'rgb(38,38,38)',
captchaImage: '', // 新增:用于存储验证码图片的 Base64 编码
encryptedCode: '' // 新增:用于存储加密的验证码
}
},
created() {
// 页面加载时初始化验证码
this.refreshCaptcha();
},
methods: {
// 跳转注册页面
toDoRegister(){
this.$router.push('/register');
},
// 刷新验证码
async refreshCaptcha() {
try {
const { data } = await request.get(`http://localhost:8088/api/online-travel-sys/v1.0/captcha/get`);
this.captchaImage = `data:image/png;base64,${data.image}`;
this.encryptedCode = data.encryptedCode;
this.code = ''; // 刷新验证码时清空输入框
} catch (error) {
console.error('获取验证码出错:', error);
}
},
async login() {
if (!this.act || !this.pwd) {
this.$swal.fire({
title: '填写校验',
text: '账号或密码不能为空',
icon: 'error',
showConfirmButton: false,
timer: DELAY_TIME,
});
return;
}
if (!this.code) {
this.$swal.fire({
title: '填写校验',
text: '验证码不能为空',
icon: 'error',
showConfirmButton: false,
timer: DELAY_TIME,
});
return;
}
// 先验证验证码是否正确
try {
const { data } = await request.post(`http://localhost:8088/api/online-travel-sys/v1.0/captcha/verify`, { code: this.code, encryptedCode: this.encryptedCode });
if (data.code !== 200) {
this.$swal.fire({
title: '验证码错误',
text: data.msg,
icon: 'error',
showConfirmButton: false,
timer: DELAY_TIME,
});
this.refreshCaptcha(); // 刷新验证码
return;
}
} catch (error) {
console.error('验证码验证请求错误:', error);
this.$message.error('验证码验证出错,请重试!');
this.refreshCaptcha(); // 刷新验证码
return;
}
const hashedPwd = md5(md5(this.pwd));
const paramDTO = { userAccount: this.act, userPwd: hashedPwd };
try {
const { data } = await request.post(`user/login`, paramDTO);
if (data.code !== 200) {
this.$swal.fire({
title: '登录失败',
text: data.msg,
icon: 'error',
showConfirmButton: false,
timer: DELAY_TIME,
});
return;
}
setToken(data.data.token);
// 使用Swal通知登录成功,延迟后跳转
// this.$swal.fire({
// title: '登录成功',
// text: '即将进入系统...',
// icon: 'success',
// showConfirmButton: false,
// timer: DELAY_TIME,
// });
// 根据角色延迟跳转
setTimeout(() => {
const { role } = data.data;
this.navigateToRole(role);
}, DELAY_TIME);
} catch (error) {
console.error('登录请求错误:', error);
this.$message.error('登录请求出错,请重试!');
}
},
navigateToRole(role) {
switch (role) {
case ADMIN_ROLE:
this.$router.push('/admin');
break;
case USER_ROLE:
this.$router.push('/user');
break;
default:
console.warn('未知的角色类型:', role);
break;
}
},
}
};
</script>
<style lang="scss" scoped>
* {
user-select: none;
}
.login-container {
width: 100%;
min-height: 100vh;
background-color: rgb(255, 255, 255);
display: flex;
/* 启用Flexbox布局 */
justify-content: center;
/* 水平居中 */
align-items: center;
/* 垂直居中 */
flex-direction: column;
/* 如果需要垂直居中,确保子元素也是这样排列 */
.login-panel {
width: 313px;
height: auto;
padding: 40px 30px 16px 30px;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(36, 36, 36, 0.1), 0 1px 3px rgba(40, 40, 40, 0.06);
.logo {
margin: 10px 0 30px 0;
}
.act,
.pwd {
margin: 8px 0;
height: 53px;
line-height: 53px;
width: 100%;
padding: 0 8px;
box-sizing: border-box;
border: 1px solid rgb(232, 230, 230);
border-radius: 5px;
font-size: 18px;
padding: 0 15px;
margin-top: 13px;
}
.act:focus,
.pwd:focus {
outline: none;
border: 1px solid rgb(206, 205, 205);
transition: 1.2s;
}
.role {
display: inline-block;
color: rgb(30, 102, 147);
font-size: 14px;
padding-right: 10px;
}
}
.login-btn {
display: inline-block;
text-align: center;
border-radius: 3px;
margin-top: 20px;
height: 43px;
line-height: 43px;
width: 100%;
background-color: rgb(155, 191, 93);
font-size: 14px !important;
border: none;
color: rgb(250,250,250);
padding: 0 !important;
cursor: pointer;
user-select: none;
}
.tip {
margin: 20px 0;
p {
padding: 3px 0;
margin: 0;
font-size: 14px;
color: #647897;
i{
margin-right: 3px;
}
span {
color: #3b3c3e;
border-radius: 2px;
margin: 0 6px;
}
.no-act:hover{
color: #3e77c2;
cursor: pointer;
}
}
}
}
</style>
结果展示