完整说明使用SpringBoot+js实现滑动图片验证
常见的网站验证方式有手机短信验证,图片字符验证,滑块验证,滑块图片验证.本文主要讲解的是滑块图片验证的实现流程.包括后台和前端的实现.
实现效果

使用的API
java.awt.image.BufferedImage
BufferedImage是Java类库中是一个带缓冲区图像类,主要作用是将一幅图片加载到内存中(BufferedImage生成的图片在内存里有一个图像缓冲区,利用这个缓冲区我们可以很方便地操作这个图片),提供获得绘图对象、图像缩放、选择图像平滑度等功能,通常用来做图片大小变换、图片变灰、设置透明不透明等。
常见的api有
读取一张图片
String imgPath = "/demo.jpg";
BufferedImage image = ImageIO.read(new FileInputStream(imgPath));
保存文件
ImageIO.write(image,"png",new File("xx.png"));
ImageIO提供read()和write()静态方法,读写图片,比以往的InputStream读写更方便。
像素处理
getRGB(int x, int y)
setRGB(intx ,inty,int rgb)
获取Graphics2D对象
Graphics2D g2d = parentImage.createGraphics();
Graphics2D是一个画图工具,可以实现对图片进行画画处理,比如花直线,圆,方形等操作.除此之外,还可以创建透明背景的图片.本方案就是用它来对扣出来的图透明化处理.
Thumbnails
该类用于对图片进行压缩以符合大小要求.
Thumbnails.of(image)
.forceSize(width,height)
//.width(width).height(height)
.asBufferedImage();
使用forceSize强制大小时,对图片会有一定的像素损耗.使用width(width).height(height)时图片的大小不会和设定的一致.
这里用来对网上下载的图片进行大小处理,当然也可以用其他图像处理工具,比如PS
依赖
<dependency>
<groupId>net.coobird</groupId>
<artifactId>thumbnailator</artifactId>
<version>0.4.11</version>
</dependency>
基础知识
一张图片的大小通常用像素数量来表示,比如1320*600(宽*高),对于彩色图片,每一个像素点有RGB来表示.比如(250,180,0)表示黄色,或者使用十六进制表示#FFB400.
RGB查询
https://tool.oschina.net/commons?type=3
因此要想把一张图扣出来一部分,只要确定好抠图区域,使用抠图区域的像素值来创建另一张图,原抠图区域填充其他像素值.就可以得到两张分体图,组合在一起构成一副完整的图.而对于被扣出来的图,还要对其进行透明化处理.

