前后端滑块验证码实例附源码(java jquery)

背景:

        发送短信验证码或者登录等场景操作之前都需要进行图片验证码校验或者滑块验证码校验;此举是为了减少黑盒对服务端进行暴力破解密码或者频发短信轰炸请求的操作;

       但如果滑块验证完全由前端进行操作,实际上是不能很好的进行黑盒测试的防御,很容易绕过,所以在这里推出一个前后端结合滑块验证码的实例;

步骤:

  1. 后台生成背景图+模版抠图+抠图横坐标+抠图纵坐标;
  2. 后台生成流水号,保存抠图的横坐标到内存或者redis等,返回前端背景图+模版抠图+抠图纵坐标;
  3. 前端根据背景图+模版抠图+抠图纵坐标进行滑块插件展示;
  4. 用户操作成功后传输流水号+滑块横坐标到后台,后台进行比较是否在误差范围内(比如5像素)返回前端显示;

效果图:

源码解析:

1、后端java生成滑块图片工具类,传入背景图及抠图模版进行抠图返回背景图+模版抠图+抠图横坐标+抠图纵坐标;

package cn.cc2gjx.sliderverificationcode.sliding;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.Base64Utils;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;

/**
 * 
 * @模块名:javaslidingverification
 * @包名:com.liangyt.javaslidingverification.sliding
 * @类名称: VerifyImageUtil
 * @类描述:【类描述】滑块验证码生成工具类
 * @版本:1.0
 * @创建人:cc
 * @创建时间:2019年10月24日上午10:11:22
 */
public class VerifyImageUtil {

    private static Logger log = LoggerFactory.getLogger(VerifyImageUtil.class);

    private static int BOLD = 5;

    private static final String IMG_FILE_TYPE = "jpg";

    private static final String TEMP_IMG_FILE_TYPE = "png";

    /**
     * 根据模板切图
     * 
     * @param templateFile
     * @param targetFile
     * @return
     * @throws Exception
     */
    public static Map < String, Object > pictureTemplatesCut(File templateFile, File targetFile) throws Exception {
        Map < String, Object > pictureMap = new HashMap <>();
        // 模板图
        BufferedImage imageTemplate = ImageIO.read(templateFile);
        int templateWidth = imageTemplate.getWidth();
        int templateHeight = imageTemplate.getHeight();

        // 原图
        BufferedImage oriImage = ImageIO.read(targetFile);
        int oriImageWidth = oriImage.getWidth();
        int oriImageHeight = oriImage.getHeight();

        // 随机生成抠图坐标X,Y
        // X轴距离右端targetWidth Y轴距离底部targetHeight以上
        Random random = new Random();
        int widthRandom = random.nextInt(oriImageWidth - 2 * templateWidth) + templateWidth;
        // int heightRandom = 1;
        int heightRandom = random.nextInt(oriImageHeight - templateHeight);
        log.info("原图大小{} x {},随机生成的坐标 X,Y 为({},{})", oriImageWidth, oriImageHeight, widthRandom, heightRandom);

        // 新建一个和模板一样大小的图像,TYPE_4BYTE_ABGR表示具有8位RGBA颜色分量的图像,正常取imageTemplate.getType()
        BufferedImage newImage = new BufferedImage(templateWidth, templateHeight, imageTemplate.getType());
        // 得到画笔对象
        Graphics2D graphics = newImage.createGraphics();
        // 如果需要生成RGB格式,需要做如下配置,Transparency 设置透明
        newImage = graphics.getDeviceConfiguration().createCompatibleImage(templateWidth, templateHeight,
                Transparency.TRANSLUCENT);

        // 新建的图像根据模板颜色赋值,源图生成遮罩
        cutByTemplate(oriImage, imageTemplate, newImage, widthRandom, heightRandom);

        // 设置“抗锯齿”的属性
        graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        graphics.setStroke(new BasicStroke(BOLD, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL));
        graphics.drawImage(newImage, 0, 0, null);
        graphics.dispose();

        ByteArrayOutputStream newImageOs = new ByteArrayOutputStream();// 新建流。
        ImageIO.write(newImage, TEMP_IMG_FILE_TYPE, newImageOs);// 利用ImageIO类提供的write方法,将bi以png图片的数据模式写入流。
        byte[] newImagebyte = newImageOs.toByteArray();

        ByteArrayOutputStream oriImagesOs = new ByteArrayOutputStream();// 新建流。
        ImageIO.write(oriImage, IMG_FILE_TYPE, oriImagesOs);// 利用ImageIO类提供的write方法,将bi以jpg图片的数据模式写入流。
        byte[] oriImageByte = oriImagesOs.toByteArray();

        pictureMap.put("slidingImage", Base64Utils.encodeToString(newImagebyte));
        pictureMap.put("backImage", Base64Utils.encodeToString(oriImageByte));
        pictureMap.put("xWidth", widthRandom);
        pictureMap.put("yHeight", heightRandom);
        return pictureMap;
    }

