TIANAI-CAPTCHA
  • v1.5.6 (latest)
  • Contents

TIANAI-CAPTCHA

Possibly the most practical open-source behavioral CAPTCHA toolkit

Introduction

About

TIANAI-CAPTCHA (TAC) is an open-source behavioral CAPTCHA toolkit with Java, Go, and other implementations.

The open-source edition includes slider, rotate, word-click, concat, proof-of-work, and more.

Live Demo

https://captcha.tianai.cloud


Documentation

Doc version: v1.5.6

Reading Guide

Suggested order:

  1. Quick Start — get Spring Boot + frontend SDK working
  2. Backend / Frontend Config — YAML, resources, styles
  3. Secondary verification, GenerateParam, POW — as needed
  4. Advanced — cache, interceptors, image transform

Runnable demo: tianai-captcha-demo
Source: tianai-captcha
Chinese docs: 中文文档


Quick Start

CAPTCHA Types

ConstantDescriptionNeeds backgroundNeeds template
SLIDERSlider puzzleYesYes (gap)
ROTATERotationYesYes (outline)
CONCATConcat / swipe restoreYesNo
WORD_IMAGE_CLICKWord clickYesNo (fonts optional)
POWProof of Work (new in 1.5.6)NoNo

See CaptchaTypeConstant.

Spring Boot (minimal)

1. Dependency

<dependency>
    <groupId>cloud.tianai.captcha</groupId>
    <artifactId>tianai-captcha-springboot-starter</artifactId>
    <version>1.5.6</version>
</dependency>

2. Minimal application.yaml

captcha:
  # Load built-in slider/rotate templates
  init-default-resource: true
  expire:
    default: 10000
    WORD_IMAGE_CLICK: 60000

More options: Backend Configuration.

3. Background images (recommended)

With built-in templates, backgrounds should be 600x360. POW does not need backgrounds.

@Configuration
public class CaptchaResourceConfiguration {

    @Bean
    public ResourceStore resourceStore() {
        LocalMemoryResourceStore resourceStore = new LocalMemoryResourceStore();
        // Resource: type (classpath / file / URL), path, tag
        resourceStore.addResource(CaptchaTypeConstant.SLIDER,
                new Resource("classpath", "bgimages/a.jpg", "default"));
        resourceStore.addResource(CaptchaTypeConstant.ROTATE,
                new Resource("classpath", "bgimages/48.jpg", "default"));
        resourceStore.addResource(CaptchaTypeConstant.CONCAT,
                new Resource("classpath", "bgimages/48.jpg", "default"));
        resourceStore.addResource(CaptchaTypeConstant.WORD_IMAGE_CLICK,
                new Resource("classpath", "bgimages/c.jpg", "default"));
        return resourceStore;
    }
}

4. Generate / check APIs

Same pattern as the official demo:

@RestController
public class CaptchaController {

    @Autowired
    private ImageCaptchaApplication imageCaptchaApplication;

    @RequestMapping("/gen")
    public ApiResponse<ImageCaptchaVO> genCaptcha(
            @RequestParam(value = "type", required = false) String type) {
        if (StringUtils.isBlank(type)) {
            type = CaptchaTypeConstant.SLIDER;
        }
        GenerateParam param = new GenerateParam();
        param.setType(type);
        return imageCaptchaApplication.generateCaptcha(param);
    }

    @PostMapping("/check")
    public ApiResponse<?> checkCaptcha(@RequestBody CaptchaData data) {
        ApiResponse<?> response = imageCaptchaApplication.matching(data.getId(), data.getData());
        if (response.isSuccess()) {
            // Return id for secondary verification / business binding
            return ApiResponse.ofSuccess(Collections.singletonMap("id", data.getId()));
        }
        return response;
    }

    @lombok.Data
    public static class CaptchaData {
        private String id;
        private ImageCaptchaTrack data;
    }
}
  • Generate: GET /gen?type=SLIDER (also ROTATE / CONCAT / WORD_IMAGE_CLICK / POW)
  • Check: POST /check, body { "id": "...", "data": { ...track or POW result... } }

Tip: Prefer matching(id, new MatchParam(track)). If secondary verification is enabled, the starter wrapper mainly hooks matching(String, ImageCaptchaTrack) — matching the demo is safer.

