feat: 修复登录页面验证码功能
后端修复: - 启用验证码功能(captchaEnabled: true) - 实现验证码图片生成(Java Graphics2D绘制) - 实现验证码存储和验证逻辑(内存ConcurrentHashMap) - 验证码包含干扰线、噪点、随机颜色和角度 前端修复: - 正确获取登录响应中的token(res.data.token) - 登录失败时显示错误消息并刷新验证码
This commit is contained in:
parent
20a7ebfd0b
commit
b67187f5ab
|
|
@ -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 "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -143,9 +143,14 @@ const handleLogin = () => {
|
|||
})
|
||||
|
||||
if (res.code === 200) {
|
||||
userStore.setToken(res.token)
|
||||
// token在res.data.token或res.token中
|
||||
const token = res.data?.token || res.token
|
||||
userStore.setToken(token)
|
||||
ElMessage.success('登录成功')
|
||||
router.push('/')
|
||||
} else {
|
||||
ElMessage.error(res.msg || '登录失败')
|
||||
getCaptcha()
|
||||
}
|
||||
} catch (error) {
|
||||
// 登录失败刷新验证码
|
||||
|
|
|
|||
Loading…
Reference in New Issue