前言
现在大部分网部都是图片滑块验证码,这个得要与后端联动起来才是确保接口安全性
通过我们系统在发送手机短息时都会选进行滑块验证,但是我们要保证发送短息接口的全安,具体路思如下
那么这个滑块的必须是与后端交互才能保证安全性,而不是前端简单的交互。我们一起来学习一下这个案例怎么实吧
1、验证通过效果如图
2、验证失败效果如图
案例开始
1、我们使用java新建一个springboot工程,并准备几张图片,尺寸是390*180,如下图
添加依赖
<dependency>
<groupId>net.coobird</groupId>
<artifactId>thumbnailator</artifactId>
<version>0.4.11</version>
</dependency>
2、新建三个响应的类
2.1 WebReturn类如下
@Data
public class WebReturn {
RetCode code;
Object data;
public WebReturn(RetCode code, Object data) {
this.code = code;
this.data = data;
}
}
2.2 RetCode类如下
public enum RetCode {
IMAGE_REQ_SUCCESS(1,"图片请求成功"),
IMAGE_REQ_FAIL(2,"图片请求失败"),
VERIFI_REQ_SUCCESS(3,"图片验证成功"),
VERIFI_REQ_FAIL(4,"图片验证失败");
int code;
String message;
RetCode(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
2.3 ImageResult类如下
@Data
public class ImageResult {
int xpos;//滑块的坐标x轴
int ypos;//滑块的坐标y轴
int cutImageWidth;//滑块的宽
int cutImageHeight;//滑块的高
String cutImage;//滑块图片
String oriImage;//背景图(初扣掉滑块的图)
}
3、新建一个图片滑块生成的工具类
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import net.coobird.thumbnailator.Thumbnails;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
@Slf4j
public class ImgUtil {
//图片的路径
private String classpath = "classpath*:img/slider/*.*";
//图片的最大大小 (可以根据实际需要进行高调整,对应的图片尺寸也得是一致)
private static int IMAGE_MAX_WIDTH = 380;
private static int IMAGE_MAX_HEIGHT = 190;
//抠图上面的半径
private static int RADIUS = IMAGE_MAX_WIDTH/38;
//抠图区域的高度
private static int CUT_HEIGHT = IMAGE_MAX_WIDTH/7;
//抠图区域的宽度
private static int CUT_WIDTH = IMAGE_MAX_WIDTH/7;
//被扣地方填充的颜色
private static int FLAG = 0xffffff;
//抠图部分凸起的方向
private Location location;
ImageResult imageResult = new ImageResult();
private String ORI_IMAGE_KEY = "ORI_IMAGE_KEY";
private String CUT_IMAGE_KEY = "CUT_IMAGE_KEY";
private int XPOS;
private int YPOS;
@Data
private class ImageMessage{
private int xpos;
private int ypos;
private int cutImageWidth;
private int cutImageHeight;
}
ImageMessage imageMessage = new ImageMessage();
/**
*功能描述 获取抠图区的坐标原点
*/
public void createXYPos(BufferedImage oriImage){
int height = oriImage.getHeight();
int width = oriImage.getWidth();
XPOS = new Random().nextInt(width-CUT_WIDTH-RADIUS);
YPOS = new Random().nextInt(height-CUT_HEIGHT-RADIUS-RADIUS)+RADIUS;
//确保横坐标位于2/4--3/4
int div = (IMAGE_MAX_WIDTH/4);
if(XPOS/div == 0 ){
XPOS = XPOS + div*2;
}
else if(XPOS/div == 1 ){
XPOS = XPOS + div;
}
else if(XPOS/div == 3 ){
XPOS = XPOS - div;
}
}
/**
*功能描述 对外提供的接口
*/
public ImageResult imageResult() throws IOException{
return imageResult(getRandomImage());
}
public ImageResult imageResult(BufferedImage oriBufferedImage) throws IOException {
//检测图片大小
oriBufferedImage = checkImage(oriBufferedImage);
//初始化原点坐标
createXYPos(oriBufferedImage);
//获取被扣图像的标志图
int[][] blockData = getBlockData(oriBufferedImage);
//printBlockData(blockData);
//计算抠图区域的信息
createImageMessage();
//获取扣了图的原图和被扣部分的图
Map<String,BufferedImage> imageMap = cutByTemplate(oriBufferedImage,blockData);
imageResult.setOriImage(ImageBase64(imageMap.get(ORI_IMAGE_KEY)));
imageResult.setCutImage(ImageBase64(imageMap.get(CUT_IMAGE_KEY)));
imageResult.setXpos(imageMessage.getXpos());
imageResult.setYpos(imageMessage.getYpos());
imageResult.setCutImageWidth(imageMessage.getCutImageWidth());
imageResult.setCutImageHeight(imageMessage.getCutImageHeight());
return imageResult;
}
/**
*功能描述
* @Description 计算抠图的相关参数
*/
private void createImageMessage(){
int x = 0,y = 0;
int w = 0, h = 0;
if(location == Location.UP){
x = XPOS;
y = YPOS - RADIUS;
w = CUT_WIDTH;
h = CUT_HEIGHT + RADIUS;
}else if(location == Location.LEFT){
x = XPOS-RADIUS;
y = YPOS;
w = CUT_WIDTH + RADIUS;
h = CUT_HEIGHT;
}else if(location == Location.DOWN){
x = XPOS;
y = YPOS;
w = CUT_WIDTH;
h = CUT_HEIGHT + RADIUS;
}else if(location == Location.RIGHT){
x = XPOS;
y = YPOS;
w = CUT_WIDTH + RADIUS;
h = CUT_HEIGHT;
}
imageMessage.setXpos(x);
imageMessage.setYpos(y);
imageMessage.setCutImageHeight(h);
imageMessage.setCutImageWidth(w);
}
/**
*功能描述
* @Description 检测图片大小是否符合要求
*/
private BufferedImage checkImage(BufferedImage image) throws IOException {
if((image.getWidth() == IMAGE_MAX_WIDTH) || (image.getHeight() == IMAGE_MAX_HEIGHT)){
return image;
}else if((image.getWidth() < IMAGE_MAX_WIDTH) || (image.getHeight() < IMAGE_MAX_HEIGHT)){
log.info("图片太小.不符合要求w*h[380*190]");
throw new IllegalArgumentException("图片太小.不符合要求w*h[380*190]");
} else {
log.info("压缩图片");
return compressImage(image,IMAGE_MAX_WIDTH,IMAGE_MAX_HEIGHT);
}
}
private Color color(int rgb){
int b = (0xff & rgb);
int g = (0xff & (rgb >> 8));
int r = (0xff & (rgb >> 16));
return new Color(r, g, b);
}
/**
*功能描述 获取抠完图的原图和被扣出来的图
*/
public Map<String,BufferedImage> cutByTemplate(BufferedImage oriImage, int[][] blockData){
Map<String,BufferedImage> imgMap = new HashMap<>();
BufferedImage cutImage = new BufferedImage(imageMessage.cutImageWidth,imageMessage.cutImageHeight,oriImage.getType());
// 获取Graphics2D
Graphics2D g2d = cutImage.createGraphics();
//透明化整张图
cutImage = g2d.getDeviceConfiguration()
.createCompatibleImage(imageMessage.cutImageWidth,imageMessage.cutImageHeight, Transparency.BITMASK);
g2d.dispose();
g2d = cutImage.createGraphics();
// 背景透明代码结束
int xmax = imageMessage.xpos + imageMessage.cutImageWidth;
int ymax = imageMessage.ypos + imageMessage.cutImageHeight;
for(int x = imageMessage.xpos; x< xmax && x>=0; x++){
for(int y = imageMessage.ypos; y < ymax && y>=0; y++){
int oriRgb = oriImage.getRGB(x,y);
if(blockData[x][y] == FLAG){
oriImage.setRGB(x,y,FLAG);
//描边 判断是否为边界,如果是边界则填充为白色
if(blockData[x-1][y] != FLAG || blockData[x+1][y] != FLAG || blockData[x][y+1] != FLAG || blockData[x][y-1] != FLAG){
g2d.setColor(color(0xffffff));
}else{
g2d.setColor(color(oriRgb));
}
g2d.setStroke(new BasicStroke(1f));
g2d.fillRect(x-imageMessage.xpos, y-imageMessage.ypos, 1, 1);
}
}
}
// 释放对象
g2d.dispose();
imgMap.put(ORI_IMAGE_KEY,oriImage);
imgMap.put(CUT_IMAGE_KEY,cutImage);
return imgMap;
}
/**
*功能描述
* @Description 获取抠图数据,被扣的像素点将使用FLAG进行标记
* @return: int[][]
*/
public int[][] getBlockData(BufferedImage oriImage){
int height = oriImage.getHeight();
int width = oriImage.getWidth();
int[][] blockData =new int[width][height];
Location locations[] = {Location.UP,Location.LEFT,Location.DOWN,Location.RIGHT};
//矩形
//此处还可以优化,进行区域扫描
for(int x = 0; x< width && x>=0; x++){
for(int y = 0; y < height && y>=0; y++){
blockData[x][y] = 0;
if ( (x > XPOS) && (x < (XPOS+CUT_WIDTH))
&& (y > YPOS) && (y < (YPOS+CUT_HEIGHT))){
blockData[x][y] = FLAG;
}
}
}
//圆形突出区域
//突出圆形的原点坐标(x,y)
int xBulgeCenter=0,yBulgeCenter=0;
int xConcaveCenter=0,yConcaveCenter=0;
//位于矩形的哪一边,0123--上下左右
location = locations[new Random().nextInt(4)];
if(location == Location.UP){
//上 凸起
xBulgeCenter = XPOS + CUT_WIDTH/2;
yBulgeCenter = YPOS;
//左 凹陷
xConcaveCenter = XPOS ;
yConcaveCenter = YPOS + CUT_HEIGHT/2;
}else if(location == Location.DOWN){
//下 凸起
xBulgeCenter = XPOS + CUT_WIDTH/2;
yBulgeCenter = YPOS + CUT_HEIGHT;
//右 凹陷
xConcaveCenter = XPOS + CUT_WIDTH;
yConcaveCenter = YPOS + CUT_HEIGHT/2;
}else if(location == Location.LEFT){
//左 凸起
xBulgeCenter = XPOS ;
yBulgeCenter = YPOS + CUT_HEIGHT/2;
//下 凹陷
xConcaveCenter = XPOS + CUT_WIDTH/2;
yConcaveCenter = YPOS + CUT_HEIGHT;
}else {
//Location.RIGHT
//右 凸起
xBulgeCenter = XPOS + CUT_WIDTH;
yBulgeCenter = YPOS + CUT_HEIGHT/2;
//上 凹陷
xConcaveCenter = XPOS + CUT_WIDTH/2;
yConcaveCenter = YPOS;
}
//半径的平方
int RADIUS_POW2 = RADIUS * RADIUS;
//凸起部分
for(int x = xBulgeCenter-RADIUS; x< xBulgeCenter+RADIUS && x>=0; x++){
for(int y = yBulgeCenter-RADIUS; y < yBulgeCenter+RADIUS && y>=0; y++){
if(Math.pow((x-xBulgeCenter),2) + Math.pow((y-yBulgeCenter),2) < RADIUS_POW2){
blockData[x][y] = FLAG;
}
}
}
//凹陷部分
for(int x = xConcaveCenter-RADIUS; x< xConcaveCenter+RADIUS && x>=0; x++){
for(int y = yConcaveCenter-RADIUS; y < yConcaveCenter+RADIUS && y>=0; y++){
if(Math.pow((x-xConcaveCenter),2) + Math.pow((y-yConcaveCenter),2) <= RADIUS_POW2){
blockData[x][y] = 0;
}
}
}
return blockData;
}
/**
*功能描述 将图片转为base64存储
*/
private String ImageBase64(BufferedImage bufferedImage) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
ImageIO.write(bufferedImage, "png", out);
//转成byte数组
byte[] bytes = out.toByteArray();
Base64.Encoder encoder = Base64.getEncoder();
//生成BASE64编码
return encoder.encodeToString(bytes);
}
/**
* 随机获取一个图片文件
* @return
* @throws Exception
*/
private BufferedImage getRandomImage() throws IOException {
try {
//使用resource获取resource文件【注意:即使打成jar包也有效】
ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
Resource[] resources = resourcePatternResolver.getResources(classpath);
if (resources.length <= 0) {
throw new IOException("该文件夹内没有文件!");
} else {
int index = new Random().nextInt(resources.length);
InputStream inputStream = resources[index].getInputStream();
BufferedImage marioBufferedImage = ImageIO.read(inputStream);
return marioBufferedImage;
}
} catch (IOException e) {
log.info("读取文件失败:{}", e);
throw new IOException("读取文件失败!");
}
}
/**
*功能描述 压缩图片
* @author lgj
* @Description
* @date 3/30/20
* @param:
* @return: java.awt.image.BufferedImage
*
*/
private BufferedImage compressImage(BufferedImage image,int width,int height) throws IOException{
return Thumbnails.of(image)
.forceSize(width,height)
//.width(width).height(height)
.asBufferedImage();
}
/**
*功能描述
* @Description 抠图部分凸起的区域
*/
private enum Location {
UP,
LEFT,
DOWN,
RIGHT;
}
}
4、新建一个controller
@Slf4j
@RestController
@RequestMapping("/slider")
public class SliderController {
private int xPosCache = 0;//生产环境请把这个值存入redis中
@RequestMapping("/image")
public WebReturn image(){
ImageResult imageResult = null;
try{
imageResult = new ImgUtil().imageResult();//生成图片
xPosCache = imageResult.getXpos();//生产环境请把这个值存入redis中
imageResult.setXpos(0);//清空x值
return new WebReturn(RetCode.IMAGE_REQ_SUCCESS,imageResult);
}catch(Exception ex){
log.error(ex.getMessage());
ex.printStackTrace();
return new WebReturn(RetCode.IMAGE_REQ_FAIL,null);
}
}
@RequestMapping("/verification")
public WebReturn verification(@RequestParam("moveX") int moveX){
log.info("/slider/verification/{}",moveX);
int MOVE_CHECK_ERROR = 2;//允许的误差范围,这里设置为2个像素
//xPosCache 生产请从redis中读取,使用完并立即请除
if(( moveX < ( xPosCache + MOVE_CHECK_ERROR)) && ( moveX > (xPosCache - MOVE_CHECK_ERROR))){
log.info("验证正确");
//生产这个可以返回临时授权码
return new WebReturn(RetCode.VERIFI_REQ_SUCCESS,true);
}
return new WebReturn(RetCode.VERIFI_REQ_FAIL,false);
}
}
5、最后新建一个页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>首页</title>
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.js"></script>
<style >
html body {
height: 100%;
width: 100%;
}
#captchaContainer{
position: absolute;
top: 50px;
left: 40%;
/* background-color: #f57a7a;*/
height: 275px;
width: 260px;
}
.header{
position: absolute;
top: 0px;
left: 0px;
background-color: rgb(245, 236, 236);
height: 40px;
width: 380px;
}
.headerText{
position: absolute;
top: 13px;
left: 140px;
height: 40px;
color:#66c523;
font:18px/14px Georgia, "Times New Roman", Times, serif;
}
#captchaImg{
position: absolute;
left: 0;
top: 40px;
height: 190px;
width: 380px;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
border: none
}
#oriImg{
position: absolute;
left: 0px;
top: 0px;
width: 380px;
height: 190px;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
border: none
}
#cutImg{
position: absolute;
border: none;
left: 0px;
}
.sliderContainer {
position: absolute;
bottom: 0;
left: 0px;
text-align: center;
width: 380px;
height: 40px;
line-height: 40px;
background: #f7f9fa;
color: #45494c;
border: 1px solid #e4e7eb;
}
.sliderContainer_success{
border: 1px solid hsl(125, 93%, 44%);
}
.sliderContainer_fail{
border: 1px solid #ec3655;
}
.slider {
position: absolute;
top: 0;
left: 0px;
width: 40px;
height: 40px;
background: #fff;
box-shadow: 0 0 3px rgba(0, 0, 0, 0.3);
transition: background .2s linear;
cursor: pointer;
cursor: grab;
}
.slider_success{
border: 1px solid hsl(125, 93%, 44%);
}
.slider_fail{
border: 1px solid #ec3655;
}
.sliderContainer_active .slider {
height: 38px;
top: -1px;
border: 1px solid #1991FA;
}
.sliderContainer_active .sliderMask {
height: 38px;
border-width: 1px;
}
.sliderContainer_success .slider {
height: 38px;
top: -1px;
margin-left: -1px;
border: 1px solid #52CCBA;
background-color: #52CCBA !important;
}
.sliderContainer_success .sliderMask {
height: 38px;
border: 1px solid #52CCBA;
background-color: #D2F4EF;
}
.sliderContainer_success .sliderIcon {
background-position: 0 0 !important;
}
.sliderContainer_fail .slider {
height: 38px;
top: -1px;
border: 1px solid #f57a7a;
background-color: #f57a7a !important;
}
.sliderContainer_fail .sliderMask {
height: 38px;
border: 1px solid #f57a7a;
background-color: #fce1e1;
}
.sliderContainer_fail .sliderIcon {
top: 14px;
background-position: 0 -82px !important;
}
.sliderContainer_active .sliderText, .sliderContainer_success .sliderText, .sliderContainer_fail .sliderText {
display: none;
}
.sliderMask {
position: absolute;
left: 0;
top: 0;
height: 40px;
border: 0 solid #1991FA;
background: #D1E9FE;
}
.slider:active {
cursor: grabbing;
}
.slider:hover {
background: #1991FA;
}
.slider:hover .sliderIcon {
background-position: 0 -13px;
}
.sliderIcon {
position: absolute;
top: 15px;
left: 13px;
width: 14px;
height: 12px;
background: url(http://cstaticdun.126.net//2.6.3/images/icon_light.f13cff3.png) 0 -26px;
background-size: 34px 471px;
}
.refreshIcon {
position: absolute;
right: 0;
top: 0;
width: 34px;
height: 34px;
cursor: pointer;
background: url(http://cstaticdun.126.net//2.6.3/images/icon_light.f13cff3.png) 0 -437px;
background-size: 34px 471px;
}
</style>
</head>
<body>
<div id="captchaContainer">
<!-- 标题栏 -->
<div class="header">
<span class="headerText">图片滑动验证</span>
<span class="refreshIcon"/>
</div>
<!-- 图片显示区域 -->
<div id="captchaImg">
<img id="oriImg" src="dd" alt="原图"/>
<img id="cutImg" src="sa" alt="抠图"/>
</div>
<!--滑块显示区域-->
<div class="sliderContainer">
<div class="sliderMask">
<div class="slider">
<span class="sliderIcon"></span>
</div>
</div>
<span class="sliderText">向右滑动填充拼图</span>
</div>
</div>
</body>
<script>
//图片显示使用base64时的前缀,src=base64PrefixPath + imgBase64Value
var base64PrefixPath="data:image/png;base64,";
var IMAGE_WIDTH = 380;
//初始化
//滑块初始偏移量
var sliderInitOffset = 0;
//滑块移动的最值
var MIN_MOVE = 0;
var MAX_MOVE = 0;
//鼠标按下标志
var mousedownFlag=false;
//滑块移动的距离
var moveX;
//滑块位置检测允许的误差,正负2
var MOVE_CHECK_ERROR = 2;
//滑块滑动使能
var moveEnable = true;
var ImageMsg = {
//抠图的坐标
xpos: 0,
ypos: 0,
//抠图的大小
cutImageWidth: 0,
cutImageHeight: 0,
//原图的base64
oriImageSrc: 0,
//抠图的base64
cutImageSrc: 0,
}
//加载页面时进行初始化
function init(){
console.log("init")
moveEnable = true;
mousedownFlag=false;
$(".slider").css("left",0+"px");
initClass();
MAX_MOVE = IMAGE_WIDTH - ImageMsg.cutImageWidth;
console.log("ImageMsg = " + ImageMsg)
$("#cutImg").css("left",0+"px");
$("#oriImg").attr("src",ImageMsg.oriImageSrc)
$("#cutImg").attr("src",ImageMsg.cutImageSrc)
$("#cutImg").css("width",ImageMsg.cutImageWidth)
$("#cutImg").css("height",ImageMsg.cutImageHeight)
$("#cutImg").css("top",ImageMsg.ypos)
}
//加载页面时
$(function(){
httpRequest.requestImage.request();
})
var httpRequest={
//请求获取图片
requestImage:{
path: "slider/image",
request:function(){
$.get(httpRequest.requestImage.path,function(data,status){
console.log(data)
console.log(data.message);
if(data.data != null){
ImageMsg.oriImageSrc = base64PrefixPath + data.data.oriImage;
ImageMsg.cutImageSrc = base64PrefixPath + data.data.cutImage;
ImageMsg.xpos = data.data.xpos;
ImageMsg.ypos = data.data.ypos;
ImageMsg.cutImageWidth = data.data.cutImageWidth;
ImageMsg.cutImageHeight = data.data.cutImageHeight;
init();
}
});
},
},
//请求验证
requestVerification:{
path: "slider/verification",
request:function(){
$.get(httpRequest.requestVerification.path,{moveX:(moveX)},function(data,status){
console.log(data)
console.log(data.code);
console.log(data.message);
if(data.data == true){
checkSuccessHandle();
}
else{
checkFailHandle();
}
});
},
},
}
//刷新图片操作
$(".refreshIcon").on("click",function(){
httpRequest.requestImage.request();
})
//滑块鼠标按下
$(".slider").mousedown(function(event){
console.log("鼠标按下mousedown:"+event.clientX + " " + event.clientY);
sliderInitOffset = event.clientX;
mousedownFlag = true;
//滑块绑定鼠标滑动事件
$(".slider").on("mousemove",function(event){
if(mousedownFlag == false){
return;
}
if(moveEnable == false){
return
}
moveX = event.clientX - sliderInitOffset;
moveX<MIN_MOVE?moveX=MIN_MOVE:moveX=moveX;
moveX>MAX_MOVE?moveX=MAX_MOVE:moveX=moveX;
$(this).css("left",moveX+"px");
$("#cutImg").css("left",moveX+"px");
})
})
//滑块鼠标弹起操作
$(".slider").mouseup(function(event){
console.log("mouseup:"+event.clientX + " " + event.clientY);
sliderInitOffset = 0;
$(this).off("mousemove");
mousedownFlag=false;
console.log("moveX = " + moveX)
checkLocation();
})
//检测滑块 位置是否正确
function checkLocation(){
moveEnable = false;
//后端请求检测滑块位置
httpRequest.requestVerification.request();
}
function checkSuccessHandle(){
$(".sliderContainer").addClass("sliderContainer_success");
$(".slider").addClass("slider_success");
}
function checkFailHandle(){
$(".sliderContainer").addClass("sliderContainer_fail");
$(".slider").addClass("slider_success");
}
function initClass(){
$(".sliderContainer").removeClass("sliderContainer_success");
$(".slider").removeClass("slider_success");
$(".sliderContainer").removeClass("sliderContainer_fail");
$(".slider").removeClass("slider_fail");
}
</script>
</html>
最后
到这里我们的案例代码已经写好了,运程工程访问就可以了。
http://localhost:9005/home.html
本文章提供大家学习,欢迎你大家留言提供您的保贵意见
=如果本文对您有帮助,麻烦您给博主点个赞吧!=