Frontend (minimal)

1. Get the SDK

  • Download: https://gitee.com/dromara/tianai-captcha/releases/download/tianai-captcha-1.5.6/tac-web.zip
  • Or build: https://gitee.com/dromara/tianai-captcha/tree/master/tianai-captcha-web-sdk (npm run build)

Keep frontend SDK version aligned with the backend. Put the unzipped tac folder where the browser can reach it (e.g. Spring Boot static/tac).

2. Minimal example (direct include — easiest)

<link rel="stylesheet" href="/tac/css/tac.css">
<script src="/tac/js/tac.min.js"></script>
<div id="captcha-box"></div>
<script>
  new TAC({
    requestCaptchaDataUrl: "/gen?type=SLIDER",
    validCaptchaUrl: "/check",
    bindEl: "#captcha-box",
    validSuccess: (res, c, tac) => {
      tac.destroyWindow();
      console.log("captcha id =", res.data.id);
    }
  }, {}).init();
</script>

For production, prefer on-demand load.js (lighter first paint). Full config / styles / type switching: Frontend Configuration.


Backend Configuration

Config class: cloud.tianai.captcha.spring.autoconfiguration.SpringImageCaptchaProperties (prefix = captcha).

Full YAML

captcha:
  # Cache key prefix (default: captcha)
  prefix: captcha
  # TTL in ms; built-in default ~20s if expire.default is omitted
  expire:
    default: 10000
    WORD_IMAGE_CLICK: 60000
    POW: 60000
  # Load built-in templates (default: false)
  init-default-resource: true
  # Built-in template path prefix (usually unchanged)
  # default-resource-prefix: classpath:META-INF/cut-image/template
  # Local pre-generate cache
  local-cache-enabled: true
  local-cache-size: 20
  local-cache-wait-time: 1000
  local-cache-period: 5000
  # local-cache-expire-time: 10000
  # Fonts for word-click; supports ?tag=xxx
  font-path:
    - classpath:font/SimHei.ttf
    # - classpath:font/other.ttf?tag=font1
  secondary:
    enabled: false
    expire: 120000
    keyPrefix: "captcha:secondary"

Notes:

  • With Spring Data Redis, the starter prefers RedisCacheStore / RedisResourceStore; otherwise local implementations.
  • POW does not depend on init-default-resource or background images.

Custom templates

Slider / rotate also need templates (gap / outline). With init-default-resource: true you can use official templates; custom example:

@Bean
public ResourceStore resourceStore() {
    LocalMemoryResourceStore resourceStore = new LocalMemoryResourceStore();

    ResourceMap sliderTemplate = new ResourceMap("default", 2);
    sliderTemplate.put("active.png", new Resource("classpath", "META-INF/cut-image/template/slider_1/active.png"));
    sliderTemplate.put("fixed.png", new Resource("classpath", "META-INF/cut-image/template/slider_1/fixed.png"));
    resourceStore.addTemplate(CaptchaTypeConstant.SLIDER, sliderTemplate);

    ResourceMap rotateTemplate = new ResourceMap("default", 2);
    rotateTemplate.put("active.png", new Resource("classpath", "META-INF/cut-image/template/rotate_1/active.png"));
    rotateTemplate.put("fixed.png", new Resource("classpath", "META-INF/cut-image/template/rotate_1/fixed.png"));
    resourceStore.addTemplate(CaptchaTypeConstant.ROTATE, rotateTemplate);

    resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/a.jpg", "default"));
    // Also: file / URL
    // resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("file", "/data/bg.jpg", "default"));
    // resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("URL", "https://xx/bg.jpg", "default"));
    return resourceStore;
}
  • Common template keys: active.png (piece), fixed.png (gap); optional mask.png, obfuscate_fixed.png
  • Word-click, concat, and POW do not need templates

Plain Java (non-Spring Boot)

<dependency>
    <groupId>cloud.tianai.captcha</groupId>
    <artifactId>tianai-captcha</artifactId>
    <version>1.5.6</version>
