43 Commits

Author SHA1 Message Date
天爱有情 e7b9fd923b 1.5.5 2026-02-28 09:58:04 +08:00
天爱有情 a7fca6ee57 solon插件暂时无人维护,先考虑单独搬迁项目到另一个仓库 2026-02-10 08:47:28 +08:00
tianai cfab8ce3ac fix: TacBuilder中添加资源只能添加一条的bug 2026-02-09 19:28:56 +08:00
天爱有情 387ed937f8 1.5.4 2026-01-26 16:43:15 +08:00
天爱有情 fbbf3e8204 fix: springboot4的适配。上次代码被覆盖了,重新提交一下。 2026-01-26 11:58:02 +08:00
tianai 655b7faf57 fix: 修复点选验证码图片文字缩放的bug 2026-01-24 18:12:13 +08:00
tianai 356d3ab62c feat: CacheImageCaptchaGenerator支持自定义忽略某些参数不参与校验
fix: 修复TACBuilder.builder无法创建的bug
perf: 优化AnyMap结构
2026-01-24 13:39:30 +08:00
天爱有情 a4f8a99093 增加可配置自定义容错值的选项
cloud.tianai.captcha.generator.common.model.dto.ParamKeyEnum#TOLERANT
2026-01-23 17:24:25 +08:00
天爱有情 16e517c69e 修复资源泄露问题并优化资源管理
- 添加 AutoCloseable 接口实现,支持自动资源释放
   - 修复 Graphics2D、ImageOutputStream 等资源未正确关闭的问题
   - 为 Spring Bean 添加 destroyMethod 配置
   - 添加定时任务线程池的正确关闭逻辑
   - FontCache 添加缓存大小限制防止内存泄露
   - 优化 StandardWordClickImageCaptchaGenerator 代码结构
2026-01-14 17:14:45 +08:00
天爱有情 29279e8c56 修复资源泄露问题并优化资源管理
- 添加 AutoCloseable 接口实现,支持自动资源释放
   - 修复 Graphics2D、ImageOutputStream 等资源未正确关闭的问题
   - 为 Spring Bean 添加 destroyMethod 配置
   - 添加定时任务线程池的正确关闭逻辑
   - FontCache 添加缓存大小限制防止内存泄露
   - 优化 StandardWordClickImageCaptchaGenerator 代码结构
2026-01-14 17:09:59 +08:00
天爱有情 db0603a124 feat: 支持springboot4 2025-12-30 16:00:47 +08:00
天爱有情 7c8730f73b feat: 支持springboot4 2025-12-30 15:59:26 +08:00
天爱有情 55b3510360 feat: 升级lombok版本,支持java21 2025-12-30 15:06:45 +08:00
天爱有情 b71cc8cd28 Merge remote-tracking branch 'origin/master' 2025-12-30 09:10:57 +08:00
天爱有情 a4fb7fa1fd fix: gitee #IDGKCI 修复BasicCaptchaTrackValidator基本校验失败后,返回失败的问题 2025-12-30 09:10:45 +08:00
天爱有情 5a90fa6ec4 !24 创建tianai-captcha-springboot4-starter模块,以适配springboot4版本。
Merge pull request !24 from lichenpark/master
2025-12-08 09:45:13 +00:00
Disenchanted 42b2102faf 创建tianai-captcha-springboot4-starter模块,以适配springboot4版本。 2025-12-04 14:51:20 +08:00
天爱有情 a2e6ae0ca6 !22 1、缓存前缀处理逻辑修改为提供专用的缓存前缀处理接口 StoreCacheKeyPrefix 类,方便用户个性缓存前缀处理逻辑扩展需求。
Merge pull request !22 from Alay/master
2025-11-12 02:56:07 +00:00
chxlay a323c2b262 - 1、增加接口类 StoreCacheKeyPrefix,用于处理 DefaultImageCaptchaApplication 的缓存 key 处理
- 2、DefaultImageCaptchaApplication 中的缓存 key 处理修改为 接口,便于扩展使用
- 3、DefaultImageCaptchaApplication的构建器 TACBuilder 中增加对 StoreCacheKeyPrefix 的支持
2025-11-11 14:55:12 +08:00
天爱有情 a46cb7d5fd 重构系统项目结构,将 tianai-captcha、tianai-captcha-springboot-starter、tianai-captcha-web-sdk、tianai-captcha-solon-plugin、整合到一块 2025-10-27 15:25:39 +08:00
天爱有情 b6442a7e12 重构系统项目结构,将 tianai-captcha、tianai-captcha-springboot-starter、tianai-captcha-web-sdk、tianai-captcha-solon-plugin、整合到一块 2025-10-27 15:17:51 +08:00
天爱有情 5eb258215b 重构系统项目结构, 将 tianai-captcha
tianai-captcha-springboot-starter
   tianai-captcha-web-sdk
   tianai-captcha-solon-plugin
   整合到一块