    /**
     * 添加水印
     * 
     * @param oriImage
     */
    /*
     * private static BufferedImage addWatermark(BufferedImage oriImage) throws IOException { Graphics2D graphics2D =
     * oriImage.createGraphics(); graphics2D .setRenderingHint(RenderingHints.KEY_INTERPOLATION,RenderingHints
     * .VALUE_INTERPOLATION_BILINEAR); // 设置水印文字颜色 graphics2D.setColor(Color.BLUE); // 设置水印文字Font graphics2D.setFont(new
     * java.awt.Font("宋体", java.awt.Font.BOLD, 50)); // 设置水印文字透明度 graphics2D.setComposite
     * (AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, 0.5f)); // 第一参数->设置的内容,后面两个参数->文字在图片上的坐标位置(x,y)
     * graphics2D.drawString("zhoujin@qq.com", 400,300); graphics2D.dispose(); //释放 return oriImage; }
     */

    /**
     * @param oriImage 原图
     * @param templateImage 模板图
     * @param newImage 新抠出的小图
     * @param x 随机扣取坐标X
     * @param y 随机扣取坐标y
     * @throws Exception
     */
    private static void cutByTemplate(BufferedImage oriImage, BufferedImage templateImage, BufferedImage newImage,
            int x, int y) {
        // 临时数组遍历用于高斯模糊存周边像素值
        int[][] martrix = new int[3][3];
        int[] values = new int[9];

        int xLength = templateImage.getWidth();
        int yLength = templateImage.getHeight();
        // 模板图像宽度
        for (int i = 0; i < xLength; i++) {
            // 模板图片高度
            for (int j = 0; j < yLength; j++) {
                // 如果模板图像当前像素点不是透明色 copy源文件信息到目标图片中
                int rgb = templateImage.getRGB(i, j);
                if (rgb < 0) {
                    newImage.setRGB(i, j, oriImage.getRGB(x + i, y + j));

                    // 抠图区域高斯模糊
                    readPixel(oriImage, x + i, y + j, values);
                    fillMatrix(martrix, values);
                    oriImage.setRGB(x + i, y + j, avgMatrix(martrix));
                }

                // 防止数组越界判断
                if (i == (xLength - 1) || j == (yLength - 1)) {
                    continue;
                }
                int rightRgb = templateImage.getRGB(i + 1, j);
                int downRgb = templateImage.getRGB(i, j + 1);
                // 描边处理,,取带像素和无像素的界点,判断该点是不是临界轮廓点,如果是设置该坐标像素是白色
                if ((rgb >= 0 && rightRgb < 0) || (rgb < 0 && rightRgb >= 0) || (rgb >= 0 && downRgb < 0)
                        || (rgb < 0 && downRgb >= 0)) {
                    newImage.setRGB(i, j, Color.white.getRGB());
                    oriImage.setRGB(x + i, y + j, Color.white.getRGB());
                }
            }
        }
    }