</dependency>
ImageCaptchaApplication application = TACBuilder.builder()
        .addDefaultTemplate()
        .expire("default", 10000L)
        .expire(CaptchaTypeConstant.WORD_IMAGE_CLICK, 60000L)
        .expire(CaptchaTypeConstant.POW, 60000L)
        .setInterceptor(EmptyCaptchaInterceptor.INSTANCE)
        .addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "META-INF/cut-image/resource/1.jpg"))
        .addResource(CaptchaTypeConstant.WORD_IMAGE_CLICK, new Resource("classpath", "META-INF/cut-image/resource/1.jpg"))
        .addResource(CaptchaTypeConstant.ROTATE, new Resource("classpath", "META-INF/cut-image/resource/1.jpg"))
        .addResource(CaptchaTypeConstant.CONCAT, new Resource("classpath", "META-INF/cut-image/resource/1.jpg"))
        .cached(10, 1000, 5000, 10000L)
        .addFont(new Resource("file", "/path/to/font.ttf"))
        .setCacheStore(new LocalCacheStore())
        .setTransform(new Base64ImageTransform())
        .build();

ApiResponse<ImageCaptchaVO> captcha = application.generateCaptcha(CaptchaTypeConstant.SLIDER);
ApiResponse<?> match = application.matching(captchaId, track);

ImageCaptchaApplication is thread-safe — use one global instance; do not build() repeatedly.


Frontend Configuration

Compatibility: Chrome / Firefox / Safari / Opera / major mobile browsers / iOS & Android WebView
Frameworks: H5, Vue2/3, React, Angular (vanilla JS SDK)

Use direct tac.min.js include for the quickest start.
For production, prefer load.js on-demand loading: only a tiny load.js on first paint; load tac.min.js / tac.css when the user clicks login.
window.initTAC and window.loadTAC are the same method.

ApproachWhenEntry
Direct tac.min.jsQuick start / demosnew TAC(config, style).init()
load.js on-demand (prod)Load assets on login/submitwindow.loadTAC(tacDir, config, style)

captchaConfig

OptionRequiredDescription
bindElYesMount selector, e.g. #captcha-box
requestCaptchaDataUrlYesGenerate API, e.g. /gen?type=SLIDER
validCaptchaUrlYesValidate API, e.g. /check
validSuccess(res, c, tac)RecommendedSuccess callback — implement your business logic
validFail(res, c, tac)NoDefault: reloadCaptcha()
btnRefreshFun(el, tac)NoDefault: reload
btnCloseFun(el, tac)NoDefault: destroyWindow()
requestHeadersNoCustom headers (auth, etc.)

Instance methods: init() / reloadCaptcha() / destroyWindow() / destroyCaptcha()

styleConfig

OptionDescription
logoUrlLogo URL; null hides logo; omit for default
btnUrlSlider button image (built-in default)
bgUrlWidget background image
moveTrackMaskBgColorTrack fill, default #89d2ff
moveTrackMaskBorderColorTrack border, default #0298f8
i18nCopy & font sizes; override only what you need

i18n keys (1.5.6)

KeyPurpose
tips_successSuccess tip
tips_errorError tip
slider_title / slider_title_sizeSlider
rotate_title / rotate_title_sizeRotate
concat_title / concat_title_sizeConcat
image_click_title / image_click_title_sizeClick
pow_title / pow_title_sizePOW (default: verifying...)

Sample styles from the author:

const style = {
  btnUrl: "https://minio.tianai.cloud/public/captcha-btn/btn3.png",
  bgUrl: "https://minio.tianai.cloud/public/captcha-btn/btn3-bg.jpg",
  logoUrl: "https://minio.tianai.cloud/public/static/captcha/images/logo.png",
  moveTrackMaskBgColor: "#f7b645",
  moveTrackMaskBorderColor: "#ef9c0d"
};

Demo 1: load.js on-demand (recommended)

Good for login pages: load only load.js first; fetch CAPTCHA assets after click.

Prep:

  1. Unzip tac-web.zip to e.g. /tac (js/tac.min.js, css/tac.css)
  2. Put load.js at /js/load.js
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>TAC load.js Demo</title>
  <script src="/js/load.js"></script>
  <style>
    .top { overflow: auto; margin-bottom: 12px; }
    .top li {
      float: left; list-style: none; width: 120px; height: 40px;
      margin-right: 5px; line-height: 40px; text-align: center;
      background: #409eff; color: #fff; cursor: pointer;
    }
    #login-btn {
      width: 160px; height: 44px; border: none; border-radius: 6px;
      background: #4BC065; color: #fff; cursor: pointer;
    }
  </style>
