Skip to content

Commit b47f16a

Browse files
committed
slider
0 parents  commit b47f16a

28 files changed

+6113
-0
lines changed

README.md

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# SpringBoot + 原生JS自实现滑块验证码
2+
3+
## 功能描述:
4+
5+
从服务端静态文件夹随机读取一张本地图片,然后随机生成坐标(x, y),并将坐标x保存在session会话中,在坐标(x, y)处抠出滑块形状,设置透明度阴影更明显(可调),然后将生成的背景图片和滑块图片传递给前端,前端渲染这两张图片构成滑块验证码,监听拖拉事件,将拖拉停止处的 x坐标 传递给服务端进行校验,若差值在一定范围内则验证通过。
6+
7+
## 优点:
8+
9+
坐标(x, y)为服务端生成,不会传递给前端,因此前端无法窃取到 x坐标 的值,只能通过认为拉取滑块,有效防止了机器盗刷接口(比如短信、登录等)
10+
11+
加以鼠标拖动过程中y轴轨迹、以及拖动时间辅助验证
12+
13+
因本人使用原因,此滑块验证码会读取屏幕宽度,整体宽度 = 屏幕宽度
14+
15+
## 效果图:
16+
17+
![image-20240523203731245](image/README.assets/image-20240523203731245.png)
18+
19+
![image-20240523203845946](image/README.assets/image-20240523203845946.png)
20+
21+
## 运行方式:
22+
23+
```
24+
git clone https://github.com/spongehah/slider-captcha.git
25+
```
26+
27+
打开SpringBoot项目,可直接运行,访问方式:http://localhost:8080
28+
29+
## 自定义:
30+
31+
在index.html文件中:
32+
33+
```html
34+
<script>
35+
var captcha = sliderCaptcha({
36+
id: 'captcha',
37+
width: 280, //画布宽度(因为整体宽度 = 屏幕宽度,所以此参数废弃)
38+
height: 155, //画布高度(因为整体宽度 = 屏幕宽度,所以此参数废弃)
39+
offset: 5, //容错偏差:(废弃)
40+
loadingText: '正在加载中...',
41+
failedText: '再试一次',
42+
barText: '向右滑动填充拼图',
43+
repeatIcon: 'fa fa-redo',
44+
remoteUrl: 'http://localhost:8080', //服务端访问路径前缀
45+
onSuccess: function () { //滑块验证码验证成功调用(可替换)
46+
var handler = setTimeout(function () {
47+
window.clearTimeout(handler);
48+
}, 500);
49+
},
50+
onFail: function () { //滑块验证码验证失败调用(可替换)
51+
var handler = setTimeout(function () {
52+
window.clearTimeout(handler);
53+
}, 500);
54+
},
55+
verify: function (startTime, endTime, left, trail, url) {//向服务端发送参数进行校验
56+
var result = false;
57+
$.ajax({
58+
url: url + "/verifyCode", //url为上面弄自定义的remoteUrl,拼接上服务端验证接口的uri
59+
data: {
60+
startTime: startTime,
61+
endTime: endTime,
62+
left: left,
63+
trail: trail
64+
},
65+
async: false,
66+
cache: false,
67+
type: 'POST',
68+
contentType: 'application/x-www-form-urlencoded',
69+
dataType: 'json',
70+
success: function (response) {
71+
result = response;
72+
},
73+
error: function (error) {
74+
console.log(error);
75+
}
76+
});
77+
return result;
78+
}
79+
});
80+
</script>
81+
</body>
82+
```
83+
84+
在longbow.slidercaptcha.js文件中:
85+
86+
第94 ~ 114 行:
87+
88+
```js
89+
_proto.initSliderCaptcha = function () {
90+
var that = this;
91+
var data;
92+
$.ajax({
93+
url: this.options.remoteUrl + '/getSliderCaptcha', //拼接上服务端获取两张图片的接口uri
94+
type: 'GET',
95+
async: false,
96+
cache: false,
97+
data: {
98+
width: Math.floor(that.options.width),
99+
height: Math.floor(that.options.height)
100+
},
101+
success: function (res) {
102+
data = res;
103+
},
104+
error: function () {
105+
console.error('Failed to fetch slider captcha data');
106+
}
107+
});
108+
that.init(data);
109+
};
110+
```
254 KB
Loading
209 KB
Loading

