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.

Programmer XiaoFu
Programmer XiaoFu
Programmer XiaoFu
Spring Boot Distributed Captcha Login: A Complete Guide with Redis and Kaptcha

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

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

backendredisSpring Bootcaptchakaptchadistributed-login
Programmer XiaoFu
Written by

Programmer XiaoFu

xiaofucode.com – a programmer learning guide driven by the pursuit of profit

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.