大家好,我是一安,最近接到一个将登录验证码改为滑块验证码的需求。
原始登录验证码为了绿盟渗透加了很多复杂的处理,虽然通过绿盟安检,但地市用户多次反馈改造后验证码自己都不好识别加上超过失败登陆次数会临时锁定IP,故领导建议修改为加减乘除法的形式或者滑块的形式。
小编这里最后选择了滑块的形式,滑动验证码一方面对用户体验来说,比较新颖,操作简单,另一方面相对图形验证码来说,安全性并没有很大的降低。
滑块验证码原理
-
1.从服务器随机取一张图片,并对图片上的随机x,y坐标和宽高一块区域抠图; -
2.根据步骤一的坐标和宽高,使用二维数组保存原图上抠图区域的像素点坐标; -
3.根据步骤二的坐标点,对原图的抠图区域的颜色进行处理。 -
4.完成以上步骤之后得到两张图(扣下来的方块图,带有抠图区域阴影的原图),将这两张图和抠图区域的y坐标传到前台,前端在移动方块验证时,将移动后的x坐标传递到后台与原来的x坐标作比较,如果在阈值内则验证通过。 -
5.请求验证的步骤:前台向后台发起请求,后台随机一张图片做处理将处理完的两张图片的base64,抠图y坐标和token(token为后台缓存验证码的唯一token,可以用缓存和分布式缓存)返回给前台。 -
6.前台滑动图片将x坐标和token作为参数请求后台验证,服务器根据token取出x坐标与参数的x进行比较。
具体实现
原项目是采用SSH+JSP框架,代码也适配SpingBoot
Captcha
public class Captcha{
//生成的画布的base64
private String canvasSrc;
//画布宽度
private Integer canvasWidth;
//画布高度
private Integer canvasHeight;
//生成的阻塞块的base64
private String blockSrc;
//阻塞块宽度
private Integer blockWidth;
//阻塞块高度
private Integer blockHeight;
//阻塞块凸凹半径
private Integer blockRadius;
// 阻塞块的横轴坐标
private Integer blockX;
// 阻塞块的纵轴坐标
private Integer blockY;
public String getCanvasSrc() {
return canvasSrc;
}
public void setCanvasSrc(String canvasSrc) {
this.canvasSrc = canvasSrc;
}
public Integer getCanvasWidth() {
return canvasWidth;
}
public void setCanvasWidth(Integer canvasWidth) {
this.canvasWidth = canvasWidth;
}
public Integer getCanvasHeight() {
return canvasHeight;
}
public void setCanvasHeight(Integer canvasHeight) {
this.canvasHeight = canvasHeight;
}
public String getBlockSrc() {
return blockSrc;
}
public void setBlockSrc(String blockSrc) {
this.blockSrc = blockSrc;
}
public Integer getBlockWidth() {
return blockWidth;
}
public void setBlockWidth(Integer blockWidth) {
this.blockWidth = blockWidth;
}
public Integer getBlockHeight() {
return blockHeight;
}
public void setBlockHeight(Integer blockHeight) {
this.blockHeight = blockHeight;
}
public Integer getBlockRadius() {
return blockRadius;
}
public void setBlockRadius(Integer blockRadius) {
this.blockRadius = blockRadius;
}
public Integer getBlockX() {
return blockX;
}
public void setBlockX(Integer blockX) {
this.blockX = blockX;
}
public Integer getBlockY() {
return blockY;
}
public void setBlockY(Integer blockY) {
this.blockY = blockY;
}
@Override
public String toString() {
return "{"canvasSrc":"" + canvasSrc + "", "canvasWidth":"
+ canvasWidth + ", "canvasHeight":" + canvasHeight
+ ", "blockSrc":"" + blockSrc + "", "blockWidth":" + blockWidth
+ ", "blockHeight":" + blockHeight + ", "blockRadius":"
+ blockRadius + ", "blockX":" + blockX + ", "blockY":" + blockY
+ "}";
}
}
生成滑块验证码
适配SpringBoot项目,复制生成滑块验证码过程代码即可
//保存用户和验证码
public static Map<String, Captcha>map_captcha = new HashMap<String, Captcha>();
public void getCaptcha(ActionMapping mapping, ActionForm form,HttpServletRequest request, HttpServletResponse response)throws AAAException {
response.setContentType("application/json;charset=gb2312");
response.setCharacterEncoding("gb2312");
PrintWriter pw = null;
try {
Captcha captcha = new Captcha();
//参数校验默认值
checkCaptcha(captcha);
//获取画布的宽高
int canvasWidth = captcha.getCanvasWidth();
int canvasHeight = captcha.getCanvasHeight();
//获取阻塞块的宽高/半径
int blockWidth = captcha.getBlockWidth();
int blockHeight = captcha.getBlockHeight();
int blockRadius = captcha.getBlockRadius();
//获取资源图
BufferedImage canvasImage = getBufferedImage("F:/testExample/paas_test/src/main/resources/image/%s.jpg");
//调整原图到指定大小
canvasImage = imageResize(canvasImage, canvasWidth, canvasHeight);
//随机生成阻塞块坐标
int blockX = getNonceByRange(blockWidth, canvasWidth - blockWidth - 10);
int blockY = getNonceByRange(10, canvasHeight - blockHeight + 1);
//阻塞块
BufferedImage blockImage = new BufferedImage(blockWidth, blockHeight, BufferedImage.TYPE_4BYTE_ABGR);
//新建的图像根据轮廓图颜色赋值,源图生成遮罩
cutByTemplate(canvasImage, blockImage, blockWidth, blockHeight, blockRadius, blockX, blockY);
captcha.setBlockX(blockX);
captcha.setBlockY(blockY);
captcha.setBlockSrc(toBase64(blockImage, "png"));
captcha.setCanvasSrc(toBase64(canvasImage, "png"));
//也可以存储到Redis
map_captcha.put(request.getParameter("j_username"), captcha);
pw = response.getWriter();
pw.write(captcha.toString());
pw.flush();
} catch (Exception e) {
e.printStackTrace();
}finally {
if (null != pw) {
pw.close();
}
}
}
工具类
参数校验默认值checkCaptcha
/**
* @param captcha
* @return void
* @description 入参校验设置默认值
*/
private static void checkCaptcha(Captcha captcha) {
//设置画布宽度默认值
if (captcha.getCanvasWidth() == null) {
captcha.setCanvasWidth(320);
}
//设置画布高度默认值
if (captcha.getCanvasHeight() == null) {
captcha.setCanvasHeight(155);
}
//设置阻塞块宽度默认值
if (captcha.getBlockWidth() == null) {
captcha.setBlockWidth(65);
}
//设置阻塞块高度默认值
if (captcha.getBlockHeight() == null) {
captcha.setBlockHeight(55);
}
//设置阻塞块凹凸半径默认值
if (captcha.getBlockRadius() == null) {
captcha.setBlockRadius(9);
}
}
获取验证码资源图getBufferedImage
/**
* @return java.awt.image.BufferedImage
* @description 获取验证码资源图
*/
private static BufferedImage getBufferedImage(String img_path) {
try {
//随机图片,获取验证码资源图
int nonce = getNonceByRange(0, 19);
//获取网络资源图片
String imgPath = String.format(img_path, nonce);
File file = new File(imgPath);
return ImageIO.read(file);
} catch (Exception e) {
log.error("获取拼图资源失败");
//异常处理
return null;
}
}
调整图片大小imageResize
/**
* @param bufferedImage
* @param width
* @param height
* @return java.awt.image.BufferedImage
* @description 调整图片大小
*/
public static BufferedImage imageResize(BufferedImage bufferedImage, int width, int height) {
Image image = bufferedImage.getScaledInstance(width, height, Image.SCALE_SMOOTH);
BufferedImage resultImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics2D = resultImage.createGraphics();
graphics2D.drawImage(image, 0, 0, null);
graphics2D.dispose();
return resultImage;
}
获取指定范围内的随机数getNonceByRange
/**
* @param start
* @param end
* @return int
* @description 获取指定范围内的随机数
*/
public static int getNonceByRange(int start, int end) {
Random random = new Random();
//结果为一个0~(end - start + 1)的任意整数
return random.nextInt(end - start + 1) + start;
}
抠图,并生成阻塞块cutByTemplate
/**
* @param canvasImage
* @param blockImage
* @param blockWidth
* @param blockHeight
* @param blockRadius
* @param blockX
* @param blockY
* @return void
* @description 抠图,并生成阻塞块
*/
private static void cutByTemplate(BufferedImage canvasImage, BufferedImage blockImage, int blockWidth, int blockHeight, int blockRadius, int blockX, int blockY) {
BufferedImage waterImage = new BufferedImage(blockWidth, blockHeight, BufferedImage.TYPE_4BYTE_ABGR);
//阻塞块的轮廓图
int[][] blockData = getBlockData(blockWidth, blockHeight, blockRadius);
//创建阻塞块具体形状
for (int i = 0; i < blockWidth; i++) {
for (int j = 0; j < blockHeight; j++) {
try {
//原图中对应位置变色处理
if (blockData[i][j] == 1) {
//背景设置为黑色
waterImage.setRGB(i, j, Color.BLACK.getRGB());
blockImage.setRGB(i, j, canvasImage.getRGB(blockX + i, blockY + j));
//轮廓设置为白色,取带像素和无像素的界点,判断该点是不是临界轮廓点
if (blockData[i + 1][j] == 0 || blockData[i][j + 1] == 0 || blockData[i - 1][j] == 0 || blockData[i][j - 1] == 0) {
blockImage.setRGB(i, j, Color.WHITE.getRGB());
waterImage.setRGB(i, j, Color.WHITE.getRGB());
}
}
//这里把背景设为透明
else {
blockImage.setRGB(i, j, Color.TRANSLUCENT);
waterImage.setRGB(i, j, Color.TRANSLUCENT);
}
} catch (ArrayIndexOutOfBoundsException e) {
//防止数组下标越界异常
}
}
}
//在画布上添加阻塞块水印
addBlockWatermark(canvasImage, waterImage, blockX, blockY);
}
构建拼图轮廓轨迹getBlockData
/**
* @param blockWidth
* @param blockHeight
* @param blockRadius
* @return int[][]
* @description 构建拼图轮廓轨迹
*/
private static int[][] getBlockData(int blockWidth, int blockHeight, int blockRadius) {
int[][] data = new int[blockWidth][blockHeight];
double po = Math.pow(blockRadius, 2);
//随机生成两个圆的坐标,在4个方向上 随机找到2个方向添加凸/凹
//凸/凹1
int face1 = RandomUtils.nextInt(4);
//凸/凹2
int face2;
//保证两个凸/凹不在同一位置
do {
face2 = RandomUtils.nextInt(4);
} while (face1 == face2);
//获取凸/凹起位置坐标
int[] circle1 = getCircleCoords(face1, blockWidth, blockHeight, blockRadius);
int[] circle2 = getCircleCoords(face2, blockWidth, blockHeight, blockRadius);
//随机凸/凹类型
int shape = getNonceByRange(0, 1);
//圆的标准方程 (x-a)²+(y-b)²=r²,标识圆心(a,b),半径为r的圆
//计算需要的小图轮廓,用二维数组来表示,二维数组有两张值,0和1,其中0表示没有颜色,1有颜色
for (int i = 0; i < blockWidth; i++) {
for (int j = 0; j < blockHeight; j++) {
data[i][j] = 0;
//创建中间的方形区域
if ((i >= blockRadius && i <= blockWidth - blockRadius && j >= blockRadius && j <= blockHeight - blockRadius)) {
data[i][j] = 1;
}
double d1 = Math.pow(i - Objects.requireNonNull(circle1)[0], 2) + Math.pow(j - circle1[1], 2);
double d2 = Math.pow(i - Objects.requireNonNull(circle2)[0], 2) + Math.pow(j - circle2[1], 2);
//创建两个凸/凹
if (d1 <= po || d2 <= po) {
data[i][j] = shape;
}
}
}
return data;
}
根据朝向获取圆心坐标getCircleCoords
/**
* @param face
* @param blockWidth
* @param blockHeight
* @param blockRadius
* @return int[]
* @description 根据朝向获取圆心坐标
*/
private static int[] getCircleCoords(int face, int blockWidth, int blockHeight, int blockRadius) {
//上
if (0 == face) {
return new int[]{blockWidth / 2 - 1, blockRadius};
}
//左
else if (1 == face) {
return new int[]{blockRadius, blockHeight / 2 - 1};
}
//下
else if (2 == face) {
return new int[]{blockWidth / 2 - 1, blockHeight - blockRadius - 1};
}
//右
else if (3 == face) {
return new int[]{blockWidth - blockRadius - 1, blockHeight / 2 - 1};
}
return null;
}
画布上添加阻塞块水印addBlockWatermark
/**
* @param canvasImage
* @param blockImage
* @param x
* @param y
* @return void
* @description 在画布上添加阻塞块水印
*/
private static void addBlockWatermark(BufferedImage canvasImage, BufferedImage blockImage, int x, int y) {
Graphics2D graphics2D = canvasImage.createGraphics();
graphics2D.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, 0.8f));
graphics2D.drawImage(blockImage, x, y, null);
graphics2D.dispose();
}
BufferedImage转BASE64toBase64
/**
* @param bufferedImage
* @param type
* @return java.lang.String
* @description BufferedImage转BASE64
*/
public static String toBase64(BufferedImage bufferedImage, String type) {
try {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ImageIO.write(bufferedImage, type, byteArrayOutputStream);
String base64 = Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
return String.format("data:image/%s;base64,%s", type, base64);
} catch (IOException e) {
log.error("图片资源转换BASE64失败");
//异常处理
return null;
}
}
页面
<%@ page contentType="text/html;charset=gb2312"%>
<%@ include file="/commons/taglibs.jsp"%>
<%
String path = request.getContextPath();
String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path +"/";
%>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>滑块验证码</title>
<script
crossorigin="anonymous"
integrity="sha384-nvAa0+6Qg9clwYCGGPpDQLVpLNn0fRaROjHqs13t4Ggj3Ez50XnGQqc/r8MhnRDZ"
src="https://lib.baomitu.com/jquery/1.12.4/jquery.min.js"
></script>
<style>
#model {
position: fixed;
top: 0px;
left: 0px;
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.3);
display: none;
align-items: center;
justify-content: center;
}
.card {
box-sizing: border-box;
margin: 0;
padding: 0;
color: rgba(0, 0, 0, 0.85);
font-size: 14px;
font-variant: tabular-nums;
line-height: 1.5715;
list-style: none;
font-feature-settings: tnum;
position: relative;
background: #fff;
border-radius: 6px;
}
.card-header {
min-height: 57px;
margin-bottom: -1px;
padding: 0 24px;
color: rgba(0, 0, 0, 0.85);
font-weight: 500;
font-size: 16px;
background: transparent;
border-bottom: 1px solid #f0f0f0;
border-radius: 6px 6px 0;
line-height: 57px;
display: flex;
align-items: center;
justify-content: space-between;
}
.card-header .close {
width: 14px;
cursor: pointer;
}
.card-content {
width: 350px;
padding: 24px;
}
.canvas-wrap {
position: relative;
}
#slider {
border: 1px solid #e4e7eb;
width: 100%;
background-color: #f7f9fa;
height: 40px;
border-radius: 4px;
position: relative;
}
#slider-bar {
height: 40px;
width: 40px;
background-color: #1890ff;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
}
#slider-track {
position: absolute;
height: 40px;
border: 1px solid #1890ff;
background-color: rgba(24, 144, 255, 0.2);
box-sizing: border-box;
width: 0px;
left: 0px;
top: 0px;
}
.arrow {
width: 18px;
}
</style>
</head>
<body>
<div id="model">
<div class="card">
<div class="card-header">
<span> 请完成安全检验 </span>
<img src="img/close.png" class="close" />
</div>
<div class="card-content">
<div class="canvas-wrap">
<img id="canvasSrc" />
<img id="blockSrc" />
</div>
<div style="height: 5px"></div>
<div id="slider">
<div id="slider-track"></div>
<div id="slider-bar">
<img src="img/arrow.png" class="arrow" />
</div>
</div>
</div>
</div>
</div>
<script>
let deviation = 0;
let curPageX = 0;
let status = "normal";
var result = null;
function showModel() {
init();
$("#model").css("display", "flex");
}
function closeModel() {
$("#model").css("display", "none");
}
$(".close").on("click", closeModel);
function init() {
$.ajax({
type: 'get',
url: '<%=basePath%>login.do?method=getCaptcha',
dataType: 'json',
async: false,
success: function (data){
result = data;
}
});
deviation = 0;
curPageX = 0;
status = "normal";
$("#canvasSrc").attr({
src: result.canvasSrc,
});
$("#blockSrc")
.attr({
src: result.blockSrc,
})
.css({
position: "absolute",
top: result.blockY,
left: deviation,
});
$(".card-content").css({
width: result.canvasWidth,
});
$("#slider-bar").css({
left: deviation,
backgroundColor: "#1890ff",
});
$("#slider-track").css({
borderColor: "#1890ff",
width: deviation,
backgroundColor: "rgba(24, 144, 255, 0.2)",
});
$("#slider-bar img").attr({
src: "img/arrow.png",
});
}
function moving(e) {
const blockWidth = $("#blockSrc")[0].clientWidth;
if (deviation < 0) {
return null;
}
if (deviation >= result.canvasWidth - blockWidth - 1) {
return null;
}
deviation = e.pageX - curPageX;
$("#slider-bar").css({
left: deviation,
});
$("#blockSrc").css({
left: deviation,
});
$("#slider-track").css({
width: deviation,
});
}
function mouseUp() {
checking();
document.removeEventListener("mousemove", moving);
document.removeEventListener("mouseup", mouseUp);
}
function mouseDown(e) {
if (status !== "normal") return null;
e.preventDefault();
curPageX = e.pageX;
document.addEventListener("mousemove", moving, false);
document.addEventListener("mouseup", mouseUp, false);
}
$("#slider-bar").on("mousedown", mouseDown);
function checking() {
console.log(deviation);
if(deviation>=(result.blockX-2)&&deviation<=(result.blockX+2)){
showSuccess();
closeModel();
//走后台逻辑判断用户名,密码及滑块位移X
}else{
showError();
}
}
//输入用户名和密码,点击登录显示滑块验证码,小编这里方便测试跳过了页面输入
showModel();
function showError() {
status = "error";
$("#slider-track").css({
borderColor: "rgba(245, 108, 108)",
backgroundColor: "rgba(245, 108, 108,0.2)",
});
$("#slider-bar").css({
backgroundColor: "rgba(245, 108, 108)",
});
$("#slider-bar img").attr({
src: "img/err.png",
});
setTimeout(function () {
init();
}, 1500);
}
function showSuccess() {
status = "success";
$("#slider-track").css({
borderColor: "#67c23a",
backgroundColor: "rgba(103,194,58,0.2)",
});
$("#slider-bar").css({
backgroundColor: "#67c23a",
});
$("#slider-bar img").attr({
src: "img/succ.png",
});
}
</script>
</body>
</html>
效果图

公众号回复【验证码】,即可获取图片资源
号外!号外!
如果这篇文章对你有所帮助,或者有所启发的话,帮忙点赞、在看、转发、收藏,你的支持就是我坚持下去的最大动力!
原文始发于微信公众号(一安未来):Java 实现滑块验证码登录原理及完整流程
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/44584.html