diff --git a/src/main/java/com/filesystem/config/SecurityConfig.java b/src/main/java/com/filesystem/config/SecurityConfig.java index d287582..42024c3 100644 --- a/src/main/java/com/filesystem/config/SecurityConfig.java +++ b/src/main/java/com/filesystem/config/SecurityConfig.java @@ -46,6 +46,7 @@ public class SecurityConfig { .authorizeHttpRequests(auth -> auth // API 公开接口 .requestMatchers("/api/auth/login", "/api/auth/register", "/api/auth/logout").permitAll() + .requestMatchers("/api/captcha").permitAll() .requestMatchers("/api/files/test").permitAll() .requestMatchers("/api/users/config").permitAll() // WebSocket diff --git a/src/main/java/com/filesystem/controller/AuthController.java b/src/main/java/com/filesystem/controller/AuthController.java index c11b166..b95434c 100644 --- a/src/main/java/com/filesystem/controller/AuthController.java +++ b/src/main/java/com/filesystem/controller/AuthController.java @@ -4,6 +4,7 @@ import com.filesystem.entity.User; import com.filesystem.security.UserPrincipal; import com.filesystem.service.UserService; import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpSession; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; @@ -19,17 +20,23 @@ public class AuthController { private UserService userService; @PostMapping("/login") - public ResponseEntity login(@RequestBody Map request) { + public ResponseEntity login(@RequestBody Map request, HttpSession session) { String username = request.get("username"); String password = request.get("password"); + String captcha = request.get("captcha"); + + // 验证码校验 + if (!CaptchaController.verify(session, captcha)) { + return ResponseEntity.badRequest().body(Map.of("message", "验证码错误或已过期")); + } try { String token = userService.login(username, password); User user = userService.findByUsername(username); - // 精确重算存储空间 - long storageUsed = userService.recalculateStorage(user.getId()); - long storageLimit = user.getStorageLimit(); + // 直接读取存储空间,不重算 + long storageUsed = user.getStorageUsed() != null ? user.getStorageUsed() : 0L; + long storageLimit = user.getStorageLimit() != null ? user.getStorageLimit() : 0L; Map userData = new HashMap<>(); userData.put("id", user.getId()); diff --git a/src/main/java/com/filesystem/controller/CaptchaController.java b/src/main/java/com/filesystem/controller/CaptchaController.java new file mode 100644 index 0000000..f2d40e7 --- /dev/null +++ b/src/main/java/com/filesystem/controller/CaptchaController.java @@ -0,0 +1,130 @@ +package com.filesystem.controller; + +import jakarta.servlet.http.HttpSession; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import java.util.Random; + +@RestController +@RequestMapping("/api/captcha") +public class CaptchaController { + + private static final String CHARS = "ABCDEFGHJKMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789"; + private static final int WIDTH = 140; + private static final int HEIGHT = 40; + private static final int CODE_LENGTH = 5; + + @GetMapping + public ResponseEntity getCaptcha(HttpSession session) { + // 生成随机验证码 + String code = generateCode(); + + // 存入 session + session.setAttribute("captcha", code.toLowerCase()); + session.setAttribute("captchaTime", System.currentTimeMillis()); + + // 生成图片 + String imageBase64 = generateCaptchaImage(code); + + return ResponseEntity.ok(Map.of( + "data", Map.of( + "image", "data:image/png;base64," + imageBase64 + ) + )); + } + + /** + * 验证验证码 + */ + public static boolean verify(HttpSession session, String inputCode) { + if (inputCode == null || inputCode.isEmpty()) { + return false; + } + + String storedCode = (String) session.getAttribute("captcha"); + Long captchaTime = (Long) session.getAttribute("captchaTime"); + + if (storedCode == null || captchaTime == null) { + return false; + } + + // 验证码 5 分钟有效 + if (System.currentTimeMillis() - captchaTime > 5 * 60 * 1000) { + session.removeAttribute("captcha"); + session.removeAttribute("captchaTime"); + return false; + } + + // 验证后清除,防止重复使用 + session.removeAttribute("captcha"); + session.removeAttribute("captchaTime"); + + return storedCode.equals(inputCode.toLowerCase()); + } + + private String generateCode() { + Random random = new Random(); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < CODE_LENGTH; i++) { + sb.append(CHARS.charAt(random.nextInt(CHARS.length()))); + } + return sb.toString(); + } + + private String generateCaptchaImage(String code) { + 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(Color.WHITE); + g.fillRect(0, 0, WIDTH, HEIGHT); + + // 干扰线 + Random random = new Random(); + for (int i = 0; i < 6; i++) { + g.setColor(new Color(random.nextInt(200), random.nextInt(200), random.nextInt(200))); + g.drawLine(random.nextInt(WIDTH), random.nextInt(HEIGHT), + random.nextInt(WIDTH), random.nextInt(HEIGHT)); + } + + // 干扰点 + for (int i = 0; i < 40; i++) { + g.setColor(new Color(random.nextInt(255), random.nextInt(255), random.nextInt(255))); + g.fillOval(random.nextInt(WIDTH), random.nextInt(HEIGHT), 2, 2); + } + + // 验证码文字 + g.setFont(new Font("Arial", Font.BOLD, 26)); + for (int i = 0; i < code.length(); i++) { + // 随机颜色 + g.setColor(new Color(random.nextInt(150), random.nextInt(150), random.nextInt(150))); + // 随机角度 + double angle = (random.nextDouble() - 0.5) * 0.4; + g.rotate(angle, 18 + i * 24, 26); + g.drawString(String.valueOf(code.charAt(i)), 10 + i * 24, 28); + g.rotate(-angle, 18 + i * 24, 26); + } + + g.dispose(); + + // 转为 Base64 + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + javax.imageio.ImageIO.write(image, "png", baos); + return Base64.getEncoder().encodeToString(baos.toByteArray()); + } catch (IOException e) { + return ""; + } + } +} diff --git a/src/main/java/com/filesystem/controller/MessageController.java b/src/main/java/com/filesystem/controller/MessageController.java index dad100e..93743d8 100644 --- a/src/main/java/com/filesystem/controller/MessageController.java +++ b/src/main/java/com/filesystem/controller/MessageController.java @@ -165,6 +165,8 @@ public class MessageController { m.put("fromNickname", fromUser.getNickname() != null ? fromUser.getNickname() : fromUser.getUsername()); m.put("fromAvatar", fromUser.getAvatar()); m.put("fromSignature", fromUser.getSignature()); + m.put("fromPhone", fromUser.getPhone()); + m.put("fromEmail", fromUser.getEmail()); } return m; }).collect(Collectors.toList()); diff --git a/src/main/java/com/filesystem/websocket/ChatHandler.java b/src/main/java/com/filesystem/websocket/ChatHandler.java index c25378d..bd6ad9b 100644 --- a/src/main/java/com/filesystem/websocket/ChatHandler.java +++ b/src/main/java/com/filesystem/websocket/ChatHandler.java @@ -89,6 +89,8 @@ public class ChatHandler extends TextWebSocketHandler { messageData.put("fromNickname", fromUser.getNickname() != null ? fromUser.getNickname() : fromUser.getUsername()); messageData.put("fromAvatar", fromUser.getAvatar()); messageData.put("fromSignature", fromUser.getSignature()); + messageData.put("fromPhone", fromUser.getPhone()); + messageData.put("fromEmail", fromUser.getEmail()); } Map resp = Map.of("type", "chat", "message", messageData); diff --git a/web-vue/src/api/auth.js b/web-vue/src/api/auth.js index 33faf26..fee66ec 100644 --- a/web-vue/src/api/auth.js +++ b/web-vue/src/api/auth.js @@ -5,3 +5,5 @@ export const login = (data) => request.post('/auth/login', data) export const register = (data) => request.post('/auth/register', data) export const getCurrentUser = () => request.get('/auth/info') + +export const getCaptcha = () => request.get('/captcha') diff --git a/web-vue/src/components/ChatDialog.vue b/web-vue/src/components/ChatDialog.vue index 644b8f9..32bc9d0 100644 --- a/web-vue/src/components/ChatDialog.vue +++ b/web-vue/src/components/ChatDialog.vue @@ -84,7 +84,7 @@