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.

Architect's Guide
Architect's Guide
Architect's Guide
Building a Distributed Captcha Login with SpringBoot and Redis

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

backendRedisCaptchaSpringBootKaptchaDistributed Login
Architect's Guide
Written by

Architect's Guide

Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.

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.