pom.xml

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
<groupId>org.example</groupId>
8+
<artifactId>slider-demo</artifactId>
9+
<version>1.0-SNAPSHOT</version>
10+
<parent>
11+
<groupId>org.springframework.boot</groupId>
12+
<artifactId>spring-boot-dependencies</artifactId>
13+
<version>2.6.13</version>
14+
</parent>
15+
16+
<properties>
17+
<java.version>1.8</java.version>
18+
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
19+
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
20+
<spring-boot.version>2.6.13</spring-boot.version>
21+
</properties>
22+
23+
<dependencies>
24+
<dependency>
25+
<groupId>org.springframework.boot</groupId>
26+
<artifactId>spring-boot-starter-web</artifactId>
27+
</dependency>
28+
<dependency>
29+
<groupId>org.springframework.boot</groupId>
30+
<artifactId>spring-boot-starter-test</artifactId>
31+
<scope>test</scope>
32+
</dependency>
33+
<dependency>
34+
<groupId>org.springframework.boot</groupId>
35+
<artifactId>spring-boot-starter-thymeleaf</artifactId>
36+
</dependency>
37+
</dependencies>
38+
39+
<build>
40+
<plugins>
41+
<plugin>
42+
<groupId>org.apache.maven.plugins</groupId>
43+
<artifactId>maven-compiler-plugin</artifactId>
44+
<version>3.8.1</version>
45+
<configuration>
46+
<source>1.8</source>
47+
<target>1.8</target>
48+
<encoding>UTF-8</encoding>
49+
</configuration>
50+
</plugin>
51+
<plugin>
52+
<groupId>org.springframework.boot</groupId>
53+
<artifactId>spring-boot-maven-plugin</artifactId>
54+
<version>${spring-boot.version}</version>
55+
<configuration>
56+
<mainClass>com.example.demo.DemoApplication</mainClass>
57+
<skip>true</skip>
58+
</configuration>
59+
<executions>
60+
<execution>
61+
<id>repackage</id>
62+
<goals>
63+
<goal>repackage</goal>
64+
</goals>
65+
</execution>
66+
</executions>
67+
</plugin>
68+
</plugins>
69+
</build>
70+
</project>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.hah.demo;
2+
3+
import org.springframework.boot.SpringApplication;
4+
import org.springframework.boot.autoconfigure.SpringBootApplication;
5+
6+
@SpringBootApplication
7+
public class SliderApplication {
8+
9+
public static void main(String[] args) {
10+
SpringApplication.run(SliderApplication.class);
11+
}
12+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.hah.demo.controller;
2+
3+
import com.hah.demo.service.ISliderCaptchaService;
4+
import org.springframework.beans.factory.annotation.Autowired;
5+
import org.springframework.http.ResponseEntity;
6+
import org.springframework.stereotype.Controller;
7+
import org.springframework.web.bind.annotation.*;
8+
9+
import java.util.ArrayList;
10+
import java.util.Map;
11+
12+
@Controller
13+
public class SliderCaptchaController {
14+
15+
@Autowired
16+
private ISliderCaptchaService sliderCaptchaService;
17+
18+
@GetMapping("/index")
19+
public String index() {
20+
return "index";
21+
}
22+
23+
@GetMapping("/getSliderCaptcha")
24+
@ResponseBody
25+
public ResponseEntity<Map<String, String>> getSliderCaptcha(@RequestParam(value = "width", defaultValue = "280") Integer width,
26+
@RequestParam(value = "height", defaultValue = "155") Integer height) {
27+
return ResponseEntity.ok(sliderCaptchaService.getSliderImage(width, height));
28+
}
29+
30+
@PostMapping("/verifyCode")
31+
@ResponseBody
32+
public boolean verifyCode(Long startTime, Long endTime, Integer left, ArrayList<Integer> trail) {
33+
return sliderCaptchaService.verifyCode(startTime, endTime, left, trail);
34+
}
35+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.hah.demo.service;
2+
3+
import java.util.List;
4+
import java.util.Map;
5+
6+
public interface ISliderCaptchaService {
7+
8+
Map<String, String> getSliderImage(Integer width, Integer height);
9+
10+
boolean verifyCode(Long startTime, Long endTime, Integer left, List<Integer> trail);
11+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package com.hah.demo.service.impl;
2+
3+
import com.hah.demo.service.ISliderCaptchaService;
4+
import com.hah.demo.utils.DrawCaptchaUtil;
5+
import org.springframework.beans.factory.annotation.Autowired;
6+
import org.springframework.stereotype.Service;
7+
import org.springframework.util.ClassUtils;
8+
9+
import javax.servlet.http.HttpServletRequest;
10+
import javax.servlet.http.HttpSession;
11+
import java.io.File;
12+
import java.util.*;
13+
14+
@Service
15+
public class SliderCaptchaService implements ISliderCaptchaService {
16+
17+
@Autowired
18+
private HttpServletRequest request;
19+
20+
@Override
21+
public Map<String, String> getSliderImage(Integer width, Integer height) {
22+
String imageFilePath = ClassUtils.getDefaultClassLoader().getResource("static").getPath();
23+
File bgImageFile = new File(imageFilePath + "/images/");
24+
File slideImageFile = new File(imageFilePath + "/slide/slide1.png");// 若要无边框选择slide.png
25+
if (bgImageFile.isDirectory()) {
26+
File[] files = bgImageFile.listFiles();
27+
Random random = new Random();
28+
int i = random.nextInt(files.length);
29+
bgImageFile = files[i];
30+
}
31+
int[] point = DrawCaptchaUtil.randomAnchorPoint(width, height);
32+
HttpSession session = request.getSession();
33+
session.setAttribute(session.getId(), point[0]);
34+
String bgImageBase64 = DrawCaptchaUtil.getBgImageBase64(bgImageFile, slideImageFile, point, width, height);
35+
String slideImageBase64 = DrawCaptchaUtil.getSlideImageBase64(bgImageFile, slideImageFile, point, width, height);
36+
Map<String, String> images = new HashMap<>();
37+
images.put("bgImage", bgImageBase64);
38+
images.put("slideImage", slideImageBase64);
39+
return images;
40+
}
41+
42+
@Override
43+
public boolean verifyCode(Long startTime, Long endTime, Integer left, List<Integer> trail) {
44+
try {
45+
// 校验时间
46+
long dif = endTime - startTime;
47+
if (dif > 10000L) {// 只允许拖10s
48+
return false;
49+
}
50+
// 校验最后落点
51+
HttpSession session = request.getSession();
52+
Integer offset = (Integer) session.getAttribute(session.getId());
53+
// 获取offset后立即删除,防止重复验证
54+
session.removeAttribute(session.getId());
55+
Integer padding = offset - left;
56+
if (padding > 5 || padding < -5) {
57+
return false;
58+
}
59+
// 校验y轴轨迹
60+
int sum = 0;
61+
for (Integer num : trail) {
62+
sum += num;
63+
}
64+
double avg = sum * 1.0 / trail.size();
65+
double sum2 = 0.0;
66+
for (Integer data : trail) {
67+
sum2 += Math.pow(data - avg, 2);
68+
}
69+
double stddev = sum2 / trail.size();
70+
return stddev != 0;
71+
} catch (Exception e) {
72+
return false;
73+
}
74+
}
75+
}

0 commit comments

Comments
 (0)