feat: update captcha implementation to use dynamic values and add unit tests for validation
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,6 +95,11 @@ public class Constants
|
||||
*/
|
||||
public static final Integer CAPTCHA_EXPIRATION = 2;
|
||||
|
||||
/**
|
||||
* 通用图形验证码
|
||||
*/
|
||||
public static final String GENERAL_CAPTCHA_CODE = "820820";
|
||||
|
||||
/**
|
||||
* 令牌
|
||||
*/
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user