700字范文,内容丰富有趣,生活中的好帮手!
700字范文 > 滑块验证码后台生成及校验

滑块验证码后台生成及校验

时间:2022-07-22 03:45:12

相关推荐

滑块验证码后台生成及校验

目录

前言

思路流程

工具类代码

pom

抠图工具类

imageUtils

调用实现类

前言

在自己做的某一个项目中要求具备滑块验证码的功能,在翻阅部分博客后自己参考编写了一个后台的滑块验证码工具类,这里要给提供参考资料的博客说声谢谢,但是因时间久远忘记了之前的博客地址,所以也要说声抱歉。

思路流程

验证码的生成及验证流程是利用jhlabs jar包工具对图片进行抠图,将抠出的图片与背景图片通过base64编码转转成图片字符串发送给前台,并记录抠出图片的X坐标值,后续校验过程中我们会根据前台用户滑动的X坐标值与我们后台记录的X坐标值进行比较,在坐标值范围内则可证明验证成功。

注意点:

1.背景图使我们提前准备好的静态资源,图片像素大小为需均一致,并且前台编写时也要根据像素定义好验证码的长和高,这里默认设置了350X213像素的大小,后续更改图片大小也需要更改工具类中的宽和高。

2.在编译使用加载静态资源背景图时通过idea运行与直接运行jar包中的资源地址是不同的,这里我编写了jar包静态资源的加载函数,使用时可根据实际地址进行修改。

3.这里的验证码存储使用的是Ehcache,可根据自己的缓存技术修改存储。

工具类代码

pom

<dependency><groupId>com.jhlabs</groupId><artifactId>filters</artifactId><version>2.0.235</version></dependency>

<!-- /artifact/cn.hutool/hutool-all --><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.2.3</version></dependency>

抠图工具类

