feat: 修复登录页面验证码功能

后端修复:
- 启用验证码功能(captchaEnabled: true)
- 实现验证码图片生成(Java Graphics2D绘制)
- 实现验证码存储和验证逻辑(内存ConcurrentHashMap)
- 验证码包含干扰线、噪点、随机颜色和角度

前端修复:
- 正确获取登录响应中的token(res.data.token)
- 登录失败时显示错误消息并刷新验证码
This commit is contained in:
神码-方晓辉 2026-02-01 13:54:36 +08:00
parent 20a7ebfd0b
commit b67187f5ab
2 changed files with 129 additions and 6 deletions

View File

@ -3,8 +3,12 @@ package com.pangu.web.controller.system;
import com.pangu.common.core.domain.AjaxResult;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* 登录控制器
@ -14,6 +18,11 @@ import java.util.Map;
@RestController
public class SysLoginController {
/**
* 验证码存储开发阶段使用内存生产环境应使用Redis
*/
private static final Map<String, String> CAPTCHA_CACHE = new ConcurrentHashMap<>();
/**
* 获取用户信息
*/
@ -39,6 +48,27 @@ public class SysLoginController {
*/
@PostMapping("/api/login")
public AjaxResult login(@RequestBody Map<String, String> loginBody) {
String username = loginBody.get("username");
String password = loginBody.get("password");
String code = loginBody.get("code");
String uuid = loginBody.get("uuid");
// 验证码校验
if (uuid != null && !uuid.isEmpty()) {
String cachedCode = CAPTCHA_CACHE.remove(uuid);
if (cachedCode == null) {
return AjaxResult.error("验证码已过期");
}
if (code == null || !cachedCode.equalsIgnoreCase(code)) {
return AjaxResult.error("验证码错误");
}
}
// 用户名密码校验开发阶段简单校验
if (!"admin".equals(username) || !"admin123".equals(password)) {
return AjaxResult.error("用户名或密码错误");
}
Map<String, Object> data = new HashMap<>();
data.put("token", "mock-token-" + System.currentTimeMillis());
return AjaxResult.success(data);
@ -57,10 +87,98 @@ public class SysLoginController {
*/
@GetMapping("/api/captchaImage")
public AjaxResult getCaptchaImage() {
String uuid = UUID.randomUUID().toString();
String code = generateCaptchaCode(4);
// 存储验证码
CAPTCHA_CACHE.put(uuid, code);
// 生成验证码图片
String imgBase64 = generateCaptchaImage(code);
Map<String, Object> data = new HashMap<>();
data.put("uuid", "mock-uuid-" + System.currentTimeMillis());
data.put("img", "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==");
data.put("captchaEnabled", false);
data.put("uuid", uuid);
data.put("img", "data:image/png;base64," + imgBase64);
data.put("captchaEnabled", true);
return AjaxResult.success(data);
}
/**
* 生成随机验证码字符
*/
private String generateCaptchaCode(int length) {
String chars = "ABCDEFGHJKMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789";
Random random = new Random();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < length; i++) {
sb.append(chars.charAt(random.nextInt(chars.length())));
}
return sb.toString();
}
/**
* 生成验证码图片Base64编码
*/
private String generateCaptchaImage(String code) {
int width = 120;
int height = 40;
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics2D g = image.createGraphics();
// 设置抗锯齿
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// 背景色
g.setColor(new Color(240, 240, 240));
g.fillRect(0, 0, width, height);
// 绘制干扰线
Random random = new Random();
g.setColor(new Color(200, 200, 200));
for (int i = 0; i < 6; i++) {
int x1 = random.nextInt(width);
int y1 = random.nextInt(height);
int x2 = random.nextInt(width);
int y2 = random.nextInt(height);
g.drawLine(x1, y1, x2, y2);
}
// 绘制验证码文字
g.setFont(new Font("Arial", Font.BOLD, 28));
Color[] colors = {
new Color(65, 105, 225),
new Color(220, 20, 60),
new Color(34, 139, 34),
new Color(255, 140, 0)
};
for (int i = 0; i < code.length(); i++) {
g.setColor(colors[i % colors.length]);
// 随机旋转角度
double angle = (random.nextDouble() - 0.5) * 0.3;
g.rotate(angle, 25 + i * 25, 28);
g.drawString(String.valueOf(code.charAt(i)), 15 + i * 25, 30);
g.rotate(-angle, 25 + i * 25, 28);
}
// 绘制噪点
for (int i = 0; i < 50; i++) {
int x = random.nextInt(width);
int y = random.nextInt(height);
g.setColor(new Color(random.nextInt(200), random.nextInt(200), random.nextInt(200)));
g.fillRect(x, y, 1, 1);
}
g.dispose();
// 转换为Base64
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(image, "png", baos);
return Base64.getEncoder().encodeToString(baos.toByteArray());
} catch (Exception e) {
return "";
}
}
}

View File

@ -143,9 +143,14 @@ const handleLogin = () => {
})
if (res.code === 200) {
userStore.setToken(res.token)
// tokenres.data.tokenres.token
const token = res.data?.token || res.token
userStore.setToken(token)
ElMessage.success('登录成功')
router.push('/')
} else {
ElMessage.error(res.msg || '登录失败')
getCaptcha()
}
} catch (error) {
//