    private static void readPixel(BufferedImage img, int x, int y, int[] pixels) {
        int xStart = x - 1;
        int yStart = y - 1;
        int current = 0;
        for (int i = xStart; i < 3 + xStart; i++)
            for (int j = yStart; j < 3 + yStart; j++) {
                int tx = i;
                if (tx < 0) {
                    tx = -tx;

                }
                else if (tx >= img.getWidth()) {
                    tx = x;
                }
                int ty = j;
                if (ty < 0) {
                    ty = -ty;
                }
                else if (ty >= img.getHeight()) {
                    ty = y;
                }
                pixels[current++] = img.getRGB(tx, ty);

            }
    }

    private static void fillMatrix(int[][] matrix, int[] values) {
        int filled = 0;
        for (int i = 0; i < matrix.length; i++) {
            int[] x = matrix[i];
            for (int j = 0; j < x.length; j++) {
                x[j] = values[filled++];
            }
        }
    }

    private static int avgMatrix(int[][] matrix) {
        int r = 0;
        int g = 0;
        int b = 0;
        for (int i = 0; i < matrix.length; i++) {
            int[] x = matrix[i];
            for (int j = 0; j < x.length; j++) {
                if (j == 1) {
                    continue;
                }
                Color c = new Color(x[j]);
                r += c.getRed();
                g += c.getGreen();
                b += c.getBlue();
            }
        }
        return new Color(r / 8, g / 8, b / 8).getRGB();
    }

    public static void main(String[] args) {

    }
}

2、进行后台编码,主要有生成滑块并保存横坐标的getPic()和进行校验的checkcapcode()方法;

package cn.cc2gjx.sliderverificationcode.controller;

import java.io.File;
import java.io.IOException;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.TimeUnit;

import javax.servlet.http.HttpServletRequest;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.ResourceUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import cn.cc2gjx.sliderverificationcode.sliding.VerifyImageUtil;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;

/**
 * 
 * @模块名:javaslidingverification
 * @包名:com.liangyt.javaslidingverification.controller
 * @类名称: SliderIMageController
 * @类描述:【类描述】滑块验证码控制层
 * @版本:1.0
 * @创建人:cc
 * @创建时间:2019年10月24日上午10:44:30
 */
@Controller
public class SliderIMageController {
    // 保存横轴位置用于对比,并设置最大数量为10000,多了就先进先出,并设置超时时间为70秒
    public static Cache < String, Integer > cacheg = CacheBuilder.newBuilder().expireAfterWrite(70, TimeUnit.SECONDS)
            .maximumSize(10000).build();

    @GetMapping
    @RequestMapping("index")
    public String test(HttpServletRequest request, Model model) throws IOException {
        return "index";
    }