</head>
<body>
<ul class="top">
  <li data-type="SLIDER">Slider</li>
  <li data-type="ROTATE">Rotate</li>
  <li data-type="CONCAT">Concat</li>
  <li data-type="WORD_IMAGE_CLICK">Click</li>
  <li data-type="POW">POW</li>
</ul>
<button id="login-btn">Login (open CAPTCHA)</button>
<div id="captcha-box"></div>

<script>
  let globalTAC = null;
  let currentType = "SLIDER";

  const captchaConfig = {
    requestCaptchaDataUrl: "/gen?type=SLIDER",
    validCaptchaUrl: "/check",
    bindEl: "#captcha-box",
    validSuccess: (res, c, tac) => {
      tac.destroyWindow();
      login(res.data.id);
    },
    validFail: (res, c, tac) => {
      console.log("validate failed", res);
      tac.reloadCaptcha();
    },
    btnRefreshFun: (el, tac) => tac.reloadCaptcha(),
    btnCloseFun: (el, tac) => tac.destroyWindow()
  };

  const style = {
    logoUrl: null,
    moveTrackMaskBgColor: "#89d2ff",
    moveTrackMaskBorderColor: "#0298f8",
    i18n: {
      tips_success: "Success, took %s seconds",
      tips_error: "Failed, please try again!",
      slider_title: "Drag the slider to complete the puzzle",
      slider_title_size: "15px",
      rotate_title: "Drag the slider to complete the puzzle",
      rotate_title_size: "15px",
      concat_title: "Drag the slider to complete the puzzle",
      concat_title_size: "15px",
      image_click_title: "Please click in order:",
      image_click_title_size: "20px",
      pow_title: "Verifying...",
      pow_title_size: "15px"
    }
  };

  const TAC_DIR = "/tac";

  function login(captchaId) {
    fetch("/check2?id=" + encodeURIComponent(captchaId))
      .then(r => r.json())
      .then(ok => alert(ok ? "Login success" : "Secondary check failed"))
      .catch(err => console.error(err));
  }

  function openCaptcha(type) {
    currentType = type || currentType;
    captchaConfig.requestCaptchaDataUrl = "/gen?type=" + currentType;
    if (globalTAC) {
      globalTAC.destroyWindow();
      globalTAC = null;
    }
    window.loadTAC(TAC_DIR, captchaConfig, style)
      .then(tac => {
        globalTAC = tac;
        tac.init();
      })
      .catch(e => console.log("load failed", e));
  }

  document.getElementById("login-btn").addEventListener("click", function () {
    openCaptcha(currentType);
  });

  document.querySelectorAll(".top li").forEach(li => {
    li.addEventListener("click", function () {
      openCaptcha(this.getAttribute("data-type"));
    });
  });
</script>
</body>
</html>

loadTAC fetches:

  • {prefix}/js/tac.min.js
  • {prefix}/css/tac.css

Notes:

  • Prefix must point at the unzipped tac directory
  • While loading, bindEl shows a waiting placeholder
  • Before switching type: destroyWindow(), then change URL and loadTAC / init again
  • RANDOM must be implemented by your /gen (official demo excludes POW from the random pool)

Demo 2: Direct tac.min.js (optional)

Same idea as tianai-captcha-demo:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>TAC Direct Include Demo</title>
  <link rel="stylesheet" href="/tac/css/tac.css">
  <script src="/tac/js/tac.min.js"></script>
</head>
<body>
<button id="login-btn">Login</button>
<div id="captcha-box"></div>
<script>
  const config = {
    requestCaptchaDataUrl: "/gen?type=SLIDER",
    validCaptchaUrl: "/check",
    bindEl: "#captcha-box",
    validSuccess: (res, c, tac) => {
      tac.destroyWindow();
      console.log("captcha id =", res.data.id);
    },
    validFail: (res, c, tac) => tac.reloadCaptcha()
  };
  const style = { logoUrl: null };

  document.getElementById("login-btn").onclick = function () {
    new TAC(config, style).init();
  };
</script>
</body>
</html>

Secondary Verification

