Spring Boot Distributed Captcha Login: A Complete Guide with Redis and Kaptcha
This article walks through building a distributed image captcha login for Spring Boot applications, contrasting traditional session‑based approaches with a front‑end/back‑end separated design that stores captcha codes in Redis, generates images via Kaptcha, and returns tokens to the client.
Traditional Session‑Based Captcha Login
In monolithic Spring MVC or JSP projects the captcha is stored in the HTTP session. The login request therefore needs three fields: username, password, and captcha.
Captcha Generation Flow
Login Verification Flow
The whole process depends on the session context and the backend renders the login page.
Front‑End/Back‑End Separated Captcha Login
To support micro‑service or distributed deployments the project is split into independent front‑end and back‑end modules. Captcha data is no longer kept in the session; instead a Redis cache stores the generated code together with a unique identifier.
Captcha Generation Flow
Compared with the original scheme a Redis middleware and a token are added, eliminating session dependence.
Login Verification Flow
The front‑end now receives a token and the captcha identifier, while the back‑end validates the captcha from Redis.
Implementation Steps
Project Setup and Dependencies
Add Spring Boot, Kaptcha, Redis and related libraries to pom.xml:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.lzp</groupId>
<artifactId>kaptcha</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.0.RELEASE</version>
<relativePath/>
</parent>
<dependencies>
<!-- Kaptcha -->
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Data Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- commons-pool2 required by Redis -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.3</version>
</dependency>
<!-- Jackson Databind -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>Redis Configuration
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setConnectionFactory(redisConnectionFactory);
return redisTemplate;
}
}Kaptcha Configuration
@Configuration
public class KaptchaConfig {
@Bean
public DefaultKaptcha producer() {
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
Properties properties = new Properties();
properties.setProperty("kaptcha.border", "no");
properties.setProperty("kaptcha.border.color", "105,179,90");
properties.setProperty("kaptcha.textproducer.font.color", "black");
properties.setProperty("kaptcha.image.width", "110");
properties.setProperty("kaptcha.image.height", "40");
properties.setProperty("kaptcha.textproducer.char.string", "23456789abcdefghkmnpqrstuvwxyzABCDEFGHKMNPRSTUVWXYZ");
properties.setProperty("kaptcha.textproducer.font.size", "30");
properties.setProperty("kaptcha.textproducer.char.space", "3");
properties.setProperty("kaptcha.session.key", "code");
properties.setProperty("kaptcha.textproducer.char.length", "4");
properties.setProperty("kaptcha.textproducer.font.names", "宋体,楷体,微软雅黑");
// properties.setProperty("kaptcha.obscurificator.impl","com.xxx"); // optional custom implementation
properties.setProperty("kaptcha.noise.impl", "com.google.code.kaptcha.impl.NoNoise");
Config config = new Config(properties);
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}Captcha Controller
@RestController
@RequestMapping("/captcha")
public class CaptchaController {
@Autowired
private DefaultKaptcha producer;
@Autowired
private CaptchaService captchaService;
@ResponseBody
@GetMapping("/get")
public CaptchaVO getCaptcha() throws IOException {
// generate text captcha
String content = producer.createText();
// generate image captcha
ByteArrayOutputStream outputStream = null;
BufferedImage image = producer.createImage(content);
outputStream = new ByteArrayOutputStream();
ImageIO.write(image, "jpg", outputStream);
// Base64 encode
BASE64Encoder encoder = new BASE64Encoder();
String prefix = "data:image/jpeg;base64,";
String base64Img = prefix + encoder.encode(outputStream.toByteArray())
.replace("
", "").replace("\r", "");
CaptchaVO captchaVO = captchaService.cacheCaptcha(content);
captchaVO.setBase64Img(base64Img);
return captchaVO;
}
}Captcha VO
public class CaptchaVO {
/** 验证码标识符 */
private String captchaKey;
/** 验证码过期时间 */
private Long expire;
/** base64字符串 */
private String base64Img;
public String getCaptchaKey() { return captchaKey; }
public void setCaptchaKey(String captchaKey) { this.captchaKey = captchaKey; }
public Long getExpire() { return expire; }
public void setExpire(Long expire) { this.expire = expire; }
public String getBase64Img() { return base64Img; }
public void setBase64Img(String base64Img) { this.base64Img = base64Img; }
}Captcha Service
@Service
public class CaptchaService {
@Value("${server.session.timeout:300}")
private Long timeout;
@Autowired
private RedisUtils redisUtils;
private final String CAPTCHA_KEY = "captcha:verification:";
public CaptchaVO cacheCaptcha(String captcha) {
// generate a random identifier
String captchaKey = UUID.randomUUID().toString();
// cache captcha with expiration
redisUtils.set(CAPTCHA_KEY.concat(captchaKey), captcha, timeout);
CaptchaVO captchaVO = new CaptchaVO();
captchaVO.setCaptchaKey(captchaKey);
captchaVO.setExpire(timeout);
return captchaVO;
}
}Login DTO
public class LoginDTO {
private String userName;
private String pwd;
private String captchaKey;
private String captcha;
public String getUserName() { return userName; }
public void setUserName(String userName) { this.userName = userName; }
public String getPwd() { return pwd; }
public void setPwd(String pwd) { this.pwd = pwd; }
public String getCaptchaKey() { return captchaKey; }
public void setCaptchaKey(String captchaKey) { this.captchaKey = captchaKey; }
public String getCaptcha() { return captcha; }
public void setCaptcha(String captcha) { this.captcha = captcha; }
}User Controller
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private RedisUtils redisUtils;
@PostMapping("/login")
public UserVO login(@RequestBody LoginDTO loginDTO) {
Object captch = redisUtils.get(loginDTO.getCaptchaKey());
if (captch == null) {
// throw captcha expired
}
if (!loginDTO.getCaptcha().equals(captch)) {
// throw captcha error
}
// query user info, verify password, generate token, construct UserVO
return new UserVO();
}
}Captcha Retrieval Demo
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Programmer XiaoFu
xiaofucode.com – a programmer learning guide driven by the pursuit of profit
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
