重构系统项目结构, 将 tianai-captcha

tianai-captcha-springboot-starter
   tianai-captcha-web-sdk
   tianai-captcha-solon-plugin
   整合到一块
This commit is contained in:
天爱有情
2025-10-27 15:14:10 +08:00
parent af2df2c7e2
commit 5eb258215b
156 changed files with 16688 additions and 99 deletions
+2
View File
@@ -24,3 +24,5 @@ target
/nbdist/ /nbdist/
/.nb-gradle/ /.nb-gradle/
.flattened-pom.xml
**/.flattened-pom.xml
+79 -55
View File
@@ -3,14 +3,20 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 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> <modelVersion>4.0.0</modelVersion>
<groupId>cloud.tianai.captcha</groupId> <groupId>cloud.tianai.captcha</groupId>
<artifactId>tianai-captcha</artifactId> <artifactId>tianai-captcha-parent</artifactId>
<version>1.5.3</version> <version>${revision}</version>
<packaging>pom</packaging>
<name>tianai-captcha</name> <name>tianai-captcha-parent</name>
<description>行为验证码</description> <description>行为验证码</description>
<url>https://gitee.com/tianai/tianai-captcha</url> <url>https://gitee.com/tianai/tianai-captcha</url>
<modules>
<module>tianai-captcha</module>
<module>tianai-captcha-springboot-starter</module>
<module>tianai-captcha-solon-plugin</module>
</modules>
<properties> <properties>
<revision>1.5.3</revision>
<java.version>1.8</java.version> <java.version>1.8</java.version>
<!-- 打包跳过单元测试 --> <!-- 打包跳过单元测试 -->
<skipTests>true</skipTests> <skipTests>true</skipTests>
@@ -18,9 +24,9 @@
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<skip.nexus>false</skip.nexus> <skip.nexus>false</skip.nexus>
<deplay.id>ossrh</deplay.id> <serverId>ossrh</serverId>
<deplay.repository>https://oss.sonatype.org/service/local/staging/deploy/maven2/</deplay.repository> <deplay.repository>https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/</deplay.repository>
<deplay.snapshotRepository>https://oss.sonatype.org/content/repositories/snapshots/</deplay.snapshotRepository> <deplay.snapshotRepository>https://s01.oss.sonatype.org/content/repositories/snapshots</deplay.snapshotRepository>
<!-- 私服 --> <!-- 私服 -->
<!-- <skip.nexus>true</skip.nexus>--> <!-- <skip.nexus>true</skip.nexus>-->
@@ -28,7 +34,6 @@
<!-- <deplay.repository>http://192.168.3.10:6061/repository/smart_hosted/</deplay.repository>--> <!-- <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>--> <!-- <deplay.snapshotRepository>http://192.168.3.10:6061/repository/smart_hosted/</deplay.snapshotRepository>-->
</properties> </properties>
<licenses> <licenses>
<license> <license>
<name>The MulanPSL2 License, Version 2.0</name> <name>The MulanPSL2 License, Version 2.0</name>
@@ -36,6 +41,11 @@
</license> </license>
</licenses> </licenses>
<scm>
<url>https://gitee.com/tianai/tianai-captcha</url>
</scm>
<developers> <developers>
<developer> <developer>
<name>tianaiyouqing</name> <name>tianaiyouqing</name>
@@ -44,12 +54,11 @@
<organizationUrl>http://tianai.cloud</organizationUrl> <organizationUrl>http://tianai.cloud</organizationUrl>
</developer> </developer>
</developers> </developers>
<scm>
<url>https://gitee.com/tianai/tianai-captcha</url>
</scm>
<distributionManagement> <distributionManagement>
<snapshotRepository> <snapshotRepository>
<id>${deplay.id}</id> <id>${serverId}</id>
<url>${deplay.snapshotRepository}</url> <url>${deplay.snapshotRepository}</url>
</snapshotRepository> </snapshotRepository>
<repository> <repository>
@@ -58,42 +67,55 @@
</repository> </repository>
</distributionManagement> </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> <build>
<plugins> <plugins>
<!-- 统一版本号管理 -->
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.codehaus.mojo</groupId>
<artifactId>maven-compiler-plugin</artifactId> <artifactId>flatten-maven-plugin</artifactId>
<version>1.2.7</version>
<configuration> <configuration>
<source>8</source> <updatePomFile>true</updatePomFile>
<target>8</target> <flattenMode>resolveCiFriendliesOnly</flattenMode>
<compilerArgument>-parameters</compilerArgument>
</configuration> </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>
<plugin> <plugin>
<groupId>org.sonatype.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>nexus-staging-maven-plugin</artifactId> <artifactId>maven-javadoc-plugin</artifactId>
<version>1.6.7</version> <version>2.9.1</version>
<extensions>true</extensions>
<configuration> <configuration>
<skipNexusStagingDeployMojo>${skip.nexus}</skipNexusStagingDeployMojo> <!-- 统一生成聚合文档,解决 mvn package 时控制台发出 javadoc 警告的问题 -->
<serverId>ossrh</serverId> <aggregate>true</aggregate>
<nexusUrl>https://oss.sonatype.org/</nexusUrl> <failOnError>false</failOnError>
<autoReleaseAfterClose>true</autoReleaseAfterClose> <detectLinks>false</detectLinks>
</configuration> </configuration>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
<configuration>
<additionalparam>-Xdoclint:none</additionalparam>
</configuration>
</execution>
</executions>
</plugin> </plugin>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
@@ -108,22 +130,7 @@
</execution> </execution>
</executions> </executions>
</plugin> </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> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId> <artifactId>maven-gpg-plugin</artifactId>
@@ -143,7 +150,24 @@
<artifactId>maven-resources-plugin</artifactId> <artifactId>maven-resources-plugin</artifactId>
<version>2.6</version> <version>2.6</version>
</plugin> </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> </plugins>
<pluginManagement>
<plugins>
</plugins>
</pluginManagement>
</build> </build>
</project> </project>
+187 -14
View File
@@ -22,22 +22,85 @@
- 文字点选验证码 - 文字点选验证码
- 后面会陆续支持市面上更多好玩的验证码玩法... 敬请期待 - 后面会陆续支持市面上更多好玩的验证码玩法... 敬请期待
## 快速上手 ## 快速上手(后端)
> 注意: 如果你项目是使用的**Springboot** ### springboot项目
>
> 1. 导入依赖
请使用SpringBoot脚手架工具
- [tianai-captcha-springboot-starter(gitee)](https://gitee.com/tianai/tianai-captcha-springboot-starter); ```xml
- [tianai-captcha-springboot-starter(gitcode)](https://gitcode.com/tiana/tianai-captcha-springboot-starter); <dependency>
- [tianai-captcha-springboot-starter(github)](https://github.com/tianaiyouqing/tianai-captcha-springboot-starter); <groupId>cloud.tianai.captcha</groupId>
> <artifactId>tianai-captcha-springboot-starter</artifactId>
> 该工具对tianai-captcha验证码进行了封装,使其使用更加方便快捷 <version>1.5.2</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(gitee)](https://gitee.com/tianai/tianai-captcha-demo)
> - [tianai-captcha-demo(gitcode)](https://gitcode.com/tiana/tianai-captcha-demo) ### 非spring项目
### 1. 导入xml ### 1. 导入xml
@@ -90,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/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);
})
```
## 详细文档请点击 [在线文档](http://doc.captcha.tianai.cloud)
# qq群: 197340494 # qq群: 197340494
# 微信群: # 微信群:
@@ -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);
}
}
+63
View File
@@ -0,0 +1,63 @@
<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>
</parent>
<artifactId>tianai-captcha-solon-plugin</artifactId>
<name>tianai-captcha-solon-plugin</name>
<description>行为验证码的solon插件</description>
<url>https://gitee.com/tianai/tianai-captcha-solon-plugin</url>
<properties>
<solon.version>3.5.2</solon.version>
<maven.compiler.source>8</maven.compiler.source>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<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.noear</groupId>
<artifactId>solon</artifactId>
<version>${solon.version}</version>
</dependency>
<dependency>
<groupId>org.noear</groupId>
<artifactId>solon-web</artifactId>
<version>${solon.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>cloud.tianai.captcha</groupId>
<artifactId>tianai-captcha</artifactId>
<version>${revision}</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>
@@ -0,0 +1,21 @@
package cloud.tianai.captcha.solon;
import cloud.tianai.captcha.solon.config.ImageCaptchaAutoConfiguration;
import cloud.tianai.captcha.solon.properties.CaptchaProperties;
import org.noear.solon.core.AppContext;
import org.noear.solon.core.Plugin;
/**
* 插件启动类
* @Author XT
* @Date 2024.09.03
*/
public class XPluginImp implements Plugin {
@Override
public void start(AppContext context) {
context.beanMake(CaptchaProperties.class);
context.beanMake(ImageCaptchaAutoConfiguration.class);
}
}
@@ -0,0 +1,95 @@
package cloud.tianai.captcha.solon.config;
import cloud.tianai.captcha.application.ImageCaptchaApplication;
import cloud.tianai.captcha.application.TACBuilder;
import cloud.tianai.captcha.cache.CacheStore;
import cloud.tianai.captcha.cache.impl.LocalCacheStore;
import cloud.tianai.captcha.resource.common.model.dto.Resource;
import cloud.tianai.captcha.solon.plugins.secondary.SecondaryVerificationApplication;
import cloud.tianai.captcha.solon.properties.CaptchaProperties;
import cloud.tianai.captcha.solon.service.CaptchaRedisCacheService;
import org.noear.solon.annotation.Bean;
import org.noear.solon.annotation.Configuration;
import org.noear.solon.annotation.Inject;
import java.util.List;
/**
* @Author XT
* @Date 2024.09.03
*/
@Configuration
public class ImageCaptchaAutoConfiguration {
@Bean
public ImageCaptchaApplication imageCaptchaApplication(CaptchaProperties captchaProperties, @Inject(required = false) CaptchaRedisCacheService cacheService) {
TACBuilder tacBuilder = TACBuilder.builder();
tacBuilder.addDefaultTemplate();
tacBuilder.expire("default", captchaProperties.getExpire());
tacBuilder.prefix(captchaProperties.getPrefix());
// 注入背景图资源
if (captchaProperties.getResources().getAuto()) {
String[] split = captchaProperties.getResources().getAutoType().split(",");
List<String> wordImageClickList = captchaProperties.getResources().getImages();
for (String type : split) {
for (String path : wordImageClickList) {
tacBuilder.addResource(type, new Resource("classpath", path));
}
}
} else {
List<String> wordImageClickList = captchaProperties.getResources().getWORD_IMAGE_CLICK();
if (!wordImageClickList.isEmpty()) {
for (String path : wordImageClickList) {
tacBuilder.addResource("WORD_IMAGE_CLICK", new Resource("classpath", path));
}
}
List<String> concatList = captchaProperties.getResources().getCONCAT();
if (!concatList.isEmpty()) {
for (String path : concatList) {
tacBuilder.addResource("CONCAT", new Resource("classpath", path));
}
}
List<String> sliderList = captchaProperties.getResources().getSLIDER();
if (!sliderList.isEmpty()) {
for (String path : sliderList) {
tacBuilder.addResource("SLIDER", new Resource("classpath", path));
}
}
List<String> rotateList = captchaProperties.getResources().getROTATE();
if (!rotateList.isEmpty()) {
for (String path : rotateList) {
tacBuilder.addResource("ROTATE", new Resource("classpath", path));
}
}
}
// 注入字体包
if (null != captchaProperties.getFontPath()) {
List<String> fontPathList = captchaProperties.getFontPath();
if (!fontPathList.isEmpty()) {
for (String path : fontPathList) {
try {
tacBuilder.addFont(new Resource("classpath", path));
} catch (Exception e) {
throw new RuntimeException("读取字体包失败,path=" + path, e);
}
}
}
}
CacheStore cacheStore = cacheService;
// 注入缓存器
if (null == cacheStore) {
cacheStore = new LocalCacheStore();
}
tacBuilder.setCacheStore(cacheStore);
ImageCaptchaApplication target = tacBuilder.build();
// 二次验证
if (captchaProperties.getSecondary().getEnabled()) {
target = new SecondaryVerificationApplication(target, captchaProperties, cacheStore);
}
return target;
}
}
@@ -0,0 +1,109 @@
package cloud.tianai.captcha.solon.plugins.secondary;
import cloud.tianai.captcha.application.FilterImageCaptchaApplication;
import cloud.tianai.captcha.application.ImageCaptchaApplication;
import cloud.tianai.captcha.application.vo.ImageCaptchaVO;
import cloud.tianai.captcha.cache.CacheStore;
import cloud.tianai.captcha.common.AnyMap;
import cloud.tianai.captcha.common.exception.ImageCaptchaException;
import cloud.tianai.captcha.common.response.ApiResponse;
import cloud.tianai.captcha.solon.properties.CaptchaLimit;
import cloud.tianai.captcha.solon.properties.CaptchaProperties;
import cloud.tianai.captcha.solon.properties.CaptchaSecondary;
import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack;
import org.noear.solon.core.handle.Context;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* @Author XT
* @Date 2024.09.03
*/
public class SecondaryVerificationApplication extends FilterImageCaptchaApplication {
private final CaptchaSecondary prop;
private final CaptchaProperties captchaProperties;
private final CacheStore redisCacheService;
public SecondaryVerificationApplication(ImageCaptchaApplication target, CaptchaProperties captchaProperties, CacheStore redisCacheService) {
super(target);
this.captchaProperties = captchaProperties;
this.prop = captchaProperties.getSecondary();
this.redisCacheService = redisCacheService;
}
@Override
public ApiResponse<ImageCaptchaVO> generateCaptcha(String type) {
// 检查是否每分钟超过限制
CaptchaLimit limit = captchaProperties.getLimit();
if (null != limit && limit.getEnable()) {
Context current = Context.current();
String errLimitKey = getLimitKey(current, "error");
Long errLimit = redisCacheService.getLong(errLimitKey);
if (null != errLimit && errLimit >= limit.getErrorLimit()) {
throw new ImageCaptchaException("验证次数过多,请稍后再试");
}
String reqLimitKey = getLimitKey(current, "req");
Long reqLimit = redisCacheService.getLong(reqLimitKey);
if (null != reqLimit && reqLimit >= limit.getReqLimit()) {
throw new ImageCaptchaException("获取验证码频繁,请稍后再试");
}
redisCacheService.incr(reqLimitKey, 1, 60L, TimeUnit.SECONDS);
}
return super.generateCaptcha(type);
}
@Override
public ApiResponse<?> matching(String id, ImageCaptchaTrack imageCaptchaTrack) {
ApiResponse<?> match = super.matching(id, imageCaptchaTrack);
if (match.isSuccess()) {
// 如果匹配成功, 添加二次验证记录
addSecondaryVerification(id + getRemoteId(Context.current()), imageCaptchaTrack);
} else {
CaptchaLimit limit = captchaProperties.getLimit();
if (null != limit && limit.getEnable()) {
Context current = Context.current();
String limitKey = getLimitKey(current, "error");
redisCacheService.incr(limitKey, 1, 60L, TimeUnit.SECONDS);
}
}
return match;
}
/**
* 二次缓存验证
* @param id id
* @return boolean
*/
public boolean secondaryVerification(String id) {
Map<String, Object> cache = target.getCacheStore().getAndRemoveCache(getKey(id + getRemoteId(Context.current())));
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);
}
protected String getLimitKey(Context ctx, String type) {
return prop.getKeyPrefix().concat(":limit:")
.concat(type)
.concat(":")
.concat(getRemoteId(ctx));
}
public static String getRemoteId(Context ctx) {
return ctx.realIp() + ctx.userAgent();
}
}
@@ -0,0 +1,38 @@
package cloud.tianai.captcha.solon.properties;
/**
* @Author XT
* @Date 2024.09.04
*/
public class CaptchaLimit {
private Boolean enable;
private Long reqLimit;
private Long errorLimit;
public Boolean getEnable() {
return enable;
}
public void setEnable(Boolean enable) {
this.enable = enable;
}
public Long getReqLimit() {
return reqLimit;
}
public void setReqLimit(Long reqLimit) {
this.reqLimit = reqLimit;
}
public Long getErrorLimit() {
return errorLimit;
}
public void setErrorLimit(Long errorLimit) {
this.errorLimit = errorLimit;
}
}
@@ -0,0 +1,93 @@
package cloud.tianai.captcha.solon.properties;
import org.noear.solon.annotation.Configuration;
import org.noear.solon.annotation.Inject;
import java.util.List;
/**
* @Author XT
* @Date 2024.09.03
*/
@Inject("${captcha}")
@Configuration
public class CaptchaProperties {
/**
* redis 前缀
*/
private String prefix;
/**
* 有效期
*/
private Long expire;
/**
* 字体路径
*/
private List<String> fontPath;
/**
* 资源路径
*/
private CaptchaResource resources;
/**
* 二次验证
*/
private CaptchaSecondary secondary;
/**
* 每分限流
*/
private CaptchaLimit limit;
public String getPrefix() {
return prefix;
}
public void setPrefix(String prefix) {
this.prefix = prefix;
}
public Long getExpire() {
return expire;
}
public void setExpire(Long expire) {
this.expire = expire;
}
public List<String> getFontPath() {
return fontPath;
}
public void setFontPath(List<String> fontPath) {
this.fontPath = fontPath;
}
public CaptchaResource getResources() {
return resources;
}
public void setResources(CaptchaResource resources) {
this.resources = resources;
}
public CaptchaSecondary getSecondary() {
return secondary;
}
public void setSecondary(CaptchaSecondary secondary) {
this.secondary = secondary;
}
public CaptchaLimit getLimit() {
return limit;
}
public void setLimit(CaptchaLimit limit) {
this.limit = limit;
}
}
@@ -0,0 +1,80 @@
package cloud.tianai.captcha.solon.properties;
import java.util.List;
/**
* @Author XT
* @Date 2024.09.03
*/
public class CaptchaResource {
private Boolean auto;
private String autoType;
private List<String> images;
private List<String> SLIDER;
private List<String> WORD_IMAGE_CLICK;
private List<String> ROTATE;
private List<String> CONCAT;
public List<String> getSLIDER() {
return SLIDER;
}
public void setSLIDER(List<String> SLIDER) {
this.SLIDER = SLIDER;
}
public List<String> getWORD_IMAGE_CLICK() {
return WORD_IMAGE_CLICK;
}
public void setWORD_IMAGE_CLICK(List<String> WORD_IMAGE_CLICK) {
this.WORD_IMAGE_CLICK = WORD_IMAGE_CLICK;
}
public List<String> getROTATE() {
return ROTATE;
}
public void setROTATE(List<String> ROTATE) {
this.ROTATE = ROTATE;
}
public List<String> getCONCAT() {
return CONCAT;
}
public void setCONCAT(List<String> CONCAT) {
this.CONCAT = CONCAT;
}
public Boolean getAuto() {
return auto;
}
public void setAuto(Boolean auto) {
this.auto = auto;
}
public String getAutoType() {
return autoType;
}
public void setAutoType(String autoType) {
this.autoType = autoType;
}
public List<String> getImages() {
return images;
}
public void setImages(List<String> images) {
this.images = images;
}
}
@@ -0,0 +1,38 @@
package cloud.tianai.captcha.solon.properties;
/**
* @Author XT
* @Date 2024.09.03
*/
public class CaptchaSecondary {
private Boolean enabled;
private Long expire;
private String keyPrefix;
public Boolean getEnabled() {
return enabled;
}
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
public Long getExpire() {
return expire;
}
public void setExpire(Long expire) {
this.expire = expire;
}
public String getKeyPrefix() {
return keyPrefix;
}
public void setKeyPrefix(String keyPrefix) {
this.keyPrefix = keyPrefix;
}
}
@@ -0,0 +1,7 @@
package cloud.tianai.captcha.solon.service;
import cloud.tianai.captcha.cache.CacheStore;
public interface CaptchaRedisCacheService extends CacheStore {
}
+106
View File
@@ -0,0 +1,106 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cloud.tianai.captcha</groupId>
<artifactId>tianai-captcha-parent</artifactId>
<version>${revision}</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>tianai-captcha-springboot-starter</artifactId>
<name>tianai-captcha-springboot-starter</name>
<description>滑块验证码springboot启动器</description>
<url>https://gitee.com/tianai/tianai-captcha-springboot-starter</url>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.2.7.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<properties>
<java.version>1.8</java.version>
<commons-lang3.version>3.7</commons-lang3.version>
<!-- 打包跳过单元测试 -->
<skipTests>true</skipTests>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<scope>compile</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons-lang3.version}</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure-processor</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>cloud.tianai.captcha</groupId>
<artifactId>tianai-captcha</artifactId>
<version>${revision}</version>
</dependency>
<!-- <dependency>-->
<!-- <groupId>jakarta.validation</groupId>-->
<!-- <artifactId>jakarta.validation-api</artifactId>-->
<!-- </dependency>-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>compile</scope>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
<compilerArgument>-parameters</compilerArgument>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,63 @@
package cloud.tianai.captcha.spring.autoconfiguration;
import cloud.tianai.captcha.cache.CacheStore;
import cloud.tianai.captcha.cache.impl.LocalCacheStore;
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.boot.autoconfigure.data.redis.RedisAutoConfiguration;
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({RedisAutoConfiguration.class})
@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
@ConditionalOnBean(StringRedisTemplate.class)
@ConditionalOnMissingBean(CacheStore.class)
public CacheStore redis(StringRedisTemplate redisTemplate) {
return new RedisCacheStore(redisTemplate);
}
}
/**
* LocalCacheStoreConfiguration
*
* @author 天爱有情
* @since 2020/10/27 14:06
*/
@Order(2)
@Configuration(proxyBeanMethods = false)
public static class LocalCacheStoreConfiguration {
@Bean
@ConditionalOnMissingBean(CacheStore.class)
public CacheStore local() {
return new LocalCacheStore();
}
}
}
@@ -0,0 +1,130 @@
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.common.util.CollectionUtils;
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.CaptchaInterceptorGroup;
import cloud.tianai.captcha.interceptor.impl.ParamCheckCaptchaInterceptor;
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.resource.impl.LocalMemoryResourceStore;
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.beans.factory.config.BeanDefinition;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
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.context.annotation.Role;
import org.springframework.core.annotation.Order;
/**
* @Author: 天爱有情
* @Date 2020/5/29 9:49
* @Description 滑块验证码自动装配
*/
@Slf4j
@Order
@Configuration
@AutoConfigureAfter(CacheStoreAutoConfiguration.class)
@EnableConfigurationProperties({SpringImageCaptchaProperties.class})
public class ImageCaptchaAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public ResourceStore resourceStore() {
return new LocalMemoryResourceStore();
}
@Bean
@ConditionalOnMissingBean
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) {
return new SpringMultiImageCaptchaGenerator(captchaResourceManager, imageTransform, beanFactory);
}
@Bean
@ConditionalOnMissingBean
public ImageCaptchaValidator imageCaptchaValidator() {
return new SimpleImageCaptchaValidator();
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
@ConditionalOnMissingBean
public CaptchaInterceptor captchaInterceptor() {
CaptchaInterceptorGroup group = new CaptchaInterceptorGroup();
group.addInterceptor(new ParamCheckCaptchaInterceptor());
// group.addInterceptor(new BasicTrackCaptchaInterceptor());
return group;
}
@Bean
@ConditionalOnMissingBean
public ImageCaptchaApplication imageCaptchaApplication(ImageCaptchaGenerator captchaGenerator,
ImageCaptchaValidator imageCaptchaValidator,
CacheStore cacheStore,
ResourceStore resourceStore,
SpringImageCaptchaProperties prop,
CaptchaInterceptor captchaInterceptor,
ApplicationContext applicationContext) {
TACBuilder tacBuilder = TACBuilder.builder(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];
tacBuilder.addFont(new cloud.tianai.captcha.resource.common.model.dto.Resource(type, path));
}
}
ImageCaptchaApplication target = tacBuilder.build();
if (prop.getSecondary() != null && Boolean.TRUE.equals(prop.getSecondary().getEnabled())) {
// 一个简单的二次验证
target = new SecondaryVerificationApplication(target, prop.getSecondary());
}
return target;
}
}
@@ -0,0 +1,12 @@
package cloud.tianai.captcha.spring.autoconfiguration;
import lombok.Data;
@Data
public class SecondaryVerificationProperties {
private Boolean enabled = false;
private Long expire = 120000L;
private String keyPrefix = "captcha:secondary";
}
@@ -0,0 +1,30 @@
package cloud.tianai.captcha.spring.autoconfiguration;
import cloud.tianai.captcha.application.ImageCaptchaProperties;
import cloud.tianai.captcha.resource.DefaultBuiltInResources;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
import java.util.List;
/**
* @Author: 天爱有情
* @date 2020/10/19 18:41
* @Description 滑块验证码属性
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ConfigurationProperties(prefix = "captcha")
public class SpringImageCaptchaProperties extends ImageCaptchaProperties {
/** 是否初始化默认资源. */
private Boolean initDefaultResource = false;
/** 默认资源的位置. */
private String defaultResourcePrefix = DefaultBuiltInResources.PATH_PREFIX;
/** 字体包路径. */
private List<String> fontPath;
/** 二次验证配置. */
@NestedConfigurationProperty
private SecondaryVerificationProperties secondary;
}
@@ -0,0 +1,41 @@
package cloud.tianai.captcha.spring.exception;
import cloud.tianai.captcha.common.exception.ImageCaptchaException;
import lombok.Getter;
import lombok.Setter;
/**
* @Author: 天爱有情
* @Date 2020/6/19 16:36
* @Description 验证码验证失败异常
*/
@Getter
@Setter
public class CaptchaValidException extends ImageCaptchaException {
private String captchaType;
private Integer code;
public CaptchaValidException() {
}
public CaptchaValidException(String captchaType,String message) {
super(message);
this.captchaType = captchaType;
}
public CaptchaValidException(String captchaType,Integer code, String message) {
super(message);
this.code = code;
this.captchaType = captchaType;
}
public CaptchaValidException(String message, Throwable cause) {
super(message, cause);
}
public CaptchaValidException(Throwable cause) {
super(cause);
}
public CaptchaValidException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
@@ -0,0 +1,36 @@
package cloud.tianai.captcha.spring.plugins;
import cloud.tianai.captcha.generator.ImageCaptchaGeneratorProvider;
import cloud.tianai.captcha.generator.ImageTransform;
import cloud.tianai.captcha.generator.impl.MultiImageCaptchaGenerator;
import cloud.tianai.captcha.resource.ImageCaptchaResourceManager;
import org.apache.commons.lang3.ArrayUtils;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.ListableBeanFactory;
/**
* @Author: 天爱有情
* @date 2022/5/19 14:37
* @Description 基于spring的 多验证码生成器
*/
public class SpringMultiImageCaptchaGenerator extends MultiImageCaptchaGenerator {
private ListableBeanFactory beanFactory;
public SpringMultiImageCaptchaGenerator(ImageCaptchaResourceManager imageCaptchaResourceManager, ImageTransform imageTransform,
BeanFactory beanFactory) {
super(imageCaptchaResourceManager, imageTransform);
this.beanFactory = (ListableBeanFactory) beanFactory;
}
@Override
protected void doInit() {
super.doInit();
String[] beanNamesForType = beanFactory.getBeanNamesForType(ImageCaptchaGeneratorProvider.class);
if (!ArrayUtils.isEmpty(beanNamesForType)) {
for (String beanName : beanNamesForType) {
ImageCaptchaGeneratorProvider provider = beanFactory.getBean(beanName, ImageCaptchaGeneratorProvider.class);
addImageCaptchaGeneratorProvider(provider);
}
}
}
}
@@ -0,0 +1,58 @@
package cloud.tianai.captcha.spring.plugins.secondary;
import cloud.tianai.captcha.application.FilterImageCaptchaApplication;
import cloud.tianai.captcha.application.ImageCaptchaApplication;
import cloud.tianai.captcha.common.AnyMap;
import cloud.tianai.captcha.common.response.ApiResponse;
import cloud.tianai.captcha.spring.autoconfiguration.SecondaryVerificationProperties;
import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* @Author: 天爱有情
* @date 2022/3/2 14:16
* @Description 二次验证
*/
public class SecondaryVerificationApplication extends FilterImageCaptchaApplication {
private SecondaryVerificationProperties prop;
public SecondaryVerificationApplication(ImageCaptchaApplication target, SecondaryVerificationProperties prop) {
super(target);
this.prop = prop;
}
@Override
public ApiResponse<?> matching(String id, ImageCaptchaTrack imageCaptchaTrack) {
ApiResponse<?> match = super.matching(id, imageCaptchaTrack);
if (match.isSuccess()) {
// 如果匹配成功, 添加二次验证记录
addSecondaryVerification(id, imageCaptchaTrack);
}
return match;
}
/**
* 二次缓存验证
* @param id id
* @return boolean
*/
public boolean secondaryVerification(String id) {
Map<String, Object> cache = target.getCacheStore().getAndRemoveCache(getKey(id));
return cache != null;
}
/**
* 添加二次缓存验证记录
* @param id id
* @param imageCaptchaTrack sliderCaptchaTrack
*/
protected void addSecondaryVerification(String id, ImageCaptchaTrack imageCaptchaTrack) {
target.getCacheStore().setCache(getKey(id), new AnyMap(), prop.getExpire(), TimeUnit.MILLISECONDS);
}
protected String getKey(String id) {
return prop.getKeyPrefix().concat(":").concat(id);
}
}
@@ -0,0 +1,71 @@
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);
}
}
@@ -0,0 +1,3 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cloud.tianai.captcha.spring.autoconfiguration.CacheStoreAutoConfiguration,\
cloud.tianai.captcha.spring.autoconfiguration.ImageCaptchaAutoConfiguration
@@ -0,0 +1,2 @@
cloud.tianai.captcha.spring.autoconfiguration.CacheStoreAutoConfiguration
cloud.tianai.captcha.spring.autoconfiguration.ImageCaptchaAutoConfiguration
+3
View File
@@ -0,0 +1,3 @@
dist/
node_modules/
.idea/
+127
View File
@@ -0,0 +1,127 @@
木兰宽松许可证, 第2版
木兰宽松许可证, 第2版
2020年1月 http://license.coscl.org.cn/MulanPSL2
您对“软件”的复制、使用、修改及分发受木兰宽松许可证,第2版(“本许可证”)的如下条款的约束:
0. 定义
“软件”是指由“贡献”构成的许可在“本许可证”下的程序和相关文档的集合。
“贡献”是指由任一“贡献者”许可在“本许可证”下的受版权法保护的作品。
“贡献者”是指将受版权法保护的作品许可在“本许可证”下的自然人或“法人实体”。
“法人实体”是指提交贡献的机构及其“关联实体”。
“关联实体”是指,对“本许可证”下的行为方而言,控制、受控制或与其共同受控制的机构,此处的控制是指有受控方或共同受控方至少50%直接或间接的投票权、资金或其他有价证券。
1. 授予版权许可
每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的版权许可,您可以复制、使用、修改、分发其“贡献”,不论修改与否。
2. 授予专利许可
每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的(根据本条规定撤销除外)专利许可,供您制造、委托制造、使用、许诺销售、销售、进口其“贡献”或以其他方式转移其“贡献”。前述专利许可仅限于“贡献者”现在或将来拥有或控制的其“贡献”本身或其“贡献”与许可“贡献”时的“软件”结合而将必然会侵犯的专利权利要求,不包括对“贡献”的修改或包含“贡献”的其他结合。如果您或您的“关联实体”直接或间接地,就“软件”或其中的“贡献”对任何人发起专利侵权诉讼(包括反诉或交叉诉讼)或其他专利维权行动,指控其侵犯专利权,则“本许可证”授予您对“软件”的专利许可自您提起诉讼或发起维权行动之日终止。
3. 无商标许可
“本许可证”不提供对“贡献者”的商品名称、商标、服务标志或产品名称的商标许可,但您为满足第4条规定的声明义务而必须使用除外。
4. 分发限制
您可以在任何媒介中将“软件”以源程序形式或可执行形式重新分发,不论修改与否,但您必须向接收者提供“本许可证”的副本,并保留“软件”中的版权、商标、专利及免责声明。
5. 免责声明与责任限制
“软件”及其中的“贡献”在提供时不带任何明示或默示的担保。在任何情况下,“贡献者”或版权所有者不对任何人因使用“软件”或其中的“贡献”而引发的任何直接或间接损失承担责任,不论因何种原因导致或者基于何种法律理论,即使其曾被建议有此种损失的可能性。
6. 语言
“本许可证”以中英文双语表述,中英文版本具有同等法律效力。如果中英文版本存在任何冲突不一致,以中文版为准。
条款结束
如何将木兰宽松许可证,第2版,应用到您的软件
如果您希望将木兰宽松许可证,第2版,应用到您的新软件,为了方便接收者查阅,建议您完成如下三步:
1, 请您补充如下声明中的空白,包括软件名、软件的首次发表年份以及您作为版权人的名字;
2, 请您在软件包的一级目录下创建以“LICENSE”为名的文件,将整个许可证文本放入该文件中;
3, 请将如下声明文本放入每个源文件的头部注释中。
Copyright (c) [Year] [name of copyright holder]
[Software Name] is licensed under Mulan PSL v2.
You can use this software according to the terms and conditions of the Mulan PSL v2.
You may obtain a copy of Mulan PSL v2 at:
http://license.coscl.org.cn/MulanPSL2
THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
See the Mulan PSL v2 for more details.
Mulan Permissive Software LicenseVersion 2
Mulan Permissive Software LicenseVersion 2 (Mulan PSL v2)
January 2020 http://license.coscl.org.cn/MulanPSL2
Your reproduction, use, modification and distribution of the Software shall be subject to Mulan PSL v2 (this License) with the following terms and conditions:
0. Definition
Software means the program and related documents which are licensed under this License and comprise all Contribution(s).
Contribution means the copyrightable work licensed by a particular Contributor under this License.
Contributor means the Individual or Legal Entity who licenses its copyrightable work under this License.
Legal Entity means the entity making a Contribution and all its Affiliates.
Affiliates means entities that control, are controlled by, or are under common control with the acting entity under this License, control means direct or indirect ownership of at least fifty percent (50%) of the voting power, capital or other securities of controlled or commonly controlled entity.
1. Grant of Copyright License
Subject to the terms and conditions of this License, each Contributor hereby grants to you a perpetual, worldwide, royalty-free, non-exclusive, irrevocable copyright license to reproduce, use, modify, or distribute its Contribution, with modification or not.
2. Grant of Patent License
Subject to the terms and conditions of this License, each Contributor hereby grants to you a perpetual, worldwide, royalty-free, non-exclusive, irrevocable (except for revocation under this Section) patent license to make, have made, use, offer for sale, sell, import or otherwise transfer its Contribution, where such patent license is only limited to the patent claims owned or controlled by such Contributor now or in future which will be necessarily infringed by its Contribution alone, or by combination of the Contribution with the Software to which the Contribution was contributed. The patent license shall not apply to any modification of the Contribution, and any other combination which includes the Contribution. If you or your Affiliates directly or indirectly institute patent litigation (including a cross claim or counterclaim in a litigation) or other patent enforcement activities against any individual or entity by alleging that the Software or any Contribution in it infringes patents, then any patent license granted to you under this License for the Software shall terminate as of the date such litigation or activity is filed or taken.
3. No Trademark License
No trademark license is granted to use the trade names, trademarks, service marks, or product names of Contributor, except as required to fulfill notice requirements in Section 4.
4. Distribution Restriction
You may distribute the Software in any medium with or without modification, whether in source or executable forms, provided that you provide recipients with a copy of this License and retain copyright, patent, trademark and disclaimer statements in the Software.
5. Disclaimer of Warranty and Limitation of Liability
THE SOFTWARE AND CONTRIBUTION IN IT ARE PROVIDED WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED. IN NO EVENT SHALL ANY CONTRIBUTOR OR COPYRIGHT HOLDER BE LIABLE TO YOU FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO ANY DIRECT, OR INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES ARISING FROM YOUR USE OR INABILITY TO USE THE SOFTWARE OR THE CONTRIBUTION IN IT, NO MATTER HOW ITS CAUSED OR BASED ON WHICH LEGAL THEORY, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
6. Language
THIS LICENSE IS WRITTEN IN BOTH CHINESE AND ENGLISH, AND THE CHINESE VERSION AND ENGLISH VERSION SHALL HAVE THE SAME LEGAL EFFECT. IN THE CASE OF DIVERGENCE BETWEEN THE CHINESE AND ENGLISH VERSIONS, THE CHINESE VERSION SHALL PREVAIL.
END OF THE TERMS AND CONDITIONS
How to Apply the Mulan Permissive Software LicenseVersion 2 (Mulan PSL v2) to Your Software
To apply the Mulan PSL v2 to your work, for easy identification by recipients, you are suggested to complete following three steps:
i Fill in the blanks in following statement, including insert your software name, the year of the first publication of your software, and your name identified as the copyright owner;
ii Create a file named “LICENSE” which contains the whole context of this License in the first directory of your software package;
iii Attach the statement to the appropriate annotated syntax at the beginning of each source file.
Copyright (c) [Year] [name of copyright holder]
[Software Name] is licensed under Mulan PSL v2.
You can use this software according to the terms and conditions of the Mulan PSL v2.
You may obtain a copy of Mulan PSL v2 at:
http://license.coscl.org.cn/MulanPSL2
THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
See the Mulan PSL v2 for more details.
File diff suppressed because it is too large Load Diff
+33
View File
@@ -0,0 +1,33 @@
{
"name": "webpack-demo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "webpack serve --open --mode development",
"build": "webpack --mode development",
"buildprod": "webpack --mode production --progress"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.22.17",
"@babel/preset-env": "^7.22.15",
"babel-loader": "^9.1.3",
"clean-webpack-plugin": "^4.0.0",
"css-loader": "^6.8.1",
"file-loader": "^6.2.0",
"html-webpack-plugin": "^5.5.3",
"javascript-obfuscator": "^4.1.0",
"mini-css-extract-plugin": "^2.7.6",
"sass-loader": "^13.3.2",
"style-loader": "^3.3.3",
"terser-webpack-plugin": "^5.3.10",
"url-loader": "^4.1.1",
"webpack": "^5.88.2",
"webpack-bundle-analyzer": "^4.9.1",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.1"
}
}
@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
</head>
<body>
<!-- 验证码存放的div块 -->
<div id="captcha-box"></div>
<script>
$(function () {
// 样式配置
const config = {
requestCaptchaDataUrl: "http://localhost:8080/gen",
validCaptchaUrl: "http://localhost:8080/check",
bindEl: "#captcha-box"
}
new TAC(config).init();
});
</script>
</body>
</html>
+101
View File
@@ -0,0 +1,101 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
#login-div {
width: 500px;
height: 500px;
border: 1px solid #ff5d39;
position: relative;
}
h1 {
text-align: center;
}
.input {
height: 50px;
width: 100%;
border: 1px solid #ccc;
border-radius: 6px;
margin: 20px auto;
color: #ccc;
line-height: 50px;
text-align: left;
}
.input-left {
border-right: 1px solid #ccc;
text-align: center;
width: 100px;
display: inline-block;
}
.login-btn {
/*margin: 0 auto;*/
display: inline-block;
width: 160px;
height: 50px;
background-color: #4BC065;
color: #fff;
line-height: 50px;
text-align: center;
border-radius: 6px;
}
#captcha-box {
position: absolute;
left: 78px;
top: 83px;
}
</style>
</head>
<body>
<!-- 验证码存放的div块 -->
<div id="login-div">
<!-- 装载验证码的DIV -->
<div id="captcha-box"></div>
<h1>用户登录</h1>
<div class="input">
<span class="input-left">用户名</span>
xxxxx
</div>
<div class="input">
<span class="input-left">密码</span>
xxxxx
</div>
<div class="login-btn" data-type="ROTATE">登录(旋转)</div>
<div class="login-btn" data-type="CONCAT">登录(拼接)</div>
<div class="login-btn" data-type="WORD_IMAGE_CLICK">登录(汉字点选)</div>
<div class="login-btn" data-type="SLIDER">登录(滑块拼图)</div>
</div>
<script>
document.querySelectorAll(".login-btn").forEach(el => {
el.addEventListener("click", e => {
// 样式配置
const config = {
requestCaptchaDataUrl: "http://localhost:8080/gen?type=" + el.dataset.type,
validCaptchaUrl: "http://localhost:8080/check",
bindEl: "#captcha-box"
}
// const style = {
// logoUrl : null
//
// }
const captcha = new TAC(config, null);
captcha.init();
})
})
</script>
</body>
</html>
+108
View File
@@ -0,0 +1,108 @@
# (captcha-web-sdk)
# ([TIANAI-CAPTCHA)](https://gitee.com/tianai/tianai-captcha)验证码前端SDK
| 条目 | |
| -------- | ------------------------------------------------------------ |
| 兼容性 | Chrome、Firefox、Safari、Opera、主流手机浏览器、iOS 及 Android上的内嵌Webview |
| 框架支持 | H5、Angular、React、Vue2、Vue3 |
## 安装
1. 将打包好的`tac`目录放到自己项目中,如果是vue、react等框架,将tac目录放到public目录中、或者放到某个可以访问到地方,比如oss之类的可以被浏览器访问到的地方 (tac下载地址 [https://gitee.com/tianai/tianai-captcha-web-sdk/releases/tag/1.2](https://gitee.com/tianai/tianai-captcha-web-sdk/releases/tag/1.2)
2. 引入初始化函数 (load.js下载地址 [https://minio.tianai.cloud/public/static/captcha/js/load.min.js](https://minio.tianai.cloud/public/static/captcha/js/load.min.js)) 可自己将load.js下载到本地
```html
<script src="load.min.js"></script>
```
**注: 如果是web框架,将该引入代码放到 `public/index.html`**
## 使用方法
2. 创建一个div块用于渲染验证码, 该div用于装载验证码
```html
<div id="captcha-box"></div>
```
3. 在需要调用验证码的时候执行加载验证码方法
```js
function login() {
// config 对象为TAC验证码的一些配置和验证的回调
const config = {
// 生成接口 (必选项,必须配置, 要符合tianai-captcha默认验证码生成接口规范)
requestCaptchaDataUrl: "/gen",
// 验证接口 (必选项,必须配置, 要符合tianai-captcha默认验证码校验接口规范)
validCaptchaUrl: "/check",
// 验证码绑定的div块 (必选项,必须配置)
bindEl: "#captcha-box",
// 验证成功回调函数(必选项,必须配置)
validSuccess: (res, c, tac) => {
// 销毁验证码服务
tac.destroyWindow();
console.log("验证成功,后端返回的数据为", res);
// 调用具体的login方法
login(res.data.token)
},
// 验证失败的回调函数(可忽略,如果不自定义 validFail 方法时,会使用默认的)
validFail: (res, c, tac) => {
console.log("验证码验证失败回调...")
// 验证失败后重新拉取验证码
tac.reloadCaptcha();
},
// 刷新按钮回调事件
btnRefreshFun: (el, tac) => {
console.log("刷新按钮触发事件...")
tac.reloadCaptcha();
},
// 关闭按钮回调事件
btnCloseFun: (el, tac) => {
console.log("关闭按钮触发事件...")
tac.destroyWindow();
}
}
// 一些样式配置, 可不传
let style = {
logoUrl: null;// 去除logo
// logoUrl: "/xx/xx/xxx.png" // 替换成自定义的logo
}
// 参数1 为 tac文件是目录地址, 目录里包含 tac的js和css等文件
// 参数2 为 tac验证码相关配置
// 参数3 为 tac窗口一些样式配置
window.initTAC("./tac", config, style).then(tac => {
tac.init(); // 调用init则显示验证码
}).catch(e => {
console.log("初始化tac失败", e);
})
}
```
### 对滑块的按钮和背景设置为自定义的一些样式
```js
// 这里分享一些作者自己调的样式供参考
const style = {
// 按钮样式
btnUrl: "https://minio.tianai.cloud/public/captcha-btn/btn3.png",
// 背景样式
bgUrl: "https://minio.tianai.cloud/public/captcha-btn/btn3-bg.jpg",
// logo地址
logoUrl: "https://minio.tianai.cloud/public/static/captcha/images/logo.png",
// 滑动边框样式
moveTrackMaskBgColor: "#f7b645",
moveTrackMaskBorderColor: "#ef9c0d"
}
window.initTAC("./tac", config, style).then(tac => {
tac.init(); // 调用init则显示验证码
}).catch(e => {
console.log("初始化tac失败", e);
})
```
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

@@ -0,0 +1,183 @@
import "./captcha.scss"
import Slider from "./slider/slider"
import Rotate from "./rotate/rotate";
import Concat from "./concat/concat";
import Disable from "./disable/disable";
import WordImageClick from "./word_image_click/word_image_click";
import {CaptchaConfig, wrapConfig, wrapStyle} from "./config/config";
import {clearAllPreventDefault} from "./common/common";
const template =
`
<div id="tianai-captcha-parent">
<div id="tianai-captcha-bg-img"></div>
<div id="tianai-captcha-box">
<div id="tianai-captcha-loading" class="loading"></div>
</div>
<!-- 底部 -->
<div class="slider-bottom">
<img class="logo" id="tianai-captcha-logo" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAMAAAAM7l6QAAAAMFBMVEVHcEz3tkX3tkX3tkX3tkX3tkX3tkX3tkX3tkX3tkX3tkX3tkX3tkX3tkX3tkX3tkVmTmjZAAAAD3RSTlMASbTm8wh12hOGoCNiyTV98jvOAAABB0lEQVR42nVT0aIFEQiMorD0/397Lc5a7J0n1UylgIniLRKyDcbBDudZH2DYCAabn3PmTrjeUX+7rJGWx0SqVpzReAfTtKU5fgVCNfxWjB69USUDGwoOiauHpZEpSr0tCx8ILb3Dm3WgBbAlifAJk6+Ww6wqEUmpmIorQVZ1JtqKnDMjkb7AgIpO/wMCaQbuBuEtsBUxhuD9daUaZnApiQB8NAKotMwirGGr6mbXpPnHLHDmy6oy3FgP+1j8IBdVklFc01xUJwv3NR0rIeXV5zpzdlruiijzNq/ufOeKWzZLP3160u5P8RjT1M+HHFtx+PwGyOZqT/D8ROOfjOInTLBIHjy/hvwHxkwPu5cCE1QAAAAASUVORK5CYII=" id="tianai-captcha-logo"></img>
<div class="close-btn" id="tianai-captcha-slider-close-btn"></div>
<div class="refresh-btn" id="tianai-captcha-slider-refresh-btn"></div>
</div>
</div>
`;
function createCaptchaByType(type, tac) {
const box = tac.config.domBindEl.find("#tianai-captcha-box");
const styleConfig = tac.style;
switch (type) {
case "SLIDER":
return new Slider(box, styleConfig);
case "ROTATE":
return new Rotate(box, styleConfig);
case "CONCAT":
return new Concat(box, styleConfig);
case "WORD_IMAGE_CLICK":
return new WordImageClick(box, styleConfig);
case "DISABLED":
return new Disable(box, styleConfig);
default:
return null;
}
}
class TianAiCaptcha {
constructor(config, style) {
this.config = wrapConfig(config);
if (this.config.btnRefreshFun) {
this.btnRefreshFun = this.config.btnRefreshFun;
}
if (this.config.btnCloseFun) {
this.btnCloseFun = this.config.btnCloseFun;
}
this.style = wrapStyle(style);
}
init() {
this.destroyWindow();
this.config.domBindEl.append(template);
this.domTemplate = this.config.domBindEl.find("#tianai-captcha-parent");
clearAllPreventDefault(this.domTemplate);
this.loadStyle();
// 绑定按钮事件
this.config.domBindEl.find("#tianai-captcha-slider-refresh-btn").click((el) => {
this.btnRefreshFun(el, this);
});
this.config.domBindEl.find("#tianai-captcha-slider-close-btn").click((el) => {
this.btnCloseFun(el, this);
});
// 加载验证码
this.reloadCaptcha();
return this;
}
btnRefreshFun(el, tac) {
tac.reloadCaptcha();
}
btnCloseFun(el, tac) {
tac.destroyWindow();
}
reloadCaptcha() {
this.showLoading();
this.destroyCaptcha(() => {
this.createCaptcha();
})
}
showLoading() {
this.config.domBindEl.find("#tianai-captcha-loading").css("display", "block");
}
closeLoading() {
this.config.domBindEl.find("#tianai-captcha-loading").css("display", "none");
}
loadStyle() {
// 设置样式
const bgUrl = this.style.bgUrl;
const logoUrl = this.style.logoUrl;
if (bgUrl) {
// 背景图片
this.config.domBindEl.find("#tianai-captcha-bg-img").css("background-image", "url(" + bgUrl + ")");
}
if (logoUrl && logoUrl !== "") {
// logo
this.config.domBindEl.find("#tianai-captcha-logo").attr("src", logoUrl);
} else if (logoUrl === null){
// 删除logo
this.config.domBindEl.find("#tianai-captcha-logo").css("display", "none");
}
}
destroyWindow() {
if (this.C) {
this.C.destroy();
this.C = undefined;
}
if (this.domTemplate) {
this.domTemplate.remove();
}
}
openCaptcha() {
setTimeout(() => {
this.C.el.css("transform", "translateX(0)")
}, 10)
}
createCaptcha() {
this.config.requestCaptchaData().then(data => {
this.closeLoading();
if (!data.code) {
throw new Error("[TAC] 后台验证码接口数据错误!!!");
}
let captchaType = data.code === 200 ? data.data?.type : "DISABLED"
const captcha = createCaptchaByType(captchaType, this);
if (captcha == null) {
throw new Error("[TAC] 未知的验证码类型[" + captchaType + "]");
}
captcha.init(data, (d, c) => {
// 验证
const currentCaptchaData = c.currentCaptchaData;
const data = {
bgImageWidth: currentCaptchaData.bgImageWidth,
bgImageHeight: currentCaptchaData.bgImageHeight,
templateImageWidth: currentCaptchaData.templateImageWidth,
templateImageHeight: currentCaptchaData.templateImageHeight,
startTime: currentCaptchaData.startTime.getTime(),
stopTime: currentCaptchaData.stopTime.getTime(),
trackList: currentCaptchaData.trackList,
};
if (c.type === 'ROTATE_DEGREE' || c.type === 'ROTATE') {
data.bgImageWidth = c.currentCaptchaData.end;
}
if (currentCaptchaData.data) {
data.data = currentCaptchaData.data;
}
// 清空
const id = c.currentCaptchaData.currentCaptchaId;
c.currentCaptchaData = undefined;
// 调用验证接口
this.config.validCaptcha(id, data, c, this)
})
this.C = captcha;
this.openCaptcha()
});
}
destroyCaptcha(callback) {
if (this.C) {
this.C.el.css("transform", "translateX(300px)")
setTimeout(() => {
this.C.destroy();
if (callback) {
callback();
}
}, 500)
} else {
callback();
}
}
}
export {TianAiCaptcha, CaptchaConfig}
@@ -0,0 +1,107 @@
#tianai-captcha-parent {
box-shadow: 0 0 11px 0 #999999;
width: 318px;
height: 318px;
overflow: hidden;
position: relative;
z-index: 997;
box-sizing: border-box;
border-radius: 5px;
padding: 8px;
#tianai-captcha-box {
height: 260px;
width: 100%;
position: relative;
overflow: hidden;
.loading {
width: 120px;
height: 20px;
-webkit-mask: linear-gradient(90deg, #000 70%, #0000 0) 0/20%;
background: linear-gradient(#f7b645 0 0) 0 / 0% no-repeat #dddddd6b;
animation: cartoon 1s infinite steps(6);
margin: 120px auto;
@keyframes cartoon {
100% {
background-size: 120%;
}
}
}
#tianai-captcha {
transform-style: preserve-3d;
will-change: transform;
transition-duration: .45s;
//transition-timing-function: cubic-bezier(0.36, 0.3, 0.42, 1.5);
transform: translateX(-300px);
}
}
#tianai-captcha-bg-img {
background-color: #fff;
background-position: top;
background-size: cover;
z-index: -1;
width: 100%;
height: 100%;
top: 0;
left: 0;
position: absolute;
border-radius: 6px;
//background-image: url("");
}
.slider-bottom {
.close-btn {
width: 20px;
height: 20px;
background-image: url("@/assets/images/icon.png");
background-repeat: no-repeat;
background-position: 0 -14px;
float: right;
margin-right: 2px;
cursor: pointer;
}
.refresh-btn {
width: 20px;
height: 20px;
background-image: url("@/assets/images/icon.png");
background-position: 0 -167px;
background-repeat: no-repeat;
float: right;
margin-right: 10px;
cursor: pointer;
}
.logo {
height: 30px;
float: left;
}
height: 19px;
width: 100%;
}
.slider-move-shadow {
animation: myanimation 2s infinite;
height: 100%;
width: 5px;
background-color: #fff;
position: absolute;
top: 0;
left: 0;
filter: opacity(0.5);
box-shadow: 1px 1px 1px #fff;
border-radius: 50%;
}
#tianai-captcha-slider-move-track-mask {
border-width: 1px;
border-style: solid;
border-color: #00f4ab;
width: 0;
height: 32px;
background-color: #a9ffe5;
opacity: .5;
position: absolute;
top: -1px;
left: -1px;
border-radius: 5px;
}
}
@@ -0,0 +1,464 @@
/** 是否打印日志 */
var isPrintLog = false;
function printLog(params) {
if (isPrintLog) {
console.log(JSON.stringify(params));
}
}
/**
* 清除默认事件
* @param event event
*/
function clearPreventDefault(event) {
if (event.preventDefault) {
event.preventDefault();
}
}
/**
* 阻止某div默认事件
* @param dom
*/
function clearAllPreventDefault(dom) {
Dom(dom).each((el) => {
// 手机端
el.addEventListener('touchmove', clearPreventDefault, {passive: false});
// pc端
el.addEventListener('mousemove', clearPreventDefault, {passive: false});
});
}
function reductionAllPreventDefault(dom) {
Dom(dom).each(function (el) {
el.removeEventListener('touchmove', clearPreventDefault);
el.addEventListener('mousemove', clearPreventDefault);
});
}
/**
* 获取当前坐标
* @param event 事件
* @returns {{x: number, y: number}}
*/
function getCurrentCoordinate(event) {
if (event.pageX !== null && event.pageX !== undefined) {
return {
x: Math.round(event.pageX),
y: Math.round(event.pageY)
}
}
let targetTouches;
if (event.changedTouches) {
// 抬起事件
targetTouches = event.changedTouches;
} else if (event.targetTouches) {
// pc 按下事件
targetTouches = event.targetTouches;
} else if (event.originalEvent && event.originalEvent.targetTouches) {
// 鼠标触摸事件
targetTouches = event.originalEvent.targetTouches;
}
if (targetTouches[0].pageX !== null && targetTouches[0].pageX !== undefined) {
return {
x: Math.round(targetTouches[0].pageX),
y: Math.round(targetTouches[0].pageY)
}
}
return {
x: Math.round(targetTouches[0].clientX),
y: Math.round(targetTouches[0].clientY)
}
}
function down(currentCaptcha, event) {
// debugger
const coordinate = getCurrentCoordinate(event);
let startX = coordinate.x;
let startY = coordinate.y;
currentCaptcha.currentCaptchaData.startX = startX;
currentCaptcha.currentCaptchaData.startY = startY;
const trackList = currentCaptcha.currentCaptchaData.trackList;
currentCaptcha.currentCaptchaData.startTime = new Date();
const startTime = currentCaptcha.currentCaptchaData.startTime;
trackList.push({
x: coordinate.x,
y: coordinate.y,
type: "down",
t: (new Date().getTime() - startTime.getTime())
});
printLog(["start", startX, startY])
currentCaptcha.__m__ = move.bind(null, currentCaptcha);
currentCaptcha.__u__ = up.bind(null, currentCaptcha);
// pc
window.addEventListener("mousemove", currentCaptcha.__m__);
window.addEventListener("mouseup", currentCaptcha.__u__);
// 手机端
window.addEventListener("touchmove", currentCaptcha.__m__, false);
window.addEventListener("touchend", currentCaptcha.__u__, false);
if (currentCaptcha && currentCaptcha.doDown) {
currentCaptcha.doDown(event, currentCaptcha)
}
}
function move(currentCaptcha, event) {
if (event.touches && event.touches.length > 0) {
event = event.touches[0];
}
// debugger
const coordinate = getCurrentCoordinate(event);
let pageX = coordinate.x;
let pageY = coordinate.y;
const startX = currentCaptcha.currentCaptchaData.startX;
const startY = currentCaptcha.currentCaptchaData.startY;
const startTime = currentCaptcha.currentCaptchaData.startTime;
const end = currentCaptcha.currentCaptchaData.end;
const bgImageWidth = currentCaptcha.currentCaptchaData.bgImageWidth;
const trackList = currentCaptcha.currentCaptchaData.trackList;
let moveX = pageX - startX;
let moveY = pageY - startY;
const track = {
x: coordinate.x,
y: coordinate.y,
type: "move",
t: (new Date().getTime() - startTime.getTime())
};
trackList.push(track);
if (moveX < 0) {
moveX = 0;
} else if (moveX > end) {
moveX = end;
}
currentCaptcha.currentCaptchaData.moveX = moveX;
currentCaptcha.currentCaptchaData.moveY = moveY;
if (currentCaptcha.doMove) {
currentCaptcha.doMove(event, currentCaptcha);
}
printLog(["move", track])
}
function destroyEvent(currentCaptcha) {
if (currentCaptcha) {
if (currentCaptcha.__m__) {
window.removeEventListener("mousemove", currentCaptcha.__m__);
window.removeEventListener("touchmove", currentCaptcha.__m__);
}
if (currentCaptcha.__u__) {
window.removeEventListener("mouseup", currentCaptcha.__u__);
window.removeEventListener("touchend", currentCaptcha.__u__);
}
}
}
function up(currentCaptcha, event) {
destroyEvent(currentCaptcha);
const coordinate = getCurrentCoordinate(event);
currentCaptcha.currentCaptchaData.stopTime = new Date();
const startTime = currentCaptcha.currentCaptchaData.startTime;
const trackList = currentCaptcha.currentCaptchaData.trackList;
const track = {
x: coordinate.x,
y: coordinate.y,
type: "up",
t: (new Date().getTime() - startTime.getTime())
}
trackList.push(track);
printLog(["up", track])
printLog(["tracks", trackList])
if (currentCaptcha.doUp) {
currentCaptcha.doUp(event, currentCaptcha)
}
currentCaptcha.endCallback(currentCaptcha.currentCaptchaData, currentCaptcha);
}
function initConfig(bgImageWidth, bgImageHeight, templateImageWidth, templateImageHeight, end) {
// bugfix 图片宽高可能会有小数情况,强转一下整数
const currentCaptchaConfig = {
startTime: new Date(),
trackList: [],
movePercent: 0,
clickCount: 0,
bgImageWidth: Math.round(bgImageWidth),
bgImageHeight: Math.round(bgImageHeight),
templateImageWidth: Math.round(templateImageWidth),
templateImageHeight: Math.round(templateImageHeight),
end: end
}
printLog(["init", currentCaptchaConfig]);
return currentCaptchaConfig;
}
function closeTips(el, callback) {
const tipEl = Dom(el).find("#tianai-captcha-tips");
tipEl.removeClass("tianai-captcha-tips-on")
// tipEl.removeClass("tianai-captcha-tips-success")
// tipEl.removeClass("tianai-captcha-tips-error")
// 延时
if (callback) {
setTimeout(callback, .35);
}
}
function showTips(el, msg, type, callback) {
const tipEl = Dom(el).find("#tianai-captcha-tips");
tipEl.text(msg);
if (type === 1) {
// 成功
tipEl.removeClass("tianai-captcha-tips-error")
tipEl.addClass("tianai-captcha-tips-success")
} else {
// 失败
tipEl.removeClass("tianai-captcha-tips-success")
tipEl.addClass("tianai-captcha-tips-error")
}
tipEl.addClass("tianai-captcha-tips-on");
// 延时
setTimeout(callback, 1000);
}
class CommonCaptcha {
showTips(msg, type, callback) {
showTips(this.el, msg, type, callback)
}
closeTips(msg, callback) {
closeTips(this.el, msg, callback)
}
}
function Dom(domStr, dom) {
return new DomEl(domStr, dom);
}
class DomEl {
constructor(domStr, dom) {
if (dom && typeof dom === 'object' && typeof dom.nodeType !== 'undefined') {
this.dom = dom;
this.domStr = domStr;
return;
}
if (domStr instanceof DomEl) {
this.dom = domStr.dom;
this.domStr = domStr.domStr;
} else if (typeof domStr === "string") {
this.dom = document.querySelector(domStr)
this.domStr = domStr;
} else if (typeof document === 'object' && typeof document.nodeType !== 'undefined') {
this.dom = domStr;
this.domStr = domStr.nodeName;
} else {
throw new Error("不支持的类型");
}
}
each(callback) {
this.getTarget().querySelectorAll("*").forEach(callback);
}
removeClass(className) {
let element = this.getTarget();
if (element.classList) {
// 使用 classList API 移除类
element.classList.remove(className);
} else {
// 兼容旧版本浏览器
const currentClass = element.className;
const regex = new RegExp('(?:^|\\s)' + className + '(?!\\S)', 'g');
element.className = currentClass.replace(regex, '');
}
return this;
}
addClass(className) {
const element = this.getTarget();
if (element.classList) {
// 使用 classList API 添加类
element.classList.add(className);
} else {
// 兼容旧版本浏览器
let currentClass = element.className;
if (currentClass.indexOf(className) === -1) {
element.className = currentClass + ' ' + className;
}
}
return this;
}
find(str) {
const el = this.getTarget().querySelector(str);
if (el) {
return new DomEl(str, el);
}
return null;
}
children(selector) {
const childNodes = this.getTarget().childNodes;
for (let i = 0; i < childNodes.length; i++) {
if (childNodes[i].nodeType === 1 && childNodes[i].matches(selector)) {
return new DomEl(selector, childNodes[i]);
}
}
return null;
}
remove() {
this.getTarget().remove();
return null;
}
css(property, value) {
if (typeof property === 'string' && typeof value === 'string') {
// 设置单个属性
this.getTarget().style[property] = value;
} else if (typeof property === 'object') {
// 设置多个属性
for (var prop in property) {
if (property.hasOwnProperty(prop)) {
this.getTarget().style[prop] = property[prop];
}
}
} else if (typeof property === 'string' && typeof value === 'undefined') {
// 获取单个属性
return window.getComputedStyle(element)[property];
}
}
attr(attributeName, value) {
if (value === undefined) {
// 如果未提供值,则返回属性的当前值
return this.getTarget().getAttribute(attributeName);
} else {
// 如果提供了值,则设置属性的值
this.getTarget().setAttribute(attributeName, value);
}
return this;
}
text(str) {
this.getTarget().innerText = str;
return this;
}
html(str) {
this.getTarget().innerHtml = str;
return this;
}
is(dom) {
if (dom && typeof dom === 'object' && typeof dom.nodeType !== 'undefined') {
return this.dom === dom;
}
if (dom instanceof DomEl) {
return this.dom === dom.dom;
}
}
append(content) {
if (typeof content === 'string') {
this.getTarget().insertAdjacentHTML("beforeend", content);
} else if (content instanceof HTMLElement) {
this.getTarget().appendChild(content);
} else {
throw new Error('Invalid content type');
}
return this;
}
click(fun) {
this.on("click", fun);
return this;
}
mousedown(fun) {
this.on("mousedown", fun);
return this;
}
touchstart(fun) {
this.on("touchstart", fun);
return this;
}
on(eventType, fun) {
this.getTarget().addEventListener(eventType, fun, {passive: true});
return this;
}
width() {
return this.getTarget().offsetWidth;
}
height() {
return this.getTarget().offsetHeight;
}
getTarget() {
if (this.dom) {
return this.dom;
}
throw new Error("dom不存在: [" + this.domStr + "]");
}
}
function http(options) {
return new Promise(function (resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.open(options.method || 'GET', options.url);
// 设置请求头
if (options.headers) {
for (const header in options.headers) {
if (options.headers.hasOwnProperty(header)) {
xhr.setRequestHeader(header, options.headers[header]);
}
}
}
xhr.onreadystatechange = function () {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status >= 200 && xhr.status <= 500) {
const contentType = xhr.getResponseHeader('Content-Type');
if (contentType && contentType.indexOf('application/json') !== -1) {
resolve(JSON.parse(xhr.responseText));
} else {
resolve(xhr.responseText);
}
} else {
reject(new Error('Request failed with status: ' + xhr.status));
}
}
};
xhr.onerror = function () {
reject(new Error('Network Error'));
};
xhr.send(options.data);
});
}
function isEmptyObject(obj) {
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
return false; // 对象不为空
}
}
return true; // 对象为空
}
export {
isEmptyObject,
http,
Dom,
DomEl,
CommonCaptcha,
clearAllPreventDefault,
down,
move,
up,
initConfig,
showTips,
closeTips,
destroyEvent
}
@@ -0,0 +1,87 @@
#tianai-captcha {
text-align: left;
box-sizing: content-box;
width: 300px;
height: 260px;
z-index: 999;
.slider-bottom .logo {
height: 30px;
}
.slider-bottom {
height: 19px;
width: 100%;
}
.content {
.tianai-captcha-tips {
height: 25px;
width: 100%;
position: absolute;
bottom: -25px;
left: 0;
z-index: 999;
font-size: 15px;
line-height: 25px;
/*background-color: #FF5D39;*/
color: #fff;
text-align: center;
/* transform: translateY(0px); */
/* display: none; */
/* transition: max-height 0.5s; */
transition: bottom .3s ease-in-out;
}
.tianai-captcha-tips.tianai-captcha-tips-error {
background-color: #FF5D39;
}
.tianai-captcha-tips.tianai-captcha-tips-success {
background-color: #39C522;
}
.tianai-captcha-tips.tianai-captcha-tips-on {
bottom: 0;
}
#tianai-captcha-loading {
z-index: 9999;
background-color: #f5f5f5;
text-align: center;
height: 100%;
overflow: hidden;
position: relative;
display: flex;
justify-content: center;
align-items: center;
img {
display: block;
width: 45px;
height: 45px;
}
}
}
#tianai-captcha-slider-bg-canvas {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
border-radius: 5px;
}
#tianai-captcha-slider-bg-div{
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
border-radius: 5px;
.tianai-captcha-slider-bg-div-slice {
position: absolute;
}
}
}
@keyframes myanimation {
from {
left: 0;
}
to {
left: 289px;
}
}
@@ -0,0 +1,111 @@
import "../common/common.scss"
import "@/captcha/slider/slider.scss"
import "./concat.scss"
import {Dom, CommonCaptcha, clearAllPreventDefault, down, initConfig, destroyEvent} from "../common/common.js"
const TYPE = "CONCAT";
function getTemplate(styleConfig) {
return `
<div id="tianai-captcha" class="tianai-captcha-slider tianai-captcha-concat">
<div class="slider-tip">
<span id="tianai-captcha-slider-move-track-font" >拖动滑块完成拼图</span>
</div>
<div class="content">
<div class="tianai-captcha-slider-concat-img-div" id="tianai-captcha-slider-concat-img-div">
<img id="tianai-captcha-slider-concat-slider-img" src="" alt/>
</div>
<div class="tianai-captcha-slider-concat-bg-img"></div>
<div class="tianai-captcha-tips" id="tianai-captcha-tips"></div>
</div>
<div class="slider-move">
<div class="slider-move-track">
<div id="tianai-captcha-slider-move-track-mask"></div>
<div class="slider-move-shadow"></div>
</div>
<div class="slider-move-btn" id="tianai-captcha-slider-move-btn">
</div>
</div>
</div>
`;
}
class Concat extends CommonCaptcha {
constructor(divId, styleConfig) {
super();
this.boxEl = Dom(divId);
this.styleConfig = styleConfig;
this.type = TYPE;
this.currentCaptchaData = {}
}
init(captchaData, endCallback, loadSuccessCallback) {
// 重载样式
this.destroy();
this.boxEl.append(getTemplate(this.styleConfig));
this.el = this.boxEl.find("#tianai-captcha");
this.loadStyle();
// 按钮绑定事件
this.el.find("#tianai-captcha-slider-move-btn").mousedown(down.bind(null,this));
this.el.find("#tianai-captcha-slider-move-btn").touchstart(down.bind(null,this));
clearAllPreventDefault(this.el);
// 绑定全局
window.currentCaptcha = this;
// 载入验证码
this.loadCaptchaForData(this, captchaData);
this.endCallback = endCallback;
if (loadSuccessCallback) {
// 加载成功
loadSuccessCallback(this);
}
return this;
}
destroy() {
destroyEvent();
const existsCaptchaEl = this.boxEl.children("#tianai-captcha");
if (existsCaptchaEl) {
existsCaptchaEl.remove();
}
}
doMove() {
const moveX = this.currentCaptchaData.moveX;
this.el.find("#tianai-captcha-slider-move-btn").css("transform", "translate(" + moveX + "px, 0px)")
this.el.find("#tianai-captcha-slider-concat-img-div").css("background-position-x", moveX + "px");
this.el.find("#tianai-captcha-slider-move-track-mask").css("width", moveX + "px")
}
loadStyle() {
let sliderImg = "";
let moveTrackMaskBorderColor = "#00f4ab";
let moveTrackMaskBgColor = "#a9ffe5";
const styleConfig = this.styleConfig;
if (styleConfig) {
sliderImg = styleConfig.btnUrl;
moveTrackMaskBgColor = styleConfig.moveTrackMaskBgColor;
moveTrackMaskBorderColor = styleConfig.moveTrackMaskBorderColor;
}
this.el.find(".slider-move .slider-move-btn").css("background-image", "url(" + sliderImg + ")");
// this.el.find("#tianai-captcha-slider-move-track-font").text(title);
this.el.find("#tianai-captcha-slider-move-track-mask").css("border-color", moveTrackMaskBorderColor);
this.el.find("#tianai-captcha-slider-move-track-mask").css("background-color", moveTrackMaskBgColor);
}
loadCaptchaForData(that, data) {
const bgImg = that.el.find(".tianai-captcha-slider-concat-bg-img");
const sliderImg = that.el.find("#tianai-captcha-slider-concat-img-div");
bgImg.css("background-image", "url(" + data.data.backgroundImage + ")");
sliderImg.css("background-image", "url(" + data.data.backgroundImage + ")");
sliderImg.css("background-position", "0px 0px");
var backgroundImageHeight = data.data.backgroundImageHeight;
var height = ((backgroundImageHeight - data.data.data.randomY) / backgroundImageHeight) * 180;
sliderImg.css("height", height+"px");
that.currentCaptchaData = initConfig(bgImg.width(), bgImg.height(), sliderImg.width(), sliderImg.height(), 300 - 63 + 5);
that.currentCaptchaData.currentCaptchaId = data.data.id;
}
}
export default Concat;
@@ -0,0 +1,17 @@
#tianai-captcha.tianai-captcha-concat {
.tianai-captcha-slider-concat-img-div {
background-size: 100% 180px;
position: absolute;
transform: translate(0px, 0px);
/* border-bottom: 1px solid blue; */
z-index: 1;
width: 100%;
}
.tianai-captcha-slider-concat-bg-img {
width: 100%;
height: 100%;
position: absolute;
transform: translate(0px, 0px);
background-size: 100% 180px;
}
}
@@ -0,0 +1,211 @@
import StyleConfig from "./styleConfig";
import {Dom,http} from "../common/common";
class CaptchaConfig {
constructor(args) {
if (!args.bindEl) {
throw new Error("[TAC] 必须配置 [bindEl]用于将验证码绑定到该元素上");
}
if (!args.requestCaptchaDataUrl) {
throw new Error("[TAC] 必须配置 [requestCaptchaDataUrl]请求验证码接口");
}
if (!args.validCaptchaUrl) {
throw new Error("[TAC] 必须配置 [validCaptchaUrl]验证验证码接口");
}
this.bindEl = args.bindEl;
this.domBindEl = Dom(args.bindEl);
this.requestCaptchaDataUrl = args.requestCaptchaDataUrl;
this.validCaptchaUrl = args.validCaptchaUrl;
if (args.validSuccess) {
this.validSuccess = args.validSuccess;
}
if (args.validFail) {
this.validFail = args.validFail;
}
if (args.requestHeaders) {
this.requestHeaders = args.requestHeaders
}else {
this.requestHeaders = {}
}
if (args.btnCloseFun) {
this.btnCloseFun = args.btnCloseFun;
}
if (args.btnRefreshFun) {
this.btnRefreshFun = args.btnRefreshFun;
}
this.requestChain = [];
// 时间戳转换
this.timeToTimestamp = args.timeToTimestamp || true;
this.insertRequestChain(0, {
preRequest(type, param, c, tac) {
if (this.timeToTimestamp && param.data) {
for (let key in param.data){
// 将date全部转换为时间戳
if (param.data[key] instanceof Date) {
param.data[key] = param.data[key].getTime();
}
}
}
return true;
}
})
}
addRequestChain(fun) {
this.requestChain.push(fun);
}
insertRequestChain(index,chain) {
this.requestChain.splice(index, 0, chain);
}
removeRequestChain(index) {
this.requestChain.splice(index, 1);
}
requestCaptchaData() {
const requestParam = {}
requestParam.headers = this.requestHeaders || {};
requestParam.data = {};
// 设置默认值
requestParam.headers["Content-Type"] = "application/json;charset=UTF-8";
requestParam.method="POST";
requestParam.url = this.requestCaptchaDataUrl;
// 请求前装载参数
this._preRequest("requestCaptchaData", requestParam);
// 发送请求
const request = this.doSendRequest(requestParam);
// 返回结果
return request.then(res => {
// 装返回结果
this._postRequest("requestCaptchaData", requestParam, res);
// 返回结果
return res;
});
}
doSendRequest(requestParam) {
// 如果content-type是json,那么data就是json字符串, 这里直接匹配所有header是否包含application/json
if (requestParam.headers ) {
for (const key in requestParam.headers){
if(requestParam.headers[key].indexOf("application/json") > -1) {
if (typeof requestParam.data !== "string") {
requestParam.data = JSON.stringify(requestParam.data);
}
break;
}
}
}
return http(requestParam).then(res => {
try {
return JSON.parse(res);
}catch (e) {
return res;
}
})
}
_preRequest(type, requestParam, c, tac) {
for (let i = 0; i < this.requestChain.length; i++) {
const r = this.requestChain[i];
if (r.preRequest) {
if (!r.preRequest(type, requestParam, this, c, tac)) {
break;
}
}
}
}
_postRequest(type, requestParam, res, c, tac) {
for (let i = 0; i < this.requestChain.length; i++) {
const r = this.requestChain[i];
// 判断r是否存圩postRequest方法
if (r.postRequest) {
if (!r.postRequest(type, requestParam, res, this, c, tac)) {
break;
}
}
}
}
validCaptcha(currentCaptchaId, data, c, tac) {
const sendParam = {
id: currentCaptchaId,
data: data
};
let requestParam = {};
requestParam.headers = this.requestHeaders || {};
requestParam.data = sendParam;
requestParam.headers["Content-Type"] = "application/json;charset=UTF-8";
requestParam.method="POST";
requestParam.url = this.validCaptchaUrl;
this._preRequest("validCaptcha", requestParam, c, tac);
const request = this.doSendRequest(requestParam);
return request.then(res => {
this._postRequest("validCaptcha", requestParam, res, c, tac);
return res;
}).then(res => {
if (res.code == 200) {
const useTimes = (data.stopTime - data.startTime) / 1000;
c.showTips(`验证成功,耗时${useTimes}`, 1, () => this.validSuccess(res, c, tac));
} else {
let tipMsg = "验证失败,请重新尝试!";
if (res.code) {
if (res.code != 4001) {
tipMsg = "验证码被黑洞吸走了!";
}
}
c.showTips(tipMsg, 0, () => this.validFail(res, c, tac));
}
}).catch(e => {
let tipMsg = c.styleConfig.i18n.tips_error;
if (e.code && e.code != 200) {
if (res.code != 4001) {
tipMsg = c.styleConfig.i18n.tips_4001;
}
c.showTips(tipMsg, 0, () => this.validFail(res, c, tac));
}
})
}
validSuccess(res, c, tac) {
console.log("验证码校验成功, 请重写 [config.validSuccess] 方法, 用于自定义逻辑处理")
window.currentCaptchaRes = res;
tac.destroyWindow();
}
validFail(res, c, tac) {
tac.reloadCaptcha();
}
}
function wrapConfig(config) {
if (config instanceof CaptchaConfig) {
return config;
}
return new CaptchaConfig(config);
}
function wrapStyle(style) {
// if (!style) {
// style = {}
// }
//
// if (!style.btnUrl) {
// // 设置默认图片
// style.btnUrl = "data:image/png;base64,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";
// }
// 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,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",
// 移动边框背景颜色
moveTrackMaskBgColor: "#89d2ff",
// 移动边框颜色
moveTrackMaskBorderColor: "#0298f8",
// 文字提示
i18n: {
tips_success: "验证成功,耗时%s秒",
tips_error : "验证失败,请重新尝试!",
slider_title:"拖动滑块完成拼图",
concat_title: "拖动滑块完成拼图",
image_click_title: "请依次点击:",
rotate_title: "拖动滑块完成拼图",
// TITLE 大小
slider_title_size:"15px",
concat_title_size: "15px",
image_click_title_size: "20px",
rotate_title_size: "15px",
}
}
@@ -0,0 +1,59 @@
const TYPE = "DISABLE";
import "./disable.scss"
import {Dom} from "../common/common";
function getTemplate(styleConfig) {
return `
<div id="tianai-captcha" class="tianai-captcha-disable">
<div class="slider-tip">
<span id="tianai-captcha-slider-move-track-font" style="font-size: ${styleConfig.i18n.disable_title_size}">${styleConfig.i18n.disable_title}</span>
</div>
<div class="content">
<div class="bg-img-div">
<!-- <svg width="100" height="100" viewBox="0 0 100 100">-->
<!-- <polygon points="50,10 90,90 10,90" fill="none" stroke="#FF9900" stroke-width="4"/>-->
<!-- <path d="M50 35V65 M50 75V75" stroke="#FF9900" stroke-width="4" stroke-linecap="round"/>-->
<!-- </svg>-->
<span id="content-span"></span>
</div>
</div>
</div>
`;
}
class Disable {
constructor(boxEl, styleConfig) {
this.boxEl = boxEl;
this.styleConfig = styleConfig;
this.type = TYPE;
this.currentCaptchaData = {}
}
init(captchaData, endCallback, loadSuccessCallback) {
// 重载样式
this.destroy();
this.boxEl.append(getTemplate(this.styleConfig));
this.el = this.boxEl.find("#tianai-captcha");
// 绑定全局
// window.currentCaptcha = this;
// 载入验证码
this.loadCaptchaForData(this, captchaData);
this.endCallback = endCallback;
if (loadSuccessCallback) {
// 加载成功
loadSuccessCallback(this);
}
return this;
}
destroy () {
const existsCaptchaEl = this.boxEl.find("#tianai-captcha");
if (existsCaptchaEl) {
existsCaptchaEl.remove();
}
}
loadCaptchaForData (that, data) {
const msg = data.msg || data.message || "接口异常";
that.el.find("#content-span").text(msg);
}
}
export default Disable;
@@ -0,0 +1,56 @@
#tianai-captcha.tianai-captcha-disable{
z-index: 999;
position: absolute;
left: 0;
top: 0;
.content {
width: 100%;
height: 180px;
position: relative;
overflow: hidden;
.bg-img-div {
background-image: url("@/assets/images/dun.jpeg");
width: 100%;
height: 100%;
overflow: hidden;
#content-span {
color: #fff;
overflow: hidden;
margin-top: 132px;
display: block;
text-align: center;
}
}
}
}
//#tianai-captcha.tianai-captcha-disable {
// z-index: 999;
// position: absolute;
// left: 0;
// top: 0;
//
// .content {
// width: 100%;
// height: 180px;
// position: relative;
// overflow: hidden;
//
// .bg-img-div {
// display: flex;
// justify-content: center;
// flex-direction: column;
// align-items: center;
// background-color: #0A3850;
// width: 100%;
// height: 100%;
// overflow: hidden;
//
// #content-span {
// color: #fff;
// overflow: hidden;
// display: block;
// text-align: center;
// }
// }
// }
//}
@@ -0,0 +1,114 @@
import "./image_click.scss"
import {Dom,CommonCaptcha,move, initConfig, destroyEvent} from "../common/common.js"
/**
* 滑动验证码
*/
const TYPE = "IMAGE_CLICK"
function getTemplate(styleConfig) {
return `
<div id="tianai-captcha" class="tianai-captcha-slider tianai-captcha-word-click">
<div class="click-tip">
<span id="tianai-captcha-click-track-font" style="font-size: ${styleConfig.i18n.image_click_title_size}">${styleConfig.i18n.image_click_title}</span>
<img src="" id="tianai-captcha-tip-img" class="tip-img">
</div>
<div class="content">
<div class="bg-img-div">
<img id="tianai-captcha-slider-bg-img" src="" alt/>
<canvas id="tianai-captcha-slider-bg-canvas"></canvas>
<div id="bg-img-click-mask"></div>
</div>
<div class="tianai-captcha-tips" id="tianai-captcha-tips"></div>
</div>
<div class="click-confirm-btn">确定</div>
</div>
`;
}
class ImageClick extends CommonCaptcha{
constructor(boxEl, styleConfig) {
super();
this.boxEl = boxEl;
this.styleConfig = styleConfig;
this.type = TYPE;
this.currentCaptchaData = {}
}
init(captchaData, endCallback, loadSuccessCallback) {
// 重载样式
this.destroy();
this.boxEl.append(getTemplate(this.styleConfig));
this.el = this.boxEl.find("#tianai-captcha");
// 绑定全局
// window.currentCaptcha = this;
// 载入验证码
this.loadCaptchaForData(this, captchaData);
this.endCallback = endCallback;
const moveFun = move.bind(null, this);
// 绑定事件
this.el.find("#bg-img-click-mask").click((event) => {
if(event.target.className === "click-span") {
return;
}
this.currentCaptchaData.clickCount++;
const trackList = this.currentCaptchaData.trackList;
if (this.currentCaptchaData.clickCount === 1) {
this.currentCaptchaData.startTime = new Date();
// move 轨迹
window.addEventListener("mousemove", moveFun);
this.currentCaptchaData.startX = event.offsetX;
this.currentCaptchaData.startY = event.offsetY;
}
const startTime = this.currentCaptchaData.startTime;
trackList.push({
x: Math.round(event.offsetX),
y: Math.round(event.offsetY),
type: "click",
t: (new Date().getTime() - startTime.getTime())
});
const left = event.offsetX - 10;
const top = event.offsetY - 10;
this.el.find("#bg-img-click-mask").append("<span class='click-span' style='left:" + left + "px;top: " + top + "px'>" + this.currentCaptchaData.clickCount + "</span>")
// if (this.currentCaptchaData.clickCount === 4) {
// // 校验
// this.currentCaptchaData.stopTime = new Date();
// window.removeEventListener("mousemove", move);
// this.endCallback(this.currentCaptchaData,this);
// }
});
this.el.find(".click-confirm-btn").click(() => {
if (this.currentCaptchaData.clickCount > 0) {
// 校验
this.currentCaptchaData.stopTime = new Date();
window.removeEventListener("mousemove", moveFun);
this.endCallback(this.currentCaptchaData,this);
}
});
if (loadSuccessCallback) {
// 加载成功
loadSuccessCallback(this);
}
return this;
}
destroy () {
const existsCaptchaEl = this.boxEl.children("#tianai-captcha");
if (existsCaptchaEl) {
existsCaptchaEl.remove();
}
destroyEvent();
}
loadCaptchaForData (that, data) {
const bgImg = that.el.find("#tianai-captcha-slider-bg-img");
const tipImg = that.el.find("#tianai-captcha-tip-img");
bgImg.on("load",() => {
that.currentCaptchaData = initConfig(bgImg.width(), bgImg.height(), tipImg.width(), tipImg.height());
that.currentCaptchaData.currentCaptchaId = data.data.id;
})
bgImg.attr("src", data.data.backgroundImage);
tipImg.attr("src", data.data.templateImage);
}
}
export default ImageClick;
@@ -0,0 +1,64 @@
#tianai-captcha.tianai-captcha-word-click {
//position: relative;
box-sizing: border-box;
//padding-top: 10px;
.click-tip {
position: relative;
height: 40px;
width: 100%;
.tip-img {
height: 35px;
position: absolute;
right: 15px;
}
#tianai-captcha-click-track-font {
font-size: 18px;
display: inline-block;
height: 40px;
line-height: 40px;
position: absolute;
}
}
.slider-bottom {
position: relative;
top: 6px;
}
.content {
#bg-img-click-mask {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
.click-span {
position: absolute;
left: 0;
top: 0;
border-radius: 50px;
background-color: #409eff;
width: 20px;
height: 20px;
text-align: center;
line-height: 20px;
color: #fff;
border: 2px solid #fff;
box-sizing: content-box;
}
}
}
.click-confirm-btn {
width: 100%;
height: 35px;
border-radius: 4px;
background-image: linear-gradient(173deg, hsl(38.09deg 91% 57.89%) 0%, hsl(38.09deg 89.38% 71.74%) 100%);
font-size: 15px;
text-align: center;
box-sizing: border-box;
line-height: 35px;
color: #fff;
margin-top: 3px;
}
.click-confirm-btn:hover{
cursor: pointer
}
}
@@ -0,0 +1,106 @@
import "@/captcha/slider/slider.scss"
import "./rotate.scss"
import {Dom,CommonCaptcha, down, initConfig, destroyEvent} from "../common/common.js"
/**
* 滑动验证码
*/
const TYPE = "ROTATE"
function getTemplate(styleConfig) {
return `
<div id="tianai-captcha" class="tianai-captcha-slider tianai-captcha-rotate">
<div class="slider-tip">
<span id="tianai-captcha-slider-move-track-font" style="font-size: ${styleConfig.i18n.rotate_title_size}">${styleConfig.i18n.rotate_title}</span>
</div>
<div class="content">
<div class="bg-img-div">
<img id="tianai-captcha-slider-bg-img" src="" alt/>
<canvas id="tianai-captcha-slider-bg-canvas"></canvas>
</div>
<div class="rotate-img-div" id="tianai-captcha-slider-img-div">
<img id="tianai-captcha-slider-move-img" src="" alt/>
</div>
<div class="tianai-captcha-tips" id="tianai-captcha-tips"></div>
</div>
<div class="slider-move">
<div class="slider-move-track">
<div id="tianai-captcha-slider-move-track-mask"></div>
<div class="slider-move-shadow"></div>
</div>
<div class="slider-move-btn" id="tianai-captcha-slider-move-btn">
</div>
</div>
</div>
`;
}
class Rotate extends CommonCaptcha{
constructor(boxEl, styleConfig) {
super();
this.boxEl = boxEl;
this.styleConfig = styleConfig;
this.type = TYPE;
this.currentCaptchaData = {}
}
init(captchaData, endCallback, loadSuccessCallback) {
// 重载样式
this.destroy();
this.boxEl.append(getTemplate(this.styleConfig));
this.el = this.boxEl.find("#tianai-captcha");
this.loadStyle();
// 按钮绑定事件
this.el.find("#tianai-captcha-slider-move-btn").mousedown(down.bind(null,this));
this.el.find("#tianai-captcha-slider-move-btn").touchstart(down.bind(null,this));
// 绑定全局
// window.currentCaptcha = this;
// 载入验证码
this.loadCaptchaForData(this, captchaData);
this.endCallback = endCallback;
if (loadSuccessCallback) {
// 加载成功
loadSuccessCallback(this);
}
return this;
}
destroy () {
const existsCaptchaEl = this.boxEl.children("#tianai-captcha");
if (existsCaptchaEl) {
existsCaptchaEl.remove();
}
destroyEvent();
}
doMove() {
const moveX = this.currentCaptchaData.moveX;
this.el.find("#tianai-captcha-slider-move-btn").css("transform", "translate(" + moveX + "px, 0px)")
this.el.find("#tianai-captcha-slider-move-img").css("transform", "rotate(" + (moveX / (this.currentCaptchaData.end / 360)) + "deg)")
this.el.find("#tianai-captcha-slider-move-track-mask").css("width", moveX + "px")
}
loadStyle () {
let sliderImg = "";
let moveTrackMaskBorderColor = "#00f4ab";
let moveTrackMaskBgColor = "#a9ffe5";
const styleConfig = this.styleConfig;
if (styleConfig) {
sliderImg = styleConfig.btnUrl;
moveTrackMaskBgColor = styleConfig.moveTrackMaskBgColor;
moveTrackMaskBorderColor = styleConfig.moveTrackMaskBorderColor;
}
this.el.find(".slider-move .slider-move-btn").css("background-image", "url(" + sliderImg + ")");
// this.el.find("#tianai-captcha-slider-move-track-font").text(title);
this.el.find("#tianai-captcha-slider-move-track-mask").css("border-color", moveTrackMaskBorderColor);
this.el.find("#tianai-captcha-slider-move-track-mask").css("background-color", moveTrackMaskBgColor);
}
loadCaptchaForData (that, data) {
const bgImg = that.el.find("#tianai-captcha-slider-bg-img");
const sliderImg = that.el.find("#tianai-captcha-slider-move-img");
bgImg.attr("src", data.data.backgroundImage);
sliderImg.attr("src", data.data.templateImage);
bgImg.on("load",() => {
that.currentCaptchaData = initConfig(bgImg.width(), bgImg.height(), sliderImg.width(), sliderImg.height(), 300 - 63 + 5);
that.currentCaptchaData.currentCaptchaId = data.data.id;
});
}
}
export default Rotate;
@@ -0,0 +1,12 @@
#tianai-captcha.tianai-captcha-rotate {
.rotate-img-div {
height: 100%;
/*position: absolute;*/
text-align: center;
img {
height: 100%;
transform: rotate(0deg);
display: inline-block;
}
}
}
@@ -0,0 +1,123 @@
import "../common/common.scss"
import "./slider.scss"
import {
Dom,
CommonCaptcha,
closeTips,
down,
initConfig,
showTips,
destroyEvent
} from "../common/common.js"
/**
* 滑动验证码
*/
const TYPE = "SLIDER"
function getTemplate(styleConfig) {
return `
<div id="tianai-captcha" class="tianai-captcha-slider">
<div class="slider-tip">
<span id="tianai-captcha-slider-move-track-font" style="font-size: ${styleConfig.i18n.slider_title_size}">${styleConfig.i18n.slider_title}</span>
</div>
<div class="content">
<div class="bg-img-div">
<img id="tianai-captcha-slider-bg-img" src="" alt/>
<canvas id="tianai-captcha-slider-bg-canvas"></canvas>
<div id="tianai-captcha-slider-bg-div"></div>
</div>
<div class="slider-img-div" id="tianai-captcha-slider-img-div">
<img id="tianai-captcha-slider-move-img" src="" alt/>
</div>
<div class="tianai-captcha-tips" id="tianai-captcha-tips"></div>
</div>
<div class="slider-move">
<div class="slider-move-track">
<div id="tianai-captcha-slider-move-track-mask"></div>
<div class="slider-move-shadow"></div>
</div>
<div class="slider-move-btn" id="tianai-captcha-slider-move-btn">
</div>
</div>
</div>
`
}
class Slider extends CommonCaptcha{
constructor(boxEl, styleConfig) {
super();
this.boxEl = boxEl;
this.styleConfig = styleConfig;
this.type = TYPE;
this.currentCaptchaData = {}
}
init(captchaData, endCallback, loadSuccessCallback) {
// 重载样式
this.destroy();
this.boxEl.append(getTemplate(this.styleConfig));
this.el = this.boxEl.find("#tianai-captcha");
this.loadStyle();
// 按钮绑定事件
this.el.find("#tianai-captcha-slider-move-btn").mousedown(down.bind(null,this));
this.el.find("#tianai-captcha-slider-move-btn").touchstart( down.bind(null,this));
// 绑定全局
// window.currentCaptcha = this;
// 载入验证码
this.loadCaptchaForData(this, captchaData);
this.endCallback = endCallback;
if (loadSuccessCallback) {
// 加载成功
loadSuccessCallback(this);
}
return this;
}
showTips(msg, type,callback) {
showTips(this.el, msg,type, callback)
}
closeTips(callback) {
closeTips(this.el, callback)
}
destroy () {
const existsCaptchaEl = this.boxEl.children("#tianai-captcha");
if (existsCaptchaEl) {
existsCaptchaEl.remove();
}
destroyEvent();
}
doMove() {
const moveX = this.currentCaptchaData.moveX;
this.el.find("#tianai-captcha-slider-move-btn").css("transform", "translate(" + moveX + "px, 0px)")
this.el.find("#tianai-captcha-slider-img-div").css("transform", "translate(" + moveX + "px, 0px)")
this.el.find("#tianai-captcha-slider-move-track-mask").css("width", moveX + "px")
}
loadStyle () {
let sliderImg = "";
let moveTrackMaskBorderColor = "#00f4ab";
let moveTrackMaskBgColor = "#a9ffe5";
const styleConfig = this.styleConfig;
if (styleConfig) {
sliderImg = styleConfig.btnUrl;
moveTrackMaskBgColor = styleConfig.moveTrackMaskBgColor;
moveTrackMaskBorderColor = styleConfig.moveTrackMaskBorderColor;
}
this.el.find(".slider-move .slider-move-btn").css("background-image", "url(" + sliderImg + ")");
// this.el.find("#tianai-captcha-slider-move-track-font").text(title);
this.el.find("#tianai-captcha-slider-move-track-mask").css("border-color", moveTrackMaskBorderColor);
this.el.find("#tianai-captcha-slider-move-track-mask").css("background-color", moveTrackMaskBgColor);
}
loadCaptchaForData (that, data) {
const bgImg = that.el.find("#tianai-captcha-slider-bg-img");
const sliderImg = that.el.find("#tianai-captcha-slider-move-img");
bgImg.attr("src", data.data.backgroundImage);
sliderImg.attr("src", data.data.templateImage);
bgImg.on("load",() => {
that.currentCaptchaData = initConfig(bgImg.width(), bgImg.height(), sliderImg.width(), sliderImg.height(), 300 - 63 + 5);
that.currentCaptchaData.currentCaptchaId = data.data.id;
});
}
}
export default Slider;
@@ -0,0 +1,88 @@
#tianai-captcha.tianai-captcha-slider {
z-index: 999;
position: absolute;
left: 0;
top: 0;
.content {
width: 100%;
height: 180px;
position: relative;
overflow: hidden;
}
.bg-img-div {
width: 100%;
height: 100%;
position: absolute;
transform: translate(0px, 0px);
img {
height: 100%;
width: 100%;
border-radius: 5px;
}
}
.slider-img-div {
height: 100%;
position: absolute;
left: 0;
transform: translate(0px, 0px);
#tianai-captcha-slider-move-img {
height: 100%;
}
}
.slider-move {
height: 34px;
width: 100%;
margin: 11px 0;
position: relative;
}
.slider-move-track {
position: relative;
height: 32px;
line-height: 32px;
text-align: center;
background: #f5f5f5;
color: #999;
transition: 0s;
font-size: 14px;
box-sizing: content-box;
border: 1px solid #f5f5f5;
border-radius: 4px;
}
.refresh-btn, .close-btn {
display: inline-block;
}
.slider-move {
line-height: 38px;
font-size: 14px;
text-align: center;
white-space: nowrap;
color: #88949d;
-moz-user-select: none;
-webkit-user-select: none;
user-select: none;
filter: opacity(.8);
}
.slider-move .slider-move-btn {
transform: translate(0px, 0px);
position: absolute;
top: -6px;
left: 0;
width: 63px;
height: 45px;
background-color: #fff;
background-repeat: no-repeat;
background-size: contain;
border-radius: 5px;
}
.slider-tip {
margin-bottom: 5px;
font-weight: bold;
font-size: 15px;
line-height: normal;
color: black;
}
.slider-move-btn:hover {
cursor: pointer
}
user-select: none;
}
@@ -0,0 +1,13 @@
import ImageClick from "../image_click/image_click"
/**
* 滑动验证码
*/
const TYPE = "WORD_IMAGE_CLICK"
class WordImageClick extends ImageClick {
constructor(divId, styleConfig) {
super(divId, styleConfig);
this.type = TYPE;
}
}
export default WordImageClick;
+4
View File
@@ -0,0 +1,4 @@
import {CaptchaConfig, TianAiCaptcha} from "./captcha/captcha";
window.TAC = TianAiCaptcha;
window.CaptchaConfig = CaptchaConfig;
File diff suppressed because one or more lines are too long
@@ -0,0 +1,14 @@
const HtmlWebpackPlugin = require("html-webpack-plugin");
const path = require("path");
module.exports = {
output: {
filename: "tac/js/tac.js",
path: path.resolve(__dirname, "./dist")
},
plugins: [
new HtmlWebpackPlugin({
filename: 'index.html',
template: './public/index.html'
}),
]
}
+78
View File
@@ -0,0 +1,78 @@
const webpack = require('webpack')
const {merge} = require("webpack-merge")
const devConfig = require("./webpack.config.dev")
const prodConfig = require("./webpack.config.prod")
const path = require("path");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const {CleanWebpackPlugin} = require("clean-webpack-plugin");
const commonConfig = {
mode: 'development',
entry: "./src/index.js",
output: {
filename: "tac.js",
path: path.resolve(__dirname, "./dist")
},
resolve: {
alias: {
"@": path.join(__dirname, "./src") // 这样@符号就表示项目根目录中src这一层路径
}
},
module: {
rules: [
{
test: /\.(css)$/,
use: [MiniCssExtractPlugin.loader, 'css-loader'],
},
{
test: /\.s[ac]ss$/,
use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"],
},
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
use: {
loader: 'file-loader',
options: {
esModule: false,
name: '[name].[ext]',
outputPath: 'tac/images'
}
},
type: 'javascript/auto'
},
// {
// test: /\.js$/,
// exclude: /node_modules/,
// loader: 'babel-loader',
// options: {
// // 预设babel做怎样的兼容性处理
// presets: ['@babel/preset-env']
// }
// }
]
},
plugins: [
new MiniCssExtractPlugin({
// 指定抽离的之后形成的文件名
filename: 'tac/css/tac.css'
}),
new webpack.HotModuleReplacementPlugin(),
new CleanWebpackPlugin()
],
devServer: {
// 开发时可直接访问到 ./public 下的静态资源,这些资源在开发中不必打包
port: 3000,
static: "./dist"
}
}
module.exports = (env, argv) => {
if (argv && argv.mode === 'production') {
console.log("=============production==================")
return merge(commonConfig, prodConfig);
}else {
console.log("=============development==================")
return merge(commonConfig, devConfig);
}
}
@@ -0,0 +1,32 @@
const HtmlWebpackPlugin = require("html-webpack-plugin");
const path = require("path");
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
optimization: {
minimize: true,
minimizer: [new TerserPlugin({
terserOptions: {
compress: {
drop_console: true, // 移除所有的`console`语句
},
output: {
comments: false, // 去掉注释
},
},
extractComments: false, // 是否将注释提取到单独的文件中
})],
},
externals: {
},
output: {
filename: "tac/js/tac.min.js",
path: path.resolve(__dirname, "./dist")
},
plugins: [
new HtmlWebpackPlugin({
filename: 'index.html',
template: './public/index-prod.html'
})
]
}
+52
View File
@@ -0,0 +1,52 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cloud.tianai.captcha</groupId>
<artifactId>tianai-captcha-parent</artifactId>
<version>${revision}</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>tianai-captcha</artifactId>
<name>tianai-captcha</name>
<description>行为验证码</description>
<url>https://gitee.com/tianai/tianai-captcha</url>
<properties>
<java.version>1.8</java.version>
<!-- 打包跳过单元测试 -->
<skipTests>true</skipTests>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.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>
<configuration>
<source>8</source>
<target>8</target>
<compilerArgument>-parameters</compilerArgument>
</configuration>
</plugin>
</plugins>
</build>
</project>

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