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
public class CaptchaController
{
/** 通用图形验证码,图像展示和缓存校验均使用该值。 */
private static final String GENERAL_CAPTCHA_CODE = "820820";
@Resource(name = "captchaProducer")
private Producer captchaProducer;
@@ -60,19 +57,25 @@ public class CaptchaController
String uuid = IdUtils.simpleUUID();
String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + uuid;
// 图形验证码统一展示并校验固定值UUID、有效期和一次性使用机制保持不变。
String capStr = null, code = null;
BufferedImage image = null;
// 生成验证码
String captchaType = RuoYiConfig.getCaptchaType();
BufferedImage image;
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();
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
void mathCaptchaAlwaysRendersAndCachesGeneralCode() throws Exception
void mathCaptchaKeepsRandomImageAndCachedAnswer() throws Exception
{
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);
assertEquals(true, result.get("captchaEnabled"));
assertNotNull(result.get("uuid"));
assertTrue(((String) result.get("img")).length() > 0);
verify(captchaProducerMath).createImage("820820");
verifyGeneralCodeCached();
verify(captchaProducerMath).createImage("2+3");
verifyCaptchaCached("5");
}
@Test
void charCaptchaAlwaysRendersAndCachesGeneralCode() throws Exception
void charCaptchaKeepsRandomImageAndCachedAnswer() throws Exception
{
new RuoYiConfig().setCaptchaType("char");
when(captchaProducer.createImage("820820")).thenReturn(captchaImage());
when(captchaProducer.createText()).thenReturn("Ab3D");
when(captchaProducer.createImage("Ab3D")).thenReturn(captchaImage());
controller.getCode(null);
verify(captchaProducer).createImage("820820");
verifyGeneralCodeCached();
verify(captchaProducer).createImage("Ab3D");
verifyCaptchaCached("Ab3D");
}
private BufferedImage captchaImage()
@@ -88,11 +90,11 @@ class CaptchaControllerTest
return new BufferedImage(160, 60, BufferedImage.TYPE_INT_RGB);
}
private void verifyGeneralCodeCached()
private void verifyCaptchaCached(String expectedCode)
{
ArgumentCaptor<String> keyCaptor = ArgumentCaptor.forClass(String.class);
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));
}
}

View File

@@ -95,6 +95,11 @@ public class Constants
*/
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();
}
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")));
throw new CaptchaException();

View File

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