2025-10-27 15:14:10 +08:00
天爱有情 af2df2c7e2 feat(captcha):重构点选验证码生成逻辑并优化验证轨迹处理
- 修改 AbstractClickImageCaptchaGenerator 中的图片提示资源结构,支持 ResourceMap 类型
- 引入 CommonConstant 常量用于区分提示图标与点击图标资源
- 新增 Block 类用于管理背景图上的可点击区域分块逻辑
- 更新点击图片的位置计算方式,从随机坐标改为基于分块的确定位置
- 添加 obfuscateImage 方法用于后续图像混淆处理扩展
- 调整时间戳字段类型由 Date 改为 Long,提升性能及一致性
- 在 ParamKeyEnum 中新增 FONT_TAG 参数键,支持按标签读取字体资源
- 标准化字体获取方法,允许通过 GenerateParam 指定字体标签
-修正 DefaultBuiltInResources 中字体路径拼写错误(fontS → fonts)
- 补充 ResourceStore 接口的 getTarget 默认方法实现
- 更新测试类 TACBuilderTest2 示例代码中的验证码类型调用参数
2025-10-15 17:12:15 +08:00
天爱有情 3d28302db5 !21 增加classLoader设置
Merge pull request !21 from Mr_Li/master
2025-09-29 08:15:18 +00:00
xiangtuo 47cc2445f5 增加classLoader设置 2025-09-24 08:57:58 +08:00
天爱有情 25bf75b804 refactor(resource): 重构资源存储和管理逻辑
- 移除了 AbstractResourceStore 类
- 新增了 CrudResourceStore 接口,定义了 CRUD操作
- 修改了 DefaultImageCaptchaResourceManager,支持批量获取资源和模板
- 重构了 FontCache 类,改为实现 ResourceStore 接口
- 更新了相关应用类,使用新的资源管理逻辑
2025-06-30 16:59:16 +08:00
天爱有情 cb92a224d5 refactor(resource): 重构资源存储和管理逻辑
- 移除了 AbstractResourceStore 类
- 新增了 CrudResourceStore 接口,定义了 CRUD操作
- 修改了 DefaultImageCaptchaResourceManager,支持批量获取资源和模板
- 重构了 FontCache 类,改为实现 ResourceStore 接口
- 更新了相关应用类,使用新的资源管理逻辑
2025-06-30 16:34:24 +08:00
天爱有情 12d290919a Merge branch 'master' of https://gitcode.com/dromara/tianai-captcha 2025-06-30 16:19:08 +08:00
tiana 3fba6825cc update: 更新文件 readme.md
Signed-off-by: tiana <tianaiyouqing@163.com>
2025-05-21 10:53:50 +08:00
天爱有情 6d8736e52e Merge remote-tracking branch 'origin/master' 2025-04-09 16:06:21 +08:00
天爱有情 48bfa27ec8 设置加载默认资源时, 给文字点选验证码加默认的字体包 2025-04-09 16:06:10 +08:00
天爱有情 a2557a71d3 !18 去除 lombok 的注解@Builder,@Builder 本身设计的问题,导致 通过 构建器构件的 GenerateParam 实例丢失默认值
Merge pull request !18 from Alay/master
2025-03-20 03:53:46 +00:00
chxlay 68252bf0d6 去除 lombok 的注解@Builder,@Builder 本身设计的问题,导致 通过 构建器构件的 GenerateParam 实例丢失默认值
改用手动编写构建器 Builder ,完成实例的构建操作
2025-03-19 10:47:22 +08:00
chxlay 938112f2dc 去除 lombok 的注解@Builder,@Builder 本身设计的问题,导致 通过 构建器构件的 GenerateParam 实例丢失默认值
改用手动声明操作函数完成 GenerateParam 实例创建和链式赋值操作函数
2025-03-19 10:34:05 +08:00
天爱有情 09abccc3e8 v 1.5.2 2025-03-18 10:55:01 +08:00
天爱有情 0ec2e1b137 v 1.5.2 2025-03-18 10:53:29 +08:00
天爱有情 3b1b211629 feat(captcha): 优化点选验证码逻辑
- 修改 AbstractClickImageCaptchaGenerator 中的 getClickImg 方法,增加 randomColor 参数
- 更新 MultiImageCaptchaGenerator 中的 StandardWordClickImageCaptchaGenerator 实例创建方式
- 新增 ParamKeyEnum 类,用于定义点选验证码的参数键
- 更新 StandardWordClickImageCaptchaGenerator 中的随机字体选择逻辑
-调整 filterAndSortClickImageCheckDefinition 方法,支持自定义校验数量
2025-03-12 17:35:22 +08:00
天爱有情 5767d98f15 refactor: 删除 StaticCaptchaPostProcessorManager 类
删除了 src/main/java/cloud/tianai/captcha/generator/impl/StaticCaptchaPostProcessorManager.java 文件。该类似乎未被使用,移除无用代码有助于简化项目结构。
2025-01-13 17:11:57 +08:00
天爱有情 4874116bc5 fix(application): 修复设置 CaptchaInterceptor 时对模板图片的处理
- 修复 setCaptchaInterceptor 方法,确保 captchaGenerator 正确设置拦截器
- 优化模板图片加载逻辑,提高验证码生成性能- 添加测试代码,验证 SLIDER 验证码生成耗时
2024-11-22 16:30:46 +08:00
天爱有情 600878f6bd feat(resource): 重构资源存储和加载机制
- 新增 AbstractResourceStore 类,实现 ResourceStore 接口的通用逻辑
- 创建 FontCache 类,用于缓存和管理字体资源
- 重构 DefaultImageCaptchaResourceManager 类,支持资源提供者和监听器
- 更新 Resource 和 ResourceMap 类,增加唯一 ID 字段
- 新增 ResourceListener 接口,用于扩展资源存储功能
- 创建 ResourceProviders 类,统一管理资源提供者
- 更新 TACBuilder 类,支持新的资源存储和加载机制
2024-11-22 11:54:21 +08:00
天爱有情 2be22591bf Merge branch 'master' of github.com:dromara/tianai-captcha 2024-08-26 09:44:27 +08:00
天爱有情 916f65f2bc 更新文档 2024-08-26 09:43:06 +08:00
天爱有情 3a218a798d 更新验证码 2024-08-26 09:41:08 +08:00
161 changed files with 19001 additions and 1209 deletions
+2
View File
@@ -24,3 +24,5 @@ target
/nbdist/
/.nb-gradle/
.flattened-pom.xml
**/.flattened-pom.xml
+233
View File
@@ -0,0 +1,233 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
必须用中文回复我
## Project Overview
tianai-captcha (天爱验证码/TAC) is a Java-based behavioral CAPTCHA library supporting multiple verification types:
- **SLIDER**: Slide-to-fit puzzle captcha
- **ROTATE**: Rotation verification captcha
- **CONCAT**: Slide-to-restore captcha
- **WORD_IMAGE_CLICK**: Text-click verification captcha
The project is a multi-module Maven project with Java 8 compatibility.
## Build Commands
```bash
# Build the entire project (skips tests by default)
mvn clean install
# Build without skipping tests
mvn clean install -DskipTests=false
# Build specific module
cd tianai-captcha
mvn clean install
# Package for deployment
mvn clean package
# Generate javadoc
mvn javadoc:javadoc
```
## Testing
```bash
# Run all tests
mvn test
# Run tests for specific module
cd tianai-captcha
mvn test
# Run a specific test class
mvn test -Dtest=TACBuilderTest
# Run a specific test method
mvn test -Dtest=TACBuilderTest#testMethod
```
## Project Structure
### Modules
1. **tianai-captcha** (core module)
- Core captcha generation and validation logic
- Generator implementations for each captcha type
- Resource management system
- Cache and storage abstractions
2. **tianai-captcha-springboot-starter**
- Spring Boot auto-configuration
- Redis integration support
- Configuration properties binding
3. **tianai-captcha-solon-plugin**
- Solon framework integration
4. **tianai-captcha-web-sdk**
- Frontend JavaScript SDK
## Core Architecture
### Key Components
**ImageCaptchaApplication** (cloud.tianai.captcha.application.ImageCaptchaApplication)
- Main entry point for captcha operations
- Handles captcha generation via `generateCaptcha(type)`
- Handles validation via `matching(id, matchParam)`
- Manages lifecycle of generators, validators, and cache
**TACBuilder** (cloud.tianai.captcha.application.TACBuilder)
- Builder pattern for constructing ImageCaptchaApplication
- Fluent API for configuration
- Key methods:
- `addDefaultTemplate()`: Load built-in templates
- `addResource(type, resource)`: Add background images
- `addTemplate(type, resourceMap)`: Add custom templates
- `addFont(resource)`: Add fonts for WORD_IMAGE_CLICK
- `cached(size, waitTime, period, expireTime)`: Enable pre-generation cache
- `setCacheStore(store)`: Set cache implementation (local or distributed)
- `setResourceStore(store)`: Set resource storage
- `setTransform(transform)`: Set image transformation (default: Base64)
### Generator Layer
**MultiImageCaptchaGenerator** (cloud.tianai.captcha.generator.impl.MultiImageCaptchaGenerator)
- Delegates to specific generators based on captcha type
- Manages resource loading and image transformation
**Specific Generators**:
- `StandardSliderImageCaptchaGenerator`: Generates slider puzzles
- `StandardRotateImageCaptchaGenerator`: Generates rotation captchas
- `StandardConcatImageCaptchaGenerator`: Generates slide-to-restore captchas
- `StandardWordClickImageCaptchaGenerator`: Generates text-click captchas
**CacheImageCaptchaGenerator** (cloud.tianai.captcha.generator.impl.CacheImageCaptchaGenerator)
- Wrapper that pre-generates captchas for improved performance
- Configurable cache size and refresh intervals
### Validator Layer
**ImageCaptchaValidator** (cloud.tianai.captcha.validator.ImageCaptchaValidator)
- Interface for captcha validation logic
- Default implementation: `SimpleImageCaptchaValidator`
**BasicCaptchaTrackValidator** (cloud.tianai.captcha.validator.impl.BasicCaptchaTrackValidator)
- Validates mouse/touch track data
- Checks for suspicious behavior patterns
### Resource Management
**ResourceStore** (cloud.tianai.captcha.resource.ResourceStore)
- Abstraction for storing captcha resources (images, templates)
- Implementations:
- `LocalMemoryResourceStore`: In-memory storage
- Custom implementations for distributed scenarios
**ImageCaptchaResourceManager** (cloud.tianai.captcha.resource.ImageCaptchaResourceManager)
- Manages resource loading and retrieval
- Default implementation: `DefaultImageCaptchaResourceManager`
**Resource** (cloud.tianai.captcha.resource.common.model.dto.Resource)
- Represents a resource with type and location
- Types: "classpath", "file", "url"
### Cache Layer
**CacheStore** (cloud.tianai.captcha.cache.CacheStore)
- Abstraction for caching captcha data
- Implementations:
- `LocalCacheStore`: Local in-memory cache
- Redis-based implementation in Spring Boot starter
### Image Transformation
**ImageTransform** (cloud.tianai.captcha.generator.ImageTransform)
- Converts generated images to desired format
- Default: `Base64ImageTransform` (JPG for background, PNG for template)
## Configuration
### Non-Spring Projects
Use TACBuilder to configure:
```java
ImageCaptchaApplication app = TACBuilder.builder()
.addDefaultTemplate()
.addResource("SLIDER", new Resource("classpath", "path/to/image.jpg"))
.cached(20, 5000, 2000, 120000L)
.build();
```
### Spring Boot Projects
Configure via application.yml:
```yaml
captcha:
prefix: captcha # Cache key prefix
expire:
default: 120000 # Default expiration (ms)
WORD_IMAGE_CLICK: 180000 # Type-specific expiration
init-default-resource: false # Load built-in resources
local-cache-enabled: true # Enable pre-generation cache
local-cache-size: 20 # Cache size
local-cache-wait-time: 5000 # Wait time on cache miss (ms)
local-cache-period: 2000 # Cache refresh interval (ms)
font-path: # Font files for WORD_IMAGE_CLICK
- classpath:font/simhei.ttf
```
## Important Constants
**CaptchaTypeConstant** (cloud.tianai.captcha.common.constant.CaptchaTypeConstant)
- `SLIDER`: Slide-to-fit captcha
- `ROTATE`: Rotation captcha
- `CONCAT`: Slide-to-restore captcha
- `WORD_IMAGE_CLICK`: Text-click captcha
**ParamKeyEnum** (cloud.tianai.captcha.generator.common.model.dto.ParamKeyEnum)
- `TOLERANT`: Configurable tolerance value for validation (added in recent commit)
## Key Data Flow
### Generation Flow
1. Client calls `ImageCaptchaApplication.generateCaptcha(type)`
2. `MultiImageCaptchaGenerator` selects appropriate generator
3. Generator loads resources via `ImageCaptchaResourceManager`
4. Generator creates captcha with random parameters
5. `ImageTransform` converts images to Base64 (or custom format)
6. Captcha data cached in `CacheStore` with unique ID
7. Returns `ImageCaptchaVO` with ID and image data
### Validation Flow
1. Client submits ID and track data via `matching(id, matchParam)`
2. Application retrieves cached captcha data by ID
3. `ImageCaptchaValidator` validates track against expected answer
4. `BasicCaptchaTrackValidator` checks track behavior patterns
5. Returns `ApiResponse` with success/failure status
## Extension Points
- **Custom Generators**: Implement `ImageCaptchaGenerator` interface
- **Custom Validators**: Implement `ImageCaptchaValidator` interface
- **Custom Cache**: Implement `CacheStore` (e.g., Redis, Memcached)
- **Custom Resources**: Implement `ResourceStore` for centralized resource management
- **Custom Transform**: Implement `ImageTransform` for different image formats
- **Interceptors**: Implement `CaptchaInterceptor` for pre/post processing
## Recent Changes
- Added support for Spring Boot 4 (commit: db0603a, 7c8730f)
- Fixed resource leak issues (commits: 16e517c, 29279e8)
- Added configurable tolerance value via `ParamKeyEnum.TOLERANT` (commit: a4f8a99)
## Documentation
- Online Demo: http://captcha.tianai.cloud
- Online Documentation: http://doc.captcha.tianai.cloud
- License: MulanPSL-2.0
+78 -55
View File
@@ -3,14 +3,19 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cloud.tianai.captcha</groupId>
<artifactId>tianai-captcha</artifactId>
<version>1.5.1</version>
<name>tianai-captcha</name>
<artifactId>tianai-captcha-parent</artifactId>
<version>${revision}</version>
<packaging>pom</packaging>
<name>tianai-captcha-parent</name>
<description>行为验证码</description>
<url>https://gitee.com/tianai/tianai-captcha</url>
<modules>
<module>tianai-captcha</module>
<module>tianai-captcha-springboot-starter</module>
</modules>
<properties>
<revision>1.5.5</revision>
<java.version>1.8</java.version>
<!-- 打包跳过单元测试 -->
<skipTests>true</skipTests>
@@ -18,9 +23,9 @@
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<skip.nexus>false</skip.nexus>
<deplay.id>ossrh</deplay.id>
<deplay.repository>https://oss.sonatype.org/service/local/staging/deploy/maven2/</deplay.repository>
<deplay.snapshotRepository>https://oss.sonatype.org/content/repositories/snapshots/</deplay.snapshotRepository>
<serverId>ossrh</serverId>
<deplay.repository>https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/</deplay.repository>
<deplay.snapshotRepository>https://s01.oss.sonatype.org/content/repositories/snapshots</deplay.snapshotRepository>
<!-- 私服 -->
<!-- <skip.nexus>true</skip.nexus>-->
@@ -28,7 +33,6 @@
<!-- <deplay.repository>http://192.168.3.10:6061/repository/smart_hosted/</deplay.repository>-->
<!-- <deplay.snapshotRepository>http://192.168.3.10:6061/repository/smart_hosted/</deplay.snapshotRepository>-->
</properties>
<licenses>
<license>
<name>The MulanPSL2 License, Version 2.0</name>
@@ -36,6 +40,11 @@
</license>
</licenses>
<scm>
<url>https://gitee.com/tianai/tianai-captcha</url>
</scm>
<developers>
<developer>
<name>tianaiyouqing</name>
@@ -44,12 +53,11 @@
<organizationUrl>http://tianai.cloud</organizationUrl>
</developer>
</developers>
<scm>
<url>https://gitee.com/tianai/tianai-captcha</url>
</scm>
<distributionManagement>
<snapshotRepository>
<id>${deplay.id}</id>
<id>${serverId}</id>
<url>${deplay.snapshotRepository}</url>
</snapshotRepository>
<repository>
@@ -58,42 +66,55 @@
</repository>
</distributionManagement>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.30</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 统一版本号管理 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<groupId>org.codehaus.mojo</groupId>
<artifactId>flatten-maven-plugin</artifactId>
<version>1.2.7</version>
<configuration>
<source>8</source>
<target>8</target>
<compilerArgument>-parameters</compilerArgument>
<updatePomFile>true</updatePomFile>
<flattenMode>resolveCiFriendliesOnly</flattenMode>
</configuration>
<executions>
<execution>
<id>flatten</id>
<phase>process-resources</phase>
<goals>
<goal>flatten</goal>
</goals>
</execution>
<execution>
<id>flatten.clean</id>
<phase>clean</phase>
<goals>
<goal>clean</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.sonatype.plugins</groupId>
<artifactId>nexus-staging-maven-plugin</artifactId>
<version>1.6.7</version>
<extensions>true</extensions>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>2.9.1</version>
<configuration>
<skipNexusStagingDeployMojo>${skip.nexus}</skipNexusStagingDeployMojo>
<serverId>ossrh</serverId>
<nexusUrl>https://oss.sonatype.org/</nexusUrl>
<autoReleaseAfterClose>true</autoReleaseAfterClose>
<!-- 统一生成聚合文档,解决 mvn package 时控制台发出 javadoc 警告的问题 -->
<aggregate>true</aggregate>
<failOnError>false</failOnError>
<detectLinks>false</detectLinks>
</configuration>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
<configuration>
<additionalparam>-Xdoclint:none</additionalparam>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
@@ -108,22 +129,7 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>2.9.1</version>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
<configuration>
<additionalparam>-Xdoclint:none</additionalparam>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
@@ -143,7 +149,24 @@
<artifactId>maven-resources-plugin</artifactId>
<version>2.6</version>
</plugin>
<plugin>
<groupId>org.sonatype.central</groupId>
<artifactId>central-publishing-maven-plugin</artifactId>
<version>0.5.0</version>
<extensions>true</extensions>
<configuration>
<publishingServerId>ossrh</publishingServerId>
<!-- <tokenAuth>true</tokenAuth>-->
<autoPublish>true</autoPublish>
</configuration>
</plugin>
</plugins>
<pluginManagement>
<plugins>
</plugins>
</pluginManagement>
</build>
</project>
+213 -17
View File
@@ -1,14 +1,18 @@
## 可能是开源界最好用的行为验证码工具
<div align="center">
-----
![][image-logo]
## pc版在线体验 [在线体验](http://captcha.tianai.cloud)
![star](https://gitcode.com/dromara/tianai-captcha/star/badge.svg)
### tianaiCAPTCHA - 天爱验证码(TAC)
#### 基于 JAVA实现的行为验证码
### **[在线体验 🚀][online-demo-link]**
### **[在线文档 🚀][doc-link]**
</div>
## 在线文档 [在线文档](http://doc.captcha.tianai.cloud)
![](https://minio.tianai.cloud/public/%E6%A0%87%E9%A2%98%E5%9B%BE%E7%89%87.jpg)
## 简单介绍
- tianai-captcha 目前支持的行为验证码类型
@@ -18,30 +22,98 @@
- 文字点选验证码
- 后面会陆续支持市面上更多好玩的验证码玩法... 敬请期待
## 快速上手
## 快速上手(后端)
> 注意: 如果你项目是使用的**Springboot**
>
>
请使用SpringBoot脚手架工具[tianai-captcha-springboot-starter](https://gitee.com/tianai/tianai-captcha-springboot-starter);
>
> 该工具对tianai-captcha验证码进行了封装,使其使用更加方便快捷
### springboot项目
1. 导入依赖
```xml
<dependency>
<groupId>cloud.tianai.captcha</groupId>
<artifactId>tianai-captcha-springboot-starter</artifactId>
<version>1.5.5</version>
</dependency>
```
2. 使用`ImageCaptchaApplication`生成和校验验证码
```java
public class Test2 {
@Autowired
private ImageCaptchaApplication application;
// 生成验证码
public void gen() {
ApiResponse<ImageCaptchaVO> res1 = application.generateCaptcha(CaptchaTypeConstant.SLIDER);
// 匹配验证码是否正确
// 该参数包含了滑动轨迹滑动时间等数据,用于校验滑块验证码。 由前端传入
ImageCaptchaTrack sliderCaptchaTrack = new ImageCaptchaTrack();
ApiResponse<?> match = application.matching(res1.getId(), sliderCaptchaTrack);
}
// 校验验证码
public boolean valid(@RequestBody ImageCaptchaTrack captchaTrack) {
ApiResponse<?> matching = captchaApplication.matching(data.getId(), sliderCaptchaTrack);
return matching.isSuccess();
}
}
```
3. springboot配置文件说明
```yaml
# 滑块验证码配置, 详细请看 cloud.tianai.captcha.autoconfiguration.ImageCaptchaProperties 类
captcha:
# 如果项目中使用到了redis,滑块验证码会自动把验证码数据存到redis中, 这里配置redis的key的前缀,默认是captcha:slider
prefix: captcha
# 验证码过期时间,默认是2分钟,单位毫秒, 可以根据自身业务进行调整
expire:
# 默认缓存时间 2分钟
default: 10000
# 针对 点选验证码 过期时间设置为 2分钟, 因为点选验证码验证比较慢,把过期时间调整大一些
WORD_IMAGE_CLICK: 20000
# 使用加载系统自带的资源, 默认是 false
init-default-resource: false
# 缓存控制, 默认为false不开启
local-cache-enabled: true
# 验证码会提前缓存一些生成好的验证数据, 默认是20
local-cache-size: 20
# 缓存拉取失败后等待时间 默认是 5秒钟
local-cache-wait-time: 5000
# 缓存检查间隔 默认是2秒钟
local-cache-period: 2000
# 配置字体库,文字点选验证码的字体库,可以配置多个
font-path:
- classpath:font/simhei.ttf
secondary:
# 二次验证, 默认false 不开启
enabled: false
# 二次验证过期时间, 默认 2分钟
expire: 120000
# 二次验证缓存key前缀,默认是 captcha:secondary
keyPrefix: "captcha:secondary"
```
> **写好的验证码demo移步 [tianai-captcha-demo](https://gitee.com/tianai/tianai-captcha-demo)**
### 1. 导入xml
### 非spring项目
1. 导入xml
```xml
<!-- maven 导入 -->
<dependency>
<groupId>cloud.tianai.captcha</groupId>
<artifactId>tianai-captcha</artifactId>
<version>1.5.1</version>
<version>1.5.4</version>
</dependency>
```
### 2. 构建 `ImageCaptchaApplication`负责生成和校验验证码
2. 构建 `ImageCaptchaApplication`负责生成和校验验证码
```java
import cloud.tianai.captcha.validator.common.model.dto.MatchParam;
@@ -81,7 +153,117 @@ public class ApplicationTest {
}
```
### 3.详细文档请点击 [在线文档](http://doc.captcha.tianai.cloud)
## 快速上手(前端)
| 条目 | |
| -------- | ------------------------------------------------------------ |
| 兼容性 | Chrome、Firefox、Safari、Opera、主流手机浏览器、iOS 及 Android上的内嵌Webview |
| 框架支持 | H5、Angular、React、Vue2、Vue3 |
### 安装
1. 将打包好的`tac`目录放到自己项目中,如果是vue、react等框架,将tac目录放到public目录中、或者放到某个可以访问到地方,比如oss之类的可以被浏览器访问到的地方 (tac下载地址 [https://gitee.com/dromara/tianai-captcha/releases/tag/tianai-captcha-1.5.4](https://gitee.com/dromara/tianai-captcha/releases/tag/tianai-captcha-1.5.4)
2. 引入初始化函数 (load.js下载地址 [https://minio.tianai.cloud/public/static/captcha/js/load.min.js](https://minio.tianai.cloud/public/static/captcha/js/load.min.js)) 可自己将load.js下载到本地
```html
<script src="load.min.js"></script>
```
**注: 如果是web框架,将该引入代码放到 `public/index.html`**
### 使用方法
2. 创建一个div块用于渲染验证码, 该div用于装载验证码
```html
<div id="captcha-box"></div>
```
3. 在需要调用验证码的时候执行加载验证码方法
```js
function login() {
// config 对象为TAC验证码的一些配置和验证的回调
const config = {
// 生成接口 (必选项,必须配置, 要符合tianai-captcha默认验证码生成接口规范)
requestCaptchaDataUrl: "/gen",
// 验证接口 (必选项,必须配置, 要符合tianai-captcha默认验证码校验接口规范)
validCaptchaUrl: "/check",
// 验证码绑定的div块 (必选项,必须配置)
bindEl: "#captcha-box",
// 验证成功回调函数(必选项,必须配置)
validSuccess: (res, c, tac) => {
// 销毁验证码服务
tac.destroyWindow();
console.log("验证成功,后端返回的数据为", res);
// 调用具体的login方法
login(res.data.token)
},
// 验证失败的回调函数(可忽略,如果不自定义 validFail 方法时,会使用默认的)
validFail: (res, c, tac) => {
console.log("验证码验证失败回调...")
// 验证失败后重新拉取验证码
tac.reloadCaptcha();
},
// 刷新按钮回调事件
btnRefreshFun: (el, tac) => {
console.log("刷新按钮触发事件...")
tac.reloadCaptcha();
},
// 关闭按钮回调事件
btnCloseFun: (el, tac) => {
console.log("关闭按钮触发事件...")
tac.destroyWindow();
}
}
// 一些样式配置, 可不传
let style = {
logoUrl: null;// 去除logo
// logoUrl: "/xx/xx/xxx.png" // 替换成自定义的logo
}
// 参数1 为 tac文件是目录地址, 目录里包含 tac的js和css等文件
// 参数2 为 tac验证码相关配置
// 参数3 为 tac窗口一些样式配置
window.initTAC("./tac", config, style).then(tac => {
tac.init(); // 调用init则显示验证码
}).catch(e => {
console.log("初始化tac失败", e);
})
}
```
### 对滑块的按钮和背景设置为自定义的一些样式
```js
// 这里分享一些作者自己调的样式供参考
const style = {
// 按钮样式
btnUrl: "https://minio.tianai.cloud/public/captcha-btn/btn3.png",
// 背景样式
bgUrl: "https://minio.tianai.cloud/public/captcha-btn/btn3-bg.jpg",
// logo地址
logoUrl: "https://minio.tianai.cloud/public/static/captcha/images/logo.png",
// 滑动边框样式
moveTrackMaskBgColor: "#f7b645",
moveTrackMaskBorderColor: "#ef9c0d"
}
window.initTAC("./tac", config, style).then(tac => {
tac.init(); // 调用init则显示验证码
}).catch(e => {
console.log("初始化tac失败", e);
})
```
## 详细文档请点击 [在线文档](http://doc.captcha.tianai.cloud)
# qq群: 197340494
# 微信群:
@@ -89,3 +271,17 @@ public class ApplicationTest {
## 微信群加不上的话 加微信好友 微信号: youseeseeyou-1ttd 拉你入群
[image-logo]: https://minio.tianai.cloud/public/captcha/logo/logo-519x100.png
[github-release-shield]: https://img.shields.io/github/v/release/tianaiyouqing/tianai-captcha-go?color=369eff&labelColor=black&logo=github&style=flat-square
[github-release-link]: https://github.com/tianaiyouqing/tianai-captcha-go/releases
[github-license-link]: https://github.com/tianaiyouqing/tianai-captcha-go/blob/master/LICENSE
[github-license-shield]: https://img.shields.io/badge/MulanPSL-2.0-white?labelColor=black&style=flat-square
[tianai-captcha-java-link]: https://github.com/dromara/tianai-captcha
[captcha-go-demo-link]: https://gitee.com/tianai/captcha-go-demo
[tianai-captcha-web-sdk-link]: https://github.com/tianaiyouqing/captcha-web-sdk
[online-demo-link]: http://captcha.tianai.cloud
[doc-link]: http://doc.captcha.tianai.cloud
[qrcode-link]: https://minio.tianai.cloud/public/qun4.png
@@ -1,24 +0,0 @@
package cloud.tianai.captcha.application.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* @Author: 天爱有情
* @Date 2020/5/29 8:31
* @Description 验证码返回对象
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CaptchaResponse<T> implements Serializable {
private String id;
private T captcha;
public static <T> CaptchaResponse<T> of(String id, T data) {
return new CaptchaResponse<T>(id, data);
}
}
@@ -1,197 +0,0 @@
package cloud.tianai.captcha.common;
import lombok.EqualsAndHashCode;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Function;
@EqualsAndHashCode
public class AnyMap implements Map<String, Object> {
private Map<String, Object> target;
public AnyMap() {
target = new LinkedHashMap<>();
}
public AnyMap(Map<String, Object> map) {
this.target = map;
}
public Float getFloat(String key) {
return getFloat(key, null);
}
public Float getFloat(String key, Float defaultData) {
Object data = get(key);
if (data != null) {
if (data instanceof Number) {
return ((Number) data).floatValue();
}
try {
if (data instanceof String) {
return Float.parseFloat((String) data);
}
} catch (NumberFormatException e) {
throw e;
}
}
return defaultData;
}
public Integer getInt(String key, Integer defaultData) {
Object data = get(key);
if (data != null) {
if (data instanceof Number) {
return ((Number) data).intValue();
}
try {
if (data instanceof String) {
return Integer.parseInt((String) data);
}
} catch (NumberFormatException e) {
throw e;
}
}
return defaultData;
}
public String getString(String key, String defaultData) {
Object data = get(key);
if (data != null) {
if (data instanceof String) {
return (String) data;
}
return String.valueOf(data);
}
return defaultData;
}
public static AnyMap of(Map<String, Object> map) {
return new AnyMap(map);
}
// ================== implement Map =======================
@Override
public int size() {
return target.size();
}
@Override
public boolean isEmpty() {
return target.isEmpty();
}
@Override
public boolean containsKey(Object key) {
return target.containsKey(key);
}
@Override
public boolean containsValue(Object value) {
return target.containsValue(value);
}
@Override
public Object get(Object key) {
return target.get(key);
}
@Override
public Object put(String key, Object value) {
return target.put(key, value);
}
@Override
public Object remove(Object key) {
return target.remove(key);
}
@Override
public void putAll(Map<? extends String, ?> m) {
target.putAll(m);
}
@Override
public void clear() {
target.clear();
}
@Override
public Set<String> keySet() {
return target.keySet();
}
@Override
public Collection<Object> values() {
return target.values();
}
@Override
public Set<Entry<String, Object>> entrySet() {
return target.entrySet();
}
@Override
public Object getOrDefault(Object key, Object defaultValue) {
return target.getOrDefault(key, defaultValue);
}
@Override
public void forEach(BiConsumer<? super String, ? super Object> action) {
target.forEach(action);
}
@Override
public void replaceAll(BiFunction<? super String, ? super Object, ?> function) {
target.replaceAll(function);
}
@Override
public Object putIfAbsent(String key, Object value) {
return target.putIfAbsent(key, value);
}
@Override
public boolean remove(Object key, Object value) {
return target.remove(key, value);
}
@Override
public boolean replace(String key, Object oldValue, Object newValue) {
return target.replace(key, oldValue, newValue);
}
@Override
public Object replace(String key, Object value) {
return target.replace(key, value);
}
@Override
public Object computeIfAbsent(String key, Function<? super String, ?> mappingFunction) {
return target.computeIfAbsent(key, mappingFunction);
}
@Override
public Object computeIfPresent(String key, BiFunction<? super String, ? super Object, ?> remappingFunction) {
return target.computeIfPresent(key, remappingFunction);
}
@Override
public Object compute(String key, BiFunction<? super String, ? super Object, ?> remappingFunction) {
return target.compute(key, remappingFunction);
}
@Override
public Object merge(String key, Object value, BiFunction<? super Object, ? super Object, ?> remappingFunction) {
return target.merge(key, value, remappingFunction);
}
}
@@ -1,32 +0,0 @@
package cloud.tianai.captcha.generator.common;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.awt.*;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class FontWrapper {
private Font font;
private Float currentFontTopCoef;
public FontWrapper(Font font) {
this(font, 70);
}
public FontWrapper(Font font, int fontSize) {
this.font = font;
this.font = font.deriveFont(Font.BOLD, fontSize);
}
public float getCurrentFontTopCoef() {
if (currentFontTopCoef != null) {
return currentFontTopCoef;
}
currentFontTopCoef = 0.14645833f * font.getSize() + 0.39583333f;
return currentFontTopCoef;
}
}
@@ -1,65 +0,0 @@
package cloud.tianai.captcha.generator.common.model.dto;
import cloud.tianai.captcha.common.AnyMap;
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
import lombok.*;
/**
* @Author: 天爱有情
* @date 2022/2/11 9:44
* @Description 生成参数
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
// param作为扩展字段暂时将param从equals和toString中移除掉 以适应 CacheImageCaptchaGenerator
@EqualsAndHashCode(exclude = "param")
public class GenerateParam {
/** 背景格式化类型. */
private String backgroundFormatName = "jpeg";
/** 模板图片格式化类型. */
private String templateFormatName = "png";
/** 是否混淆. */
private Boolean obfuscate = false;
/** 类型. */
private String type = CaptchaTypeConstant.SLIDER;
/** 背景图片标签, 用户二级过滤背景图片,或指定某背景图片. */
private String backgroundImageTag;
/** 滑动图片标签,用户二级过滤模板图片,或指定某模板图片.. */
private String templateImageTag;
/** 扩展参数. */
private AnyMap param = new AnyMap();
public void addParam(String key, Object value) {
doGetOrCreateParam().put(key, value);
}
public Object getParam(String key) {
return param == null ? null : param.get(key);
}
private AnyMap doGetOrCreateParam() {
if (param == null) {
param = new AnyMap();
}
return param;
}
public Object removeParam(String key) {
if (param == null) {
return null;
}
return param.remove(key);
}
public Object getOrDefault(String key, Object defaultValue) {
if (param == null) {
return defaultValue;
}
return param.getOrDefault(key, defaultValue);
}
}
@@ -1,74 +0,0 @@
package cloud.tianai.captcha.generator.impl;
//
///**
// * @Author: 天爱有情
// * @date 2023/4/24 15:23
// * @Description 验证码后处理器管理
// */
//public class StaticCaptchaPostProcessorManager {
//
// @Getter
// private static LinkedList<ImageCaptchaPostProcessor> processors = new LinkedList<>();
//
// public static void add(ImageCaptchaPostProcessor processor) {
// processors.add(processor);
// }
//
// public static void add(Integer index, ImageCaptchaPostProcessor processor) {
// processors.add(index, processor);
// }
//
// public static void addFirst(ImageCaptchaPostProcessor processor) {
// processors.addFirst(processor);
// }
//
// public static void addLast(ImageCaptchaPostProcessor processor) {
// processors.addLast(processor);
// }
//
// public static void clear() {
// processors.clear();
// }
//
// public static void add(List<ImageCaptchaPostProcessor> addPostProcessors) {
// processors.addAll(addPostProcessors);
// }
//
//
// public static ImageCaptchaInfo applyPostProcessorBeforeGenerate(CaptchaExchange captchaExchange, ImageCaptchaGenerator context) {
// for (ImageCaptchaPostProcessor processor : processors) {
// try {
// ImageCaptchaInfo imageCaptchaInfo = processor.beforeGenerateCaptchaImage(captchaExchange, context);
// if (imageCaptchaInfo != null) {
// return imageCaptchaInfo;
// }
// } catch (Exception e) {
// throw new ImageCaptchaException("apply ImageCaptchaPostProcessor.beforeGenerateCaptchaImage error, [" + processor.getClass() + "]", e);
// }
// }
// return null;
// }
//
// public static void applyPostProcessorBeforeWrapImageCaptchaInfo(CaptchaExchange captchaExchange, ImageCaptchaGenerator context) {
// for (ImageCaptchaPostProcessor processor : processors) {
// try {
// processor.beforeWrapImageCaptchaInfo(captchaExchange, context);
// } catch (Exception e) {
// throw new ImageCaptchaException("apply ImageCaptchaPostProcessor.beforeWrapImageCaptchaInfo error, [" + processor.getClass() + "]", e);
// }
// }
// }
//
//
// public static void applyPostProcessorAfterGenerateCaptchaImage(CaptchaExchange captchaExchange, ImageCaptchaInfo imageCaptchaInfo, ImageCaptchaGenerator context) {
// for (ImageCaptchaPostProcessor processor : processors) {
// try {
// processor.afterGenerateCaptchaImage(captchaExchange, imageCaptchaInfo, context);
// } catch (Exception e) {
// throw new ImageCaptchaException("apply ImageCaptchaPostProcessor.afterGenerateCaptchaImage error, [" + processor.getClass() + "]", e);
// }
// }
// }
//
//}
@@ -1,56 +0,0 @@
package cloud.tianai.captcha.resource;
import cloud.tianai.captcha.resource.common.model.dto.Resource;
import cloud.tianai.captcha.resource.common.model.dto.ResourceMap;
/**
* @Author: 天爱有情
* @date 2022/5/7 9:04
* @Description 资源存储
*/
public interface ResourceStore {
/**
* 添加资源
*
* @param type 验证码类型
* @param resource 资源
*/
void addResource(String type, Resource resource);
/**
* 添加模板
*
* @param type 验证码类型
* @param template 模板
*/
void addTemplate(String type, ResourceMap template);
/**
* 随机获取某个资源
*
* @param type type
* @return Resource
*/
Resource randomGetResourceByTypeAndTag(String type, String tag);
/**
* 随机获取某个模板通过type
*
* @param type type
* @return Map<String, Resource>
*/
ResourceMap randomGetTemplateByTypeAndTag(String type, String tag);
/**
* 清除所有内置模板
*/
void clearAllTemplates();
/**
* 清除所有内置资源
*/
void clearAllResources();
}
@@ -1,109 +0,0 @@
package cloud.tianai.captcha.resource.impl;
import cloud.tianai.captcha.resource.ImageCaptchaResourceManager;
import cloud.tianai.captcha.resource.ResourceProvider;
import cloud.tianai.captcha.resource.ResourceStore;
import cloud.tianai.captcha.resource.common.model.dto.Resource;
import cloud.tianai.captcha.resource.common.model.dto.ResourceMap;
import cloud.tianai.captcha.resource.impl.provider.ClassPathResourceProvider;
import cloud.tianai.captcha.resource.impl.provider.FileResourceProvider;
import cloud.tianai.captcha.resource.impl.provider.URLResourceProvider;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* @Author: 天爱有情
* @date 2021/8/7 15:35
* @Description 默认的滑块验证码资源管理
*/
public class DefaultImageCaptchaResourceManager implements ImageCaptchaResourceManager {
/** 资源存储. */
private ResourceStore resourceStore;
/** 资源转换 转换为stream流. */
private final List<ResourceProvider> resourceProviderList = new ArrayList<>(8);
public DefaultImageCaptchaResourceManager() {
init();
}
public DefaultImageCaptchaResourceManager(ResourceStore resourceStore) {
this.resourceStore = resourceStore;
init();
}
private void init() {
if (this.resourceStore == null) {
this.resourceStore = new LocalMemoryResourceStore();
}
// 注入一些默认的提供者
registerResourceProvider(new URLResourceProvider());
registerResourceProvider(new ClassPathResourceProvider());
registerResourceProvider(new FileResourceProvider());
}
@Override
public ResourceMap randomGetTemplate(String type, String tag) {
ResourceMap resourceMap = resourceStore.randomGetTemplateByTypeAndTag(type, tag);
if (resourceMap == null) {
throw new IllegalStateException("随机获取模板错误,store中模板为空, type:" + type);
}
return resourceMap;
}
@Override
public Resource randomGetResource(String type, String tag) {
Resource resource = resourceStore.randomGetResourceByTypeAndTag(type, tag);
if (resource == null) {
throw new IllegalStateException("随机获取资源错误,store中资源为空, type:" + type);
}
return resource;
}
@Override
public InputStream getResourceInputStream(Resource resource) {
for (ResourceProvider resourceProvider : resourceProviderList) {
if (resourceProvider.supported(resource.getType())) {
InputStream resourceInputStream = resourceProvider.getResourceInputStream(resource);
if (resourceInputStream == null) {
throw new IllegalArgumentException("滑块验证码 ResourceProvider 读到的图片资源为空,providerName=["
+ resourceProvider.getName() + "], resource=[" + resource + "]");
}
return resourceInputStream;
}
}
throw new IllegalStateException("没有找到Resource [" + resource.getType() + "]对应的资源提供者");
}
@Override
public List<ResourceProvider> listResourceProviders() {
return Collections.unmodifiableList(resourceProviderList);
}
@Override
public void registerResourceProvider(ResourceProvider resourceProvider) {
deleteResourceProviderByName(resourceProvider.getName());
resourceProviderList.add(resourceProvider);
}
@Override
public boolean deleteResourceProviderByName(String name) {
return resourceProviderList.removeIf(r -> r.getName().equals(name));
}
@Override
public void setResourceStore(ResourceStore resourceStore) {
this.resourceStore = resourceStore;
}
@Override
public ResourceStore getResourceStore() {
return resourceStore;
}
}
@@ -1,123 +0,0 @@
package cloud.tianai.captcha.resource.impl;
import cloud.tianai.captcha.common.constant.CommonConstant;
import cloud.tianai.captcha.common.util.CollectionUtils;
import cloud.tianai.captcha.common.util.ObjectUtils;
import cloud.tianai.captcha.resource.ResourceStore;
import cloud.tianai.captcha.resource.common.model.dto.Resource;
import cloud.tianai.captcha.resource.common.model.dto.ResourceMap;
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
/**
* @Author: 天爱有情
* @date 2021/8/7 15:43
* @Description 默认的资源存储
*/
public class LocalMemoryResourceStore implements ResourceStore {
private static final String TYPE_TAG_SPLIT_FLAG = "|";
/** 用于检索 type和tag. */
private Map<String, List<ResourceMap>> templateResourceTagMap = new HashMap<>(2);
private Map<String, List<Resource>> resourceTagMap = new HashMap<>(2);
@Override
public void addResource(String type, Resource resource) {
if (ObjectUtils.isEmpty(resource.getTag())) {
resource.setTag(CommonConstant.DEFAULT_TAG);
}
resourceTagMap.computeIfAbsent(mergeTypeAndTag(type, resource.getTag()), k -> new ArrayList<>(20)).add(resource);
}
@Override
public void addTemplate(String type, ResourceMap template) {
if (ObjectUtils.isEmpty(template.getTag())) {
template.setTag(CommonConstant.DEFAULT_TAG);
}
templateResourceTagMap.computeIfAbsent(mergeTypeAndTag(type, template.getTag()), k -> new ArrayList<>(2)).add(template);
}
@Override
public Resource randomGetResourceByTypeAndTag(String type, String tag) {
List<Resource> resources = resourceTagMap.get(mergeTypeAndTag(type, tag));
if (CollectionUtils.isEmpty(resources)) {
throw new IllegalStateException("随机获取资源错误,store中资源为空, type:" + type + ",tag:" + tag);
}
if (resources.size() == 1) {
return resources.get(0);
}
int randomIndex = ThreadLocalRandom.current().nextInt(resources.size());
return resources.get(randomIndex);
}
@Override
public ResourceMap randomGetTemplateByTypeAndTag(String type, String tag) {
List<ResourceMap> templateList = templateResourceTagMap.get(mergeTypeAndTag(type, tag));
if (CollectionUtils.isEmpty(templateList)) {
throw new IllegalStateException("随机获取模板错误,store中模板为空, type:" + type + ",tag:" + tag);
}
if (templateList.size() == 1) {
return templateList.get(0);
}
int randomIndex = ThreadLocalRandom.current().nextInt(templateList.size());
return templateList.get(randomIndex);
}
public String mergeTypeAndTag(String type, String tag) {
if (tag == null) {
tag = CommonConstant.DEFAULT_TAG;
}
return type + TYPE_TAG_SPLIT_FLAG + tag;
}
public void clearResources(String type, String tag) {
resourceTagMap.remove(mergeTypeAndTag(type, tag));
}
@Override
public void clearAllResources() {
resourceTagMap.clear();
}
public Map<String, List<Resource>> listAllResources() {
return resourceTagMap;
}
public List<Resource> listResourcesByType(String type, String tag) {
return resourceTagMap.getOrDefault(mergeTypeAndTag(type, tag), Collections.emptyList());
}
public int getAllResourceCount() {
int count = 0;
for (List<Resource> value : resourceTagMap.values()) {
count += value.size();
}
return count;
}
public int getResourceCount(String type, String tag) {
return resourceTagMap.getOrDefault(mergeTypeAndTag(type, tag), Collections.emptyList()).size();
}
@Override
public void clearAllTemplates() {
templateResourceTagMap.clear();
}
public void clearTemplates(String type, String tag) {
templateResourceTagMap.remove(mergeTypeAndTag(type, tag));
}
public List<ResourceMap> listTemplatesByType(String type, String tag) {
return templateResourceTagMap.getOrDefault(mergeTypeAndTag(type, tag), Collections.emptyList());
}
public Map<String, List<ResourceMap>> listAllTemplates() {
return templateResourceTagMap;
}
}
@@ -1,18 +0,0 @@
package cloud.tianai.captcha.validator.common.model.dto;
import lombok.Data;
@Data
public class Drives {
private Integer hardwareConcurrency;
private Boolean hasXhr = false;
private String href;
private String language;
private Long start;
private Long now;
private String platform;
private Integer scripts;
private String userAgent;
private Integer windowHeight;
private Integer windowWidth;
}
@@ -1,31 +0,0 @@
package cloud.tianai.captcha.validator.common.model.dto;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @Author: 天爱有情
* @date 2024/8/19 15:12
* @Description 验证码匹配的对象
*/
@Data
@NoArgsConstructor
public class MatchParam {
/** 轨迹信息. */
private ImageCaptchaTrack track;
/** 检测到的设备信息. */
private Drives drives;
/** 留一个扩展属性. */
private Object extendData;
public MatchParam(ImageCaptchaTrack track) {
this.track = track;
}
public MatchParam(ImageCaptchaTrack track, Drives drives) {
this.track = track;
this.drives = drives;
}
}
@@ -1,59 +0,0 @@
package example.readme;
import cloud.tianai.captcha.application.ImageCaptchaApplication;
import cloud.tianai.captcha.application.ImageCaptchaProperties;
import cloud.tianai.captcha.application.TACBuilder;
import cloud.tianai.captcha.application.vo.CaptchaResponse;
import cloud.tianai.captcha.application.vo.ImageCaptchaVO;
import cloud.tianai.captcha.cache.impl.LocalCacheStore;
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
import cloud.tianai.captcha.generator.impl.StandardSliderImageCaptchaGenerator;
import cloud.tianai.captcha.generator.impl.transform.Base64ImageTransform;
import cloud.tianai.captcha.interceptor.EmptyCaptchaInterceptor;
import cloud.tianai.captcha.resource.common.model.dto.Resource;
import cloud.tianai.captcha.resource.common.model.dto.ResourceMap;
import cloud.tianai.captcha.resource.impl.LocalMemoryResourceStore;
import cloud.tianai.captcha.resource.impl.provider.ClassPathResourceProvider;
import java.awt.*;
public class TACBuilderTest {
public static void main(String[] args) {
Font font= null;
ResourceMap template1 = new ResourceMap("default", 4);
template1.put(StandardSliderImageCaptchaGenerator.TEMPLATE_ACTIVE_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, "/active.png"));
template1.put(StandardSliderImageCaptchaGenerator.TEMPLATE_FIXED_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, "/fixed.png"));
ImageCaptchaApplication application = TACBuilder.builder()
// 加载系统自带的默认资源
.addDefaultTemplate()
// 设置验证码过期时间
.expire("default", 10000L)
.expire("WORD_IMAGE_CLICK", 60000L)
// 设置拦截器
.setInterceptor(EmptyCaptchaInterceptor.INSTANCE)
// 添加验证码背景图片
.addResource("SLIDER", new Resource("classpath", "META-INF/cut-image/resource/1.jpg"))
.addResource("WORD_IMAGE_CLICK", new Resource("classpath", "META-INF/cut-image/resource/1.jpg"))
.addResource("ROTATE", new Resource("classpath", "META-INF/cut-image/resource/1.jpg"))
// 添加验证码模板图片
.addTemplate("SLIDER",template1)
// 设置缓冲器,可提前生成验证码,用于增加并发性
.cached(10, 1000, 5000, 10000L)
// 添加字体包,用于给文字点选验证码提供字体
.addFont(font)
// 设置缓存存储器,如果要支持分布式,需要把这里改成分布式缓存,比如通过redis实现的 CacheStore 缓存
.setCacheStore(new LocalCacheStore())
// 设置资源存储器,如果想在分布式环境或者想统一管理以及扩展 实现 ResourceStore 接口,自定义
.setResourceStore(new LocalMemoryResourceStore())
// 图片转换器,默认是将图片转换成base64格式, 背景图为jpg, 模板图为png, 如果想要扩展,可替换成自己实现的
.setTransform(new Base64ImageTransform())
.build();
CaptchaResponse<ImageCaptchaVO> response = application.generateCaptcha("ROTATE");
System.out.println(response);
}
}
+106
View File
@@ -0,0 +1,106 @@
<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/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cloud.tianai.captcha</groupId>
<artifactId>tianai-captcha-parent</artifactId>
<version>${revision}</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>tianai-captcha-springboot-starter</artifactId>
<name>tianai-captcha-springboot-starter</name>
<description>滑块验证码springboot启动器</description>
<url>https://gitee.com/tianai/tianai-captcha-springboot-starter</url>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.2.7.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<properties>
<java.version>1.8</java.version>
<commons-lang3.version>3.7</commons-lang3.version>
<!-- 打包跳过单元测试 -->
<skipTests>true</skipTests>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<scope>compile</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons-lang3.version}</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure-processor</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>cloud.tianai.captcha</groupId>
<artifactId>tianai-captcha</artifactId>
<version>${revision}</version>
</dependency>
<!-- <dependency>-->
<!-- <groupId>jakarta.validation</groupId>-->
<!-- <artifactId>jakarta.validation-api</artifactId>-->
<!-- </dependency>-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>compile</scope>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
<compilerArgument>-parameters</compilerArgument>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,79 @@
package cloud.tianai.captcha.spring.autoconfiguration;
import cloud.tianai.captcha.cache.CacheStore;
import cloud.tianai.captcha.cache.impl.LocalCacheStore;
import cloud.tianai.captcha.resource.ResourceStore;
import cloud.tianai.captcha.resource.impl.LocalMemoryResourceStore;
import cloud.tianai.captcha.spring.plugins.RedisResourceStore;
import cloud.tianai.captcha.spring.store.impl.RedisCacheStore;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.core.StringRedisTemplate;
/**
* 缓存存储器的自动配置类
*
* @author Hccake
*/
@AutoConfigureAfter(name = {"org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration",
"org.springframework.boot.data.redis.autoconfigure.DataRedisAutoConfiguration"})
@Configuration(proxyBeanMethods = false)
public class CacheStoreAutoConfiguration {
/**
* RedisCacheStoreConfiguration
*
* @author 天爱有情
* @since 2020/10/27 14:06
*/
@Order(1)
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(StringRedisTemplate.class)
public static class RedisCacheStoreConfiguration {
@Bean(destroyMethod = "close")
@ConditionalOnBean(StringRedisTemplate.class)
@ConditionalOnMissingBean(CacheStore.class)
public CacheStore redis(StringRedisTemplate redisTemplate) {
return new RedisCacheStore(redisTemplate);
}
@Bean
@ConditionalOnBean(StringRedisTemplate.class)
@ConditionalOnMissingBean(ResourceStore.class)
public ResourceStore redisResourceStore(StringRedisTemplate redisTemplate) {
return new RedisResourceStore(redisTemplate);
}
}
/**
* LocalCacheStoreConfiguration
*
* @author 天爱有情
* @since 2020/10/27 14:06
*/
@Order(2)
@Configuration(proxyBeanMethods = false)
public static class LocalCacheStoreConfiguration {
@Bean(destroyMethod = "close")
@ConditionalOnMissingBean(CacheStore.class)
public CacheStore local() {
return new LocalCacheStore();
}
@Bean
@ConditionalOnMissingBean(ResourceStore.class)
public ResourceStore resourceStore() {
return new LocalMemoryResourceStore();
}
}
}
@@ -0,0 +1,127 @@
package cloud.tianai.captcha.spring.autoconfiguration;
import cloud.tianai.captcha.application.ImageCaptchaApplication;
import cloud.tianai.captcha.application.TACBuilder;
import cloud.tianai.captcha.cache.CacheStore;
import cloud.tianai.captcha.generator.ImageCaptchaGenerator;
import cloud.tianai.captcha.generator.ImageTransform;
import cloud.tianai.captcha.generator.impl.transform.Base64ImageTransform;
import cloud.tianai.captcha.interceptor.CaptchaInterceptor;
import cloud.tianai.captcha.interceptor.EmptyCaptchaInterceptor;
import cloud.tianai.captcha.resource.ImageCaptchaResourceManager;
import cloud.tianai.captcha.resource.ResourceProviders;
import cloud.tianai.captcha.resource.ResourceStore;
import cloud.tianai.captcha.resource.impl.DefaultImageCaptchaResourceManager;
import cloud.tianai.captcha.spring.common.util.URL;
import cloud.tianai.captcha.spring.plugins.SpringMultiImageCaptchaGenerator;
import cloud.tianai.captcha.spring.plugins.secondary.SecondaryVerificationApplication;
import cloud.tianai.captcha.validator.ImageCaptchaValidator;
import cloud.tianai.captcha.validator.impl.SimpleImageCaptchaValidator;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.util.CollectionUtils;
/**
* @Author: 天爱有情
* @Date 2020/5/29 9:49
* @Description 滑块验证码自动装配
*/
@Slf4j
@Order
@Configuration
@AutoConfigureAfter(CacheStoreAutoConfiguration.class)
@EnableConfigurationProperties({SpringImageCaptchaProperties.class})
public class ImageCaptchaAutoConfiguration {
@Bean
@ConditionalOnMissingBean
@ConditionalOnBean(ResourceStore.class)
public ImageCaptchaResourceManager imageCaptchaResourceManager(ResourceStore resourceStore) {
ResourceProviders resourceProviders = new ResourceProviders();
return new DefaultImageCaptchaResourceManager(resourceStore, resourceProviders);
}
@Bean
@ConditionalOnMissingBean
public ImageTransform imageTransform() {
return new Base64ImageTransform();
}
@Bean
@ConditionalOnMissingBean
public ImageCaptchaGenerator imageCaptchaTemplate(SpringImageCaptchaProperties prop,
ImageCaptchaResourceManager captchaResourceManager,
ImageTransform imageTransform,
BeanFactory beanFactory) {
// 构建多验证码生成器
ImageCaptchaGenerator captchaGenerator = new SpringMultiImageCaptchaGenerator(captchaResourceManager, imageTransform, beanFactory);
return captchaGenerator;
}
@Bean
@ConditionalOnMissingBean
public ImageCaptchaValidator imageCaptchaValidator() {
return new SimpleImageCaptchaValidator();
}
@Bean
@ConditionalOnMissingBean
public CaptchaInterceptor captchaInterceptor() {
return new EmptyCaptchaInterceptor();
}
@Bean(destroyMethod = "close")
@ConditionalOnMissingBean
@ConditionalOnBean(CacheStore.class)
public ImageCaptchaApplication imageCaptchaApplication(ImageCaptchaGenerator captchaGenerator,
ImageCaptchaValidator imageCaptchaValidator,
CacheStore cacheStore,
ResourceStore resourceStore,
SpringImageCaptchaProperties prop,
CaptchaInterceptor captchaInterceptor,
ApplicationContext applicationContext
) {
TACBuilder tacBuilder = TACBuilder.builder()
.setResourceStore(resourceStore)
.setGenerator(captchaGenerator)
.setValidator(imageCaptchaValidator)
.setCacheStore(cacheStore)
.setProp(prop)
.setInterceptor(captchaInterceptor);
if (prop.getInitDefaultResource()) {
tacBuilder.addDefaultTemplate(prop.getDefaultResourcePrefix());
}
if (!CollectionUtils.isEmpty(prop.getFontPath())) {
// 读取字体包
for (String fontPath : prop.getFontPath()) {
int index = fontPath.indexOf(":");
String[] split = index > 0 ? new String[]{fontPath.substring(0, index), fontPath.substring(index + 1)} : new String[]{"", fontPath};
String type = split[0];
String path = split[1];
URL fontUrl = URL.valueOf(fontPath);
String tag = fontUrl.getParam(URL.PARAM_TAG_KEY, null);
tacBuilder.addFont(new cloud.tianai.captcha.resource.common.model.dto.Resource(type, path, tag));
}
}
ImageCaptchaApplication target = tacBuilder.build();
if (prop.getSecondary() != null && Boolean.TRUE.equals(prop.getSecondary().getEnabled())) {
// 一个简单的二次验证
target = new SecondaryVerificationApplication(target, prop.getSecondary());
}
return target;
}
}
@@ -0,0 +1,12 @@
package cloud.tianai.captcha.spring.autoconfiguration;
import lombok.Data;
@Data
public class SecondaryVerificationProperties {
private Boolean enabled = false;
private Long expire = 120000L;
private String keyPrefix = "captcha:secondary";
}
@@ -0,0 +1,30 @@
package cloud.tianai.captcha.spring.autoconfiguration;
import cloud.tianai.captcha.application.ImageCaptchaProperties;
import cloud.tianai.captcha.resource.DefaultBuiltInResources;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
import java.util.List;
/**
* @Author: 天爱有情
* @date 2020/10/19 18:41
* @Description 滑块验证码属性
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ConfigurationProperties(prefix = "captcha")
public class SpringImageCaptchaProperties extends ImageCaptchaProperties {
/** 是否初始化默认资源. */
private Boolean initDefaultResource = false;
/** 默认资源的位置. */
private String defaultResourcePrefix = DefaultBuiltInResources.PATH_PREFIX;
/** 字体包路径. */
private List<String> fontPath;
/** 二次验证配置. */
@NestedConfigurationProperty
private SecondaryVerificationProperties secondary;
}
@@ -0,0 +1,59 @@
package cloud.tianai.captcha.spring.common.util;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.HashMap;
import java.util.Map;
@Getter
@AllArgsConstructor
public class URL {
public static final String PARAM_TAG_KEY = "tag";
private String protocol;
private String path;
private Map<String, String> params;
public String getParam(String key, String defaultValue) {
return params.getOrDefault(key, defaultValue);
}
public static URL valueOf(String input) {
// 分割协议和剩余部分
String[] parts = input.split(":", 2);
String protocol = parts[0];
String remaining = parts[1];
// 分割路径和查询参数
String path;
String query = null;
if (remaining.contains("?")) {
String[] pathQuerySplit = remaining.split("\\?", 2);
path = pathQuerySplit[0];
query = pathQuerySplit[1];
} else {
path = remaining;
}
if (path.startsWith("//")) {
path = path.substring(2);
}
// 处理查询参数,提取键值对
Map<String, String> queryParams = new HashMap<>();
if (query != null) {
for (String param : query.split("&")) {
String[] keyValue = param.split("=", 2);
String key = keyValue[0];
String value = keyValue.length > 1 ? keyValue[1] : "";
queryParams.put(key, value);
}
}
return new URL(protocol, path, queryParams);
}
}
@@ -0,0 +1,286 @@
package cloud.tianai.captcha.spring.plugins;
import cloud.tianai.captcha.common.constant.CommonConstant;
import cloud.tianai.captcha.common.util.CollectionUtils;
import cloud.tianai.captcha.resource.CrudResourceStore;
import cloud.tianai.captcha.resource.ImageCaptchaResourceManager;
import cloud.tianai.captcha.resource.common.model.dto.Resource;
import cloud.tianai.captcha.resource.common.model.dto.ResourceMap;
import com.google.gson.Gson;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
/**
* @Author: 天爱有情
* @date 2023/8/23 10:52
* @Description 基于redis的store
*/
@RequiredArgsConstructor
public class RedisResourceStore implements CrudResourceStore {
private final StringRedisTemplate redisTemplate;
@Getter
@Setter
private String resourcePrefix = "captcha:config:resource:";
@Getter
@Setter
private String templatePrefix = "captcha:config:template:";
private Gson gson = new Gson();
public String joinResourceKey(String type, String tag) {
if (tag == null) {
tag = CommonConstant.DEFAULT_TAG;
}
type = type.toUpperCase();
return resourcePrefix + tag + ":" + type;
}
public String joinTemplateKey(String type, String tag) {
if (tag == null) {
tag = CommonConstant.DEFAULT_TAG;
}
type = type.toUpperCase();
return templatePrefix + tag + ":" + type;
}
public List<Resource> getResources(String type, String tag) {
String key = joinResourceKey(type, tag);
Long size = redisTemplate.opsForList().size(key);
if (size == null || size < 1) {
return Collections.emptyList();
}
List<String> range = redisTemplate.opsForList().range(key, 0, size);
List<Resource> result = new ArrayList<>(range.size());
for (String json : range) {
result.add(gson.fromJson(json, Resource.class));
}
return result;
}
public List<ResourceMap> getTemplates(String type, String tag) {
String key = joinTemplateKey(type, tag);
Long size = redisTemplate.opsForList().size(key);
if (size == null || size < 1) {
return Collections.emptyList();
}
List<String> range = redisTemplate.opsForList().range(key, 0, size);
List<ResourceMap> result = new ArrayList<>(range.size());
for (String json : range) {
result.add(gson.fromJson(json, ResourceMap.class));
}
return result;
}
public void setResources(String type, String tag, List<Resource> resources) {
String key = joinResourceKey(type, tag);
Long size = redisTemplate.opsForList().size(key);
if (size != null && size > 0) {
redisTemplate.delete(key);
}
for (Resource resource : resources) {
addResource(type, resource);
}
}
public void setTemplates(String type, String tag, List<ResourceMap> templates) {
String key = joinTemplateKey(type, tag);
Long size = redisTemplate.opsForList().size(key);
if (size != null && size > 0) {
redisTemplate.delete(key);
}
for (ResourceMap template : templates) {
addTemplate(type, template);
}
}
@Override
public void addResource(String type, Resource resource) {
// 添加tag标签字典
redisTemplate.opsForList().rightPush(joinResourceKey(type, resource.getTag()), gson.toJson(resource));
}
@Override
public void addTemplate(String type, ResourceMap template) {
// 添加tag标签字典
redisTemplate.opsForList().rightPush(joinTemplateKey(type, template.getTag()), gson.toJson(template));
}
@Override
public Resource deleteResource(String type, String id) {
Set<String> keys = redisTemplate.keys(joinResourceKey(type, "*"));
if (!CollectionUtils.isEmpty(keys)) {
for (String key : keys) {
Long size = redisTemplate.opsForList().size(key);
if (size == null || size < 1) {
continue;
}
List<String> range = redisTemplate.opsForList().range(key, 0, size);
if (range != null) {
for (String json : range) {
Resource resource = gson.fromJson(json, Resource.class);
if (resource.getId().equals(id)) {
redisTemplate.opsForList().remove(key, 1, json);
return resource;
}
}
}
}
}
return null;
}
@Override
public ResourceMap deleteTemplate(String type, String id) {
Set<String> keys = redisTemplate.keys(joinTemplateKey(type, "*"));
if (!CollectionUtils.isEmpty(keys)) {
for (String key : keys) {
Long size = redisTemplate.opsForList().size(key);
if (size == null || size < 1) {
continue;
}
List<String> range = redisTemplate.opsForList().range(key, 0, size);
if (range != null) {
for (String json : range) {
ResourceMap resourceMap = gson.fromJson(json, ResourceMap.class);
if (resourceMap.getId().equals(id)) {
redisTemplate.opsForList().remove(key, 1, json);
return resourceMap;
}
}
}
}
}
return null;
}
@Override
public List<Resource> listResourcesByTypeAndTag(String type, String tag) {
if (StringUtils.isNotBlank(tag)) {
return getResources(type, tag);
}
Set<String> keys = redisTemplate.keys(joinResourceKey(type, "*"));
if (!CollectionUtils.isEmpty(keys)) {
List<Resource> resources = new ArrayList<>();
for (String key : keys) {
Long size1 = redisTemplate.opsForList().size(key);
if (size1 == null || size1 < 1) {
continue;
}
List<String> range = redisTemplate.opsForList().range(key, 0, size1);
if (range != null) {
for (String json : range) {
Resource resource = gson.fromJson(json, Resource.class);
resources.add(resource);
}
}
}
return resources;
}
return Collections.emptyList();
}
@Override
public List<ResourceMap> listTemplatesByTypeAndTag(String type, String tag) {
if (StringUtils.isNotBlank(tag)) {
return getTemplates(type, tag);
}
Set<String> keys = redisTemplate.keys(joinTemplateKey(type, "*"));
if (!CollectionUtils.isEmpty(keys)) {
List<ResourceMap> templates = new ArrayList<>();
for (String key : keys) {
Long size1 = redisTemplate.opsForList().size(key);
if (size1 == null || size1 < 1) {
continue;
}
List<String> range = redisTemplate.opsForList().range(key, 0, size1);
if (range != null) {
for (String json : range) {
ResourceMap template = gson.fromJson(json, ResourceMap.class);
templates.add(template);
}
}
}
return templates;
}
return Collections.emptyList();
}
@Override
public void init(ImageCaptchaResourceManager resourceManager) {
}
@Override
public List<Resource> randomGetResourceByTypeAndTag(String type, String tag, Integer quantity) {
String key = joinResourceKey(type, tag);
Long size = redisTemplate.opsForList().size(key);
if (size == null || quantity > size) {
throw new IllegalArgumentException("请求的资源数量超过可用资源总数");
}
Set<Long> indexes = new HashSet<>(quantity);
while (indexes.size() < quantity) {
indexes.add(ThreadLocalRandom.current().nextLong(size));
}
List<Resource> result = new ArrayList<>(quantity);
for (Long index : indexes) {
String resourceJson = redisTemplate.opsForList().index(key, index);
result.add(gson.fromJson(resourceJson, Resource.class));
}
return result;
}
@Override
public List<ResourceMap> randomGetTemplateByTypeAndTag(String type, String tag, Integer quantity) {
String key = joinTemplateKey(type, tag);
Long size = redisTemplate.opsForList().size(key);
if (size == null || size < 1) {
throw new IllegalStateException("随机获取模板错误,store中模板为空, type:" + type);
}
if (quantity > size) {
throw new IllegalArgumentException("请求的模板数量超过可用模板总数");
}
Set<Long> indexes = new HashSet<>(quantity);
while (indexes.size() < quantity) {
indexes.add(ThreadLocalRandom.current().nextLong(size));
}
List<ResourceMap> result = new ArrayList<>(quantity);
for (Long index : indexes) {
String resourceJson = redisTemplate.opsForList().index(key, index);
result.add(gson.fromJson(resourceJson, ResourceMap.class));
}
return result;
}
@Override
public void clearAllTemplates() {
Set<String> keys = redisTemplate.keys(templatePrefix + "*");
if (!CollectionUtils.isEmpty(keys)) {
redisTemplate.delete(keys);
}
}
@Override
public void clearAllResources() {
Set<String> keys = redisTemplate.keys(resourcePrefix + "*");
if (!CollectionUtils.isEmpty(keys)) {
redisTemplate.delete(keys);
}
}
}
@@ -0,0 +1,36 @@
package cloud.tianai.captcha.spring.plugins;
import cloud.tianai.captcha.generator.ImageCaptchaGeneratorProvider;
import cloud.tianai.captcha.generator.ImageTransform;
import cloud.tianai.captcha.generator.impl.MultiImageCaptchaGenerator;
import cloud.tianai.captcha.resource.ImageCaptchaResourceManager;
import org.apache.commons.lang3.ArrayUtils;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.ListableBeanFactory;
/**
* @Author: 天爱有情
* @date 2022/5/19 14:37
* @Description 基于spring的 多验证码生成器
*/
public class SpringMultiImageCaptchaGenerator extends MultiImageCaptchaGenerator {
private ListableBeanFactory beanFactory;
public SpringMultiImageCaptchaGenerator(ImageCaptchaResourceManager imageCaptchaResourceManager, ImageTransform imageTransform,
BeanFactory beanFactory) {
super(imageCaptchaResourceManager, imageTransform);
this.beanFactory = (ListableBeanFactory) beanFactory;
}
@Override
protected void doInit() {
super.doInit();
String[] beanNamesForType = beanFactory.getBeanNamesForType(ImageCaptchaGeneratorProvider.class);
if (!ArrayUtils.isEmpty(beanNamesForType)) {
for (String beanName : beanNamesForType) {
ImageCaptchaGeneratorProvider provider = beanFactory.getBean(beanName, ImageCaptchaGeneratorProvider.class);
addImageCaptchaGeneratorProvider(provider);
}
}
}
}
@@ -0,0 +1,58 @@
package cloud.tianai.captcha.spring.plugins.secondary;
import cloud.tianai.captcha.application.FilterImageCaptchaApplication;
import cloud.tianai.captcha.application.ImageCaptchaApplication;
import cloud.tianai.captcha.common.AnyMap;
import cloud.tianai.captcha.common.response.ApiResponse;
import cloud.tianai.captcha.spring.autoconfiguration.SecondaryVerificationProperties;
import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* @Author: 天爱有情
* @date 2022/3/2 14:16
* @Description 二次验证
*/
public class SecondaryVerificationApplication extends FilterImageCaptchaApplication {
private SecondaryVerificationProperties prop;
public SecondaryVerificationApplication(ImageCaptchaApplication target, SecondaryVerificationProperties prop) {
super(target);
this.prop = prop;
}
@Override
public ApiResponse<?> matching(String id, ImageCaptchaTrack imageCaptchaTrack) {
ApiResponse<?> match = super.matching(id, imageCaptchaTrack);
if (match.isSuccess()) {
// 如果匹配成功, 添加二次验证记录
addSecondaryVerification(id, imageCaptchaTrack);
}
return match;
}
/**
* 二次缓存验证
* @param id id
* @return boolean
*/
public boolean secondaryVerification(String id) {
Map<String, Object> cache = target.getCacheStore().getAndRemoveCache(getKey(id));
return cache != null;
}
/**
* 添加二次缓存验证记录
* @param id id
* @param imageCaptchaTrack sliderCaptchaTrack
*/
protected void addSecondaryVerification(String id, ImageCaptchaTrack imageCaptchaTrack) {
target.getCacheStore().setCache(getKey(id), new AnyMap(), prop.getExpire(), TimeUnit.MILLISECONDS);
}
protected String getKey(String id) {
return prop.getKeyPrefix().concat(":").concat(id);
}
}
@@ -0,0 +1,76 @@
package cloud.tianai.captcha.spring.store.impl;
import cloud.tianai.captcha.cache.CacheStore;
import cloud.tianai.captcha.common.AnyMap;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.util.StringUtils;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
/**
* @Author: 天爱有情
* @date 2022/3/2 14:42
* @Description redis实现的缓存
*/
public class RedisCacheStore implements CacheStore {
private static final RedisScript<String> SCRIPT_GET_CACHE = new DefaultRedisScript<>("local res = redis.call('get',KEYS[1]) if res == nil then return nil else redis.call('del',KEYS[1]) return res end", String.class);
protected StringRedisTemplate redisTemplate;
private Gson gson = new Gson();
public RedisCacheStore(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
public AnyMap getCache(String key) {
String jsonData = redisTemplate.opsForValue().get(key);
if (StringUtils.isEmpty(jsonData)) {
return null;
}
return gson.fromJson(jsonData, new TypeToken<AnyMap>() {
}.getType());
}
@Override
public AnyMap getAndRemoveCache(String key) {
String json = redisTemplate.execute(SCRIPT_GET_CACHE, Collections.singletonList(key));
if (org.apache.commons.lang3.StringUtils.isBlank(json)) {
return null;
}
return gson.fromJson(json, new TypeToken<AnyMap>() {
}.getType());
}
@Override
public boolean setCache(String key, AnyMap data, Long expire, TimeUnit timeUnit) {
redisTemplate.opsForValue().set(key, gson.toJson(data), expire, timeUnit);
return true;
}
@Override
public Long incr(String key, long delta, Long expire, TimeUnit timeUnit) {
Long increment = redisTemplate.opsForValue().increment(key, delta);
redisTemplate.expire(key, expire, timeUnit);
return increment;
}
@Override
public Long getLong(String key) {
String value = redisTemplate.opsForValue().get(key);
if (value == null) {
return null;
}
return Long.valueOf(value);
}
@Override
public void close() throws Exception {
}
}
@@ -0,0 +1,3 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cloud.tianai.captcha.spring.autoconfiguration.CacheStoreAutoConfiguration,\
cloud.tianai.captcha.spring.autoconfiguration.ImageCaptchaAutoConfiguration
@@ -0,0 +1,2 @@
cloud.tianai.captcha.spring.autoconfiguration.CacheStoreAutoConfiguration
cloud.tianai.captcha.spring.autoconfiguration.ImageCaptchaAutoConfiguration
+3
View File
@@ -0,0 +1,3 @@
dist/
node_modules/
.idea/
+127
View File
@@ -0,0 +1,127 @@
木兰宽松许可证, 第2版
木兰宽松许可证, 第2版
2020年1月 http://license.coscl.org.cn/MulanPSL2
您对“软件”的复制、使用、修改及分发受木兰宽松许可证,第2版(“本许可证”)的如下条款的约束:
0. 定义
“软件”是指由“贡献”构成的许可在“本许可证”下的程序和相关文档的集合。
“贡献”是指由任一“贡献者”许可在“本许可证”下的受版权法保护的作品。
“贡献者”是指将受版权法保护的作品许可在“本许可证”下的自然人或“法人实体”。
“法人实体”是指提交贡献的机构及其“关联实体”。
“关联实体”是指,对“本许可证”下的行为方而言,控制、受控制或与其共同受控制的机构,此处的控制是指有受控方或共同受控方至少50%直接或间接的投票权、资金或其他有价证券。
1. 授予版权许可
每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的版权许可,您可以复制、使用、修改、分发其“贡献”,不论修改与否。
2. 授予专利许可
每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的(根据本条规定撤销除外)专利许可,供您制造、委托制造、使用、许诺销售、销售、进口其“贡献”或以其他方式转移其“贡献”。前述专利许可仅限于“贡献者”现在或将来拥有或控制的其“贡献”本身或其“贡献”与许可“贡献”时的“软件”结合而将必然会侵犯的专利权利要求,不包括对“贡献”的修改或包含“贡献”的其他结合。如果您或您的“关联实体”直接或间接地,就“软件”或其中的“贡献”对任何人发起专利侵权诉讼(包括反诉或交叉诉讼)或其他专利维权行动,指控其侵犯专利权,则“本许可证”授予您对“软件”的专利许可自您提起诉讼或发起维权行动之日终止。
3. 无商标许可
“本许可证”不提供对“贡献者”的商品名称、商标、服务标志或产品名称的商标许可,但您为满足第4条规定的声明义务而必须使用除外。
4. 分发限制
您可以在任何媒介中将“软件”以源程序形式或可执行形式重新分发,不论修改与否,但您必须向接收者提供“本许可证”的副本,并保留“软件”中的版权、商标、专利及免责声明。
5. 免责声明与责任限制
“软件”及其中的“贡献”在提供时不带任何明示或默示的担保。在任何情况下,“贡献者”或版权所有者不对任何人因使用“软件”或其中的“贡献”而引发的任何直接或间接损失承担责任,不论因何种原因导致或者基于何种法律理论,即使其曾被建议有此种损失的可能性。
6. 语言
“本许可证”以中英文双语表述,中英文版本具有同等法律效力。如果中英文版本存在任何冲突不一致,以中文版为准。
条款结束
如何将木兰宽松许可证,第2版,应用到您的软件
如果您希望将木兰宽松许可证,第2版,应用到您的新软件,为了方便接收者查阅,建议您完成如下三步:
1, 请您补充如下声明中的空白,包括软件名、软件的首次发表年份以及您作为版权人的名字;
2, 请您在软件包的一级目录下创建以“LICENSE”为名的文件,将整个许可证文本放入该文件中;
3, 请将如下声明文本放入每个源文件的头部注释中。
Copyright (c) [Year] [name of copyright holder]
[Software Name] is licensed under Mulan PSL v2.
You can use this software according to the terms and conditions of the Mulan PSL v2.
You may obtain a copy of Mulan PSL v2 at:
http://license.coscl.org.cn/MulanPSL2
THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
See the Mulan PSL v2 for more details.
Mulan Permissive Software LicenseVersion 2
Mulan Permissive Software LicenseVersion 2 (Mulan PSL v2)
January 2020 http://license.coscl.org.cn/MulanPSL2
Your reproduction, use, modification and distribution of the Software shall be subject to Mulan PSL v2 (this License) with the following terms and conditions:
0. Definition
Software means the program and related documents which are licensed under this License and comprise all Contribution(s).
Contribution means the copyrightable work licensed by a particular Contributor under this License.
Contributor means the Individual or Legal Entity who licenses its copyrightable work under this License.
Legal Entity means the entity making a Contribution and all its Affiliates.
Affiliates means entities that control, are controlled by, or are under common control with the acting entity under this License, control means direct or indirect ownership of at least fifty percent (50%) of the voting power, capital or other securities of controlled or commonly controlled entity.
1. Grant of Copyright License
Subject to the terms and conditions of this License, each Contributor hereby grants to you a perpetual, worldwide, royalty-free, non-exclusive, irrevocable copyright license to reproduce, use, modify, or distribute its Contribution, with modification or not.
2. Grant of Patent License
Subject to the terms and conditions of this License, each Contributor hereby grants to you a perpetual, worldwide, royalty-free, non-exclusive, irrevocable (except for revocation under this Section) patent license to make, have made, use, offer for sale, sell, import or otherwise transfer its Contribution, where such patent license is only limited to the patent claims owned or controlled by such Contributor now or in future which will be necessarily infringed by its Contribution alone, or by combination of the Contribution with the Software to which the Contribution was contributed. The patent license shall not apply to any modification of the Contribution, and any other combination which includes the Contribution. If you or your Affiliates directly or indirectly institute patent litigation (including a cross claim or counterclaim in a litigation) or other patent enforcement activities against any individual or entity by alleging that the Software or any Contribution in it infringes patents, then any patent license granted to you under this License for the Software shall terminate as of the date such litigation or activity is filed or taken.
3. No Trademark License
No trademark license is granted to use the trade names, trademarks, service marks, or product names of Contributor, except as required to fulfill notice requirements in Section 4.
4. Distribution Restriction
You may distribute the Software in any medium with or without modification, whether in source or executable forms, provided that you provide recipients with a copy of this License and retain copyright, patent, trademark and disclaimer statements in the Software.
5. Disclaimer of Warranty and Limitation of Liability
THE SOFTWARE AND CONTRIBUTION IN IT ARE PROVIDED WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED. IN NO EVENT SHALL ANY CONTRIBUTOR OR COPYRIGHT HOLDER BE LIABLE TO YOU FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO ANY DIRECT, OR INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES ARISING FROM YOUR USE OR INABILITY TO USE THE SOFTWARE OR THE CONTRIBUTION IN IT, NO MATTER HOW ITS CAUSED OR BASED ON WHICH LEGAL THEORY, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
6. Language
THIS LICENSE IS WRITTEN IN BOTH CHINESE AND ENGLISH, AND THE CHINESE VERSION AND ENGLISH VERSION SHALL HAVE THE SAME LEGAL EFFECT. IN THE CASE OF DIVERGENCE BETWEEN THE CHINESE AND ENGLISH VERSIONS, THE CHINESE VERSION SHALL PREVAIL.
END OF THE TERMS AND CONDITIONS
How to Apply the Mulan Permissive Software LicenseVersion 2 (Mulan PSL v2) to Your Software
To apply the Mulan PSL v2 to your work, for easy identification by recipients, you are suggested to complete following three steps:
i Fill in the blanks in following statement, including insert your software name, the year of the first publication of your software, and your name identified as the copyright owner;
ii Create a file named “LICENSE” which contains the whole context of this License in the first directory of your software package;
iii Attach the statement to the appropriate annotated syntax at the beginning of each source file.
Copyright (c) [Year] [name of copyright holder]
[Software Name] is licensed under Mulan PSL v2.
You can use this software according to the terms and conditions of the Mulan PSL v2.
You may obtain a copy of Mulan PSL v2 at:
http://license.coscl.org.cn/MulanPSL2
THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
See the Mulan PSL v2 for more details.
File diff suppressed because it is too large Load Diff
+33
View File
@@ -0,0 +1,33 @@
{
"name": "webpack-demo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "webpack serve --open --mode development",
"build": "webpack --mode development",
"buildprod": "webpack --mode production --progress"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.22.17",
"@babel/preset-env": "^7.22.15",
"babel-loader": "^9.1.3",
"clean-webpack-plugin": "^4.0.0",
"css-loader": "^6.8.1",
"file-loader": "^6.2.0",
"html-webpack-plugin": "^5.5.3",
"javascript-obfuscator": "^4.1.0",
"mini-css-extract-plugin": "^2.7.6",
"sass-loader": "^13.3.2",
"style-loader": "^3.3.3",
"terser-webpack-plugin": "^5.3.10",
"url-loader": "^4.1.1",
"webpack": "^5.88.2",
"webpack-bundle-analyzer": "^4.9.1",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.1"
}
}
@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
</head>
<body>
<!-- 验证码存放的div块 -->
<div id="captcha-box"></div>
<script>
$(function () {
// 样式配置
const config = {
requestCaptchaDataUrl: "http://localhost:8080/gen",
validCaptchaUrl: "http://localhost:8080/check",
bindEl: "#captcha-box"
}
new TAC(config).init();
});
</script>
</body>
</html>
+101
View File
@@ -0,0 +1,101 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
#login-div {
width: 500px;
height: 500px;
border: 1px solid #ff5d39;
position: relative;
}
h1 {
text-align: center;
}
.input {
height: 50px;
width: 100%;
border: 1px solid #ccc;
border-radius: 6px;
margin: 20px auto;
color: #ccc;
line-height: 50px;
text-align: left;
}
.input-left {
border-right: 1px solid #ccc;
text-align: center;
width: 100px;
display: inline-block;
}
.login-btn {
/*margin: 0 auto;*/
display: inline-block;
width: 160px;
height: 50px;
background-color: #4BC065;
color: #fff;
line-height: 50px;
text-align: center;
border-radius: 6px;
}
#captcha-box {
position: absolute;
left: 78px;
top: 83px;
}
</style>
</head>
<body>
<!-- 验证码存放的div块 -->
<div id="login-div">
<!-- 装载验证码的DIV -->
<div id="captcha-box"></div>
<h1>用户登录</h1>
<div class="input">
<span class="input-left">用户名</span>
xxxxx
</div>
<div class="input">
<span class="input-left">密码</span>
xxxxx
</div>
<div class="login-btn" data-type="ROTATE">登录(旋转)</div>
<div class="login-btn" data-type="CONCAT">登录(拼接)</div>
<div class="login-btn" data-type="WORD_IMAGE_CLICK">登录(汉字点选)</div>
<div class="login-btn" data-type="SLIDER">登录(滑块拼图)</div>
</div>
<script>
document.querySelectorAll(".login-btn").forEach(el => {
el.addEventListener("click", e => {
// 样式配置
const config = {
requestCaptchaDataUrl: "http://localhost:8080/gen?type=" + el.dataset.type,
validCaptchaUrl: "http://localhost:8080/check",
bindEl: "#captcha-box"
}
// const style = {
// logoUrl : null
//
// }
const captcha = new TAC(config, null);
captcha.init();
})
})
</script>
</body>
</html>
+108
View File
@@ -0,0 +1,108 @@
# (captcha-web-sdk)
# ([TIANAI-CAPTCHA)](https://gitee.com/tianai/tianai-captcha)验证码前端SDK
| 条目 | |
| -------- | ------------------------------------------------------------ |
| 兼容性 | Chrome、Firefox、Safari、Opera、主流手机浏览器、iOS 及 Android上的内嵌Webview |
| 框架支持 | H5、Angular、React、Vue2、Vue3 |
## 安装
1. 将打包好的`tac`目录放到自己项目中,如果是vue、react等框架,将tac目录放到public目录中、或者放到某个可以访问到地方,比如oss之类的可以被浏览器访问到的地方 (tac下载地址 [https://gitee.com/tianai/tianai-captcha-web-sdk/releases/tag/1.2](https://gitee.com/tianai/tianai-captcha-web-sdk/releases/tag/1.2)
2. 引入初始化函数 (load.js下载地址 [https://minio.tianai.cloud/public/static/captcha/js/load.min.js](https://minio.tianai.cloud/public/static/captcha/js/load.min.js)) 可自己将load.js下载到本地
```html
<script src="load.min.js"></script>
```
**注: 如果是web框架,将该引入代码放到 `public/index.html`**
## 使用方法
2. 创建一个div块用于渲染验证码, 该div用于装载验证码
```html
<div id="captcha-box"></div>
```
3. 在需要调用验证码的时候执行加载验证码方法
```js
function login() {
// config 对象为TAC验证码的一些配置和验证的回调
const config = {
// 生成接口 (必选项,必须配置, 要符合tianai-captcha默认验证码生成接口规范)
requestCaptchaDataUrl: "/gen",
// 验证接口 (必选项,必须配置, 要符合tianai-captcha默认验证码校验接口规范)
validCaptchaUrl: "/check",
// 验证码绑定的div块 (必选项,必须配置)
bindEl: "#captcha-box",
// 验证成功回调函数(必选项,必须配置)
validSuccess: (res, c, tac) => {
// 销毁验证码服务
tac.destroyWindow();
console.log("验证成功,后端返回的数据为", res);
// 调用具体的login方法
login(res.data.token)
},
// 验证失败的回调函数(可忽略,如果不自定义 validFail 方法时,会使用默认的)
validFail: (res, c, tac) => {
console.log("验证码验证失败回调...")
// 验证失败后重新拉取验证码
tac.reloadCaptcha();
},
// 刷新按钮回调事件
btnRefreshFun: (el, tac) => {
console.log("刷新按钮触发事件...")
tac.reloadCaptcha();
},
// 关闭按钮回调事件
btnCloseFun: (el, tac) => {
console.log("关闭按钮触发事件...")
tac.destroyWindow();
}
}
// 一些样式配置, 可不传
let style = {
logoUrl: null;// 去除logo
// logoUrl: "/xx/xx/xxx.png" // 替换成自定义的logo
}
// 参数1 为 tac文件是目录地址, 目录里包含 tac的js和css等文件
// 参数2 为 tac验证码相关配置
// 参数3 为 tac窗口一些样式配置
window.initTAC("./tac", config, style).then(tac => {
tac.init(); // 调用init则显示验证码
}).catch(e => {
console.log("初始化tac失败", e);
})
}
```
### 对滑块的按钮和背景设置为自定义的一些样式
```js
// 这里分享一些作者自己调的样式供参考
const style = {
// 按钮样式
btnUrl: "https://minio.tianai.cloud/public/captcha-btn/btn3.png",
// 背景样式
bgUrl: "https://minio.tianai.cloud/public/captcha-btn/btn3-bg.jpg",
// logo地址
logoUrl: "https://minio.tianai.cloud/public/static/captcha/images/logo.png",
// 滑动边框样式
moveTrackMaskBgColor: "#f7b645",
moveTrackMaskBorderColor: "#ef9c0d"
}
window.initTAC("./tac", config, style).then(tac => {
tac.init(); // 调用init则显示验证码
}).catch(e => {
console.log("初始化tac失败", e);
})
```
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

@@ -0,0 +1,183 @@
import "./captcha.scss"
import Slider from "./slider/slider"
import Rotate from "./rotate/rotate";
import Concat from "./concat/concat";
import Disable from "./disable/disable";
import WordImageClick from "./word_image_click/word_image_click";
import {CaptchaConfig, wrapConfig, wrapStyle} from "./config/config";
import {clearAllPreventDefault} from "./common/common";
const template =
`
<div id="tianai-captcha-parent">
<div id="tianai-captcha-bg-img"></div>
<div id="tianai-captcha-box">
<div id="tianai-captcha-loading" class="loading"></div>
</div>
<!-- 底部 -->
<div class="slider-bottom">
<img class="logo" id="tianai-captcha-logo" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAMAAAAM7l6QAAAAMFBMVEVHcEz3tkX3tkX3tkX3tkX3tkX3tkX3tkX3tkX3tkX3tkX3tkX3tkX3tkX3tkX3tkVmTmjZAAAAD3RSTlMASbTm8wh12hOGoCNiyTV98jvOAAABB0lEQVR42nVT0aIFEQiMorD0/397Lc5a7J0n1UylgIniLRKyDcbBDudZH2DYCAabn3PmTrjeUX+7rJGWx0SqVpzReAfTtKU5fgVCNfxWjB69USUDGwoOiauHpZEpSr0tCx8ILb3Dm3WgBbAlifAJk6+Ww6wqEUmpmIorQVZ1JtqKnDMjkb7AgIpO/wMCaQbuBuEtsBUxhuD9daUaZnApiQB8NAKotMwirGGr6mbXpPnHLHDmy6oy3FgP+1j8IBdVklFc01xUJwv3NR0rIeXV5zpzdlruiijzNq/ufOeKWzZLP3160u5P8RjT1M+HHFtx+PwGyOZqT/D8ROOfjOInTLBIHjy/hvwHxkwPu5cCE1QAAAAASUVORK5CYII=" id="tianai-captcha-logo"></img>
<div class="close-btn" id="tianai-captcha-slider-close-btn"></div>
<div class="refresh-btn" id="tianai-captcha-slider-refresh-btn"></div>
</div>
</div>
`;
function createCaptchaByType(type, tac) {
const box = tac.config.domBindEl.find("#tianai-captcha-box");
const styleConfig = tac.style;
switch (type) {
case "SLIDER":
return new Slider(box, styleConfig);
case "ROTATE":
return new Rotate(box, styleConfig);
case "CONCAT":
return new Concat(box, styleConfig);
case "WORD_IMAGE_CLICK":
return new WordImageClick(box, styleConfig);
case "DISABLED":
return new Disable(box, styleConfig);
default:
return null;
}
}
class TianAiCaptcha {
constructor(config, style) {
this.config = wrapConfig(config);
if (this.config.btnRefreshFun) {
this.btnRefreshFun = this.config.btnRefreshFun;
}
if (this.config.btnCloseFun) {
this.btnCloseFun = this.config.btnCloseFun;
}
this.style = wrapStyle(style);
}
init() {
this.destroyWindow();
this.config.domBindEl.append(template);
this.domTemplate = this.config.domBindEl.find("#tianai-captcha-parent");
clearAllPreventDefault(this.domTemplate);
this.loadStyle();
// 绑定按钮事件
this.config.domBindEl.find("#tianai-captcha-slider-refresh-btn").click((el) => {
this.btnRefreshFun(el, this);
});
this.config.domBindEl.find("#tianai-captcha-slider-close-btn").click((el) => {
this.btnCloseFun(el, this);
});
// 加载验证码
this.reloadCaptcha();
return this;
}
btnRefreshFun(el, tac) {
tac.reloadCaptcha();
}
btnCloseFun(el, tac) {
tac.destroyWindow();
}
reloadCaptcha() {
this.showLoading();
this.destroyCaptcha(() => {
this.createCaptcha();
})
}
showLoading() {
this.config.domBindEl.find("#tianai-captcha-loading").css("display", "block");
}
closeLoading() {
this.config.domBindEl.find("#tianai-captcha-loading").css("display", "none");
}
loadStyle() {
// 设置样式
const bgUrl = this.style.bgUrl;
const logoUrl = this.style.logoUrl;
if (bgUrl) {
// 背景图片
this.config.domBindEl.find("#tianai-captcha-bg-img").css("background-image", "url(" + bgUrl + ")");
}
if (logoUrl && logoUrl !== "") {
// logo
this.config.domBindEl.find("#tianai-captcha-logo").attr("src", logoUrl);
} else if (logoUrl === null){
// 删除logo
this.config.domBindEl.find("#tianai-captcha-logo").css("display", "none");
}
}
destroyWindow() {
if (this.C) {
this.C.destroy();
this.C = undefined;
}
if (this.domTemplate) {
this.domTemplate.remove();
}
}
openCaptcha() {
setTimeout(() => {
this.C.el.css("transform", "translateX(0)")
}, 10)
}
createCaptcha() {
this.config.requestCaptchaData().then(data => {
this.closeLoading();
if (!data.code) {
throw new Error("[TAC] 后台验证码接口数据错误!!!");
}
let captchaType = data.code === 200 ? data.data?.type : "DISABLED"
const captcha = createCaptchaByType(captchaType, this);
if (captcha == null) {
throw new Error("[TAC] 未知的验证码类型[" + captchaType + "]");
}
captcha.init(data, (d, c) => {
// 验证
const currentCaptchaData = c.currentCaptchaData;
const data = {
bgImageWidth: currentCaptchaData.bgImageWidth,
bgImageHeight: currentCaptchaData.bgImageHeight,
templateImageWidth: currentCaptchaData.templateImageWidth,
templateImageHeight: currentCaptchaData.templateImageHeight,
startTime: currentCaptchaData.startTime.getTime(),
stopTime: currentCaptchaData.stopTime.getTime(),
trackList: currentCaptchaData.trackList,
};
if (c.type === 'ROTATE_DEGREE' || c.type === 'ROTATE') {
data.bgImageWidth = c.currentCaptchaData.end;
}
if (currentCaptchaData.data) {
data.data = currentCaptchaData.data;
}
// 清空
const id = c.currentCaptchaData.currentCaptchaId;
c.currentCaptchaData = undefined;
// 调用验证接口
this.config.validCaptcha(id, data, c, this)
})
this.C = captcha;
this.openCaptcha()
});
}
destroyCaptcha(callback) {
if (this.C) {
this.C.el.css("transform", "translateX(300px)")
setTimeout(() => {
this.C.destroy();
if (callback) {
callback();
}
}, 500)
} else {
callback();
}
}
}
export {TianAiCaptcha, CaptchaConfig}
@@ -0,0 +1,107 @@
#tianai-captcha-parent {
box-shadow: 0 0 11px 0 #999999;
width: 318px;
height: 318px;
overflow: hidden;
position: relative;
z-index: 997;
box-sizing: border-box;
border-radius: 5px;
padding: 8px;
#tianai-captcha-box {
height: 260px;
width: 100%;
position: relative;
overflow: hidden;
.loading {
width: 120px;
height: 20px;
-webkit-mask: linear-gradient(90deg, #000 70%, #0000 0) 0/20%;
background: linear-gradient(#f7b645 0 0) 0 / 0% no-repeat #dddddd6b;
animation: cartoon 1s infinite steps(6);
margin: 120px auto;
@keyframes cartoon {
100% {
background-size: 120%;
}
}
}
#tianai-captcha {
transform-style: preserve-3d;
will-change: transform;
transition-duration: .45s;
//transition-timing-function: cubic-bezier(0.36, 0.3, 0.42, 1.5);
transform: translateX(-300px);
}
}
#tianai-captcha-bg-img {
background-color: #fff;
background-position: top;
background-size: cover;
z-index: -1;
width: 100%;
height: 100%;
top: 0;
left: 0;
position: absolute;
border-radius: 6px;
//background-image: url("");
}
.slider-bottom {
.close-btn {
width: 20px;
height: 20px;
background-image: url("@/assets/images/icon.png");
background-repeat: no-repeat;
background-position: 0 -14px;
float: right;
margin-right: 2px;
cursor: pointer;
}
.refresh-btn {
width: 20px;
height: 20px;
background-image: url("@/assets/images/icon.png");
background-position: 0 -167px;
background-repeat: no-repeat;
float: right;
margin-right: 10px;
cursor: pointer;
}
.logo {
height: 30px;
float: left;
}
height: 19px;
width: 100%;
}
.slider-move-shadow {
animation: myanimation 2s infinite;
height: 100%;
width: 5px;
background-color: #fff;
position: absolute;
top: 0;
left: 0;
filter: opacity(0.5);
box-shadow: 1px 1px 1px #fff;
border-radius: 50%;
}
#tianai-captcha-slider-move-track-mask {
border-width: 1px;
border-style: solid;
border-color: #00f4ab;
width: 0;
height: 32px;
background-color: #a9ffe5;
opacity: .5;
position: absolute;
top: -1px;
left: -1px;
border-radius: 5px;
}
}
@@ -0,0 +1,464 @@
/** 是否打印日志 */
var isPrintLog = false;
function printLog(params) {
if (isPrintLog) {
console.log(JSON.stringify(params));
}
}
/**
* 清除默认事件
* @param event event
*/
function clearPreventDefault(event) {
if (event.preventDefault) {
event.preventDefault();
}
}
/**
* 阻止某div默认事件
* @param dom
*/
function clearAllPreventDefault(dom) {
Dom(dom).each((el) => {
// 手机端
el.addEventListener('touchmove', clearPreventDefault, {passive: false});
// pc端
el.addEventListener('mousemove', clearPreventDefault, {passive: false});
});
}
function reductionAllPreventDefault(dom) {
Dom(dom).each(function (el) {
el.removeEventListener('touchmove', clearPreventDefault);
el.addEventListener('mousemove', clearPreventDefault);
});
}
/**
* 获取当前坐标
* @param event 事件
* @returns {{x: number, y: number}}
*/
function getCurrentCoordinate(event) {
if (event.pageX !== null && event.pageX !== undefined) {
return {
x: Math.round(event.pageX),
y: Math.round(event.pageY)
}
}
let targetTouches;
if (event.changedTouches) {
// 抬起事件
targetTouches = event.changedTouches;
} else if (event.targetTouches) {
// pc 按下事件
targetTouches = event.targetTouches;
} else if (event.originalEvent && event.originalEvent.targetTouches) {
// 鼠标触摸事件
targetTouches = event.originalEvent.targetTouches;
}
if (targetTouches[0].pageX !== null && targetTouches[0].pageX !== undefined) {
return {
x: Math.round(targetTouches[0].pageX),
y: Math.round(targetTouches[0].pageY)
}
}
return {
x: Math.round(targetTouches[0].clientX),
y: Math.round(targetTouches[0].clientY)
}
}
function down(currentCaptcha, event) {
// debugger
const coordinate = getCurrentCoordinate(event);
let startX = coordinate.x;
let startY = coordinate.y;
currentCaptcha.currentCaptchaData.startX = startX;
currentCaptcha.currentCaptchaData.startY = startY;
const trackList = currentCaptcha.currentCaptchaData.trackList;
currentCaptcha.currentCaptchaData.startTime = new Date();
const startTime = currentCaptcha.currentCaptchaData.startTime;
trackList.push({
x: coordinate.x,
y: coordinate.y,
type: "down",
t: (new Date().getTime() - startTime.getTime())
});
printLog(["start", startX, startY])
currentCaptcha.__m__ = move.bind(null, currentCaptcha);
currentCaptcha.__u__ = up.bind(null, currentCaptcha);
// pc
window.addEventListener("mousemove", currentCaptcha.__m__);
window.addEventListener("mouseup", currentCaptcha.__u__);
// 手机端
window.addEventListener("touchmove", currentCaptcha.__m__, false);
window.addEventListener("touchend", currentCaptcha.__u__, false);
if (currentCaptcha && currentCaptcha.doDown) {
currentCaptcha.doDown(event, currentCaptcha)
}
}
function move(currentCaptcha, event) {
if (event.touches && event.touches.length > 0) {
event = event.touches[0];
}
// debugger
const coordinate = getCurrentCoordinate(event);
let pageX = coordinate.x;
let pageY = coordinate.y;
const startX = currentCaptcha.currentCaptchaData.startX;
const startY = currentCaptcha.currentCaptchaData.startY;
const startTime = currentCaptcha.currentCaptchaData.startTime;
const end = currentCaptcha.currentCaptchaData.end;
const bgImageWidth = currentCaptcha.currentCaptchaData.bgImageWidth;
const trackList = currentCaptcha.currentCaptchaData.trackList;
let moveX = pageX - startX;
let moveY = pageY - startY;
const track = {
x: coordinate.x,
y: coordinate.y,
type: "move",
t: (new Date().getTime() - startTime.getTime())
};
trackList.push(track);
if (moveX < 0) {
moveX = 0;
} else if (moveX > end) {
moveX = end;
}
currentCaptcha.currentCaptchaData.moveX = moveX;
currentCaptcha.currentCaptchaData.moveY = moveY;
if (currentCaptcha.doMove) {
currentCaptcha.doMove(event, currentCaptcha);
}
printLog(["move", track])
}
function destroyEvent(currentCaptcha) {
if (currentCaptcha) {
if (currentCaptcha.__m__) {
window.removeEventListener("mousemove", currentCaptcha.__m__);
window.removeEventListener("touchmove", currentCaptcha.__m__);
}
if (currentCaptcha.__u__) {
window.removeEventListener("mouseup", currentCaptcha.__u__);
window.removeEventListener("touchend", currentCaptcha.__u__);
}
}
}
function up(currentCaptcha, event) {
destroyEvent(currentCaptcha);
const coordinate = getCurrentCoordinate(event);
currentCaptcha.currentCaptchaData.stopTime = new Date();
const startTime = currentCaptcha.currentCaptchaData.startTime;
const trackList = currentCaptcha.currentCaptchaData.trackList;
const track = {
x: coordinate.x,
y: coordinate.y,
type: "up",
t: (new Date().getTime() - startTime.getTime())
}
trackList.push(track);
printLog(["up", track])
printLog(["tracks", trackList])
if (currentCaptcha.doUp) {
currentCaptcha.doUp(event, currentCaptcha)
}
currentCaptcha.endCallback(currentCaptcha.currentCaptchaData, currentCaptcha);
}
function initConfig(bgImageWidth, bgImageHeight, templateImageWidth, templateImageHeight, end) {
// bugfix 图片宽高可能会有小数情况,强转一下整数
const currentCaptchaConfig = {
startTime: new Date(),
trackList: [],
movePercent: 0,
clickCount: 0,
bgImageWidth: Math.round(bgImageWidth),
bgImageHeight: Math.round(bgImageHeight),
templateImageWidth: Math.round(templateImageWidth),
templateImageHeight: Math.round(templateImageHeight),
end: end
}
printLog(["init", currentCaptchaConfig]);
return currentCaptchaConfig;
}
function closeTips(el, callback) {
const tipEl = Dom(el).find("#tianai-captcha-tips");
tipEl.removeClass("tianai-captcha-tips-on")
// tipEl.removeClass("tianai-captcha-tips-success")
// tipEl.removeClass("tianai-captcha-tips-error")
// 延时
if (callback) {
setTimeout(callback, .35);
}
}
function showTips(el, msg, type, callback) {
const tipEl = Dom(el).find("#tianai-captcha-tips");
tipEl.text(msg);
if (type === 1) {
// 成功
tipEl.removeClass("tianai-captcha-tips-error")
tipEl.addClass("tianai-captcha-tips-success")
} else {
// 失败
tipEl.removeClass("tianai-captcha-tips-success")
tipEl.addClass("tianai-captcha-tips-error")
}
tipEl.addClass("tianai-captcha-tips-on");
// 延时
setTimeout(callback, 1000);
}
class CommonCaptcha {
showTips(msg, type, callback) {
showTips(this.el, msg, type, callback)
}
closeTips(msg, callback) {
closeTips(this.el, msg, callback)
}
}
function Dom(domStr, dom) {
return new DomEl(domStr, dom);
}
class DomEl {
constructor(domStr, dom) {
if (dom && typeof dom === 'object' && typeof dom.nodeType !== 'undefined') {
this.dom = dom;
this.domStr = domStr;
return;
}
if (domStr instanceof DomEl) {
this.dom = domStr.dom;
this.domStr = domStr.domStr;
} else if (typeof domStr === "string") {
this.dom = document.querySelector(domStr)
this.domStr = domStr;
} else if (typeof document === 'object' && typeof document.nodeType !== 'undefined') {
this.dom = domStr;
this.domStr = domStr.nodeName;
} else {
throw new Error("不支持的类型");
}
}
each(callback) {
this.getTarget().querySelectorAll("*").forEach(callback);
}
removeClass(className) {
let element = this.getTarget();
if (element.classList) {
// 使用 classList API 移除类
element.classList.remove(className);
} else {
// 兼容旧版本浏览器
const currentClass = element.className;
const regex = new RegExp('(?:^|\\s)' + className + '(?!\\S)', 'g');
element.className = currentClass.replace(regex, '');
}
return this;
}
addClass(className) {
const element = this.getTarget();
if (element.classList) {
// 使用 classList API 添加类
element.classList.add(className);
} else {
// 兼容旧版本浏览器
let currentClass = element.className;
if (currentClass.indexOf(className) === -1) {
element.className = currentClass + ' ' + className;
}
}
return this;
}
find(str) {
const el = this.getTarget().querySelector(str);
if (el) {
return new DomEl(str, el);
}
return null;
}
children(selector) {
const childNodes = this.getTarget().childNodes;
for (let i = 0; i < childNodes.length; i++) {
if (childNodes[i].nodeType === 1 && childNodes[i].matches(selector)) {
return new DomEl(selector, childNodes[i]);
}
}
return null;
}
remove() {
this.getTarget().remove();
return null;
}
css(property, value) {
if (typeof property === 'string' && typeof value === 'string') {
// 设置单个属性
this.getTarget().style[property] = value;
} else if (typeof property === 'object') {
// 设置多个属性
for (var prop in property) {
if (property.hasOwnProperty(prop)) {
this.getTarget().style[prop] = property[prop];
}
}
} else if (typeof property === 'string' && typeof value === 'undefined') {
// 获取单个属性
return window.getComputedStyle(element)[property];
}
}
attr(attributeName, value) {
if (value === undefined) {
// 如果未提供值,则返回属性的当前值
return this.getTarget().getAttribute(attributeName);
} else {
// 如果提供了值,则设置属性的值
this.getTarget().setAttribute(attributeName, value);
}
return this;
}
text(str) {
this.getTarget().innerText = str;
return this;
}
html(str) {
this.getTarget().innerHtml = str;
return this;
}
is(dom) {
if (dom && typeof dom === 'object' && typeof dom.nodeType !== 'undefined') {
return this.dom === dom;
}
if (dom instanceof DomEl) {
return this.dom === dom.dom;
}
}
append(content) {
if (typeof content === 'string') {
this.getTarget().insertAdjacentHTML("beforeend", content);
} else if (content instanceof HTMLElement) {
this.getTarget().appendChild(content);
} else {
throw new Error('Invalid content type');
}
return this;
}
click(fun) {
this.on("click", fun);
return this;
}
mousedown(fun) {
this.on("mousedown", fun);
return this;
}
touchstart(fun) {
this.on("touchstart", fun);
return this;
}
on(eventType, fun) {
this.getTarget().addEventListener(eventType, fun, {passive: true});
return this;
}
width() {
return this.getTarget().offsetWidth;
}
height() {
return this.getTarget().offsetHeight;
}
getTarget() {
if (this.dom) {
return this.dom;
}
throw new Error("dom不存在: [" + this.domStr + "]");
}
}
function http(options) {
return new Promise(function (resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.open(options.method || 'GET', options.url);
// 设置请求头
if (options.headers) {
for (const header in options.headers) {
if (options.headers.hasOwnProperty(header)) {
xhr.setRequestHeader(header, options.headers[header]);
}
}
}
xhr.onreadystatechange = function () {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status >= 200 && xhr.status <= 500) {
const contentType = xhr.getResponseHeader('Content-Type');
if (contentType && contentType.indexOf('application/json') !== -1) {
resolve(JSON.parse(xhr.responseText));
} else {
resolve(xhr.responseText);
}
} else {
reject(new Error('Request failed with status: ' + xhr.status));
}
}
};
xhr.onerror = function () {
reject(new Error('Network Error'));
};
xhr.send(options.data);
});
}
function isEmptyObject(obj) {
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
return false; // 对象不为空
}
}
return true; // 对象为空
}
export {
isEmptyObject,
http,
Dom,
DomEl,
CommonCaptcha,
clearAllPreventDefault,
down,
move,
up,
initConfig,
showTips,
closeTips,
destroyEvent
}
@@ -0,0 +1,87 @@
#tianai-captcha {
text-align: left;
box-sizing: content-box;
width: 300px;
height: 260px;
z-index: 999;
.slider-bottom .logo {
height: 30px;
}
.slider-bottom {
height: 19px;
width: 100%;
}
.content {
.tianai-captcha-tips {
height: 25px;
width: 100%;
position: absolute;
bottom: -25px;
left: 0;
z-index: 999;
font-size: 15px;
line-height: 25px;
/*background-color: #FF5D39;*/
color: #fff;
text-align: center;
/* transform: translateY(0px); */
/* display: none; */
/* transition: max-height 0.5s; */
transition: bottom .3s ease-in-out;
}
.tianai-captcha-tips.tianai-captcha-tips-error {
background-color: #FF5D39;
}
.tianai-captcha-tips.tianai-captcha-tips-success {
background-color: #39C522;
}
.tianai-captcha-tips.tianai-captcha-tips-on {
bottom: 0;
}
#tianai-captcha-loading {
z-index: 9999;
background-color: #f5f5f5;
text-align: center;
height: 100%;
overflow: hidden;
position: relative;
display: flex;
justify-content: center;
align-items: center;
img {
display: block;
width: 45px;
height: 45px;
}
}
}
#tianai-captcha-slider-bg-canvas {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
border-radius: 5px;
}
#tianai-captcha-slider-bg-div{
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
border-radius: 5px;
.tianai-captcha-slider-bg-div-slice {
position: absolute;
}
}
}
@keyframes myanimation {
from {
left: 0;
}
to {
left: 289px;
}
}
@@ -0,0 +1,111 @@
import "../common/common.scss"
import "@/captcha/slider/slider.scss"
import "./concat.scss"
import {Dom, CommonCaptcha, clearAllPreventDefault, down, initConfig, destroyEvent} from "../common/common.js"
const TYPE = "CONCAT";
function getTemplate(styleConfig) {
return `
<div id="tianai-captcha" class="tianai-captcha-slider tianai-captcha-concat">
<div class="slider-tip">
<span id="tianai-captcha-slider-move-track-font" >拖动滑块完成拼图</span>
</div>
<div class="content">
<div class="tianai-captcha-slider-concat-img-div" id="tianai-captcha-slider-concat-img-div">
<img id="tianai-captcha-slider-concat-slider-img" src="" alt/>
</div>
<div class="tianai-captcha-slider-concat-bg-img"></div>
<div class="tianai-captcha-tips" id="tianai-captcha-tips"></div>
</div>
<div class="slider-move">
<div class="slider-move-track">
<div id="tianai-captcha-slider-move-track-mask"></div>
<div class="slider-move-shadow"></div>
</div>
<div class="slider-move-btn" id="tianai-captcha-slider-move-btn">
</div>
</div>
</div>
`;
}
class Concat extends CommonCaptcha {
constructor(divId, styleConfig) {
super();
this.boxEl = Dom(divId);
this.styleConfig = styleConfig;
this.type = TYPE;
this.currentCaptchaData = {}
}
init(captchaData, endCallback, loadSuccessCallback) {
// 重载样式
this.destroy();
this.boxEl.append(getTemplate(this.styleConfig));
this.el = this.boxEl.find("#tianai-captcha");
this.loadStyle();
// 按钮绑定事件
this.el.find("#tianai-captcha-slider-move-btn").mousedown(down.bind(null,this));
this.el.find("#tianai-captcha-slider-move-btn").touchstart(down.bind(null,this));
clearAllPreventDefault(this.el);
// 绑定全局
window.currentCaptcha = this;
// 载入验证码
this.loadCaptchaForData(this, captchaData);
this.endCallback = endCallback;
if (loadSuccessCallback) {
// 加载成功
loadSuccessCallback(this);
}
return this;
}
destroy() {
destroyEvent();
const existsCaptchaEl = this.boxEl.children("#tianai-captcha");
if (existsCaptchaEl) {
existsCaptchaEl.remove();
}
}
doMove() {
const moveX = this.currentCaptchaData.moveX;
this.el.find("#tianai-captcha-slider-move-btn").css("transform", "translate(" + moveX + "px, 0px)")
this.el.find("#tianai-captcha-slider-concat-img-div").css("background-position-x", moveX + "px");
this.el.find("#tianai-captcha-slider-move-track-mask").css("width", moveX + "px")
}
loadStyle() {
let sliderImg = "";
let moveTrackMaskBorderColor = "#00f4ab";
let moveTrackMaskBgColor = "#a9ffe5";
const styleConfig = this.styleConfig;
if (styleConfig) {
sliderImg = styleConfig.btnUrl;
moveTrackMaskBgColor = styleConfig.moveTrackMaskBgColor;
moveTrackMaskBorderColor = styleConfig.moveTrackMaskBorderColor;
}
this.el.find(".slider-move .slider-move-btn").css("background-image", "url(" + sliderImg + ")");
// this.el.find("#tianai-captcha-slider-move-track-font").text(title);
this.el.find("#tianai-captcha-slider-move-track-mask").css("border-color", moveTrackMaskBorderColor);
this.el.find("#tianai-captcha-slider-move-track-mask").css("background-color", moveTrackMaskBgColor);
}
loadCaptchaForData(that, data) {
const bgImg = that.el.find(".tianai-captcha-slider-concat-bg-img");
const sliderImg = that.el.find("#tianai-captcha-slider-concat-img-div");
bgImg.css("background-image", "url(" + data.data.backgroundImage + ")");
sliderImg.css("background-image", "url(" + data.data.backgroundImage + ")");
sliderImg.css("background-position", "0px 0px");
var backgroundImageHeight = data.data.backgroundImageHeight;
var height = ((backgroundImageHeight - data.data.data.randomY) / backgroundImageHeight) * 180;
sliderImg.css("height", height+"px");
that.currentCaptchaData = initConfig(bgImg.width(), bgImg.height(), sliderImg.width(), sliderImg.height(), 300 - 63 + 5);
that.currentCaptchaData.currentCaptchaId = data.data.id;
}
}
export default Concat;
@@ -0,0 +1,17 @@
#tianai-captcha.tianai-captcha-concat {
.tianai-captcha-slider-concat-img-div {
background-size: 100% 180px;
position: absolute;
transform: translate(0px, 0px);
/* border-bottom: 1px solid blue; */
z-index: 1;
width: 100%;
}
.tianai-captcha-slider-concat-bg-img {
width: 100%;
height: 100%;
position: absolute;
transform: translate(0px, 0px);
background-size: 100% 180px;
}
}
@@ -0,0 +1,211 @@
import StyleConfig from "./styleConfig";
import {Dom,http} from "../common/common";
class CaptchaConfig {
constructor(args) {
if (!args.bindEl) {
throw new Error("[TAC] 必须配置 [bindEl]用于将验证码绑定到该元素上");
}
if (!args.requestCaptchaDataUrl) {
throw new Error("[TAC] 必须配置 [requestCaptchaDataUrl]请求验证码接口");
}
if (!args.validCaptchaUrl) {
throw new Error("[TAC] 必须配置 [validCaptchaUrl]验证验证码接口");
}
this.bindEl = args.bindEl;
this.domBindEl = Dom(args.bindEl);
this.requestCaptchaDataUrl = args.requestCaptchaDataUrl;
this.validCaptchaUrl = args.validCaptchaUrl;
if (args.validSuccess) {
this.validSuccess = args.validSuccess;
}
if (args.validFail) {
this.validFail = args.validFail;
}
if (args.requestHeaders) {
this.requestHeaders = args.requestHeaders
}else {
this.requestHeaders = {}
}
if (args.btnCloseFun) {
this.btnCloseFun = args.btnCloseFun;
}
if (args.btnRefreshFun) {
this.btnRefreshFun = args.btnRefreshFun;
}
this.requestChain = [];
// 时间戳转换
this.timeToTimestamp = args.timeToTimestamp || true;
this.insertRequestChain(0, {
preRequest(type, param, c, tac) {
if (this.timeToTimestamp && param.data) {
for (let key in param.data){
// 将date全部转换为时间戳
if (param.data[key] instanceof Date) {
param.data[key] = param.data[key].getTime();
}
}
}
return true;
}
})
}
addRequestChain(fun) {
this.requestChain.push(fun);
}
insertRequestChain(index,chain) {
this.requestChain.splice(index, 0, chain);
}
removeRequestChain(index) {
this.requestChain.splice(index, 1);
}
requestCaptchaData() {
const requestParam = {}
requestParam.headers = this.requestHeaders || {};
requestParam.data = {};
// 设置默认值
requestParam.headers["Content-Type"] = "application/json;charset=UTF-8";
requestParam.method="POST";
requestParam.url = this.requestCaptchaDataUrl;
// 请求前装载参数
this._preRequest("requestCaptchaData", requestParam);
// 发送请求
const request = this.doSendRequest(requestParam);
// 返回结果
return request.then(res => {
// 装返回结果
this._postRequest("requestCaptchaData", requestParam, res);
// 返回结果
return res;
});
}
doSendRequest(requestParam) {
// 如果content-type是json,那么data就是json字符串, 这里直接匹配所有header是否包含application/json
if (requestParam.headers ) {
for (const key in requestParam.headers){
if(requestParam.headers[key].indexOf("application/json") > -1) {
if (typeof requestParam.data !== "string") {
requestParam.data = JSON.stringify(requestParam.data);
}
break;
}
}
}
return http(requestParam).then(res => {
try {
return JSON.parse(res);
}catch (e) {
return res;
}
})
}
_preRequest(type, requestParam, c, tac) {
for (let i = 0; i < this.requestChain.length; i++) {
const r = this.requestChain[i];
if (r.preRequest) {
if (!r.preRequest(type, requestParam, this, c, tac)) {
break;
}
}
}
}
_postRequest(type, requestParam, res, c, tac) {
for (let i = 0; i < this.requestChain.length; i++) {
const r = this.requestChain[i];
// 判断r是否存圩postRequest方法
if (r.postRequest) {
if (!r.postRequest(type, requestParam, res, this, c, tac)) {
break;
}
}
}
}
validCaptcha(currentCaptchaId, data, c, tac) {
const sendParam = {
id: currentCaptchaId,
data: data
};
let requestParam = {};
requestParam.headers = this.requestHeaders || {};
requestParam.data = sendParam;
requestParam.headers["Content-Type"] = "application/json;charset=UTF-8";
requestParam.method="POST";
requestParam.url = this.validCaptchaUrl;
this._preRequest("validCaptcha", requestParam, c, tac);
const request = this.doSendRequest(requestParam);
return request.then(res => {
this._postRequest("validCaptcha", requestParam, res, c, tac);
return res;
}).then(res => {
if (res.code == 200) {
const useTimes = (data.stopTime - data.startTime) / 1000;
c.showTips(`验证成功,耗时${useTimes}`, 1, () => this.validSuccess(res, c, tac));
} else {
let tipMsg = "验证失败,请重新尝试!";
if (res.code) {
if (res.code != 4001) {
tipMsg = "验证码被黑洞吸走了!";
}
}
c.showTips(tipMsg, 0, () => this.validFail(res, c, tac));
}
}).catch(e => {
let tipMsg = c.styleConfig.i18n.tips_error;
if (e.code && e.code != 200) {
if (res.code != 4001) {
tipMsg = c.styleConfig.i18n.tips_4001;
}
c.showTips(tipMsg, 0, () => this.validFail(res, c, tac));
}
})
}
validSuccess(res, c, tac) {
console.log("验证码校验成功, 请重写 [config.validSuccess] 方法, 用于自定义逻辑处理")
window.currentCaptchaRes = res;
tac.destroyWindow();
}
validFail(res, c, tac) {
tac.reloadCaptcha();
}
}
function wrapConfig(config) {
if (config instanceof CaptchaConfig) {
return config;
}
return new CaptchaConfig(config);
}
function wrapStyle(style) {
// if (!style) {
// style = {}
// }
//
// if (!style.btnUrl) {
// // 设置默认图片
// style.btnUrl = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIwAAABkCAYAAABU19jRAAAJcUlEQVR4nO2d63MT1xmHf9rV6mr5fgNMuSW+ENsY8N0EE2BMhinJNB8y/dD2Qz/0v+gMf0w/JHTKNJAhICwbsA02TpNAHEMgQIwNBSEb8F2rvXTeY1kjYyA+TmVJmfeZ8YiRWa9299E57/mdI63Dtm3E+RjAKTDMaj4F8AU9uyzMCQBn+EQxb+EjAF+RMH8AcJrPFLMGvCSMzWeKWSN/I2GiAFx8xpi1oPBZYiTQWRhGChaGkYKFYaRgYRgpWBhGChaGkYKFYaRgYRgpWBhGChaGkYKFYaRgYRgpWBhGChaGkYKFYaRgYRgpWBhGChaGkYKFYaRgYRgpWBhGChaGkYKFYaRgYRgpWBhGCiefrtShGwZiup74+4qqwu12Z/W7lIVJEfN6FDfv3sPXfYOIRRfpm1UQKC7EkQ+PYFtRcdZKw8KkiLsPJ/CfgSFcH7yOxWhU7MSluYQoR44fxdaCoqyUhoVJEfZ8FN99c1N0Sx6PR+zEMAz0XAgBNtB14hi25OXDkWXHxUVvinA4ln6ScTqdsGwbvRd7EPwyiEcvXyDbvpyHhUkRaq4fe/c3wEWSWFZiJySNYZroCYYQPHsBY1OTWSWNevLkyb/TYwa8lt8UAb8ftluDW9UwPj4hDs0Rb3JUVRXd09j9nwELKKgoR4HXlw2Hb3INkyK8mob9NdUwLROq4sCVKwMrdqRpGkzTFN0TaWR2HcKu0rKMr2lYmBTi1jS01dUt7UBx4PKlfvHP5JaGuqseIY0DjmOHsKukNKOPiYVJMU5VRXt9PSwboO+fvHJ5QEiiKEvlIz3S86HuHiiqAhw9iJ0lpRnb0rAwG4CqKHh/Tz0UhwOWaWGg/5oofEkmJLU4wfPdQia765CQJhNHJCzMBkEtSVtdLRw2YNo2hgaGEDMMMWpahrwJBUMUCkM9djgjE2EWZgOhFqW5rlbMKdm2heHBYUT1mCiAEW9pKKfpPh8Sj5mYCLMwG4zLqWJfTZWQgL5S++uhYURjBrR4S0MtUSYnwixMGvBoGvZUV4quh0S4Pjgsaho1XtOIcM8wxJCb+qmu33dljDS/CWEeTb/E/Pw89EUdebkBVBQWrnnbWVjQoMAtsT9asGDQhf8VUbnX5UJ9VaVoZahVuXZ1cMXoiaSJxWIiEab/dPj4UXFczjRrk/VJ70/hp/jhuxF89o9TGP1+FH6fD9OxGHw5Pnicb34/PJ2dweitu7hwLojvb47A9rhQmJeXGLm8iQeP/4uRH27h88/+iZhhYs40UFZQsK7XrqkqigvyYbk18VrHH74+EX74YAzRqI66mupE15UmzKwW5kEkgtFvRxA8ex7hJ2HMzczgzu0f8fjxExRt2YzcgB9udfUJjuo6Tv/7HE6f+pe4GHd//AkwLRhuDeXFRW+U5v7EI4yMjKI3GMLt0Tt4cO8BAoEcWJoTZYXrl6asqBC6U0GOy42HY+MrZi1JmoWFRZQW5sNyuVBeUpxOabJ7aiASjiB4/iKmnj+H5loaacwvLOL2jRF4AjnY8dc/I/DKbTdoSHvr8SO8DD/DzPSMWHrg1JwYvHZdpK2NVZWU26/aF3VDTyLP0N/bh4mJR3C7XZiZnRVdht/nx7u7tsOzzg5qORFWHAocigO9vX2Jronwej24cXMEbq8XrfW169rH/4usnq02o1FEo9FEE47luN22sTAzC0OPrd7ItnHn9h0MDg3D6/WKbZdHJqYRg26ar92XDgvD39zA2Ng4VKdTbEf7mpmeRX/fAPRfeRch+luNNTXICeSu+h3ti7okUzdgp3luO6uFUTUN9lLmnniOCkdKVnML8uB0r76rD72Di4qL4NI0IUnydpZlw/WmGsY00bRvDzZvKhfFKLAU9VOG8v7BdijW+i8kLX649yyMz0+fwVQksur3NILyejzw5efCoaT3kmW1MN68AMq2bBIXXtd18WMZBt6r242DBzvgda3uWhQ4xNzOkeNdohZYXFjA4vwCfD4/Sio2i9bjdeSoGirKylFYXirykehiFHpUR2FJCbZu+x1yXlMrrQWSZWwygv6Ll3DxXBCX+66u6I7o2DRFRWtbM1o62xNdb7rI7lGSqqBs+zZMTj4XLYY/x49t7+zABx8eReWO7ciLL41ctZmqoqRiE/x+P6amp5FbkI9jx7tw+GgncqmbesPuPAEfduzcgenZOTg0FaWby/GXP/0RdZXvrOvlkyzjzyfR81UIoQs9IpRJniqglszt0tDc1oS9bc2o37lTLMhKI2bW35HtRXQRs3MLmH/xUrzzVb8HJQUFyHX/crJCQ+JwOALFqaKspGjNRWtkbg5zc7PQXC5szl/f6Ig6MFqiSavuqHCmumuFLIYBt+ZEY0sTGtua0VBTJQK/NKPzLfzSQEKWL4NiiG5a1gpZzPhMdnNrE/a3N2NPVaUI+jIAnacGNhiShdbx9pzrFgunSA4tqeUQRbuqoLW9BQ0tjSINzhBZBCzMBvPzVAS950KiG6KWJVkWGnXRELrjQBtqG/eioTqzZAELs3FQy3Iv/BR9wUtiUtGOr+tNhoptGt1V7atD4+4aEehlGizMBnH/WRj9wcuiG7LjI7Vllm8d3nnoAKoaakXq+0tzWumChUkxdlyWge4rYt0uzRMpSck01SzUDR3s7MC7e2pFRqSmOZx7GyxMCrESLcsldAd7oCgrEx6xrldRRM1SvbceHfV1K0K7TISFSREx28L41KRIcGmdruOVz82KBFd1oqWjBe/tb0ArLd3McFnAwqSOiclJ9JwP4fLFXtEtJXdDywluU2uTGDpTgZupNcur8GerU8R0eBJDV6+LRVbJLYdIcF2aSHD3tzaL9b20zjdbYGFShB0z4HY6V9QtFNLRXFATxf2U4FZXZkLcLwULkyJoaUXMNMV6HbyS4O6jicQMS3DXCguTInJKC9HU0YoPOg8k1uy0t7eivnmfSHB9WSgLwZOPKcKwLcT0GL69cxe3b46KoK6+ZS92V2zNyAR3jfBsdaox6LPSpiVyf/rEo/rq11JlFzxbnWoomEMW5CtrhWsYRgoWhpGChWGkYGEYKVgYRgoWhpGChWGkYGEYKVgYRgoWhpGChWGkYGEYKVgYRgoWhpGChWGkYGEYKVgYRgoWhpGChWGkYGEYKVgYRgoWhpGChWGkYGEYKVgYRgr6qGx6b4/BZBXUwnzCl4xZI5844g3MCQBn+Kwxb+EjAGcdST3SxwBO8RljXsOnAL4AgP8BXnVIgIvemwsAAAAASUVORK5CYII=";
// }
// if (!style.moveTrackMaskBgColor && !style.moveTrackMaskBorderColor) {
// style.moveTrackMaskBgColor = "#89d2ff";
// style.moveTrackMaskBorderColor = "#0298f8";
//
// }
// return style;
let margeStyle = {...StyleConfig, ...style};
margeStyle.i18n = {...StyleConfig.i18n, ...style?.i18n};
return margeStyle;
}
const captchaRequestChains = {}
export {CaptchaConfig, wrapConfig, wrapStyle}
@@ -0,0 +1,24 @@
export default {
// 按钮图片
btnUrl: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIwAAABkCAYAAABU19jRAAAJcUlEQVR4nO2d63MT1xmHf9rV6mr5fgNMuSW+ENsY8N0EE2BMhinJNB8y/dD2Qz/0v+gMf0w/JHTKNJAhICwbsA02TpNAHEMgQIwNBSEb8F2rvXTeY1kjYyA+TmVJmfeZ8YiRWa9299E57/mdI63Dtm3E+RjAKTDMaj4F8AU9uyzMCQBn+EQxb+EjAF+RMH8AcJrPFLMGvCSMzWeKWSN/I2GiAFx8xpi1oPBZYiTQWRhGChaGkYKFYaRgYRgpWBhGChaGkYKFYaRgYRgpWBhGChaGkYKFYaRgYRgpWBhGChaGkYKFYaRgYRgpWBhGChaGkYKFYaRgYRgpWBhGChaGkYKFYaRgYRgpWBhGCiefrtShGwZiup74+4qqwu12Z/W7lIVJEfN6FDfv3sPXfYOIRRfpm1UQKC7EkQ+PYFtRcdZKw8KkiLsPJ/CfgSFcH7yOxWhU7MSluYQoR44fxdaCoqyUhoVJEfZ8FN99c1N0Sx6PR+zEMAz0XAgBNtB14hi25OXDkWXHxUVvinA4ln6ScTqdsGwbvRd7EPwyiEcvXyDbvpyHhUkRaq4fe/c3wEWSWFZiJySNYZroCYYQPHsBY1OTWSWNevLkyb/TYwa8lt8UAb8ftluDW9UwPj4hDs0Rb3JUVRXd09j9nwELKKgoR4HXlw2Hb3INkyK8mob9NdUwLROq4sCVKwMrdqRpGkzTFN0TaWR2HcKu0rKMr2lYmBTi1jS01dUt7UBx4PKlfvHP5JaGuqseIY0DjmOHsKukNKOPiYVJMU5VRXt9PSwboO+fvHJ5QEiiKEvlIz3S86HuHiiqAhw9iJ0lpRnb0rAwG4CqKHh/Tz0UhwOWaWGg/5oofEkmJLU4wfPdQia765CQJhNHJCzMBkEtSVtdLRw2YNo2hgaGEDMMMWpahrwJBUMUCkM9djgjE2EWZgOhFqW5rlbMKdm2heHBYUT1mCiAEW9pKKfpPh8Sj5mYCLMwG4zLqWJfTZWQgL5S++uhYURjBrR4S0MtUSYnwixMGvBoGvZUV4quh0S4Pjgsaho1XtOIcM8wxJCb+qmu33dljDS/CWEeTb/E/Pw89EUdebkBVBQWrnnbWVjQoMAtsT9asGDQhf8VUbnX5UJ9VaVoZahVuXZ1cMXoiaSJxWIiEab/dPj4UXFczjRrk/VJ70/hp/jhuxF89o9TGP1+FH6fD9OxGHw5Pnicb34/PJ2dweitu7hwLojvb47A9rhQmJeXGLm8iQeP/4uRH27h88/+iZhhYs40UFZQsK7XrqkqigvyYbk18VrHH74+EX74YAzRqI66mupE15UmzKwW5kEkgtFvRxA8ex7hJ2HMzczgzu0f8fjxExRt2YzcgB9udfUJjuo6Tv/7HE6f+pe4GHd//AkwLRhuDeXFRW+U5v7EI4yMjKI3GMLt0Tt4cO8BAoEcWJoTZYXrl6asqBC6U0GOy42HY+MrZi1JmoWFRZQW5sNyuVBeUpxOabJ7aiASjiB4/iKmnj+H5loaacwvLOL2jRF4AjnY8dc/I/DKbTdoSHvr8SO8DD/DzPSMWHrg1JwYvHZdpK2NVZWU26/aF3VDTyLP0N/bh4mJR3C7XZiZnRVdht/nx7u7tsOzzg5qORFWHAocigO9vX2Jronwej24cXMEbq8XrfW169rH/4usnq02o1FEo9FEE47luN22sTAzC0OPrd7ItnHn9h0MDg3D6/WKbZdHJqYRg26ar92XDgvD39zA2Ng4VKdTbEf7mpmeRX/fAPRfeRch+luNNTXICeSu+h3ti7okUzdgp3luO6uFUTUN9lLmnniOCkdKVnML8uB0r76rD72Di4qL4NI0IUnydpZlw/WmGsY00bRvDzZvKhfFKLAU9VOG8v7BdijW+i8kLX649yyMz0+fwVQksur3NILyejzw5efCoaT3kmW1MN68AMq2bBIXXtd18WMZBt6r242DBzvgda3uWhQ4xNzOkeNdohZYXFjA4vwCfD4/Sio2i9bjdeSoGirKylFYXirykehiFHpUR2FJCbZu+x1yXlMrrQWSZWwygv6Ll3DxXBCX+66u6I7o2DRFRWtbM1o62xNdb7rI7lGSqqBs+zZMTj4XLYY/x49t7+zABx8eReWO7ciLL41ctZmqoqRiE/x+P6amp5FbkI9jx7tw+GgncqmbesPuPAEfduzcgenZOTg0FaWby/GXP/0RdZXvrOvlkyzjzyfR81UIoQs9IpRJniqglszt0tDc1oS9bc2o37lTLMhKI2bW35HtRXQRs3MLmH/xUrzzVb8HJQUFyHX/crJCQ+JwOALFqaKspGjNRWtkbg5zc7PQXC5szl/f6Ig6MFqiSavuqHCmumuFLIYBt+ZEY0sTGtua0VBTJQK/NKPzLfzSQEKWL4NiiG5a1gpZzPhMdnNrE/a3N2NPVaUI+jIAnacGNhiShdbx9pzrFgunSA4tqeUQRbuqoLW9BQ0tjSINzhBZBCzMBvPzVAS950KiG6KWJVkWGnXRELrjQBtqG/eioTqzZAELs3FQy3Iv/BR9wUtiUtGOr+tNhoptGt1V7atD4+4aEehlGizMBnH/WRj9wcuiG7LjI7Vllm8d3nnoAKoaakXq+0tzWumChUkxdlyWge4rYt0uzRMpSck01SzUDR3s7MC7e2pFRqSmOZx7GyxMCrESLcsldAd7oCgrEx6xrldRRM1SvbceHfV1K0K7TISFSREx28L41KRIcGmdruOVz82KBFd1oqWjBe/tb0ArLd3McFnAwqSOiclJ9JwP4fLFXtEtJXdDywluU2uTGDpTgZupNcur8GerU8R0eBJDV6+LRVbJLYdIcF2aSHD3tzaL9b20zjdbYGFShB0z4HY6V9QtFNLRXFATxf2U4FZXZkLcLwULkyJoaUXMNMV6HbyS4O6jicQMS3DXCguTInJKC9HU0YoPOg8k1uy0t7eivnmfSHB9WSgLwZOPKcKwLcT0GL69cxe3b46KoK6+ZS92V2zNyAR3jfBsdaox6LPSpiVyf/rEo/rq11JlFzxbnWoomEMW5CtrhWsYRgoWhpGChWGkYGEYKVgYRgoWhpGChWGkYGEYKVgYRgoWhpGChWGkYGEYKVgYRgoWhpGChWGkYGEYKVgYRgoWhpGChWGkYGEYKVgYRgoWhpGChWGkYGEYKVgYRgr6qGx6b4/BZBXUwnzCl4xZI5844g3MCQBn+Kwxb+EjAGcdST3SxwBO8RljXsOnAL4AgP8BXnVIgIvemwsAAAAASUVORK5CYII=",
// 移动边框背景颜色
moveTrackMaskBgColor: "#89d2ff",
// 移动边框颜色
moveTrackMaskBorderColor: "#0298f8",
// 文字提示
i18n: {
tips_success: "验证成功,耗时%s秒",
tips_error : "验证失败,请重新尝试!",
slider_title:"拖动滑块完成拼图",
concat_title: "拖动滑块完成拼图",
image_click_title: "请依次点击:",
rotate_title: "拖动滑块完成拼图",
// TITLE 大小
slider_title_size:"15px",
concat_title_size: "15px",
image_click_title_size: "20px",
rotate_title_size: "15px",
}
}
@@ -0,0 +1,59 @@
const TYPE = "DISABLE";
import "./disable.scss"
import {Dom} from "../common/common";
function getTemplate(styleConfig) {
return `
<div id="tianai-captcha" class="tianai-captcha-disable">
<div class="slider-tip">
<span id="tianai-captcha-slider-move-track-font" style="font-size: ${styleConfig.i18n.disable_title_size}">${styleConfig.i18n.disable_title}</span>
</div>
<div class="content">
<div class="bg-img-div">
<!-- <svg width="100" height="100" viewBox="0 0 100 100">-->
<!-- <polygon points="50,10 90,90 10,90" fill="none" stroke="#FF9900" stroke-width="4"/>-->
<!-- <path d="M50 35V65 M50 75V75" stroke="#FF9900" stroke-width="4" stroke-linecap="round"/>-->
<!-- </svg>-->
<span id="content-span"></span>
</div>
</div>
</div>
`;
}
class Disable {
constructor(boxEl, styleConfig) {
this.boxEl = boxEl;
this.styleConfig = styleConfig;
this.type = TYPE;
this.currentCaptchaData = {}
}
init(captchaData, endCallback, loadSuccessCallback) {
// 重载样式
this.destroy();
this.boxEl.append(getTemplate(this.styleConfig));
this.el = this.boxEl.find("#tianai-captcha");
// 绑定全局
// window.currentCaptcha = this;
// 载入验证码
this.loadCaptchaForData(this, captchaData);
this.endCallback = endCallback;
if (loadSuccessCallback) {
// 加载成功
loadSuccessCallback(this);
}
return this;
}
destroy () {
const existsCaptchaEl = this.boxEl.find("#tianai-captcha");
if (existsCaptchaEl) {
existsCaptchaEl.remove();
}
}
loadCaptchaForData (that, data) {
const msg = data.msg || data.message || "接口异常";
that.el.find("#content-span").text(msg);
}
}
export default Disable;
@@ -0,0 +1,56 @@
#tianai-captcha.tianai-captcha-disable{
z-index: 999;
position: absolute;
left: 0;
top: 0;
.content {
width: 100%;
height: 180px;
position: relative;
overflow: hidden;
.bg-img-div {
background-image: url("@/assets/images/dun.jpeg");
width: 100%;
height: 100%;
overflow: hidden;
#content-span {
color: #fff;
overflow: hidden;
margin-top: 132px;
display: block;
text-align: center;
}
}
}
}
//#tianai-captcha.tianai-captcha-disable {
// z-index: 999;
// position: absolute;
// left: 0;
// top: 0;
//
// .content {
// width: 100%;
// height: 180px;
// position: relative;
// overflow: hidden;
//
// .bg-img-div {
// display: flex;
// justify-content: center;
// flex-direction: column;
// align-items: center;
// background-color: #0A3850;
// width: 100%;
// height: 100%;
// overflow: hidden;
//
// #content-span {
// color: #fff;
// overflow: hidden;
// display: block;
// text-align: center;
// }
// }
// }
//}
@@ -0,0 +1,114 @@
import "./image_click.scss"
import {Dom,CommonCaptcha,move, initConfig, destroyEvent} from "../common/common.js"
/**
* 滑动验证码
*/
const TYPE = "IMAGE_CLICK"
function getTemplate(styleConfig) {
return `
<div id="tianai-captcha" class="tianai-captcha-slider tianai-captcha-word-click">
<div class="click-tip">
<span id="tianai-captcha-click-track-font" style="font-size: ${styleConfig.i18n.image_click_title_size}">${styleConfig.i18n.image_click_title}</span>
<img src="" id="tianai-captcha-tip-img" class="tip-img">
</div>
<div class="content">
<div class="bg-img-div">
<img id="tianai-captcha-slider-bg-img" src="" alt/>
<canvas id="tianai-captcha-slider-bg-canvas"></canvas>
<div id="bg-img-click-mask"></div>
</div>
<div class="tianai-captcha-tips" id="tianai-captcha-tips"></div>
</div>
<div class="click-confirm-btn">确定</div>
</div>
`;
}
class ImageClick extends CommonCaptcha{
constructor(boxEl, styleConfig) {
super();
this.boxEl = boxEl;
this.styleConfig = styleConfig;
this.type = TYPE;
this.currentCaptchaData = {}
}
init(captchaData, endCallback, loadSuccessCallback) {
// 重载样式
this.destroy();
this.boxEl.append(getTemplate(this.styleConfig));
this.el = this.boxEl.find("#tianai-captcha");
// 绑定全局
// window.currentCaptcha = this;
// 载入验证码
this.loadCaptchaForData(this, captchaData);
this.endCallback = endCallback;
const moveFun = move.bind(null, this);
// 绑定事件
this.el.find("#bg-img-click-mask").click((event) => {
if(event.target.className === "click-span") {
return;
}
this.currentCaptchaData.clickCount++;
const trackList = this.currentCaptchaData.trackList;
if (this.currentCaptchaData.clickCount === 1) {
this.currentCaptchaData.startTime = new Date();
// move 轨迹
window.addEventListener("mousemove", moveFun);
this.currentCaptchaData.startX = event.offsetX;
this.currentCaptchaData.startY = event.offsetY;
}
const startTime = this.currentCaptchaData.startTime;
trackList.push({
x: Math.round(event.offsetX),
y: Math.round(event.offsetY),
type: "click",
t: (new Date().getTime() - startTime.getTime())
});
const left = event.offsetX - 10;
const top = event.offsetY - 10;
this.el.find("#bg-img-click-mask").append("<span class='click-span' style='left:" + left + "px;top: " + top + "px'>" + this.currentCaptchaData.clickCount + "</span>")
// if (this.currentCaptchaData.clickCount === 4) {
// // 校验
// this.currentCaptchaData.stopTime = new Date();
// window.removeEventListener("mousemove", move);
// this.endCallback(this.currentCaptchaData,this);
// }
});
this.el.find(".click-confirm-btn").click(() => {
if (this.currentCaptchaData.clickCount > 0) {
// 校验
this.currentCaptchaData.stopTime = new Date();
window.removeEventListener("mousemove", moveFun);
this.endCallback(this.currentCaptchaData,this);
}
});
if (loadSuccessCallback) {
// 加载成功
loadSuccessCallback(this);
}
return this;
}
destroy () {
const existsCaptchaEl = this.boxEl.children("#tianai-captcha");
if (existsCaptchaEl) {
existsCaptchaEl.remove();
}
destroyEvent();
}
loadCaptchaForData (that, data) {
const bgImg = that.el.find("#tianai-captcha-slider-bg-img");
const tipImg = that.el.find("#tianai-captcha-tip-img");
bgImg.on("load",() => {
that.currentCaptchaData = initConfig(bgImg.width(), bgImg.height(), tipImg.width(), tipImg.height());
that.currentCaptchaData.currentCaptchaId = data.data.id;
})
bgImg.attr("src", data.data.backgroundImage);
tipImg.attr("src", data.data.templateImage);
}
}
export default ImageClick;
@@ -0,0 +1,64 @@
#tianai-captcha.tianai-captcha-word-click {
//position: relative;
box-sizing: border-box;
//padding-top: 10px;
.click-tip {
position: relative;
height: 40px;
width: 100%;
.tip-img {
height: 35px;
position: absolute;
right: 15px;
}
#tianai-captcha-click-track-font {
font-size: 18px;
display: inline-block;
height: 40px;
line-height: 40px;
position: absolute;
}
}
.slider-bottom {
position: relative;
top: 6px;
}
.content {
#bg-img-click-mask {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
.click-span {
position: absolute;
left: 0;
top: 0;
border-radius: 50px;
background-color: #409eff;
width: 20px;
height: 20px;
text-align: center;
line-height: 20px;
color: #fff;
border: 2px solid #fff;
box-sizing: content-box;
}
}
}
.click-confirm-btn {
width: 100%;
height: 35px;
border-radius: 4px;
background-image: linear-gradient(173deg, hsl(38.09deg 91% 57.89%) 0%, hsl(38.09deg 89.38% 71.74%) 100%);
font-size: 15px;
text-align: center;
box-sizing: border-box;
line-height: 35px;
color: #fff;
margin-top: 3px;
}
.click-confirm-btn:hover{
cursor: pointer
}
}
@@ -0,0 +1,106 @@
import "@/captcha/slider/slider.scss"
import "./rotate.scss"
import {Dom,CommonCaptcha, down, initConfig, destroyEvent} from "../common/common.js"
/**
* 滑动验证码
*/
const TYPE = "ROTATE"
function getTemplate(styleConfig) {
return `
<div id="tianai-captcha" class="tianai-captcha-slider tianai-captcha-rotate">
<div class="slider-tip">
<span id="tianai-captcha-slider-move-track-font" style="font-size: ${styleConfig.i18n.rotate_title_size}">${styleConfig.i18n.rotate_title}</span>
</div>
<div class="content">
<div class="bg-img-div">
<img id="tianai-captcha-slider-bg-img" src="" alt/>
<canvas id="tianai-captcha-slider-bg-canvas"></canvas>
</div>
<div class="rotate-img-div" id="tianai-captcha-slider-img-div">
<img id="tianai-captcha-slider-move-img" src="" alt/>
</div>
<div class="tianai-captcha-tips" id="tianai-captcha-tips"></div>
</div>
<div class="slider-move">
<div class="slider-move-track">
<div id="tianai-captcha-slider-move-track-mask"></div>
<div class="slider-move-shadow"></div>
</div>
<div class="slider-move-btn" id="tianai-captcha-slider-move-btn">
</div>
</div>
</div>
`;
}
class Rotate extends CommonCaptcha{
constructor(boxEl, styleConfig) {
super();
this.boxEl = boxEl;
this.styleConfig = styleConfig;
this.type = TYPE;
this.currentCaptchaData = {}
}
init(captchaData, endCallback, loadSuccessCallback) {
// 重载样式
this.destroy();
this.boxEl.append(getTemplate(this.styleConfig));
this.el = this.boxEl.find("#tianai-captcha");
this.loadStyle();
// 按钮绑定事件
this.el.find("#tianai-captcha-slider-move-btn").mousedown(down.bind(null,this));
this.el.find("#tianai-captcha-slider-move-btn").touchstart(down.bind(null,this));
// 绑定全局
// window.currentCaptcha = this;
// 载入验证码
this.loadCaptchaForData(this, captchaData);
this.endCallback = endCallback;
if (loadSuccessCallback) {
// 加载成功
loadSuccessCallback(this);
}
return this;
}
destroy () {
const existsCaptchaEl = this.boxEl.children("#tianai-captcha");
if (existsCaptchaEl) {
existsCaptchaEl.remove();
}
destroyEvent();
}
doMove() {
const moveX = this.currentCaptchaData.moveX;
this.el.find("#tianai-captcha-slider-move-btn").css("transform", "translate(" + moveX + "px, 0px)")
this.el.find("#tianai-captcha-slider-move-img").css("transform", "rotate(" + (moveX / (this.currentCaptchaData.end / 360)) + "deg)")
this.el.find("#tianai-captcha-slider-move-track-mask").css("width", moveX + "px")
}
loadStyle () {
let sliderImg = "";
let moveTrackMaskBorderColor = "#00f4ab";
let moveTrackMaskBgColor = "#a9ffe5";
const styleConfig = this.styleConfig;
if (styleConfig) {
sliderImg = styleConfig.btnUrl;
moveTrackMaskBgColor = styleConfig.moveTrackMaskBgColor;
moveTrackMaskBorderColor = styleConfig.moveTrackMaskBorderColor;
}
this.el.find(".slider-move .slider-move-btn").css("background-image", "url(" + sliderImg + ")");
// this.el.find("#tianai-captcha-slider-move-track-font").text(title);
this.el.find("#tianai-captcha-slider-move-track-mask").css("border-color", moveTrackMaskBorderColor);
this.el.find("#tianai-captcha-slider-move-track-mask").css("background-color", moveTrackMaskBgColor);
}
loadCaptchaForData (that, data) {
const bgImg = that.el.find("#tianai-captcha-slider-bg-img");
const sliderImg = that.el.find("#tianai-captcha-slider-move-img");
bgImg.attr("src", data.data.backgroundImage);
sliderImg.attr("src", data.data.templateImage);
bgImg.on("load",() => {
that.currentCaptchaData = initConfig(bgImg.width(), bgImg.height(), sliderImg.width(), sliderImg.height(), 300 - 63 + 5);
that.currentCaptchaData.currentCaptchaId = data.data.id;
});
}
}
export default Rotate;
@@ -0,0 +1,12 @@
#tianai-captcha.tianai-captcha-rotate {
.rotate-img-div {
height: 100%;
/*position: absolute;*/
text-align: center;
img {
height: 100%;
transform: rotate(0deg);
display: inline-block;
}
}
}
@@ -0,0 +1,123 @@
import "../common/common.scss"
import "./slider.scss"
import {
Dom,
CommonCaptcha,
closeTips,
down,
initConfig,
showTips,
destroyEvent
} from "../common/common.js"
/**
* 滑动验证码
*/
const TYPE = "SLIDER"
function getTemplate(styleConfig) {
return `
<div id="tianai-captcha" class="tianai-captcha-slider">
<div class="slider-tip">
<span id="tianai-captcha-slider-move-track-font" style="font-size: ${styleConfig.i18n.slider_title_size}">${styleConfig.i18n.slider_title}</span>
</div>
<div class="content">
<div class="bg-img-div">
<img id="tianai-captcha-slider-bg-img" src="" alt/>
<canvas id="tianai-captcha-slider-bg-canvas"></canvas>
<div id="tianai-captcha-slider-bg-div"></div>
</div>
<div class="slider-img-div" id="tianai-captcha-slider-img-div">
<img id="tianai-captcha-slider-move-img" src="" alt/>
</div>
<div class="tianai-captcha-tips" id="tianai-captcha-tips"></div>
</div>
<div class="slider-move">
<div class="slider-move-track">
<div id="tianai-captcha-slider-move-track-mask"></div>
<div class="slider-move-shadow"></div>
</div>
<div class="slider-move-btn" id="tianai-captcha-slider-move-btn">
</div>
</div>
</div>
`
}
class Slider extends CommonCaptcha{
constructor(boxEl, styleConfig) {
super();
this.boxEl = boxEl;
this.styleConfig = styleConfig;
this.type = TYPE;
this.currentCaptchaData = {}
}
init(captchaData, endCallback, loadSuccessCallback) {
// 重载样式
this.destroy();
this.boxEl.append(getTemplate(this.styleConfig));
this.el = this.boxEl.find("#tianai-captcha");
this.loadStyle();
// 按钮绑定事件
this.el.find("#tianai-captcha-slider-move-btn").mousedown(down.bind(null,this));
this.el.find("#tianai-captcha-slider-move-btn").touchstart( down.bind(null,this));
// 绑定全局
// window.currentCaptcha = this;
// 载入验证码
this.loadCaptchaForData(this, captchaData);
this.endCallback = endCallback;
if (loadSuccessCallback) {
// 加载成功
loadSuccessCallback(this);
}
return this;
}
showTips(msg, type,callback) {
showTips(this.el, msg,type, callback)
}
closeTips(callback) {
closeTips(this.el, callback)
}
destroy () {
const existsCaptchaEl = this.boxEl.children("#tianai-captcha");
if (existsCaptchaEl) {
existsCaptchaEl.remove();
}
destroyEvent();
}
doMove() {
const moveX = this.currentCaptchaData.moveX;
this.el.find("#tianai-captcha-slider-move-btn").css("transform", "translate(" + moveX + "px, 0px)")
this.el.find("#tianai-captcha-slider-img-div").css("transform", "translate(" + moveX + "px, 0px)")
this.el.find("#tianai-captcha-slider-move-track-mask").css("width", moveX + "px")
}
loadStyle () {
let sliderImg = "";
let moveTrackMaskBorderColor = "#00f4ab";
let moveTrackMaskBgColor = "#a9ffe5";
const styleConfig = this.styleConfig;
if (styleConfig) {
sliderImg = styleConfig.btnUrl;
moveTrackMaskBgColor = styleConfig.moveTrackMaskBgColor;
moveTrackMaskBorderColor = styleConfig.moveTrackMaskBorderColor;
}
this.el.find(".slider-move .slider-move-btn").css("background-image", "url(" + sliderImg + ")");
// this.el.find("#tianai-captcha-slider-move-track-font").text(title);
this.el.find("#tianai-captcha-slider-move-track-mask").css("border-color", moveTrackMaskBorderColor);
this.el.find("#tianai-captcha-slider-move-track-mask").css("background-color", moveTrackMaskBgColor);
}
loadCaptchaForData (that, data) {
const bgImg = that.el.find("#tianai-captcha-slider-bg-img");
const sliderImg = that.el.find("#tianai-captcha-slider-move-img");
bgImg.attr("src", data.data.backgroundImage);
sliderImg.attr("src", data.data.templateImage);
bgImg.on("load",() => {
that.currentCaptchaData = initConfig(bgImg.width(), bgImg.height(), sliderImg.width(), sliderImg.height(), 300 - 63 + 5);
that.currentCaptchaData.currentCaptchaId = data.data.id;
});
}
}
export default Slider;
@@ -0,0 +1,88 @@
#tianai-captcha.tianai-captcha-slider {
z-index: 999;
position: absolute;
left: 0;
top: 0;
.content {
width: 100%;
height: 180px;
position: relative;
overflow: hidden;
}
.bg-img-div {
width: 100%;
height: 100%;
position: absolute;
transform: translate(0px, 0px);
img {
height: 100%;
width: 100%;
border-radius: 5px;
}
}
.slider-img-div {
height: 100%;
position: absolute;
left: 0;
transform: translate(0px, 0px);
#tianai-captcha-slider-move-img {
height: 100%;
}
}
.slider-move {
height: 34px;
width: 100%;
margin: 11px 0;
position: relative;
}
.slider-move-track {
position: relative;
height: 32px;
line-height: 32px;
text-align: center;
background: #f5f5f5;
color: #999;
transition: 0s;
font-size: 14px;
box-sizing: content-box;
border: 1px solid #f5f5f5;
border-radius: 4px;
}
.refresh-btn, .close-btn {
display: inline-block;
}
.slider-move {
line-height: 38px;
font-size: 14px;
text-align: center;
white-space: nowrap;
color: #88949d;
-moz-user-select: none;
-webkit-user-select: none;
user-select: none;
filter: opacity(.8);
}
.slider-move .slider-move-btn {
transform: translate(0px, 0px);
position: absolute;
top: -6px;
left: 0;
width: 63px;
height: 45px;
background-color: #fff;
background-repeat: no-repeat;
background-size: contain;
border-radius: 5px;
}
.slider-tip {
margin-bottom: 5px;
font-weight: bold;
font-size: 15px;
line-height: normal;
color: black;
}
.slider-move-btn:hover {
cursor: pointer
}
user-select: none;
}
@@ -0,0 +1,13 @@
import ImageClick from "../image_click/image_click"
/**
* 滑动验证码
*/
const TYPE = "WORD_IMAGE_CLICK"
class WordImageClick extends ImageClick {
constructor(divId, styleConfig) {
super(divId, styleConfig);
this.type = TYPE;
}
}
export default WordImageClick;
+4
View File
@@ -0,0 +1,4 @@
import {CaptchaConfig, TianAiCaptcha} from "./captcha/captcha";
window.TAC = TianAiCaptcha;
window.CaptchaConfig = CaptchaConfig;
File diff suppressed because one or more lines are too long
@@ -0,0 +1,14 @@
const HtmlWebpackPlugin = require("html-webpack-plugin");
const path = require("path");
module.exports = {
output: {
filename: "tac/js/tac.js",
path: path.resolve(__dirname, "./dist")
},
plugins: [
new HtmlWebpackPlugin({
filename: 'index.html',
template: './public/index.html'
}),
]
}
+78
View File
@@ -0,0 +1,78 @@
const webpack = require('webpack')
const {merge} = require("webpack-merge")
const devConfig = require("./webpack.config.dev")
const prodConfig = require("./webpack.config.prod")
const path = require("path");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const {CleanWebpackPlugin} = require("clean-webpack-plugin");
const commonConfig = {
mode: 'development',
entry: "./src/index.js",
output: {
filename: "tac.js",
path: path.resolve(__dirname, "./dist")
},
resolve: {
alias: {
"@": path.join(__dirname, "./src") // 这样@符号就表示项目根目录中src这一层路径
}
},
module: {
rules: [
{
test: /\.(css)$/,
use: [MiniCssExtractPlugin.loader, 'css-loader'],
},
{
test: /\.s[ac]ss$/,
use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"],
},
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
use: {
loader: 'file-loader',
options: {
esModule: false,
name: '[name].[ext]',
outputPath: 'tac/images'
}
},
type: 'javascript/auto'
},
// {
// test: /\.js$/,
// exclude: /node_modules/,
// loader: 'babel-loader',
// options: {
// // 预设babel做怎样的兼容性处理
// presets: ['@babel/preset-env']
// }
// }
]
},
plugins: [
new MiniCssExtractPlugin({
// 指定抽离的之后形成的文件名
filename: 'tac/css/tac.css'
}),
new webpack.HotModuleReplacementPlugin(),
new CleanWebpackPlugin()
],
devServer: {
// 开发时可直接访问到 ./public 下的静态资源,这些资源在开发中不必打包
port: 3000,
static: "./dist"
}
}
module.exports = (env, argv) => {
if (argv && argv.mode === 'production') {
console.log("=============production==================")
return merge(commonConfig, prodConfig);
}else {
console.log("=============development==================")
return merge(commonConfig, devConfig);
}
}
@@ -0,0 +1,32 @@
const HtmlWebpackPlugin = require("html-webpack-plugin");
const path = require("path");
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
optimization: {
minimize: true,
minimizer: [new TerserPlugin({
terserOptions: {
compress: {
drop_console: true, // 移除所有的`console`语句
},
output: {
comments: false, // 去掉注释
},
},
extractComments: false, // 是否将注释提取到单独的文件中
})],
},
externals: {
},
output: {
filename: "tac/js/tac.min.js",
path: path.resolve(__dirname, "./dist")
},
plugins: [
new HtmlWebpackPlugin({
filename: 'index.html',
template: './public/index-prod.html'
})
]
}
+52
View File
@@ -0,0 +1,52 @@
<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/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cloud.tianai.captcha</groupId>
<artifactId>tianai-captcha-parent</artifactId>
<version>${revision}</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>tianai-captcha</artifactId>
<name>tianai-captcha</name>
<description>行为验证码</description>
<url>https://gitee.com/tianai/tianai-captcha</url>
<properties>
<java.version>1.8</java.version>
<!-- 打包跳过单元测试 -->
<skipTests>true</skipTests>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.42</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.30</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
<compilerArgument>-parameters</compilerArgument>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -1,6 +1,5 @@
package cloud.tianai.captcha.application;
import cloud.tianai.captcha.application.vo.CaptchaResponse;
import cloud.tianai.captcha.application.vo.ImageCaptchaVO;
import cloud.tianai.captcha.cache.CacheStore;
import cloud.tianai.captcha.common.AnyMap;
@@ -9,9 +8,11 @@ import cloud.tianai.captcha.common.exception.ImageCaptchaException;
import cloud.tianai.captcha.common.response.ApiResponse;
import cloud.tianai.captcha.common.response.ApiResponseStatusConstant;
import cloud.tianai.captcha.common.util.CollectionUtils;
import cloud.tianai.captcha.common.util.ObjectUtils;
import cloud.tianai.captcha.generator.ImageCaptchaGenerator;
import cloud.tianai.captcha.generator.common.model.dto.GenerateParam;
import cloud.tianai.captcha.generator.common.model.dto.ImageCaptchaInfo;
import cloud.tianai.captcha.generator.common.model.dto.ParamKeyEnum;
import cloud.tianai.captcha.generator.impl.CacheImageCaptchaGenerator;
import cloud.tianai.captcha.interceptor.CaptchaInterceptor;
import cloud.tianai.captcha.interceptor.EmptyCaptchaInterceptor;
@@ -34,15 +35,25 @@ import java.util.concurrent.TimeUnit;
@Slf4j
public class DefaultImageCaptchaApplication implements ImageCaptchaApplication {
private CaptchaInterceptor captchaInterceptor;
/** 图片验证码生成器. */
/**
* 图片验证码生成器.
*/
private ImageCaptchaGenerator captchaGenerator;
/** 图片验证码校验器. */
/**
* 图片验证码校验器.
*/
private ImageCaptchaValidator imageCaptchaValidator;
/** 缓冲存储. */
/**
* 缓冲存储.
*/
private CacheStore cacheStore;
/** 验证码配置属性. */
/**
* 验证码配置属性.
*/
private final ImageCaptchaProperties prop;
/** 默认的过期时间. */
/**
* 默认的过期时间.
*/
private long defaultExpire = 20000L;
public static final String ID_SPLIT = "_";
@@ -68,9 +79,11 @@ public class DefaultImageCaptchaApplication implements ImageCaptchaApplication {
}
captchaGenerator.setInterceptor(this.captchaInterceptor);
if (prop.isLocalCacheEnabled()) {
captchaGenerator = new CacheImageCaptchaGenerator(captchaGenerator,
CacheImageCaptchaGenerator cacheImageCaptchaGenerator = new CacheImageCaptchaGenerator(captchaGenerator,
prop.getLocalCacheSize(), prop.getLocalCacheWaitTime(),
prop.getLocalCachePeriod(), prop.getLocalCacheExpireTime());
cacheImageCaptchaGenerator.setIgnoredCacheFields(prop.getLocalCacheIgnoredCacheFields());
captchaGenerator = cacheImageCaptchaGenerator;
}
// 初始化生成器
captchaGenerator.init();
@@ -78,37 +91,41 @@ public class DefaultImageCaptchaApplication implements ImageCaptchaApplication {
}
@Override
public CaptchaResponse<ImageCaptchaVO> generateCaptcha() {
public ApiResponse<ImageCaptchaVO> generateCaptcha() {
// 生成滑块验证码
return generateCaptcha(CaptchaTypeConstant.SLIDER);
}
@Override
public CaptchaResponse<ImageCaptchaVO> generateCaptcha(String type) {
public ApiResponse<ImageCaptchaVO> generateCaptcha(String type) {
GenerateParam generateParam = new GenerateParam();
generateParam.setType(type);
return generateCaptcha(generateParam);
}
@Override
public CaptchaResponse<ImageCaptchaVO> generateCaptcha(GenerateParam param) {
CaptchaResponse<ImageCaptchaVO> captchaResponse = beforeGenerateCaptcha(param);
public ApiResponse<ImageCaptchaVO> generateCaptcha(GenerateParam param) {
ApiResponse<ImageCaptchaVO> captchaResponse = beforeGenerateCaptcha(param);
if (captchaResponse != null) {
return captchaResponse;
}
String id = generatorId(param);
ImageCaptchaInfo imageCaptchaInfo = getImageCaptchaGenerator().generateCaptchaImage(param);
captchaResponse = convertToCaptchaResponse(imageCaptchaInfo);
captchaResponse = convertToCaptchaResponse(id, imageCaptchaInfo);
afterGenerateCaptcha(imageCaptchaInfo, captchaResponse);
param.removeParam(ParamKeyEnum.ID);
return captchaResponse;
}
@Override
public CaptchaResponse<ImageCaptchaVO> generateCaptcha(CaptchaImageType captchaImageType) {
public ApiResponse<ImageCaptchaVO> generateCaptcha(CaptchaImageType captchaImageType) {
return generateCaptcha(CaptchaTypeConstant.SLIDER, captchaImageType);
}
@Override
public CaptchaResponse<ImageCaptchaVO> generateCaptcha(String type, CaptchaImageType captchaImageType) {
public ApiResponse<ImageCaptchaVO> generateCaptcha(String type, CaptchaImageType captchaImageType) {
GenerateParam param = new GenerateParam();
if (CaptchaImageType.WEBP.equals(captchaImageType)) {
param.setBackgroundFormatName("webp");
@@ -122,14 +139,14 @@ public class DefaultImageCaptchaApplication implements ImageCaptchaApplication {
}
public CaptchaResponse<ImageCaptchaVO> convertToCaptchaResponse(ImageCaptchaInfo imageCaptchaInfo) {
public ApiResponse<ImageCaptchaVO> convertToCaptchaResponse(String id, ImageCaptchaInfo imageCaptchaInfo) {
if (imageCaptchaInfo == null) {
// 要是生成失败
throw new ImageCaptchaException("生成验证码失败,验证码生成为空");
}
// 生成ID
String id = generatorId(imageCaptchaInfo);
CaptchaResponse<ImageCaptchaVO> response = beforeGenerateImageCaptchaValidData(imageCaptchaInfo);
ApiResponse<ImageCaptchaVO> response = beforeGenerateImageCaptchaValidData(imageCaptchaInfo);
if (response != null) {
return response;
}
@@ -151,7 +168,8 @@ public class DefaultImageCaptchaApplication implements ImageCaptchaApplication {
verificationVO.setTemplateImageWidth(imageCaptchaInfo.getTemplateImageWidth());
verificationVO.setTemplateImageHeight(imageCaptchaInfo.getTemplateImageHeight());
verificationVO.setData(imageCaptchaInfo.getData() == null ? null : imageCaptchaInfo.getData().getViewData());
return CaptchaResponse.of(id, verificationVO);
verificationVO.setId(id);
return ApiResponse.ofSuccess(verificationVO);
}
@@ -205,8 +223,14 @@ public class DefaultImageCaptchaApplication implements ImageCaptchaApplication {
return null;
}
protected String generatorId(ImageCaptchaInfo imageCaptchaInfo) {
return imageCaptchaInfo.getType() + ID_SPLIT + UUID.randomUUID().toString().replace("-", "");
protected String generatorId(GenerateParam param) {
String id = param.getParam(ParamKeyEnum.ID);
if (ObjectUtils.isEmpty(id)) {
id = param.getType() + ID_SPLIT + UUID.randomUUID().toString().replace("-", "");
param.addParam(ParamKeyEnum.ID, id);
}
return id;
}
/**
@@ -260,7 +284,8 @@ public class DefaultImageCaptchaApplication implements ImageCaptchaApplication {
@Override
public void setCaptchaInterceptor(CaptchaInterceptor captchaInterceptor) {
this.captchaGenerator = captchaGenerator;
this.captchaInterceptor = captchaInterceptor;
this.captchaGenerator.setInterceptor(captchaInterceptor);
}
@Override
@@ -285,15 +310,16 @@ public class DefaultImageCaptchaApplication implements ImageCaptchaApplication {
// ============== 一些模板方法 ================
private void afterGenerateCaptcha(ImageCaptchaInfo imageCaptchaInfo, CaptchaResponse<ImageCaptchaVO> captchaResponse) {
private void afterGenerateCaptcha(ImageCaptchaInfo imageCaptchaInfo, ApiResponse<ImageCaptchaVO> captchaResponse) {
captchaInterceptor.afterGenerateCaptcha(captchaInterceptor.createContext(), imageCaptchaInfo.getType(), imageCaptchaInfo, captchaResponse);
}
private CaptchaResponse<ImageCaptchaVO> beforeGenerateCaptcha(GenerateParam param) {
private ApiResponse<ImageCaptchaVO> beforeGenerateCaptcha(GenerateParam param) {
return captchaInterceptor.beforeGenerateCaptcha(captchaInterceptor.createContext(), param.getType(), param);
}
private CaptchaResponse<ImageCaptchaVO> beforeGenerateImageCaptchaValidData(ImageCaptchaInfo imageCaptchaInfo) {
private ApiResponse<ImageCaptchaVO> beforeGenerateImageCaptchaValidData(ImageCaptchaInfo imageCaptchaInfo) {
return captchaInterceptor.beforeGenerateImageCaptchaValidData(captchaInterceptor.createContext(), imageCaptchaInfo.getType(), imageCaptchaInfo);
}
@@ -309,4 +335,24 @@ public class DefaultImageCaptchaApplication implements ImageCaptchaApplication {
return captchaInterceptor.afterValid(captchaInterceptor.createContext(), getCaptchaTypeById(id), matchParam, validData, basicValid);
}
/**
* 销毁应用程序释放资源
* 建议在不再使用或应用关闭时调用
*/
@Override
public void close() {
// 如果生成器是 CacheImageCaptchaGenerator需要关闭其定时任务
if (captchaGenerator instanceof CacheImageCaptchaGenerator) {
((CacheImageCaptchaGenerator) captchaGenerator).destroy();
}
// 如果缓存存储是 AutoCloseable关闭它
if (cacheStore instanceof AutoCloseable) {
try {
((AutoCloseable) cacheStore).close();
} catch (Exception e) {
log.warn("关闭 CacheStore 时出错", e);
}
}
}
}
@@ -1,6 +1,5 @@
package cloud.tianai.captcha.application;
import cloud.tianai.captcha.application.vo.CaptchaResponse;
import cloud.tianai.captcha.application.vo.ImageCaptchaVO;
import cloud.tianai.captcha.cache.CacheStore;
import cloud.tianai.captcha.common.response.ApiResponse;
@@ -27,27 +26,27 @@ public class FilterImageCaptchaApplication implements ImageCaptchaApplication {
}
@Override
public CaptchaResponse<ImageCaptchaVO> generateCaptcha() {
public ApiResponse<ImageCaptchaVO> generateCaptcha() {
return target.generateCaptcha();
}
@Override
public CaptchaResponse<ImageCaptchaVO> generateCaptcha(String type) {
public ApiResponse<ImageCaptchaVO> generateCaptcha(String type) {
return target.generateCaptcha(type);
}
@Override
public CaptchaResponse<ImageCaptchaVO> generateCaptcha(CaptchaImageType captchaImageType) {
public ApiResponse<ImageCaptchaVO> generateCaptcha(CaptchaImageType captchaImageType) {
return target.generateCaptcha(captchaImageType);
}
@Override
public CaptchaResponse<ImageCaptchaVO> generateCaptcha(String type, CaptchaImageType captchaImageType) {
public ApiResponse<ImageCaptchaVO> generateCaptcha(String type, CaptchaImageType captchaImageType) {
return target.generateCaptcha(type, captchaImageType);
}
@Override
public CaptchaResponse<ImageCaptchaVO> generateCaptcha(GenerateParam param) {
public ApiResponse<ImageCaptchaVO> generateCaptcha(GenerateParam param) {
return target.generateCaptcha(param);
}
@@ -115,4 +114,9 @@ public class FilterImageCaptchaApplication implements ImageCaptchaApplication {
public CacheStore getCacheStore() {
return target.getCacheStore();
}
@Override
public void close() throws Exception {
target.close();
}
}
@@ -1,7 +1,6 @@
package cloud.tianai.captcha.application;
import cloud.tianai.captcha.application.vo.CaptchaResponse;
import cloud.tianai.captcha.application.vo.ImageCaptchaVO;
import cloud.tianai.captcha.cache.CacheStore;
import cloud.tianai.captcha.common.response.ApiResponse;
@@ -18,14 +17,14 @@ import cloud.tianai.captcha.validator.common.model.dto.MatchParam;
* @Date 2020/5/29 8:33
* @Description 滑块验证码应用程序
*/
public interface ImageCaptchaApplication {
public interface ImageCaptchaApplication extends AutoCloseable{
/**
* 生成滑块验证码
*
* @return
*/
CaptchaResponse<ImageCaptchaVO> generateCaptcha();
ApiResponse<ImageCaptchaVO> generateCaptcha();
/**
* 生成滑块验证码
@@ -33,7 +32,7 @@ public interface ImageCaptchaApplication {
* @param type type类型
* @return CaptchaResponse<SliderCaptchaVO>
*/
CaptchaResponse<ImageCaptchaVO> generateCaptcha(String type);
ApiResponse<ImageCaptchaVO> generateCaptcha(String type);
/**
* 生成滑块验证码
@@ -41,7 +40,7 @@ public interface ImageCaptchaApplication {
* @param captchaImageType 要生成webp还是jpg类型的图片
* @return CaptchaResponse<SliderCaptchaVO>
*/
CaptchaResponse<ImageCaptchaVO> generateCaptcha(CaptchaImageType captchaImageType);
ApiResponse<ImageCaptchaVO> generateCaptcha(CaptchaImageType captchaImageType);
/**
* 生成验证码
@@ -50,7 +49,7 @@ public interface ImageCaptchaApplication {
* @param captchaImageType CaptchaImageType
* @return CaptchaResponse<ImageCaptchaVO>
*/
CaptchaResponse<ImageCaptchaVO> generateCaptcha(String type, CaptchaImageType captchaImageType);
ApiResponse<ImageCaptchaVO> generateCaptcha(String type, CaptchaImageType captchaImageType);
/**
@@ -59,7 +58,7 @@ public interface ImageCaptchaApplication {
* @param param param
* @return CaptchaResponse<SliderCaptchaVO>
*/
CaptchaResponse<ImageCaptchaVO> generateCaptcha(GenerateParam param);
ApiResponse<ImageCaptchaVO> generateCaptcha(GenerateParam param);
/**
* 匹配
@@ -4,6 +4,7 @@ import lombok.Data;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* @Author: 天爱有情
@@ -23,4 +24,5 @@ public class ImageCaptchaProperties {
private int localCacheWaitTime = 1000;
private int localCachePeriod = 5000;
private Long localCacheExpireTime;
private Set<String> localCacheIgnoredCacheFields;
}
@@ -1,29 +1,23 @@
package cloud.tianai.captcha.application;
import cloud.tianai.captcha.cache.CacheStore;
import cloud.tianai.captcha.cache.StoreCacheKeyPrefix;
import cloud.tianai.captcha.cache.impl.LocalCacheStore;
import cloud.tianai.captcha.common.util.CollectionUtils;
import cloud.tianai.captcha.generator.ImageCaptchaGenerator;
import cloud.tianai.captcha.generator.ImageTransform;
import cloud.tianai.captcha.generator.common.FontWrapper;
import cloud.tianai.captcha.generator.impl.MultiImageCaptchaGenerator;
import cloud.tianai.captcha.interceptor.CaptchaInterceptor;
import cloud.tianai.captcha.interceptor.EmptyCaptchaInterceptor;
import cloud.tianai.captcha.resource.DefaultBuiltInResources;
import cloud.tianai.captcha.resource.ResourceStore;
import cloud.tianai.captcha.resource.*;
import cloud.tianai.captcha.resource.common.model.dto.Resource;
import cloud.tianai.captcha.resource.common.model.dto.ResourceMap;
import cloud.tianai.captcha.resource.impl.DefaultImageCaptchaResourceManager;
import cloud.tianai.captcha.resource.impl.LocalMemoryResourceStore;
import cloud.tianai.captcha.resource.impl.provider.ClassPathResourceProvider;
import cloud.tianai.captcha.validator.ImageCaptchaValidator;
import cloud.tianai.captcha.validator.impl.SimpleImageCaptchaValidator;
import java.awt.*;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.*;
/**
* @Author: 天爱有情
@@ -39,23 +33,26 @@ public class TACBuilder {
private ImageCaptchaProperties prop = new ImageCaptchaProperties();
private ResourceStore resourceStore;
private ImageTransform imageTransform;
private List<FontWrapper> fontWrappers = new ArrayList<>();
// private List<FontWrapper> fontWrappers = new ArrayList<>();
private Map<String, List<Resource>> resourceCache = new HashMap<>(8);
private Map<String, List<ResourceMap>> templateCache = new HashMap<>(8);
private String defaultTemplatePrefix = null;
public static TACBuilder builder() {
TACBuilder builder = new TACBuilder();
// 默认设置本地的
LocalMemoryResourceStore resourceStore = new LocalMemoryResourceStore();
builder.resourceStore = resourceStore;
builder.prop = new ImageCaptchaProperties();
return builder;
return new TACBuilder();
}
private TACBuilder() {
}
public TACBuilder setResourceStore(ResourceStore resourceStore) {
this.resourceStore = resourceStore;
return this;
}
public TACBuilder addDefaultTemplate(String defaultPathPrefix) {
DefaultBuiltInResources defaultBuiltInResources = new DefaultBuiltInResources(defaultPathPrefix);
defaultBuiltInResources.addDefaultTemplate(resourceStore);
this.defaultTemplatePrefix = defaultPathPrefix;
return this;
}
@@ -83,22 +80,24 @@ public class TACBuilder {
return this;
}
public TACBuilder addFont(FontWrapper fontWrapper) {
this.fontWrappers.add(fontWrapper);
public TACBuilder addFont(Resource resource) {
this.addResource(FontCache.FONT_TYPE, resource);
return this;
}
public TACBuilder addFont(Font font) {
return addFont(new FontWrapper(font));
}
public TACBuilder cached(int size, int waitTime, int period, Long expireTime) {
cached(size, waitTime, period, expireTime, null);
return this;
}
public TACBuilder cached(int size, int waitTime, int period, Long expireTime, Set<String> ignoredCacheFields) {
prop.setLocalCacheEnabled(true);
prop.setLocalCacheSize(size);
prop.setLocalCacheWaitTime(waitTime);
prop.setLocalCachePeriod(period);
prop.setLocalCacheExpireTime(expireTime);
prop.setLocalCacheIgnoredCacheFields(ignoredCacheFields);
return this;
}
@@ -117,19 +116,25 @@ public class TACBuilder {
return this;
}
public TACBuilder setResourceStore(ResourceStore resourceStore) {
this.resourceStore = resourceStore;
return this;
}
// public TACBuilder setResourceStore(ResourceStore resourceStore) {
// this.resourceStore = resourceStore;
// return this;
// }
public TACBuilder addResource(String captchaType, Resource imageResource) {
this.resourceStore.addResource(captchaType, imageResource);
// if (resourceStore instanceof CrudResourceStore) {
// ((CrudResourceStore) resourceStore).addResource(captchaType, imageResource);
// }
cacheResource(captchaType, imageResource);
return this;
}
public TACBuilder addTemplate(String captchaType, ResourceMap resourceMap) {
this.resourceStore.addTemplate(captchaType, resourceMap);
// if (resourceStore instanceof CrudResourceStore) {
// ((CrudResourceStore) resourceStore).addTemplate(captchaType, resourceMap);
// }
cacheTemplate(captchaType, resourceMap);
return this;
}
@@ -142,33 +147,53 @@ public class TACBuilder {
if (cacheStore == null) {
cacheStore = new LocalCacheStore();
}
if (resourceStore == null) {
resourceStore = new LocalMemoryResourceStore();
}
if (resourceStore instanceof CrudResourceStore) {
CrudResourceStore crudResourceStore = (CrudResourceStore) resourceStore;
if (!CollectionUtils.isEmpty(resourceCache)) {
resourceCache.forEach((type,resources) -> {
resources.forEach(resource -> crudResourceStore.addResource(type,resource));
});
resourceCache.clear();
}
if (!CollectionUtils.isEmpty(templateCache)) {
templateCache.forEach((type, templates) -> {
templates.forEach(template -> crudResourceStore.addTemplate(type, template));
});
templateCache.clear();
}
}
// 添加默认模板
if (defaultTemplatePrefix != null) {
DefaultBuiltInResources defaultBuiltInResources = new DefaultBuiltInResources(defaultTemplatePrefix);
defaultBuiltInResources.addDefaultTemplate(resourceStore);
}
if (generator == null) {
DefaultImageCaptchaResourceManager resourceManager = new DefaultImageCaptchaResourceManager(resourceStore);
ResourceProviders resourceProviders = new ResourceProviders();
DefaultImageCaptchaResourceManager resourceManager = new DefaultImageCaptchaResourceManager(resourceStore, resourceProviders);
generator = new MultiImageCaptchaGenerator(resourceManager, imageTransform);
}
if (generator instanceof MultiImageCaptchaGenerator) {
if (CollectionUtils.isEmpty(fontWrappers)) {
// 添加默认字体
try {
ClassPathResourceProvider resourceProvider = new ClassPathResourceProvider();
InputStream stream = resourceProvider.getResourceInputStream(new Resource("classpath", "META-INF/fonts/SIMSUN.TTC"));
Font font = Font.createFont(Font.TRUETYPE_FONT, stream);
stream.close();
fontWrappers.add(new FontWrapper(font));
} catch (Exception e) {
throw new RuntimeException("读取默认字体包报错",e);
}
}
((MultiImageCaptchaGenerator) generator).setFontWrappers(fontWrappers);
}
// if (generator instanceof MultiImageCaptchaGenerator) {
// ((MultiImageCaptchaGenerator) generator).setFontWrappers(fontWrappers);
// }
if (validator == null) {
validator = new SimpleImageCaptchaValidator();
}
if (interceptor == null) {
interceptor = EmptyCaptchaInterceptor.INSTANCE;
}
// 增加前缀处理接口
DefaultImageCaptchaApplication application = new DefaultImageCaptchaApplication(generator, validator, cacheStore, prop, interceptor);
return application;
}
private void cacheResource(String captchaType, Resource imageResource) {
resourceCache.computeIfAbsent(captchaType, k -> new ArrayList<>(4)).add(imageResource);
}
private void cacheTemplate(String captchaType, ResourceMap resourceMap) {
templateCache.computeIfAbsent(captchaType, k -> new ArrayList<>(4)).add(resourceMap);
}
}
@@ -10,6 +10,9 @@ import java.io.Serializable;
@NoArgsConstructor
@AllArgsConstructor
public class ImageCaptchaVO implements Serializable {
/** ID.*/
private String id;
/** 验证码类型.*/
private String type;
/** 背景图.*/
@@ -9,7 +9,7 @@ import java.util.concurrent.TimeUnit;
* @date 2022/3/2 14:35
* @Description 提取出用于缓存的接口
*/
public interface CacheStore {
public interface CacheStore extends AutoCloseable {
/**
* 读取缓存数据通过key
@@ -0,0 +1,26 @@
package cloud.tianai.captcha.cache;
/**
* 验证码缓存Key前缀处理
*
* @author Alay
* @since 2025-11-11 13:44
*/
public interface StoreCacheKeyPrefix {
/**
* 缓存Key 计算处理
*
* @param captchaId 原始验证码Id
* @return 处理后的验证码缓存Key
*/
String compute(String captchaId);
static StoreCacheKeyPrefix prefixed(String prefix) {
if (prefix == null || prefix.isEmpty()) {
throw new IllegalArgumentException("prefix must not be null or empty");
}
return captchaId -> prefix.concat(":").concat(captchaId);
}
}
@@ -12,11 +12,10 @@ import java.util.stream.Collectors;
/**
* @Author: 天爱有情
* @date 2020/10/12 10:02
* @Description 给予本人以前写的 expiring-map(redis淘汰策略的java实现) 项目进行改造
*/
@Slf4j
@Accessors(chain = true)
public class ConCurrentExpiringMap<K, V> implements ExpiringMap<K, V> {
public class ConCurrentExpiringMap<K, V> implements ExpiringMap<K, V>, AutoCloseable {
private ConcurrentHashMap<K, TimeMapEntity<K, V>> storage;
private SortedMap<Long, LinkedList<K>> sortedMap = new ConcurrentSkipListMap<>();
@@ -198,6 +197,37 @@ public class ConCurrentExpiringMap<K, V> implements ExpiringMap<K, V> {
throw new IllegalArgumentException("timemap not impl entrySet.");
}
/**
* 销毁方法关闭定时任务线程池
* 应该在不再使用此 Map 时调用以防止线程泄露
*
* @since 0.0.3
*/
public void destroy() {
try {
scheduledExecutor.shutdown();
if (!scheduledExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
scheduledExecutor.shutdownNow();
if (!scheduledExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
log.warn("ConCurrentExpiringMap 定时任务线程池未能正常关闭");
}
}
} catch (InterruptedException e) {
scheduledExecutor.shutdownNow();
Thread.currentThread().interrupt();
log.warn("ConCurrentExpiringMap 定时任务线程池关闭时被中断", e);
}
}
/**
* 实现 AutoCloseable 接口支持 try-with-resources 语法
* 调用 destroy() 方法释放资源
*/
@Override
public void close() {
destroy();
}
/**
* 定时执行任务
*
@@ -63,4 +63,15 @@ public class LocalCacheStore implements CacheStore {
}
return null;
}
/**
* 关闭缓存存储释放资源
* 建议在不再使用时调用或在 Spring Bean 销毁时自动调用
*/
@Override
public void close() {
if (cache instanceof ConCurrentExpiringMap) {
((ConCurrentExpiringMap<?, ?>) cache).destroy();
}
}
}
@@ -0,0 +1,380 @@
package cloud.tianai.captcha.common;
import lombok.EqualsAndHashCode;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Function;
/**
* @Author: 天爱有情
* @Description 通用Map包装类,提供类型安全的get/set方法
*/
@EqualsAndHashCode
public class AnyMap implements Map<String, Object> {
private final Map<String, Object> target;
public AnyMap() {
target = new LinkedHashMap<>();
}
/**
* 构造函数 - 防御性拷贝
* @param map 源Map(会被复制,不会共享引用)
*/
public AnyMap(Map<String, Object> map) {
this.target = map != null ? new LinkedHashMap<>(map) : new LinkedHashMap<>();
}
// ================== 类型转换方法 =======================
/**
* 获取Float值
*/
public Float getFloat(String key) {
return getFloat(key, null);
}
/**
* 获取Float值,支持默认值
*/
public Float getFloat(String key, Float defaultValue) {
return convertToNumber(key, defaultValue, Number::floatValue, Float::parseFloat);
}
/**
* 获取Integer值
*/
public Integer getInt(String key) {
return getInt(key, null);
}
/**
* 获取Integer值,支持默认值
*/
public Integer getInt(String key, Integer defaultValue) {
return convertToNumber(key, defaultValue, Number::intValue, Integer::parseInt);
}
/**
* 获取Long值
*/
public Long getLong(String key) {
return getLong(key, null);
}
/**
* 获取Long值,支持默认值
*/
public Long getLong(String key, Long defaultValue) {
return convertToNumber(key, defaultValue, Number::longValue, Long::parseLong);
}
/**
* 获取Double值
*/
public Double getDouble(String key) {
return getDouble(key, null);
}
/**
* 获取Double值,支持默认值
*/
public Double getDouble(String key, Double defaultValue) {
return convertToNumber(key, defaultValue, Number::doubleValue, Double::parseDouble);
}
/**
* 获取Boolean值
*/
public Boolean getBoolean(String key) {
return getBoolean(key, null);
}
/**
* 获取Boolean值,支持默认值
*/
public Boolean getBoolean(String key, Boolean defaultValue) {
Object data = get(key);
if (data == null) {
return defaultValue;
}
if (data instanceof Boolean) {
return (Boolean) data;
}
if (data instanceof String) {
return Boolean.parseBoolean((String) data);
}
if (data instanceof Number) {
return ((Number) data).intValue() != 0;
}
return defaultValue;
}
/**
* 获取String值
*/
public String getString(String key) {
return getString(key, null);
}
/**
* 获取String值,支持默认值
*/
public String getString(String key, String defaultValue) {
Object data = get(key);
if (data == null) {
return defaultValue;
}
if (data instanceof String) {
return (String) data;
}
return String.valueOf(data);
}
/**
* 通用数字类型转换方法(减少重复代码)
*/
private <T extends Number> T convertToNumber(
String key,
T defaultValue,
Function<Number, T> numberConverter,
Function<String, T> stringParser) {
Object data = get(key);
if (data == null) {
return defaultValue;
}
if (data instanceof Number) {
return numberConverter.apply((Number) data);
}
if (data instanceof String) {
try {
return stringParser.apply((String) data);
} catch (NumberFormatException e) {
return defaultValue;
}
}
return defaultValue;
}
// ================== ParamKey 相关方法 =======================
/**
* 添加参数(使用ParamKey
*/
public <T> void addParam(ParamKey<T> paramKey, T value) {
put(paramKey.getKey(), value);
}
/**
* 获取参数(使用ParamKey
*/
public <T> T getParam(ParamKey<T> paramKey) {
return getParam(paramKey, null);
}
/**
* 获取参数(使用ParamKey),支持默认值
*/
@SuppressWarnings("unchecked")
public <T> T getParam(ParamKey<T> paramKey, T defaultValue) {
Object value = get(paramKey.getKey());
return value != null ? (T) value : defaultValue;
}
/**
* 移除参数(使用ParamKey
*/
public <T> Object removeParam(ParamKey<T> paramKey) {
return remove(paramKey.getKey());
}
/**
* 获取参数或默认值(使用ParamKey)
*/
@SuppressWarnings("unchecked")
public <T> T getOrDefault(ParamKey<T> paramKey, T defaultValue) {
return (T) getOrDefault(paramKey.getKey(), defaultValue);
}
// ================== 便捷方法 =======================
/**
* 添加参数(String key
* 注意:这个方法等价于put(),保留是为了向后兼容
*/
public void addParam(String key, Object value) {
put(key, value);
}
/**
* 获取参数(String key
* 注意:这个方法等价于get(),保留是为了向后兼容
*/
public Object getParam(String key) {
return get(key);
}
/**
* 移除参数(String key
* 注意:这个方法等价于remove(),保留是为了向后兼容
*/
public Object removeParam(String key) {
return remove(key);
}
/**
* 链式调用 - 设置值并返回this
* @param key 键
* @param value 值
* @return this,支持链式调用
*/
public AnyMap set(String key, Object value) {
put(key, value);
return this;
}
/**
* 链式调用 - 使用ParamKey设置值
*/
public <T> AnyMap set(ParamKey<T> paramKey, T value) {
put(paramKey.getKey(), value);
return this;
}
/**
* 静态工厂方法
*/
public static AnyMap of(Map<String, Object> map) {
return new AnyMap(map);
}
/**
* 静态工厂方法 - 创建空Map
*/
public static AnyMap create() {
return new AnyMap();
}
// ================== implement Map =======================
@Override
public int size() {
return target.size();
}
@Override
public boolean isEmpty() {
return target.isEmpty();
}
@Override
public boolean containsKey(Object key) {
return target.containsKey(key);
}
@Override
public boolean containsValue(Object value) {
return target.containsValue(value);
}
@Override
public Object get(Object key) {
return target.get(key);
}
@Override
public Object put(String key, Object value) {
return target.put(key, value);
}
@Override
public Object remove(Object key) {
return target.remove(key);
}
@Override
public void putAll(Map<? extends String, ?> m) {
target.putAll(m);
}
@Override
public void clear() {
target.clear();
}
@Override
public Set<String> keySet() {
return target.keySet();
}
@Override
public Collection<Object> values() {
return target.values();
}
@Override
public Set<Entry<String, Object>> entrySet() {
return target.entrySet();
}
@Override
public Object getOrDefault(Object key, Object defaultValue) {
return target.getOrDefault(key, defaultValue);
}
@Override
public void forEach(BiConsumer<? super String, ? super Object> action) {
target.forEach(action);
}
@Override
public void replaceAll(BiFunction<? super String, ? super Object, ?> function) {
target.replaceAll(function);
}
@Override
public Object putIfAbsent(String key, Object value) {
return target.putIfAbsent(key, value);
}
@Override
public boolean remove(Object key, Object value) {
return target.remove(key, value);
}
@Override
public boolean replace(String key, Object oldValue, Object newValue) {
return target.replace(key, oldValue, newValue);
}
@Override
public Object replace(String key, Object value) {
return target.replace(key, value);
}
@Override
public Object computeIfAbsent(String key, Function<? super String, ?> mappingFunction) {
return target.computeIfAbsent(key, mappingFunction);
}
@Override
public Object computeIfPresent(String key, BiFunction<? super String, ? super Object, ?> remappingFunction) {
return target.computeIfPresent(key, remappingFunction);
}
@Override
public Object compute(String key, BiFunction<? super String, ? super Object, ?> remappingFunction) {
return target.compute(key, remappingFunction);
}
@Override
public Object merge(String key, Object value, BiFunction<? super Object, ? super Object, ?> remappingFunction) {
return target.merge(key, value, remappingFunction);
}
}
@@ -0,0 +1,12 @@
package cloud.tianai.captcha.common;
/**
* @Author: 天爱有情
* @date 2024/11/20 11:34
* @Description 此接口的作用是在给 {@link AnyMap} 添加/获取参数时做一个类型限制和转换
*/
public interface ParamKey<T> {
String getKey();
}
@@ -3,13 +3,18 @@ package cloud.tianai.captcha.common.constant;
public interface CommonConstant {
String DEFAULT_TAG = "default";
/** 图标点选资源存储类型. */
String IMAGE_CLICK_ICON = "ICON";
String IMAGE_ICON = "ICON";
/** 蜂窝点选.*/
String HONEYCOMB_CLICK_ICON = "HONEYCOMB_ICON";
/** 刮刮卡图标. */
String SCRAPE_ICON = "SCRAPE_ICON";
// String IMAGE_CLICK_ICON = "IMAGE_CLICK_ICON";
String IMAGE_TIP_ICON = "IMAGE_TIP_ICON";
String IMAGE_CLICK_ICON = "IMAGE_CLICK_ICON";
/**
* 默认的resource资源文件路径.
@@ -7,7 +7,7 @@ import java.io.Serializable;
/**
* @Author: 天爱有情
* @date 2023/4/20 9:53
* @Description 可能是最好用的API统一返回格式类
* @Description API统一返回格式类
*/
@Data
@SuppressWarnings({"unchecked", "rawtypes"})
@@ -1,5 +1,6 @@
package cloud.tianai.captcha.common.util;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
@@ -10,15 +11,22 @@ import java.util.Set;
*/
public class CaptchaTypeClassifier {
/**
* 每个类型集合的最大容量防止内存泄漏
*/
private static final int MAX_TYPE_SIZE = 100;
private static final Set<String> SLIDER_CAPTCHA_TYPES = new HashSet<>();
private static final Set<String> CLICK_CAPTCHA_TYPES = new HashSet<>();
private static final Set<String> JIGSAW_CAPTCHA_TYPES = new HashSet<>();
public static void addSliderCaptchaType(String type) {
checkCapacity(SLIDER_CAPTCHA_TYPES, "SLIDER_CAPTCHA_TYPES");
SLIDER_CAPTCHA_TYPES.add(type.toUpperCase());
}
public static void addClickCaptchaType(String type) {
checkCapacity(CLICK_CAPTCHA_TYPES, "CLICK_CAPTCHA_TYPES");
CLICK_CAPTCHA_TYPES.add(type.toUpperCase());
}
@@ -31,11 +39,11 @@ public class CaptchaTypeClassifier {
}
public static Set<String> getSliderCaptchaTypes() {
return SLIDER_CAPTCHA_TYPES;
return Collections.unmodifiableSet(SLIDER_CAPTCHA_TYPES);
}
public static Set<String> getClickCaptchaTypes() {
return CLICK_CAPTCHA_TYPES;
return Collections.unmodifiableSet(CLICK_CAPTCHA_TYPES);
}
public static void removeSliderCaptchaType(String type) {
@@ -51,6 +59,7 @@ public class CaptchaTypeClassifier {
}
public static void addJigsawCaptchaType(String type) {
checkCapacity(JIGSAW_CAPTCHA_TYPES, "JIGSAW_CAPTCHA_TYPES");
JIGSAW_CAPTCHA_TYPES.add(type.toUpperCase());
}
@@ -59,6 +68,19 @@ public class CaptchaTypeClassifier {
}
public static Set<String> getJigsawCaptchaTypes() {
return JIGSAW_CAPTCHA_TYPES;
return Collections.unmodifiableSet(JIGSAW_CAPTCHA_TYPES);
}
/**
* 检查集合容量防止内存泄漏
* @param set 要检查的集合
* @param name 集合名称用于日志
*/
private static void checkCapacity(Set<String> set, String name) {
if (set.size() >= MAX_TYPE_SIZE) {
throw new IllegalStateException(
String.format("验证码类型集合 %s 已达到最大容量 %d,无法继续添加", name, MAX_TYPE_SIZE)
);
}
}
}
@@ -0,0 +1,9 @@
package cloud.tianai.captcha.common.util;
public class UUIDUtils {
public static String getUUID() {
return java.util.UUID.randomUUID().toString().replace("-", "");
}
}
@@ -2,10 +2,7 @@ package cloud.tianai.captcha.generator;
import cloud.tianai.captcha.common.exception.ImageCaptchaException;
import cloud.tianai.captcha.common.util.CollectionUtils;
import cloud.tianai.captcha.generator.common.model.dto.CaptchaExchange;
import cloud.tianai.captcha.generator.common.model.dto.CustomData;
import cloud.tianai.captcha.generator.common.model.dto.GenerateParam;
import cloud.tianai.captcha.generator.common.model.dto.ImageCaptchaInfo;
import cloud.tianai.captcha.generator.common.model.dto.*;
import cloud.tianai.captcha.generator.common.util.CaptchaImageUtils;
import cloud.tianai.captcha.generator.impl.transform.Base64ImageTransform;
import cloud.tianai.captcha.interceptor.CaptchaInterceptor;
@@ -136,6 +133,11 @@ public abstract class AbstractImageCaptchaGenerator implements ImageCaptchaGener
public ImageCaptchaInfo wrapImageCaptchaInfo(CaptchaExchange captchaExchange) {
ImageCaptchaInfo imageCaptchaInfo = doWrapImageCaptchaInfo(captchaExchange);
imageCaptchaInfo.setData(captchaExchange.getCustomData());
// 设置自定义容错值
Number tolerant = captchaExchange.getParam().getParam(ParamKeyEnum.TOLERANT);
if (tolerant != null) {
imageCaptchaInfo.setTolerant(tolerant.floatValue());
}
return imageCaptchaInfo;
}
@@ -166,17 +168,21 @@ public abstract class AbstractImageCaptchaGenerator implements ImageCaptchaGener
protected BufferedImage getTemplateImage(ResourceMap templateImages, String imageName) {
InputStream stream = getTemplateFile(templateImages, imageName);
BufferedImage bufferedImage = CaptchaImageUtils.wrapFile2BufferedImage(stream);
try {
return CaptchaImageUtils.wrapFile2BufferedImage(stream);
} finally {
closeStream(stream);
return bufferedImage;
}
}
protected BufferedImage getResourceImage(Resource resource) {
InputStream stream = getResourceInputStream(resource, null);
BufferedImage bufferedImage = CaptchaImageUtils.wrapFile2BufferedImage(stream);
try {
return CaptchaImageUtils.wrapFile2BufferedImage(stream);
} finally {
closeStream(stream);
return bufferedImage;
}
}
protected int randomInt(int origin, int bound) {
@@ -0,0 +1,38 @@
package cloud.tianai.captcha.generator.common;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.awt.*;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class FontWrapper {
// private Font font;
// private Float currentFontTopCoef;
private Map<Float, Font> fontCache = new ConcurrentHashMap<>(4);
private Font baseFont;
public static final int DEFAULT_FONT_SIZE = 70;
public Font getFont() {
return getFont(DEFAULT_FONT_SIZE);
}
public Font getFont(float size) {
return fontCache.computeIfAbsent(size, k -> baseFont.deriveFont(Font.BOLD, size));
}
public FontWrapper(Font font) {
this.baseFont = font;
}
public float getFontTopCoef(Font font) {
return 0.14645833f * font.getSize() + 0.39583333f;
}
}
@@ -0,0 +1,69 @@
package cloud.tianai.captcha.generator.common.model.dto;
import java.util.*;
/**
* @Author: 天爱有情
* @Description 缓存键包装类,支持忽略指定字段
*/
public class CacheKey {
private final GenerateParam generateParam;
private final Set<String> ignoredFields;
/**
* 构造函数
*
* @param generateParam 生成参数
* @param ignoredFields 需要忽略的字段集合(不参与equals和hashCode计算)
*/
public CacheKey(GenerateParam generateParam, Set<String> ignoredFields) {
if (generateParam == null) {
throw new IllegalArgumentException("generateParam 不能为 null");
}
this.generateParam = generateParam;
this.ignoredFields = ignoredFields != null ? ignoredFields : Collections.emptySet();
}
/**
* 获取原始的GenerateParam
*/
public GenerateParam getGenerateParam() {
return generateParam;
}
/**
* 获取参与缓存计算的有效字段
*/
private Map<String, Object> getEffectiveFields() {
Map<String, Object> effectiveMap = new LinkedHashMap<>();
for (Map.Entry<String, Object> entry : generateParam.entrySet()) {
if (!ignoredFields.contains(entry.getKey())) {
effectiveMap.put(entry.getKey(), entry.getValue());
}
}
return effectiveMap;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
CacheKey cacheKey = (CacheKey) o;
return Objects.equals(getEffectiveFields(), cacheKey.getEffectiveFields());
}
@Override
public int hashCode() {
return Objects.hash(getEffectiveFields());
}
@Override
public String toString() {
return "CacheKey{effectiveFields=" + getEffectiveFields() + "}";
}
}
@@ -6,6 +6,7 @@ import lombok.Data;
import lombok.NoArgsConstructor;
import java.awt.*;
import java.awt.image.BufferedImage;
/**
* @Author: 天爱有情
@@ -18,6 +19,7 @@ import java.awt.*;
public class ClickImageCheckDefinition {
/** 提示. */
private Resource tip;
private ImgWrapper tipImage;
/** x. */
private Integer x;
/** y. */
@@ -29,4 +31,21 @@ public class ClickImageCheckDefinition {
/** 颜色. */
private Color imageColor;
/**
* @Author: 天爱有情
* @date 2022/4/28 14:26
* @Description 点击图片包装
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class ImgWrapper {
/** 图片. */
private BufferedImage image;
/** 提示. */
private Resource tip;
/** 图片颜色. */
private Color imageColor;
}
}
@@ -0,0 +1,161 @@
package cloud.tianai.captcha.generator.common.model.dto;
import cloud.tianai.captcha.common.AnyMap;
import cloud.tianai.captcha.common.ParamKey;
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* @Author: 天爱有情
* @date 2022/2/11 9:44
* @Description 生成参数
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class GenerateParam extends AnyMap {
public GenerateParam() {
// 设置一些默认值
setBackgroundFormatName("jpeg");
setTemplateFormatName("png");
setObfuscate(false);
setType(CaptchaTypeConstant.SLIDER);
}
/**
* 背景格式化类型.
*/
private static final ParamKey<String> backgroundFormatName = () -> "backgroundFormatName";
/**
* 模板图片格式化类型.
*/
private static final ParamKey<String> templateFormatName = () -> "templateFormatName";
/**
* 是否混淆.
*/
private static final ParamKey<Boolean> obfuscate = () -> "obfuscate";
/**
* 类型.
*/
private static final ParamKey<String> type = () -> "type";
/**
* 背景图片标签, 用户二级过滤背景图片,或指定某背景图片.
*/
private static final ParamKey<String> backgroundImageTag = () -> "backgroundImageTag";
/**
* 滑动图片标签,用户二级过滤模板图片,或指定某模板图片..
*/
private static final ParamKey<String> templateImageTag = () -> "templateImageTag";
// =============== getter and setter ====================
public void setBackgroundFormatName(String backgroundFormatName) {
addParam(GenerateParam.backgroundFormatName, backgroundFormatName);
}
public void setTemplateFormatName(String templateFormatName) {
addParam(GenerateParam.templateFormatName, templateFormatName);
}
public void setObfuscate(boolean obfuscate) {
addParam(GenerateParam.obfuscate, obfuscate);
}
public void setType(String type) {
addParam(GenerateParam.type, type);
}
public void setBackgroundImageTag(String backgroundImageTag) {
addParam(GenerateParam.backgroundImageTag, backgroundImageTag);
}
public void setTemplateImageTag(String templateImageTag) {
addParam(GenerateParam.templateImageTag, templateImageTag);
}
public String getBackgroundFormatName() {
return getParam(GenerateParam.backgroundFormatName);
}
public String getTemplateFormatName() {
return getParam(GenerateParam.templateFormatName);
}
public boolean getObfuscate() {
return getParam(GenerateParam.obfuscate);
}
public String getType() {
return getParam(GenerateParam.type);
}
public String getBackgroundImageTag() {
return getParam(GenerateParam.backgroundImageTag);
}
public String getTemplateImageTag() {
return getParam(GenerateParam.templateImageTag);
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private String backgroundFormatName = "jpeg";
private String templateFormatName = "png";
private Boolean obfuscate = false;
private String type = CaptchaTypeConstant.SLIDER;
private String backgroundImageTag;
private String templateImageTag;
private Builder() {
}
public Builder backgroundFormatName(String backgroundFormatName) {
this.backgroundFormatName = backgroundFormatName;
return this;
}
public Builder templateFormatName(String templateFormatName) {
this.templateFormatName = templateFormatName;
return this;
}
public Builder obfuscate(Boolean obfuscate) {
this.obfuscate = obfuscate;
return this;
}
public Builder type(String type) {
this.type = type;
return this;
}
public Builder backgroundImageTag(String backgroundImageTag) {
this.backgroundImageTag = backgroundImageTag;
return this;
}
public Builder templateImageTag(String templateImageTag) {
this.templateImageTag = templateImageTag;
return this;
}
public GenerateParam build() {
GenerateParam generateParam = new GenerateParam();
generateParam.setBackgroundFormatName(backgroundFormatName);
generateParam.setTemplateFormatName(templateFormatName);
generateParam.setObfuscate(obfuscate);
generateParam.setType(type);
generateParam.setBackgroundImageTag(backgroundImageTag);
generateParam.setTemplateImageTag(templateImageTag);
return generateParam;
}
}
}
@@ -0,0 +1,26 @@
package cloud.tianai.captcha.generator.common.model.dto;
import cloud.tianai.captcha.common.ParamKey;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class ParamKeyEnum<T> implements ParamKey<T> {
/** 点选验证码参与校验的数量. 值为Integer */
public static final ParamKey<Integer> CLICK_CHECK_CLICK_COUNT = new ParamKeyEnum<>("checkClickCount");
/** 点选验证码干扰数量. 值为Integer */
public static final ParamKey<Integer> CLICK_INTERFERENCE_COUNT = new ParamKeyEnum<>("interferenceCount");
/** 读取字体时,可指定字体TAG,可用于给不同的验证码指定不同的字体包.*/
public static final ParamKey<String> FONT_TAG = new ParamKeyEnum<>("fontTag");
/** 容错值.*/
public static final ParamKey<Number> TOLERANT = new ParamKeyEnum<>("tolerant");
/** 验证码ID,内部使用.*/
public static final ParamKey<String> ID = new ParamKeyEnum<>("_id");
private String key;
}

Some files were not shown because too many files have changed in this diff Show More