    @GetMapping
    @RequestMapping("getPic")
    public @ResponseBody Map < String, Object > getPic(HttpServletRequest request) throws IOException {
        // 读取图库目录
        File imgCatalog = new File(ResourceUtils.getURL("classpath:").getPath() + "sliderimage\\targets\\");
        File[] files = imgCatalog.listFiles();
        // 随机选择需要切的图
        int randNum = new Random().nextInt(files.length);
        File targetFile = files[randNum];
        // 随机选择剪切模版
        Random r = new Random();
        int num = r.nextInt(6) + 1;
        File tempImgFile = new File(ResourceUtils.getURL("classpath:").getPath() + "sliderimage\\templates\\" + num
                + "-w.png");
        // 根据模板裁剪图片
        try {
            Map < String, Object > resultMap = VerifyImageUtil.pictureTemplatesCut(tempImgFile, targetFile);
            // 生成流水号,这里就使用时间戳代替
            String lno = Calendar.getInstance().getTimeInMillis() + "";
            cacheg.put(lno, Integer.valueOf(resultMap.get("xWidth") + ""));
            resultMap.put("capcode", lno);
            // 移除横坐标送前端
            resultMap.remove("xWidth");
            return resultMap;
        }
        catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    @GetMapping
    @RequestMapping("checkcapcode")
    public @ResponseBody Map < String, Object > checkcapcode(@RequestParam("xpos") int xpos,
            @RequestParam("capcode") String capcode, HttpServletRequest request) throws IOException {
        Map < String, Object > result = new HashMap < String, Object >();
        Integer x = cacheg.getIfPresent(capcode);
        if (x == null) {
            // 超期
            result.put("code", 3);
        }
        else if (xpos - x > 5 || xpos - x < -5) {
            // 验证失败
            result.put("code", 2);
        }
        else {
            // 验证成功
            result.put("code", 1);
            // .....做自己的操作,发送验证码
        }

        return result;
    }
}

3、前端关键插件代码,和网上的有所差别,添加了纵轴的变更:

/**
 * Created by lgy on 2017/10/21. 图片验证码
 */
(function($) {
	$.fn.imgcode = function(options) {
		// 初始化参数
		var defaults = {
			frontimg : "",
			backimg : "",
			refreshImg : "",
			getsuccess : "",
			getfail : "",
			maskclose : true,
			callback : "", // 回调函数
			refreshcallback : "",
			yHeight : 1

		};
		var opts = $.extend(defaults, options);
		return this
				.each(function() {
					var $this = $(this);// 获取当前对象
					var html = '<div class="code-k-div">'
							+ '<div class="code_bg"></div>'
							+ '<div class="code-con">'
							+ '<div class="code-img">'
							+ '<div class="code-img-con">'
							+ '<div class="code-mask"><img class="code-front-img" src="'
							+ opts.frontimg
							+ '"></div>'
							+ '<img class="code-back-img" src="'
							+ opts.backimg
							+ '"></div>'
							+ '<div class="code-push"><i class="icon-login-bg icon-w-25 icon-push">刷新</i><span class="code-tip"></span></div>'
							+ '</div>' + '<div class="code-btn">'
							+ '<div class="code-btn-img code-btn-m"></div>'
							+ '<span class="code-span">按住滑块,拖动完成上方拼图</span>'
							+ '</div></div></div>';
					$this.html(html);
					$(".code-mask").css("margin-top",opts.yHeight+"px")
					// 定义拖动参数
					var $divMove = $(this).find(".code-btn-img"); // 拖动按钮
					var $divWrap = $(this).find(".code-btn");// 鼠标可拖拽区域
					var mX = 0, mY = 0;// 定义鼠标X轴Y轴
					var dX = 0, dY = 0;// 定义滑动区域左、上位置
					var isDown = false;// mousedown标记
					if (document.attachEvent) {// ie的事件监听,拖拽div时禁止选中内容,firefox与chrome已在css中设置过-moz-user-select:
												// none; -webkit-user-select:
												// none;
						$divMove[0].attachEvent('onselectstart', function() {
							return false;
						});
					}
					// 按钮拖动事件
					$divMove.unbind('mousedown').on({
						mousedown : function(e) {
							// 清除提示信息
							$this.find(".code-tip").html("");
							var event = e || window.event;
							mX = event.pageX;
							dX = $divWrap.offset().left;
							dY = $divWrap.offset().top;
							isDown = true;// 鼠标拖拽启
							$(this).addClass("active");
							// 修改按钮阴影
							$divMove.css({
								"box-shadow" : "0 0 8px #666"
							});
						}
					});
					// 点击背景关闭
					if (opts.maskclose) {
						$this.find(".code_bg").unbind('click').click(
								function() {
									$this.html("");
								})
					}
					// 刷新code码
					$this.find(".icon-push").unbind('click').click(function() {
						opts.refreshcallback();
					});
					// 鼠标点击松手事件
					$divMove.unbind('mouseup')
							.mouseup(
									function(e) {
										var lastX = $this.find(".code-mask")
												.offset().left
												- dX - 1;
										isDown = false;// 鼠标拖拽启
										$divMove.removeClass("active");
										// 还原按钮阴影
										$divMove.css({
											"box-shadow" : "0 0 3px #ccc"
										});
										returncode(lastX);
									});
					// 滑动事件
					$divWrap
							.mousemove(function(event) {
								var event = event || window.event;
								var x = event.pageX;// 鼠标滑动时的X轴
								if (isDown) {
									if (x > (dX + 30)
											&& x < dX + $(this).width() - 20) {
										$divMove.css({
											"left" : (x - dX - 20) + "px"
										});// div动态位置赋值
										$this.find(".code-mask").css({
											"left" : (x - dX - 30) + "px"
										});
									}
								}
							});
					// 返回坐标系
					function returncode(xpos) {
						opts.callback({
							xpos : xpos
						});
					}
					// 验证数据
					function checkcode(code) {
						var iscur = true;
						// 模拟ajax
						setTimeout(function() {
							if (iscur) {
								checkcoderesult(1, "验证通过");
								$this.find(".code-k-div").remove();
								opts.callback({
									code : 1000,
									msg : "验证通过",
									msgcode : "23dfdf123"
								});
							} else {
								$divMove.addClass("error");
								checkcoderesult(0, "验证不通过");
								opts.callback({
									code : 1001,
									msg : "验证不通过"
								});
								setTimeout(function() {
									$divMove.removeClass("error");
									$this.find(".code-mask").animate({
										"left" : "0px"
									}, 200);
									$divMove.animate({
										"left" : "10px"
									}, 200);
								}, 300);
							}
						}, 500)
					}
					// 刷新图标
					opts.refreshImg = function(data) {
						console.log(data)
						$this.find(".code-img-con .code-front-img").attr("src",
								data.frontImg);
						$this.find(".code-img-con .code-back-img").attr("src",
								data.backGoundImg);
					}
					// 验证成功
					opts.getsuccess = function() {
						checkcoderesult(1, "验证通过");
						setTimeout(function() {
							$this.find(".code-k-div").remove();
						}, 800);

					}
					// 验证失败
					opts.getfail = function(txt) {
						$divMove.addClass("error");
						checkcoderesult(0, txt);
						setTimeout(function() {
							$divMove.removeClass("error");
							$this.find(".code-mask").animate({
								"left" : "0px"
							}, 200);
							$divMove.animate({
								"left" : "10px"
							}, 200);
						}, 400);
					}
					// 验证结果
					function checkcoderesult(i, txt) {
						if (i == 0) {
							$this.find(".code-tip").addClass("code-tip-red");
						} else {
							$this.find(".code-tip").addClass("code-tip-green");
						}
						$this.find(".code-tip").html(txt);
					}
				})
	}
})(jQuery);

4.前端结构及业务实践:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>滑动验证码</title>
<link rel="stylesheet" href="/css/slide.css">
<script src="/js/jquery-1.11.1.min.js"></script>
<script src="/js/jquery.lgyslide.js"></script>
</head>
<body>
	<div id="imgscode"></div>
	<script>
		$(function() {
			setTimeout(function() {
				createcode();
			}, 1000)
		}());
		//显示验证码
		function createcode() {
			$
					.ajax({
						type : 'POST',
						url : '/getPic',
						dataType : 'json',
						success : function(data) {
							if (data != null) {
								$("#imgscode")
										.imgcode(
												{
													frontimg : 'data:image/png;base64,'
															+ data.slidingImage,
													backimg : 'data:image/png;base64,'
															+ data.backImage,
													yHeight : data.yHeight,
													refreshcallback : function() {
														//刷新验证码
														createcode();
													},
													callback : function(msg) {
														console.log(msg);
														var $this = this;
														$
																.ajax({
																	type : 'POST',
																	url : '/checkcapcode',
																	data : {
																		xpos : msg.xpos,
																		capcode : data.capcode
																	},
																	dataType : 'json',
																	success : function(
																			data) {
																		console
																				.log(data)
																		if (data.code == 1) {
																			$this
																					.getsuccess();
																			setTimeout(
																					function() {
																						alert("验证成功,可以做自己的操作了!");
																					},
																					800);
																		} else {
																			if (data.code == 4) {
																				createcode();
																			} else if (data.code == 3) {
																				$this
																						.getfail("验证码过期,请刷新");
																			} else {
																				$this
																						.getfail("验证不通过");
																			}
																		}

																	}
																})
													}
												});
							}
						}
					})
		}
	</script>
</body>
</html>

5.访问http://localhost:8080/index;效果如上所述;

源码地址:

https://github.com/cc6688211/sliderverificationcode.git