处理流程
图片校验最需要考虑的是安全性,因此需要将数据发送给后端进行校验,而不是在前端进行校验.
1.前端刷新图片请求给后台
2.后台收到请后后开始进行处理
3.后台随机从图片文件夹中取一张图片
4.对图片进行大小检测,图片偏小则抛出异常,偏大则进行截图处理.实际生产环境应该使用处理好的图片,就可以省略这一步
5.确定抠图区的原点坐标,为了保证效果.将图片宽度分成四段,最终的原点横坐标位于2/4-3/4范围内.坐标示意如下,图片左上脚是原点(x0,y0)
(x0,y0) (xMax,y0)
****************
* *
* *
* *
****************
(x0,yMax) (xMax,yMax)
6.根据抠图区的原点对抠图区进行标定.标定区如上图所示,由正方形和半圆组成,其中一边的半圆突出,另一边凹陷.
7.对原图的被抠区域进行灰度处理,填充其他颜色
8.对抠出来的图进行背景透明化处理,并截图(截取图片范围内的图)
9.返回扣出来图的大小,偏移量,两张图的base64数据返回给前端
10.前端渲染图片
11.移动滑块,滑块移动时抠出来的图也会跟着移动,两者移动的偏移量是一样的.鼠标释放时将偏移量发送给后端进行校验.
12.后端校验后将结果返回给前端,前端根据该结果做不同的处理
13.一般是登录才进行此类的图片验证,因此在点击登录时,偏移量仍然和帐号密码再次发送,后端再一次进行校验
14.完成
后台实现
属性说明
//图片的路径
private String basePathClasspath = "img/";
private String basePathFile = "src/main/resources/img/"; private String basePath = basePathFile;
private String basePathOutput = "src/main/resources/img/out/";
//图片的最大大小
private static int IMAGE_MAX_WIDTH = 300;
private static int IMAGE_MAX_HEIGHT = 260;
//抠图上面的半径
private static int RADIUS = IMAGE_MAX_WIDTH/20;
//抠图区域的高度
private static int CUT_HEIGHT = IMAGE_MAX_WIDTH/5;
//抠图区域的宽度
private static int CUT_WIDTH = IMAGE_MAX_WIDTH/5;
//被扣地方填充的颜色
private static int FLAG = 0x778899;
//输出图片后缀
private static String IMAGE_SUFFIX = "png"; //
private int imageOffset = 0; //抠图部分凸起的方向
private Location location; ImageResult imageResult = new ImageResult(); //
private String ORI_IMAGE_KEY = "ORI_IMAGE_KEY";
private String CUT_IMAGE_KEY = "CUT_IMAGE_KEY"; //抠图区的原点坐标(x0,y0) /*
(x0,y0) (xMax,y0)
****************
* *
* *
* *
****************
(x0,yMax) (xMax,yMax)
*/
private int XPOS;
private int YPOS;
对外提供的接口
也是任务的流程处理
public ImageResult imageResult(File file) throws IOException {
log.info("file = {}",file.getName());
BufferedImage oriBufferedImage = getBufferedImage(file);
//检测图片大小
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;
}
确定原点的坐标
这里需要注意两点
一是抠图区域不能超过原图范围,因此随机生成范围需要减去抠图区的长度和半圆的半径
二是为了保证用户体验,限定横坐标在图像的2/4-3/4处,才能保证滑块有一定的滑程,同时保证抠图不会超出原图范围.
/**
*功能描述 获取抠图区的坐标原点
* @author lgj
* @Description
* @date 3/29/20
* @param:
* @return: void
*
*/
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); //确保横坐标位于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;
} }
标记抠图区域
这里使用一个二维数组locations[width][height]来保存抠图标记数据,每一个数据表示位置和是否为抠图区,使用常量FLAG进行标记
这里的抠图区为一个巨型加上突出的半圆和凹陷的半圆.
对于半圆的处理参考该公式: (x-a)2+(y-b)2=R2,
其中(a,b)为圆中心的坐标,R为圆半径.(x,y)为任一坐标
(x,y)在圆上: (x-a)2+(y-b)2 == R2
(x,y)在圆内: (x-a)2+(y-b)2 < R2
(x,y) 在圆外: (x-a)2+(y-b)2 > R2
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++){
for(int y = 0; y < height; 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(3)];
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;
}
//for test
log.info("突出圆形位置:"+location);
log.info("XPOS={} YPOS={}",XPOS,YPOS);
log.info("xBulgeCenter={} yBulgeCenter={}",xBulgeCenter,yBulgeCenter);
log.info("xConcaveCenter={} yConcaveCenter={}",xConcaveCenter,yConcaveCenter);
//半径的平方
int RADIUS_POW2 = RADIUS * RADIUS;
//凸起部分
for(int x = xBulgeCenter-RADIUS; x< xBulgeCenter+RADIUS; x++){
for(int y = yBulgeCenter-RADIUS; y < yBulgeCenter+RADIUS; y++){
//(x-a)2+(y-b)2 = r2
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++){
for(int y = yConcaveCenter-RADIUS; y < yConcaveCenter+RADIUS; y++){
//(x-a)2+(y-b)2 = r2
if(Math.pow((x-xConcaveCenter),2) + Math.pow((y-yConcaveCenter),2) < RADIUS_POW2){
blockData[x][y] = 0;
}
}
}
return blockData;
}
获取抠完图的原图和被抠出来的图
通过遍历抠图数据blockData来进行抠图.原图被标记的位置使用FLAG进行填充,而抠出来的部分重新构成一张同样大小的图
这里的操作是:
1.创建一个与抠图区域大小(w*h)的图,并将背景设为透明
2.遍历抠图区域,原图被抠的地方填充其他颜色
3.抠出来的像素点复制到上面创建的透明图
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();
// 背景透明代码结束
log.info("imageMessage = {}",imageMessage);
int xmax = imageMessage.xpos + imageMessage.cutImageWidth;
int ymax = imageMessage.ypos + imageMessage.cutImageHeight;
//只对抠图区域进行遍历
for(int x = imageMessage.xpos; x< xmax; x++){
for(int y = imageMessage.ypos; y < ymax; y++){
int oriRgb = oriImage.getRGB(x,y);
if(blockData[x][y] == FLAG){
//原图
oriImage.setRGB(x,y,FLAG);
//抠的图
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;
}
图片原始数据转换成base64格式数据
由于图片原始数据很多是不可打印字符,因此需要将其转换成base64格式,再进行发送
private String ImageBase64(BufferedImage bufferedImage) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
ImageIO.write(bufferedImage, "png", out);
//转成byte数组
byte[] bytes = out.toByteArray();
BASE64Encoder encoder = new BASE64Encoder();
//生成BASE64编码
return encoder.encode(bytes);
}
控制器
在进行校验时,需要允许一定的误差.
package slide.picture.verification.demo.controller; import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import slide.picture.verification.demo.image.ImageResult;
import slide.picture.verification.demo.image.ImgUtil;
import slide.picture.verification.demo.ret.RetCode;
import slide.picture.verification.demo.ret.WebReturn;
import slide.picture.verification.demo.time.TimeUtil; import javax.jws.WebResult;
import java.util.concurrent.TimeUnit; @Slf4j
@RestController
@RequestMapping("/slider")
public class SliderController { private int xPosCache = 0; @RequestMapping("/image")
public WebReturn image(){ log.info("/slider/image");
ImageResult imageResult = null; try{ TimeUtil.start(1);
imageResult = new ImgUtil().imageResult();
TimeUtil.end(1); xPosCache = imageResult.getXpos();
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;
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);
} }
后台的关键代码就这些,整个处理流程大概耗时40ms(图片大小320*260).上面的getBlockdata()还可以继续优化,并不需要全局遍历.只要抠图区域遍历就可以了.
由于对BufferedImage对象的操作是操作其内存中的数据,因此在大并发的情况下需要考虑内存占用状况.
前端实现
这里需要注意的地方是抠图和原图的左边緣需要对齐,以及纵坐标位置.
鼠标按下滑块时才会开始计算偏移的距离,滑块滑动的距离会反映到抠图的偏移量.当松开鼠标时会将偏移量发送到后端进行校验.
还有,后端发送过来的数据是base64数据.由于图片原始数据很多是不可打印字符,因此需要将其转换成
图片显示使用base64时的格式.这里的xxxx是图片的base64数据.需要注意base64后面的逗号.
<img src="data:image/png;base64,xxxxxxxx">
html代码
<div id="captchaContainer">
<!-- 标题栏 -->
<div class="header">
<span class="headerText">图片滑动验证</span>
<span class="refreshIcon"/>
</div> <!-- 图片显示区域 -->
<div id="captchaImg">
<img id="oriImg" alt="原图"/>
<img id="cutImg" alt="抠图"/>
</div>
<!--滑块显示区域-->
<div class="sliderContainer">
<div class="sliderMask">
<div class="slider">
<span class="sliderIcon"></span>
</div>
</div>
<span class="sliderText">向右滑动填充拼图</span> </div>
</div>
JS代码
<script>
//图片显示使用base64时的前缀,src=base64PrefixPath + imgBase64Value
var base64PrefixPath="data:image/png;base64,";
var IMAGE_WIDTH = 300;
//初始化
//滑块初始偏移量
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>
完整说明使用SpringBoot+js实现滑动图片验证的更多相关文章
- 滑动验证 和滑动图片验证JS
滑动验证 先放效果图 <!DOCTYPE html> <html lang="en"> <head> <meta charset=&quo ...
- c# 简单的滑动图片验证
普通的验证码对用户使用体验不友好,出现了滑动图片验证的验证方式,用户只要按住滑块完成图片的拼接即可通过验证(这是最简单的方式,滑动轨迹,数据分析,滑行速度 什么的暂没考虑) 主要的实现思路: 1.先从 ...
- js鼠标滑动图片显示隐藏效果
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...
- react使用ant design pro时的滑动图片组件
react的滑动图片验证,是基于https://segmentfault.com/a/1190000018309458?utm_source=tag-newest做的修改,改动的主要有以下几点: 1. ...
- [js开源组件开发]js轮播图片支持手机滑动切换
js轮播图片支持手机滑动切换 carousel-image 轮播图片,支持触摸滑动. 例子见DEMO http://www.lovewebgames.com/jsmodule/carousel-ima ...
- 手机触屏滑动图片切换插件swiper.js
今天给大家分享一款手机触屏滑动图片切换插件swiper.js是一款swiper手机触屏滑动图片幻灯片,适合各种尺寸.效果图如下: 在线预览 源码下载 实现的代码. html代码: <div ...
- Python实现图片滑动式验证识别
1 abstract 很多人学习python,不知道从何学起.很多人学习python,掌握了基本语法过后,不知道在哪里寻找案例上手.很多已经做案例的人,却不知道如何去学习更加高深的知识.那么针对这三类 ...
- springboot整合ueditor实现图片上传和文件上传功能
springboot整合ueditor实现图片上传和文件上传功能 写在前面: 在阅读本篇之前,请先按照我的这篇随笔完成对ueditor的前期配置工作: springboot+layui 整合百度富文本 ...
- js多种切换图片
分享通过js实现多种图片切换特效,这里只有手动切换哦,自动效果需自写,效果地址:http://dwz.cn/1drD5u. 下载地址:http://***/download/index/52209 适 ...
随机推荐
- 用Python搭建简单的HTTP服务 · Zhangxu's Blog
分享一个快速用Python搭建简单的HTTP服务的方法. 平时我们可能有需要,传输某个文件到手机,或者工作中某台服务器的电脑. 假如这个手机是个测试手机/服务器,并没有微信QQ之类的软件,而且你也不想 ...
- 空间数据导入Oracle数据库备忘
- 第二章 表与指针Pro SQL Server Internal (Dmitri Korotkev)
聚集索引 聚集索引就是表中数据的物理顺序,它是按照聚集索引分类的.表只能定义一个聚集索引. 如果你要在一个有数据的堆表中创建一个聚集索引,如2-5所示,第一步要做的就是SQL服务器创建另一个根据聚集索 ...
- python类变量与构造函数的使用
类变量:可在类的所有实例之间共享的变量 实例类对象:类的实例是调用类对象来创建的.如:par = Parent(),par就是类Parent的一个实例类对象. 实例变量(成员变量):同一个类对象可以创 ...
- ORACLE数据库实现主键自增
ORACLE数据库是甲骨文公司的一款关系数据库管理系统. 实现主键自动增长需要四个步骤: 去看 创建表格 去看 创建自增序列 去看 创建触发器 去看 插入测试 1.创建表格(必须有主键) -- 创建学 ...
- 那是我夕阳下的奔跑,电商网站PC端详情页图片放大效果实现
在详情页浏览时商品大图还是不能完全看清楚商品的细节,该特效实现鼠标悬停在商品大图上时,在商品大图右侧出现放大镜效果并根据鼠标的位置来改变右侧大图的显示内容,放大镜中的内容和鼠标悬停位置的内容相同.该特 ...
- idea使用Tomcat部署war 和 war exploded的区别
war模式:将WEB工程一包的形式上传到服务器中.war exploded模式:将WEB工程以当前文件夹的位置关系上传到服务器.解析:war 模式这种可以称为是发布模式(完整的项目),将项目打成war ...
- python正则表达式之re模块方法介绍
python正则表达式之re模块其他方法 1:search(pattern,string,flags=0) 在一个字符串中查找匹配 2:findall(pattern,string,flags=0) ...
- JavaWeb中登录验证码生成
1.页面代码 <html> <head> <title>Title</title> <script type="text/javascr ...
- koa进阶史(一)
1,设置静态文件目录,将__dirname 写成_dirname,乍看没什么毛病,但是一运行之后发现,_dirname is not defined,下次注意哈 app.use(express.sta ...