diff --git a/pom.xml b/pom.xml index 26116b3..5c78b5f 100644 --- a/pom.xml +++ b/pom.xml @@ -42,6 +42,12 @@ + com.github.penggle + kaptcha + 2.3.2 + + + org.apache.poi poi-ooxml 5.2.3 diff --git a/src/main/java/com/example/demo/config/KaptchaConfig.java b/src/main/java/com/example/demo/config/KaptchaConfig.java new file mode 100644 index 0000000..b505acd --- /dev/null +++ b/src/main/java/com/example/demo/config/KaptchaConfig.java @@ -0,0 +1,31 @@ +// com.example.demo.config.KaptchaConfig.java +package com.example.demo.config; + +import com.google.code.kaptcha.impl.DefaultKaptcha; +import com.google.code.kaptcha.util.Config; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + + +import java.util.Properties; + +@Configuration +public class KaptchaConfig { + + @Bean + public DefaultKaptcha defaultKaptcha() { + DefaultKaptcha kaptcha = new DefaultKaptcha(); + Properties properties = new Properties(); + properties.setProperty("kaptcha.image.width", "130"); + properties.setProperty("kaptcha.image.height", "45"); + properties.setProperty("kaptcha.textproducer.char.length", "4"); + properties.setProperty("kaptcha.textproducer.font.size", "35"); + properties.setProperty("kaptcha.textproducer.font.color", "black"); + properties.setProperty("kaptcha.textproducer.char.space", "5"); + properties.setProperty("kaptcha.obscurificator.impl", "com.google.code.kaptcha.impl.WaterRipple"); + properties.setProperty("kaptcha.noise.impl", "com.google.code.kaptcha.impl.DefaultNoise"); + + kaptcha.setConfig(new Config(properties)); + return kaptcha; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/demo/controller/coin/AdminController.java b/src/main/java/com/example/demo/controller/coin/AdminController.java index 18ffba2..89e5388 100644 --- a/src/main/java/com/example/demo/controller/coin/AdminController.java +++ b/src/main/java/com/example/demo/controller/coin/AdminController.java @@ -12,6 +12,7 @@ import com.example.demo.service.coin.TranslationService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.web.bind.annotation.*; @@ -37,6 +38,8 @@ public class AdminController { private LanguageTranslationUtil languageTranslationUtil; @Autowired private TranslationService translationService; + @Autowired + private StringRedisTemplate redisTemplate; @PostMapping("/test") public void testGetAdmin() { @@ -48,23 +51,50 @@ public class AdminController { @Log("用户登录") @PostMapping("/login") public Result login(@RequestBody Admin admin, @RequestHeader(defaultValue = "zh_CN") String lang) { - try { + // ====== 【新增】验证码校验逻辑 ====== + if (admin.getCaptcha() == null || admin.getUuid() == null) { + String errorMsg = "验证码或验证码ID缺失"; + String translatedErrorMsg = languageTranslationUtil.translate(errorMsg, lang); + return Result.error(translatedErrorMsg); + } + + String cacheCode = redisTemplate.opsForValue().get("CAPTCHA:" + admin.getUuid()); + if (cacheCode == null) { + String errorMsg = "验证码已过期,请重新获取"; + String translatedErrorMsg = languageTranslationUtil.translate(errorMsg, lang); + return Result.error(translatedErrorMsg); + } + + if (!cacheCode.equalsIgnoreCase(admin.getCaptcha())) { + String errorMsg = "验证码错误"; + String translatedErrorMsg = languageTranslationUtil.translate(errorMsg, lang); + return Result.error(translatedErrorMsg); + } + // ====== 验证码校验结束 ====== + // 解析语言代码 String languageCode = parseLanguageCode(lang); - // 如果不是中文环境,将输入的翻译字段转换为中文简体 if (!"zh".equalsIgnoreCase(languageCode) && !"zh_cn".equalsIgnoreCase(languageCode)) { convertLoginFieldsToChinese(admin, languageCode); } + // 执行登录(此时 admin 包含用户名、密码) admin = adminService.login(admin); + + // 登录成功后,删除已使用的验证码(防止重放) + redisTemplate.delete("CAPTCHA:" + admin.getUuid()); + + // 生成 token String token = JWTUtil.createJWT(admin); - // 对返回的管理员信息进行多语言转换 + + // 多语言转换 translateAdminInfoForLogin(admin, lang); admin.setPassword(null); return Result.success(token, admin); + } catch (Exception e) { e.printStackTrace(); log.error(e.getMessage()); diff --git a/src/main/java/com/example/demo/controller/coin/CaptchaController.java b/src/main/java/com/example/demo/controller/coin/CaptchaController.java new file mode 100644 index 0000000..29f312c --- /dev/null +++ b/src/main/java/com/example/demo/controller/coin/CaptchaController.java @@ -0,0 +1,49 @@ +// com.example.demo.controller.CaptchaController.java +package com.example.demo.controller.coin; + +import com.google.code.kaptcha.Producer; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +@RestController +public class CaptchaController { + + @Autowired + private Producer kaptchaProducer; + + @Autowired + private StringRedisTemplate redisTemplate; + + /** + * 获取图形验证码 + * @param uuid 前端生成的唯一标识,用于关联验证码 + */ + @GetMapping("/captcha") + public void captcha(@RequestParam String uuid, HttpServletResponse response) throws IOException { + if (uuid == null || uuid.trim().isEmpty()) { + response.sendError(HttpServletResponse.SC_BAD_REQUEST, "uuid is required"); + return; + } + + // 生成验证码文本和图片 + String code = kaptchaProducer.createText(); + BufferedImage image = kaptchaProducer.createImage(code); + + // 存入 Redis,5分钟过期 + redisTemplate.opsForValue().set("CAPTCHA:" + uuid, code, 5, TimeUnit.MINUTES); + + // 输出图片 + response.setHeader("Cache-Control", "no-store"); + response.setContentType("image/jpeg"); + ImageIO.write(image, "jpg", response.getOutputStream()); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/demo/domain/entity/Admin.java b/src/main/java/com/example/demo/domain/entity/Admin.java index 3630b1b..6355b1f 100644 --- a/src/main/java/com/example/demo/domain/entity/Admin.java +++ b/src/main/java/com/example/demo/domain/entity/Admin.java @@ -3,6 +3,7 @@ package com.example.demo.domain.entity; import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import jakarta.validation.constraints.NotBlank; import lombok.Data; import lombok.NoArgsConstructor; import org.springframework.security.core.GrantedAuthority; @@ -34,12 +35,17 @@ public class Admin implements UserDetails, Serializable { @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai") private Date createTime; // 创建时间 + @NotBlank(message = "验证码不能为空") + private String captcha; + @NotBlank(message = "验证码ID不能为空") + private String uuid; // 用于从 Redis 中取验证码 @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai") private Date updateTime; // 更新时间 private Integer roleId; + @Override @JsonIgnore public Collection getAuthorities() { diff --git a/src/main/java/com/example/demo/security/SecurityConfig.java b/src/main/java/com/example/demo/security/SecurityConfig.java index e7c0ecb..1530afa 100644 --- a/src/main/java/com/example/demo/security/SecurityConfig.java +++ b/src/main/java/com/example/demo/security/SecurityConfig.java @@ -61,6 +61,7 @@ public class SecurityConfig { .requestMatchers( HttpMethod.POST, // 用户不登录就可以访问的路径 "/admin/login","/upload/**","/detailY/ERP","/home/java/haiwaiyanfa/gold1/**","/home/java/haiwaiyanfa/**","/statistics/**","/Mysql/**","/Temporary/**","/cashCollection/syncToCashRecord").permitAll() + .requestMatchers(HttpMethod.GET, "/captcha").permitAll() .requestMatchers( "/error","alipay/**","/upload/**","/home/java/haiwaiyanfa/gold1/**","/home/java/haiwaiyanfa/**" ).permitAll()