Building a Distributed Captcha Login with SpringBoot and Redis
This article walks through the design and implementation of a distributed image‑captcha login system using SpringBoot, Kaptcha, and Redis, comparing traditional session‑based approaches with a front‑back‑end separated architecture and providing complete code examples for each component.
Introduction
To prevent brute‑force attacks, many systems add image or SMS captchas. The article presents a front‑back‑end separated image‑captcha login solution built with SpringBoot, Kaptcha, and Redis.
Traditional non‑separated captcha login
In monolithic projects that rely on HTTP session, the captcha value is stored in the session context. The login request only needs username, password, and captcha.
Captcha generation flow
Login verification flow
The process still depends on the session context and the backend adjusts the page.
Front‑back‑end separated captcha login
With code and responsibilities split, a Redis middleware replaces the session storage. A unique identifier is added to distinguish each captcha request.
Captcha generation flow
The new flow stores the captcha in Redis and returns a token instead of relying on session.
Login verification flow
The separated architecture adds Redis and a token, removing session dependence and moving page adjustments to the frontend.
Implementation details
Kaptcha introduction
Kaptcha is an open‑source project based on SimpleCaptcha.
<!--Kaptcha is an open‑source captcha project based on SimpleCaptcha-->
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>Project setup and dependencies
<?xml version="1.0" encoding="UTF-8"?>
<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>
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.3</version>
</dependency>
<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 class
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory redisConnectionFactory){
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setConnectionFactory(redisConnectionFactory);
return redisTemplate;
}
}Kaptcha configuration class
@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
package com.lzp.kaptcha.controller;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.lzp.kaptcha.service.CaptchaService;
import com.lzp.kaptcha.vo.CaptchaVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import sun.misc.BASE64Encoder;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@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
BufferedImage image = producer.createImage(content);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ImageIO.write(image, "jpg", outputStream);
// Base64 encode the byte array
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 value object
package com.lzp.kaptcha.vo;
public class CaptchaVO {
private String captchaKey; // identifier
private Long expire; // expiration time
private String base64Img; // image data
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 (caching in Redis)
package com.lzp.kaptcha.service;
import com.lzp.kaptcha.utils.RedisUtils;
import com.lzp.kaptcha.vo.CaptchaVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.UUID;
@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 the 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
package com.lzp.kaptcha.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 (login verification)
package com.lzp.kaptcha.controller;
import com.lzp.kaptcha.dto.LoginDTO;
import com.lzp.kaptcha.utils.RedisUtils;
import com.lzp.kaptcha.vo.UserVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private RedisUtils redisUtils;
@PostMapping("/login")
public UserVO login(@RequestBody LoginDTO loginDTO) {
Object captcha = redisUtils.get(loginDTO.getCaptchaKey());
if (captcha == null) {
// throw captcha expired exception
}
if (!loginDTO.getCaptcha().equals(captcha)) {
// throw captcha mismatch exception
}
// query user information, verify existence and password
// construct UserVO, generate token, and return
return new UserVO();
}
}Captcha retrieval screenshots
Architect's Guide
Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.
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.