Primary check only proves the interaction passed. Business APIs usually send a token for a second check to bind CAPTCHA to login / order flows.

Enable

captcha:
  secondary:
    enabled: true
    expire: 120000
    keyPrefix: "captcha:secondary"

Business check

@GetMapping("/check2")
public boolean check2(@RequestParam("id") String id) {
    if (imageCaptchaApplication instanceof SecondaryVerificationApplication) {
        return ((SecondaryVerificationApplication) imageCaptchaApplication).secondaryVerification(id);
    }
    return false;
}

In validSuccess, take res.data.id and pass it to your business API (see demo).


Advanced GenerateParam

Obfuscation

GenerateParam param = new GenerateParam();
param.setType(CaptchaTypeConstant.SLIDER);
param.setObfuscate(true); // e.g. interference gap on slider
application.generateCaptcha(param);

No effect on POW.

Click: check count & interference count

GenerateParam param = new GenerateParam();
param.setType(CaptchaTypeConstant.WORD_IMAGE_CLICK);
param.setObfuscate(true);
param.addParam(ParamKeyEnum.CLICK_CHECK_CLICK_COUNT, 2);   // default ~4
param.addParam(ParamKeyEnum.CLICK_INTERFERENCE_COUNT, 3);  // default ~2
application.generateCaptcha(param);

Tolerant

Allows small error vs the answer. Default ~0.02. POW does not use tolerant.

param.addParam(ParamKeyEnum.TOLERANT, 0.02);

Font package

captcha:
  font-path:
    - classpath:font/123.ttf?tag=font1

Or:

resourceStore.addResource(FontCache.FONT_TYPE, new Resource("classpath", "font/123.ttf", "font1"));
param.addParam(ParamKeyEnum.FONT_TAG, "font1");

Image output format

param.setBackgroundFormatName("jpeg");
param.setTemplateFormatName("png");

Select background / template by tag

param.setBackgroundImageTag("tag1");
param.setTemplateImageTag("tag2");

Proof of Work (POW)

New in 1.5.6. CaptchaTypeConstant.POW
Server issues challenge params; browser Web Workers brute-force SHA-256 nonces; server verifies. No image, no drag/click.

Generate

application.generateCaptcha(CaptchaTypeConstant.POW);

GenerateParam param = new GenerateParam();
param.setType(CaptchaTypeConstant.POW);
param.addParam(ParamKeyEnum.POW_PARAM, Arrays.asList(4, 16, 4));
application.generateCaptcha(param);

Typical response:

{
  "id": "captcha-id (also POW token)",
  "type": "POW",
  "data": {
    "POW": [4, 16, 4]
  }
}

Frontend

With SDK 1.5.6+, /gen?type=POW is enough. Customize copy via style.i18n.pow_title.

Tuning

ParamMeaningDefaultNote
difficultyTarget hash prefix hex length4Higher = slower client
saltLenSalt length16Usually leave default
countNumber of challenges4More = longer total time

Advanced Extensions

Custom CacheStore

@Bean
public CacheStore cacheStore() {
    // LocalCacheStore — local
    // RedisCacheStore — provided by the starter
    return new LocalCacheStore();
}
TACBuilder.builder()
    .setCacheStore(new LocalCacheStore())
    .build();

Do not confuse ResourceStore (assets) with CacheStore (validation cache).

Interceptors

CaptchaInterceptorGroup group = new CaptchaInterceptorGroup();
group.addInterceptor(new ParamCheckCaptchaInterceptor());
group.addInterceptor(new BasicTrackCaptchaInterceptor());

Default: EmptyCaptchaInterceptor.

Custom ImageTransform

Default Base64ImageTransform. Replace to upload to OSS and return URLs, etc.

@Bean
public ImageTransform imageTransform() {
    return new Base64ImageTransform();
}

Recommended matching API

ApiResponse<?> match = application.matching(id, new MatchParam(track));

Other Frontend Frameworks

  • Vanilla JS SDK works in Vue / React / Angular
  • Vue sample available via the tip widget on Chinese pages
  • Apps / mini-programs can use WebView; contact the author for native SDKs

Questions or suggestions? Join the community

QQ Group: 993402836

WeChat Group:

最近更新: 10/20/26, 5:14 PM
Contributors: 天爱有情