import cn.hutool.core.img.ImgUtil;import cn.hutool.core.io.resource.Resource;import cn.hutool.core.util.NumberUtil;import com.jhlabs.image.ImageUtils;import com.jhlabs.image.InvertAlphaFilter;import com.jhlabs.image.ShadowFilter;import lombok.Data;import javax.imageio.stream.ImageInputStream;import java.awt.*;import java.awt.geom.Arc2D;import java.awt.geom.GeneralPath;import java.awt.image.BufferedImage;import java.io.ByteArrayInputStream;import java.io.File;import java.io.InputStream;import java.util.Random;/*** 滑块验证码** @author hexm* @date /10/23*/@Datapublic class PuzzleCaptcha {/** 默认宽度,用来计算阴影基本长度 */private static final int DEFAULT_WIDTH = 350;/** 随机数 */private static final Random RANDOM = new Random();/** 蒙版 */private static Color color = new Color(255, 255, 255, 204);/** alpha通道过滤器 */private static InvertAlphaFilter alphaFilter = new InvertAlphaFilter();/** 边距 */private static int margin = 0;/** 生成图片的宽度 */private int width = DEFAULT_WIDTH;/** 生成图片高度 */private int height = 213;/** x轴的坐标,由算法决定 */private int x;/** y轴的坐标,由算法决定 */private int y;/** 拼图长宽 */private int vwh = 10 * 3;/** 原图 */private Image image;/** 大图 */private Image artwork;/** 小图 */private Image vacancy;/** 是否注重速度 */private boolean isFast = false;/** 小图描边颜色 */private Color vacancyBorderColor;/** 小图描边线条的宽度 */private float vacancyBorderWidth = 2.5f;/** 主图描边的颜色 */private Color artworkBorderColor;/** 主图描边线条的宽度 */private float artworkBorderWidth = 5f;/*** 最高放大倍数,合理的放大倍数可以使图像平滑且提高渲染速度* 当isFast为false时,此属性生效* 放大倍数越高,生成的图像越平滑,受原始图片大小的影响。*/private double maxRatio = 2;/*** 画质** @see Image#SCALE_DEFAULT* @see Image#SCALE_FAST* @see Image#SCALE_SMOOTH* @see Image#SCALE_REPLICATE* @see Image#SCALE_AREA_AVERAGING*/private int imageQuality = Image.SCALE_SMOOTH;/*** 从文件中读取图片** @param file*/public PuzzleCaptcha(File file) {image = ImgUtil.read(file);}/*** 从文件中读取图片,请使用绝对路径,使用相对路径会相对于ClassPath** @param imageFilePath*/public PuzzleCaptcha(String imageFilePath) {image = ImgUtil.read(imageFilePath);}/*** 从{@link Resource}中读取图片** @param resource*/public PuzzleCaptcha(Resource resource) {image = ImgUtil.read(resource);}/*** 从流中读取图片** @param imageStream*/public PuzzleCaptcha(InputStream imageStream) {image = ImgUtil.read(imageStream);}/*** 从图片流中读取图片** @param imageStream*/public PuzzleCaptcha(ImageInputStream imageStream) {image = ImgUtil.read(imageStream);}/*** 加载图片** @param image*/public PuzzleCaptcha(Image image) {this.image = image;}/*** 加载图片** @param bytes*/public PuzzleCaptcha(byte[] bytes) {this.image = ImgUtil.read(new ByteArrayInputStream(bytes));}/*** 生成随机x、y坐标*/private void init() {if (x == 0 || y == 0) {this.x = random(vwh, this.width - vwh - margin);this.y = random(margin, this.height - vwh - margin);}}/*** 执行*/public void run() {init();// 缩略图Image thumbnail;GeneralPath path;int realW = image.getWidth(null);int realH = image.getHeight(null);int w = realW, h = realH;double wScale = 1, hScale = 1;// 如果原始图片比执行的图片还小,则先拉伸再裁剪boolean isFast = this.isFast || w < this.width || h < this.height;if (isFast) {// 缩放,使用平滑模式thumbnail = image.getScaledInstance(width, height, imageQuality);path = paintBrick(1, 1);w = this.width;h = this.height;} else {// 缩小到一定的宽高,保证裁剪的圆润boolean flag = false;if (realW > width * maxRatio) {// 不超过最大倍数且不超过原始图片的宽w = Math.min((int) (width * maxRatio), realW);flag = true;}if (realH > height * maxRatio) {h = Math.min((int) (height * maxRatio), realH);flag = true;}if (flag) {// 若放大倍数生效,则缩小图片至最高放大倍数,再进行裁剪thumbnail = image.getScaledInstance(w, h, imageQuality);} else {thumbnail = image;}hScale = NumberUtil.div(h, height);wScale = NumberUtil.div(w, width);path = paintBrick(wScale, hScale);}// 创建阴影过滤器float radius = 5 * ((float) w / DEFAULT_WIDTH) * (float) wScale;int left = 1;ShadowFilter shadowFilter = new ShadowFilter(radius, 2 * (float) wScale, -1 * (float) hScale, 0.8f);// 创建空白的图片BufferedImage artwork = translucent(new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB));BufferedImage localVacancy = translucent(new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB));// 画小图Graphics2D vg = localVacancy.createGraphics();// 抗锯齿vg.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);// 设置画图路径范围vg.setClip(path);// 将区域中的图像画到小图中vg.drawImage(thumbnail, null, null);//描边if (vacancyBorderColor != null) {vg.setColor(vacancyBorderColor);vg.setStroke(new BasicStroke(vacancyBorderWidth));vg.draw(path);}// 释放图像vg.dispose();// 画大图// 创建画笔Graphics2D g = artwork.createGraphics();// 抗锯齿g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);// 画上图片g.drawImage(thumbnail, null, null);// 设置画图路径范围g.setClip(path);// 填充缺口透明度 颜色混合,不透明在上g.setComposite(AlphaComposite.SrcAtop);// 填充一层白色的透明蒙版,透明度越高,白色越深 alpha:0-255g.setColor(color);g.fill(path);//描边if (artworkBorderColor != null) {g.setColor(artworkBorderColor);g.setStroke(new BasicStroke(artworkBorderWidth));g.draw(path);}// 画上基于小图的内阴影,先反转alpha通道,然后创建阴影g.drawImage(shadowFilter.filter(alphaFilter.filter(localVacancy, null), null), null, null);// 释放图像g.dispose();// 裁剪掉多余的透明背景localVacancy = ImageUtils.getSubimage(localVacancy, (int) (x * wScale - left), 0, (int) Math.ceil(path.getBounds().getWidth() + radius) + left, h);if (isFast) {// 添加阴影this.vacancy = shadowFilter.filter(localVacancy, null);this.artwork = artwork;} else {// 小图添加阴影localVacancy = shadowFilter.filter(localVacancy, null);// 大图缩放this.artwork = artwork.getScaledInstance(width, height, imageQuality);// 缩放时,需要加上阴影的宽度,再除以放大比例this.vacancy = localVacancy.getScaledInstance((int) ((path.getBounds().getWidth() + radius) / wScale), height, imageQuality);}}/*** 绘制拼图块的路径** @param xScale x轴放大比例* @param yScale y轴放大比例* @return*/private GeneralPath paintBrick(double xScale, double yScale) {double x = this.x * xScale;double y = this.y * yScale;// 直线移动的基础距离double hMoveL = vwh / 3f * yScale;double wMoveL = vwh / 3f * xScale;GeneralPath path = new GeneralPath();path.moveTo(x, y);path.lineTo(x + wMoveL, y);// 上面的圆弧正东方向0°,顺时针负数,逆时针正数path.append(arc(x + wMoveL, y - hMoveL / 2, wMoveL, hMoveL, 180, -180), true);path.lineTo(x + wMoveL * 3, y);path.lineTo(x + wMoveL * 3, y + hMoveL);// 右边的圆弧path.append(arc(x + wMoveL * 2 + wMoveL / 2, y + hMoveL, wMoveL, hMoveL, 90, -180), true);path.lineTo(x + wMoveL * 3, y + hMoveL * 3);path.lineTo(x, y + hMoveL * 3);path.lineTo(x, y + hMoveL * 2);// 左边的内圆弧path.append(arc(x - wMoveL / 2, y + hMoveL, wMoveL, hMoveL, -90, 180), true);path.lineTo(x, y);path.closePath();return path;}/*** 绘制圆形、圆弧或者是椭圆形* 正东方向0°,顺时针负数,逆时针正数** @param x左上角的x坐标* @param y左上角的y坐标* @param w宽* @param h高* @param start 开始的角度* @param extent 结束的角度* @return*/private Arc2D arc(double x, double y, double w, double h, double start, double extent) {return new Arc2D.Double(x, y, w, h, start, extent, Arc2D.OPEN);}/*** 透明背景** @param bufferedImage* @return*/private BufferedImage translucent(BufferedImage bufferedImage) {Graphics2D g = bufferedImage.createGraphics();bufferedImage = g.getDeviceConfiguration().createCompatibleImage(bufferedImage.getWidth(), bufferedImage.getHeight(), Transparency.TRANSLUCENT);g.dispose();return bufferedImage;}/*** 随机数** @param min* @param max* @return*/private static int random(int min, int max) {return RANDOM.ints(min, max + 1).findFirst().getAsInt();}}

imageUtils

import com.jhlabs.image.ImageUtils;import javax.imageio.ImageIO;import java.awt.*;import java.io.ByteArrayOutputStream;import java.io.IOException;import java.util.Base64;/*** @author hexm* @date /10/27 15:37*/public class ImageConvertUtil {/*** 将image对象转为base64字符串** @param image* @return*/public static String toBase64(Image image, String format) {return Base64.getEncoder().encodeToString(toBytes(image, format));}/*** 将image对象转为前端img标签识别的base64字符串** @param image* @param format* @return*/public static String toDataUri(Image image, String format) {return String.format("data:image/%s;base64,%s", format, toBase64(image, format));}/*** 将image对象转为字节** @param image* @param format* @return*/public static byte[] toBytes(Image image, String format) {ByteArrayOutputStream stream = new ByteArrayOutputStream();try {ImageIO.write(ImageUtils.convertImageToARGB(image), format, stream);} catch (IOException e) {e.printStackTrace();}return stream.toByteArray();}}

调用实现类

import cn.hutool.core.util.NumberUtil;import com.alibaba.fastjson.JSONObject;import com.pride.sun.app.verifycode.controller.ValidateController;import com.pride.sun.app.verifycode.service.VerifyService;import com.pride.sun.app.verifycode.util.ImageConvertUtil;import com.pride.sun.app.verifycode.util.PuzzleCaptcha;import com.pride.sun.cache.MyCacheManager;import com.pride.sun.result.ResultForm;import com.pride.sun.syslog.ErrorLog;import mons.collections.CollectionUtils;import mons.lang.StringUtils;import org.apache.poi.util.IOUtils;import org.springframework.stereotype.Service;import java.awt.*;import java.io.File;import java.io.FileInputStream;import java.io.IOException;import .JarURLConnection;import .URL;import java.util.*;import java.util.List;import java.util.jar.JarEntry;import java.util.jar.JarFile;/*** <p>* 验证码信息 服务实现类* </p>** @author lpw* @since -06-17*/@Servicepublic class VerifyServiceImpl implements VerifyService {/*** 验证码缓存KEY*/private static final String KEY_VERIFY_CODE = "KEY_VERIFY_CODE";/*** Token Cache Key*/private static final String KEY_VALIDATE_TOKEN = "KEY_VALIDATE_TOKEN";/*** Img Cache Key*/private static final String KEY_VALIDATE_IMG = "KEY_VALIDATE_IMG";/*** 校验误差范围 px*/private static final Integer CHECK_SCOPE = 5;private static List<byte[]> DEFAULT_IMG_LIST;static {try {DEFAULT_IMG_LIST = initFileByte();} catch (IOException e) {ErrorLog.error(e);}}@Overridepublic ResultForm verifyCodeInit() throws IOException {JSONObject result = new JSONObject();/*本地缓存实现*/List<byte[]> imgList = MyCacheManager.getCache(KEY_VERIFY_CODE, KEY_VALIDATE_IMG);if (CollectionUtils.isEmpty(imgList)) {imgList = initFileByte();MyCacheManager.addCache(KEY_VERIFY_CODE, KEY_VALIDATE_IMG, imgList);}//未加载到图片或缓存被外部清除if (imgList == null || imgList.size() < 1) {imgList = DEFAULT_IMG_LIST;}//随机取出一张验证图Random ra = new Random();int rd = ra.nextInt(imgList.size() - 1);byte[] targetIs = imgList.get(rd);//生成验证码PuzzleCaptcha puzzleCaptcha = new PuzzleCaptcha(targetIs);puzzleCaptcha.setImageQuality(Image.SCALE_AREA_AVERAGING);puzzleCaptcha.run();//抠块图String imgBtn = ImageConvertUtil.toDataUri(puzzleCaptcha.getVacancy(), "png");//背景图String imgBg = ImageConvertUtil.toDataUri(puzzleCaptcha.getArtwork(), "png");String token = UUID.randomUUID().toString().replaceAll("-", "");Map<String, Object> cacheObj = new HashMap<>(5);cacheObj.put("token", token);//偏移量cacheObj.put("X", puzzleCaptcha.getX());cacheObj.put("Y", puzzleCaptcha.getY());//验证起始时间cacheObj.put("time", System.currentTimeMillis());//保存验证状态cacheObj.put("verifyCount", 0);//保存2分钟MyCacheManager.addOrUpdateCacheAndSetExpireTime(KEY_VALIDATE_TOKEN, token, cacheObj, 120);result.put("imgBtn", imgBtn);result.put("imgBg", imgBg);result.put("tokenId", token);result.put("Y", puzzleCaptcha.getY());return new ResultForm(result);}@Overridepublic ResultForm checkVerifyCode(String tokenId, Integer x, Integer y) {JSONObject message = new JSONObject();int code;String resultStr;double time = 0.00;if (StringUtils.isEmpty(tokenId) || x == null || y == null) {message.put("code", 0);message.put("message", "请求参数错误:参数不能为空");return new ResultForm(message);}Map<String, Object> cacheObj = MyCacheManager.getCache(KEY_VALIDATE_TOKEN, tokenId);if (null == cacheObj) {code = -1;resultStr = "验证码超期,请重新请求!";} else {int sX = (Integer) cacheObj.get("X");int sY = (Integer) cacheObj.get("Y");int sStatus = (Integer) cacheObj.get("verifyCount");if (sY != y) {code = 0;resultStr = "请求参数错误:位置信息不正确!";} else {if (Math.abs(sX - x) <= CHECK_SCOPE) {code = 1;resultStr = "验证通过!";time = System.currentTimeMillis() - (Long) cacheObj.get("time");//更新验证状态,存储60scacheObj.put("verifyCount", sStatus + 1);MyCacheManager.updateCacheAndSetExpireTime(KEY_VALIDATE_TOKEN, tokenId, cacheObj, 60);} else {code = 2;resultStr = "验证不通过,请重试!";}}}message.put("time", NumberUtil.div(time * 1.00, 1000.00, 2));message.put("code", code);message.put("message", resultStr);return new ResultForm(message);}@Overridepublic Boolean isAccessCode(String tokenId) {if (StringUtils.isNotEmpty(tokenId)) {Map<String, Object> cacheObj = MyCacheManager.getCache(KEY_VALIDATE_TOKEN, tokenId);return cacheObj != null && 1 == (Integer) cacheObj.get("verifyCount");}return false;}/*** 读取文件字节流*/private static List<byte[]> initFileByte() throws IOException {//图片存储路径在resource/static/verify_imgs包下String imgPath = "static/verify_imgs";URL url = Thread.currentThread().getContextClassLoader().getResource(imgPath);if (url != null) {String protocol = url.getProtocol();return "jar".equalsIgnoreCase(protocol) ? initJarImg(url) : initClassImg(url);}return null;}/*** 加载项目资源图片文件** @param url* @return*/private static List<byte[]> initClassImg(URL url) throws IOException {if (url != null) {File fileDir = new File(url.getPath());File[] fs = fileDir.listFiles();if (fs != null && fs.length > 0) {List<byte[]> byteList = new ArrayList<>();byte[] bytes;for (File f : fs) {bytes = IOUtils.toByteArray(new FileInputStream(f));byteList.add(bytes);}return byteList;}}return null;}/*** 加载jar包资源图片文件** @param url* @return*/private static List<byte[]> initJarImg(URL url) throws IOException {if (url != null) {String jarPath = url.getPath().substring(0, url.getPath().indexOf("!/") + 2);URL jarUrl = new URL("jar:" + jarPath);JarURLConnection jarCon = (JarURLConnection) jarUrl.openConnection();if (jarCon != null) {JarFile jarFile = jarCon.getJarFile();Enumeration<JarEntry> jarEntrys = jarFile.entries();byte[] bytes;List<byte[]> byteList = new ArrayList<>();while (jarEntrys.hasMoreElements()) {JarEntry entry = jarEntrys.nextElement();String name = entry.getName();//打包jar后的图片资源地址if (name.startsWith("BOOT-INF/classes/static/verify_imgs") && name.contains(".jpg")) {bytes = IOUtils.toByteArray(ValidateController.class.getClassLoader().getResourceAsStream(name));byteList.add(bytes);}}return byteList;}}return null;}}

接口类

import java.io.IOException;/*** <p>* 验证码信息 服务* </p>** @author lpw* @since -06-17*/public interface VerifyService {/*** 验证码生成* @return* @throws IOException*/ResultForm verifyCodeInit() throws IOException;/*** 验证码验证* @param tokenId 验证码id* @param x* @param y* @return*/ResultForm checkVerifyCode(String tokenId, Integer x, Integer y);/*** 查询是否通过验证码验证* @param tokenId* @return*/Boolean isAccessCode(String tokenId);}

其中ResultFrom为返回消息封装,可根据实际项目中修改,里面包含状态码,消息。

MyCacheManager类为Ehcache 缓存操作工具类,可根据实际存储自行编写。

前台代码简单示例:

<!DOCTYPE html><html><head><meta charset="UTF-8"><title>验证码测试</title><script src="jquery.min.js"></script><style>.rightValidate {width: 350px;margin: 0px auto;position: relative;line-height: 33px;height: 33px;text-align: center;z-index: 99;}.v_rightBtn {position: absolute;left: 0;top: 0;height: 33px;width: 40px;background: #ddd;cursor: pointer;}.imgBtn{width:40px;height: 50px;position: absolute;left: 0;display: none;}.imgBtn img{width:100%;height: 213px;}.imgBg{position: relative;width: 350px;height: 0;box-shadow: 0px 4px 8px #3C5476;}.hkinnerWrap{border: 1px solid #eee;}.green{border-color:#34C6C2 !important;}.green .v_rightBtn{background: #34C6C2;color: #fff;}.red{border-color:red !important;}.red .v_rightBtn{background: red;color: #fff;}.refresh{position: absolute;width: 30px;height: 30px;right: 0;top: 0;font-size: 12px;color: #fff;text-shadow: 0px 0px 9px #333;cursor: pointer;display: none;}.notSel{user-select: none;-webkit-user-select: none;-moz-user-select: none;-ms-user-select: none;-webkit-touch-callout: none;}</style></head><body><div class="comImageValidate rightValidate"><div class="hkinnerWrap" style="height: 33px;position: relative"><span class="v_rightBtn "><em class="notSel">→</em></span><span class="huakuai" style="font-size: 12px;line-height: 33px;color: #A9A9A9;">向右拖动滑块完成拼图</span><input type = "hidden" name="validX"/></div><div class="imgBg"><div class="imgBtn"><img /></div><span class="refresh">刷新</span></div></div></body><script>var tokenId = "";var y = "";var x = "";$(".comImageValidate").ready(function () {validateImageInit();$(".refresh").click(function () {validateImageInit();})$(".hkinnerWrap").mouseover(function(){$(".imgBg").stop(false).animate({"height":"213px"},100,function () {$(".imgBtn").css("display","block");$(".refresh").css("display","block");});}).mouseleave(function () {$(".imgBg").stop(false).animate({"height":"0"},100,function () {$(".imgBtn").css("display","none");$(".refresh").css("display","none");});});$(".imgBg").mouseover(function () {$(".imgBg").stop(false).animate({"height":"213px"},100,function () {$(".imgBtn").css("display","block");$(".refresh").css("display","block");});}).mouseleave(function () {$(".imgBg").stop(false).animate({"height":"0"},100,function () {$(".imgBtn").css("display","none");$(".refresh").css("display","none");});})$('.v_rightBtn').on({mousedown: function(e) {$(".huakuai").html("");$(".hkinnerWrap").removeClass("red green");var el = $(this);var os = el.offset();dx = e.pageX - os.left;//$(document)$(this).parents(".hkinnerWrap").off('mousemove');$(this).parents(".hkinnerWrap").on('mousemove', function(e) {var newLeft=e.pageX - dx;el.offset({left: newLeft});var newL=parseInt($(".v_rightBtn").css("left"));if(newL<=0){newL=0;}else if (newL>=298){newL=306;}$(".v_rightBtn").css("left",newL+"px");$(".imgBtn").offset({left: newLeft});$(".imgBtn").css("left",newL+"px")}).on('mouseup', function(e) {//$(document)$(this).off('mousemove');})}}).on("mouseup",function () {$(this).parents(".hkinnerWrap").off('mousemove');var l=$(this).css("left");if(l.indexOf("px")!=-1){l=l.substring(0,l.length-2);}x = l;submitDate(l,y,tokenId)})});/*图形验证*/function submitDate(x,y,tokenId) {$.ajax({url:"http://localhost:8088/report/validate/checkVerifyCode?x="+x+"&y="+y+"&tokenId="+tokenId,dataType:'json',type: "POST",success:function (data) {if(data.status == "success") {var result = data.result;if (result.code == 1) {$(".hkinnerWrap").addClass("green").removeClass("red");$(".hkinnerWrap input[name='validX']").val(x);} else {$(".hkinnerWrap").addClass("red").removeClass("green");console.log(result.message);}}else {console.log(data.result);}}})}/*初始化图形验证码*/function validateImageInit() {$.ajax({url:"http://localhost:8088/report/validate/initVerifyCode",dataType:'json',cache:false,type: "get",success:function (data) {if(data.status == "success") {var result = data.result;$(".huakuai").html("向右滑动滑块填充拼图");$(".imgBg").css("background", '#fff url("' + result.imgBg + '")');$(".imgBtn").find("img").attr("src", result.imgBtn);tokenId = result.tokenId;y = result.Y;$(".hkinnerWrap").removeClass("red green");$(".v_rightBtn").css("left", 0);$(".imgBtn").css("left", 0);}else {console.log(data.result);}},error:function(err){console.log(err)}})}</script></html>

实际项目中使用的是vue前台代码,类似斗鱼登录验证,这里暂不提供,可参照上述html界面自己编写。

背景图片示例

验证码示例:

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。