mirror of
https://github.com/dromara/tianai-captcha.git
synced 2026-05-07 06:04:34 +08:00
Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e7b9fd923b | |||
| a7fca6ee57 | |||
| cfab8ce3ac | |||
| 387ed937f8 | |||
| fbbf3e8204 | |||
| 655b7faf57 | |||
| 356d3ab62c | |||
| a4f8a99093 | |||
| 16e517c69e | |||
| 29279e8c56 | |||
| db0603a124 | |||
| 7c8730f73b | |||
| 55b3510360 | |||
| b71cc8cd28 | |||
| a4fb7fa1fd | |||
| 5a90fa6ec4 | |||
| 42b2102faf | |||
| a2e6ae0ca6 | |||
| a323c2b262 | |||
| a46cb7d5fd | |||
| b6442a7e12 | |||
| 5eb258215b | |||
| af2df2c7e2 | |||
| 3d28302db5 | |||
| 47cc2445f5 | |||
| 25bf75b804 | |||
| cb92a224d5 | |||
| 12d290919a | |||
| 3fba6825cc | |||
| 6d8736e52e | |||
| 48bfa27ec8 | |||
| a2557a71d3 | |||
| 68252bf0d6 | |||
| 938112f2dc | |||
| 09abccc3e8 | |||
| 0ec2e1b137 | |||
| 3b1b211629 | |||
| 5767d98f15 | |||
| 4874116bc5 | |||
| 600878f6bd | |||
| 2be22591bf | |||
| 916f65f2bc | |||
| 3a218a798d |
@@ -24,3 +24,5 @@ target
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
|
||||
.flattened-pom.xml
|
||||
**/.flattened-pom.xml
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
## 可能是开源界最好用的行为验证码工具
|
||||
<div align="center">
|
||||
|
||||
-----
|
||||
![][image-logo]
|
||||
|
||||
## pc版在线体验 [在线体验](http://captcha.tianai.cloud)
|
||||

|
||||
|
||||
### tianaiCAPTCHA - 天爱验证码(TAC)
|
||||
#### 基于 JAVA实现的行为验证码
|
||||
### **[在线体验 🚀][online-demo-link]**
|
||||
### **[在线文档 🚀][doc-link]**
|
||||
</div>
|
||||
|
||||
## 在线文档 [在线文档](http://doc.captcha.tianai.cloud)
|
||||
|
||||

|
||||
|
||||
|
||||
## 简单介绍
|
||||
|
||||
- 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);
|
||||
}
|
||||
|
||||
}
|
||||
-74
@@ -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();
|
||||
|
||||
}
|
||||
-109
@@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
+79
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+127
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+12
@@ -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";
|
||||
|
||||
}
|
||||
+30
@@ -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;
|
||||
}
|
||||
+59
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+286
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+36
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+58
@@ -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);
|
||||
}
|
||||
}
|
||||
+76
@@ -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
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
cloud.tianai.captcha.spring.autoconfiguration.CacheStoreAutoConfiguration
|
||||
cloud.tianai.captcha.spring.autoconfiguration.ImageCaptchaAutoConfiguration
|
||||
@@ -0,0 +1,3 @@
|
||||
dist/
|
||||
node_modules/
|
||||
.idea/
|
||||
@@ -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 License,Version 2
|
||||
|
||||
Mulan Permissive Software License,Version 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 IT’S 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 License,Version 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.
|
||||
Generated
+12901
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
@@ -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'
|
||||
}),
|
||||
]
|
||||
}
|
||||
@@ -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'
|
||||
})
|
||||
]
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
+70
-24
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+10
-6
@@ -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();
|
||||
}
|
||||
}
|
||||
+6
-7
@@ -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);
|
||||
|
||||
/**
|
||||
* 匹配
|
||||
+2
@@ -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;
|
||||
}
|
||||
+72
-47
@@ -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);
|
||||
}
|
||||
}
|
||||
+3
@@ -10,6 +10,9 @@ import java.io.Serializable;
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ImageCaptchaVO implements Serializable {
|
||||
|
||||
/** ID.*/
|
||||
private String id;
|
||||
/** 验证码类型.*/
|
||||
private String type;
|
||||
/** 背景图.*/
|
||||
+1
-1
@@ -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
|
||||
+26
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+32
-2
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 定时执行任务
|
||||
*
|
||||
+11
@@ -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();
|
||||
|
||||
}
|
||||
+6
-1
@@ -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资源文件路径.
|
||||
+1
-1
@@ -7,7 +7,7 @@ import java.io.Serializable;
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2023/4/20 9:53
|
||||
* @Description 可能是最好用的API统一返回格式类
|
||||
* @Description API统一返回格式类
|
||||
*/
|
||||
@Data
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
+25
-3
@@ -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("-", "");
|
||||
}
|
||||
|
||||
}
|
||||
+16
-10
@@ -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);
|
||||
closeStream(stream);
|
||||
return bufferedImage;
|
||||
try {
|
||||
return CaptchaImageUtils.wrapFile2BufferedImage(stream);
|
||||
} finally {
|
||||
closeStream(stream);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected BufferedImage getResourceImage(Resource resource) {
|
||||
InputStream stream = getResourceInputStream(resource, null);
|
||||
BufferedImage bufferedImage = CaptchaImageUtils.wrapFile2BufferedImage(stream);
|
||||
closeStream(stream);
|
||||
return bufferedImage;
|
||||
try {
|
||||
return CaptchaImageUtils.wrapFile2BufferedImage(stream);
|
||||
} finally {
|
||||
closeStream(stream);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
+69
@@ -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() + "}";
|
||||
}
|
||||
}
|
||||
+25
-6
@@ -6,6 +6,7 @@ import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
@@ -16,17 +17,35 @@ import java.awt.*;
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ClickImageCheckDefinition {
|
||||
/** 提示.*/
|
||||
/** 提示. */
|
||||
private Resource tip;
|
||||
/** x.*/
|
||||
private ImgWrapper tipImage;
|
||||
/** x. */
|
||||
private Integer x;
|
||||
/** y.*/
|
||||
/** y. */
|
||||
private Integer y;
|
||||
/** 宽.*/
|
||||
/** 宽. */
|
||||
private Integer width;
|
||||
/** 高.*/
|
||||
/** 高. */
|
||||
private Integer height;
|
||||
/** 颜色.*/
|
||||
/** 颜色. */
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
+161
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+26
@@ -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
Reference in New Issue
Block a user