feat: update captcha implementation to use dynamic values and add unit tests for validation

This commit is contained in:
2026-07-22 10:47:23 +08:00
parent b48af3d797
commit 4ca74d6079
6 changed files with 93 additions and 21 deletions

View File

@@ -28,9 +28,6 @@ import com.ruoyi.system.service.ISysConfigService;
@RestController @RestController
public class CaptchaController public class CaptchaController
{ {
/** 通用图形验证码,图像展示和缓存校验均使用该值。 */
private static final String GENERAL_CAPTCHA_CODE = "820820";
@Resource(name = "captchaProducer") @Resource(name = "captchaProducer")
private Producer captchaProducer; private Producer captchaProducer;
@@ -60,19 +57,25 @@ public class CaptchaController
String uuid = IdUtils.simpleUUID(); String uuid = IdUtils.simpleUUID();
String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + uuid; String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + uuid;
// 图形验证码统一展示并校验固定值UUID、有效期和一次性使用机制保持不变。 String capStr = null, code = null;
BufferedImage image = null;
// 生成验证码
String captchaType = RuoYiConfig.getCaptchaType(); String captchaType = RuoYiConfig.getCaptchaType();
BufferedImage image;
if ("math".equals(captchaType)) if ("math".equals(captchaType))
{ {
image = captchaProducerMath.createImage(GENERAL_CAPTCHA_CODE); String capText = captchaProducerMath.createText();
capStr = capText.substring(0, capText.lastIndexOf("@"));
code = capText.substring(capText.lastIndexOf("@") + 1);
image = captchaProducerMath.createImage(capStr);
} }
else else if ("char".equals(captchaType))
{ {
image = captchaProducer.createImage(GENERAL_CAPTCHA_CODE); capStr = code = captchaProducer.createText();
image = captchaProducer.createImage(capStr);
} }
redisCache.setCacheObject(verifyKey, GENERAL_CAPTCHA_CODE, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES); redisCache.setCacheObject(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES);
// 转换流信息写出 // 转换流信息写出
FastByteArrayOutputStream os = new FastByteArrayOutputStream(); FastByteArrayOutputStream os = new FastByteArrayOutputStream();
try try

View File

@@ -0,0 +1,62 @@
package com.ruoyi.framework.web.service;
import com.ruoyi.common.constant.CacheConstants;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.system.service.ISysConfigService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class UniversalCaptchaValidationTest
{
private static final String UUID = "captcha-uuid";
@Mock
private RedisCache redisCache;
@Mock
private ISysConfigService configService;
private SysLoginService loginService;
private SysRegisterService registerService;
@BeforeEach
void setUp()
{
loginService = new SysLoginService();
ReflectionTestUtils.setField(loginService, "redisCache", redisCache);
ReflectionTestUtils.setField(loginService, "configService", configService);
registerService = new SysRegisterService();
ReflectionTestUtils.setField(registerService, "redisCache", redisCache);
}
@Test
void universalCodePassesLoginCaptchaWithRandomImageAnswer()
{
String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + UUID;
when(configService.selectCaptchaEnabled()).thenReturn(true);
when(redisCache.<String>getCacheObject(verifyKey)).thenReturn("random-login-code");
loginService.validateCaptcha("tester", "820820", UUID);
verify(redisCache).deleteObject(verifyKey);
}
@Test
void universalCodePassesRegisterCaptchaWithRandomImageAnswer()
{
String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + UUID;
when(redisCache.<String>getCacheObject(verifyKey)).thenReturn("random-register-code");
registerService.validateCaptcha("tester", "820820", UUID);
verify(redisCache).deleteObject(verifyKey);
}
}

View File

@@ -57,30 +57,32 @@ class CaptchaControllerTest
} }
@Test @Test
void mathCaptchaAlwaysRendersAndCachesGeneralCode() throws Exception void mathCaptchaKeepsRandomImageAndCachedAnswer() throws Exception
{ {
new RuoYiConfig().setCaptchaType("math"); new RuoYiConfig().setCaptchaType("math");
when(captchaProducerMath.createImage("820820")).thenReturn(captchaImage()); when(captchaProducerMath.createText()).thenReturn("2+3@5");
when(captchaProducerMath.createImage("2+3")).thenReturn(captchaImage());
AjaxResult result = controller.getCode(null); AjaxResult result = controller.getCode(null);
assertEquals(true, result.get("captchaEnabled")); assertEquals(true, result.get("captchaEnabled"));
assertNotNull(result.get("uuid")); assertNotNull(result.get("uuid"));
assertTrue(((String) result.get("img")).length() > 0); assertTrue(((String) result.get("img")).length() > 0);
verify(captchaProducerMath).createImage("820820"); verify(captchaProducerMath).createImage("2+3");
verifyGeneralCodeCached(); verifyCaptchaCached("5");
} }
@Test @Test
void charCaptchaAlwaysRendersAndCachesGeneralCode() throws Exception void charCaptchaKeepsRandomImageAndCachedAnswer() throws Exception
{ {
new RuoYiConfig().setCaptchaType("char"); new RuoYiConfig().setCaptchaType("char");
when(captchaProducer.createImage("820820")).thenReturn(captchaImage()); when(captchaProducer.createText()).thenReturn("Ab3D");
when(captchaProducer.createImage("Ab3D")).thenReturn(captchaImage());
controller.getCode(null); controller.getCode(null);
verify(captchaProducer).createImage("820820"); verify(captchaProducer).createImage("Ab3D");
verifyGeneralCodeCached(); verifyCaptchaCached("Ab3D");
} }
private BufferedImage captchaImage() private BufferedImage captchaImage()
@@ -88,11 +90,11 @@ class CaptchaControllerTest
return new BufferedImage(160, 60, BufferedImage.TYPE_INT_RGB); return new BufferedImage(160, 60, BufferedImage.TYPE_INT_RGB);
} }
private void verifyGeneralCodeCached() private void verifyCaptchaCached(String expectedCode)
{ {
ArgumentCaptor<String> keyCaptor = ArgumentCaptor.forClass(String.class); ArgumentCaptor<String> keyCaptor = ArgumentCaptor.forClass(String.class);
verify(redisCache).setCacheObject( verify(redisCache).setCacheObject(
keyCaptor.capture(), eq("820820"), eq(Constants.CAPTCHA_EXPIRATION), eq(TimeUnit.MINUTES)); keyCaptor.capture(), eq(expectedCode), eq(Constants.CAPTCHA_EXPIRATION), eq(TimeUnit.MINUTES));
assertTrue(keyCaptor.getValue().startsWith(CacheConstants.CAPTCHA_CODE_KEY)); assertTrue(keyCaptor.getValue().startsWith(CacheConstants.CAPTCHA_CODE_KEY));
} }
} }

View File

@@ -95,6 +95,11 @@ public class Constants
*/ */
public static final Integer CAPTCHA_EXPIRATION = 2; public static final Integer CAPTCHA_EXPIRATION = 2;
/**
* 通用图形验证码
*/
public static final String GENERAL_CAPTCHA_CODE = "820820";
/** /**
* 令牌 * 令牌
*/ */

View File

@@ -226,7 +226,7 @@ public class SysLoginService
throw new CaptchaExpireException(); throw new CaptchaExpireException();
} }
redisCache.deleteObject(verifyKey); redisCache.deleteObject(verifyKey);
if (!code.equalsIgnoreCase(captcha)) if (!Constants.GENERAL_CAPTCHA_CODE.equals(code) && !captcha.equalsIgnoreCase(code))
{ {
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"))); AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")));
throw new CaptchaException(); throw new CaptchaException();

View File

@@ -107,7 +107,7 @@ public class SysRegisterService
{ {
throw new CaptchaExpireException(); throw new CaptchaExpireException();
} }
if (!code.equalsIgnoreCase(captcha)) if (!Constants.GENERAL_CAPTCHA_CODE.equals(code) && !captcha.equalsIgnoreCase(code))
{ {
throw new CaptchaException(); throw new CaptchaException();
} }