  • 4
    点赞
  • 46
    收藏
    觉得还不错? 一键收藏
  • 9
    评论
Java滑块验证码可以通过以下步骤实现: 1. 创建一个JPanel作为验证码的容器,设置布局为BorderLayout。 2. 在JPanel的中央区域添加一个JPanel,作为滑块验证码的背景区域,设置布局为FlowLayout。 3. 在背景区域添加一个JLabel,用于显示验证码的文字。 4. 在背景区域添加一个JPanel,作为滑块区域,设置布局为FlowLayout。 5. 在滑块区域添加一个JLabel,用于显示滑块图片。 6. 在滑块区域添加一个JPanel,作为滑块拖动区域,设置布局为FlowLayout。 7. 在滑块拖动区域添加一个JLabel,用于显示滑块拖动图片。 8. 在滑块区域添加鼠标监听器,实现滑块的拖动。 9. 在JPanel中添加一个按钮,用于重置验证码。 10. 将验证码的字符串和拖动距离等信息保存到Session中,方便后续验证。 示例代码如下: ```java import java.awt.*; import java.awt.event.*; import javax.swing.*; public class SliderCaptcha extends JPanel { private String captchaText; private int dragDistance; public SliderCaptcha() { setLayout(new BorderLayout()); // 创建背景区域 JPanel backgroundPanel = new JPanel(new FlowLayout()); add(backgroundPanel, BorderLayout.CENTER); // 创建验证码文字 captchaText = "ABCD"; JLabel captchaLabel = new JLabel(captchaText); captchaLabel.setFont(new Font("Arial", Font.BOLD, 24)); backgroundPanel.add(captchaLabel); // 创建滑块区域 JPanel sliderPanel = new JPanel(new FlowLayout()); backgroundPanel.add(sliderPanel); // 创建滑块图片 ImageIcon sliderIcon = new ImageIcon("slider.png"); JLabel sliderLabel = new JLabel(sliderIcon); sliderPanel.add(sliderLabel); // 创建滑块拖动区域 JPanel dragPanel = new JPanel(new FlowLayout()); dragPanel.setPreferredSize(new Dimension(sliderIcon.getIconWidth(), sliderIcon.getIconHeight())); sliderPanel.add(dragPanel); // 创建滑块拖动图片 ImageIcon dragIcon = new ImageIcon("drag.png"); JLabel dragLabel = new JLabel(dragIcon); dragPanel.add(dragLabel); // 添加鼠标监听器,实现滑块的拖动 sliderPanel.addMouseListener(new MouseAdapter() { private Point dragStartPoint; public void mousePressed(MouseEvent e) { dragStartPoint = e.getPoint(); } public void mouseReleased(MouseEvent e) { int x = e.getX() + sliderPanel.getX() - dragStartPoint.x; if (x > dragDistance) { JOptionPane.showMessageDialog(null, "验证成功!"); } else { JOptionPane.showMessageDialog(null, "验证失败,请重试!"); } } }); // 创建重置按钮 JButton resetButton = new JButton("重置"); resetButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { captchaText = "ABCD"; dragDistance = (int) (Math.random() * 100 + 50); dragLabel.setLocation(dragDistance, dragLabel.getY()); } }); add(resetButton, BorderLayout.SOUTH); // 初始化滑块拖动距离 dragDistance = (int) (Math.random() * 100 + 50); dragLabel.setLocation(dragDistance, dragLabel.getY()); } public static void main(String[] args) { JFrame frame = new JFrame("滑块验证码"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setSize(400, 200); frame.setLocationRelativeTo(null); frame.setContentPane(new SliderCaptcha()); frame.setVisible(true); } } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值