Java 实现滑块验证码登录原理及完整流程

大家好,我是一安,最近接到一个将登录验证码改为滑块验证码的需求。

原始登录验证码为了绿盟渗透加了很多复杂的处理,虽然通过绿盟安检,但地市用户多次反馈改造后验证码自己都不好识别加上超过失败登陆次数会临时锁定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 实现滑块验证码登录原理及完整流程

公众号回复【验证码】,即可获取图片资源

号外!号外!

如果这篇文章对你有所帮助,或者有所启发的话,帮忙点赞、在看、转发、收藏,你的支持就是我坚持下去的最大动力!

Java 实现滑块验证码登录原理及完整流程

如何快速搭建一套生产级RabbitMQ


一文教你快速上手EasyExcel


多线程+EasyExcel实现报表优雅导出

Java 实现滑块验证码登录原理及完整流程


原文始发于微信公众号(一安未来): Java 实现滑块验证码登录原理及完整流程

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/44584.html

(0)
小半的头像小半
0 0

相关推荐

  • 【63期】谈谈MySQL 索引,B+树原理,以及建索引的几大原则(MySQL面试第六弹) 面试题

    【63期】谈谈MySQL 索引,B+树原理,以及建索引的几大原则(MySQL面试第六弹)

    0 0129
    小半的头像 小半
    2022年5月17日
  • 7min 到 40s:SpringBoot 优化居然可以玩出这么多花样! Java知音

    7min 到 40s:SpringBoot 优化居然可以玩出这么多花样!

    0 098
    小半的头像 小半
    2023年3月29日
  • 这个 hook api,曾吓退许多前端开发者 React

    这个 hook api,曾吓退许多前端开发者

    0 090
    葫芦侠五楼的头像 葫芦侠五楼
    2024年3月16日
  • 从查询语句执行流程看MySQL架构 微信精选

    从查询语句执行流程看MySQL架构

    0 0147
    小半的头像 小半
    2022年12月21日
  • Django加Vue电商项目实战17 创建商城系统模型 Python

    Django加Vue电商项目实战17 创建商城系统模型

    0 0149
    小半的头像 小半
    2022年11月2日
  • 一文浅谈,数据安全 后端开发

    一文浅谈,数据安全

    0 0127
    小半的头像 小半
    2022年9月20日
  • 你真的了解Redis的数据结构吗 后端开发

    你真的了解Redis的数据结构吗

    0 0120
    小半的头像 小半
    2022年10月9日
  • “八面玲珑”的ZooKeeper入门介绍 微信精选

    “八面玲珑”的ZooKeeper入门介绍

    0 0136
    小半的头像 小半
    2023年11月22日
  • Java工程师的进阶之路 设计模式篇(一) 微信精选

    Java工程师的进阶之路 设计模式篇(一)

    0 0167
    小半的头像 小半
    2023年11月20日
  • Java 多线程并发【7】CAS 和原子类 并发编程

    Java 多线程并发【7】CAS 和原子类

    0 0190
    小半的头像 小半
    2023年1月30日
  • 后端接入层技术的一些思考 后端开发

    后端接入层技术的一些思考

    0 0113
    小半的头像 小半
    2022年9月20日
  • H5 视频兼容性处理总结 技术漫谈

    H5 视频兼容性处理总结

    0 0114
    李, 若俞的头像 李, 若俞
    2024年4月14日

发表回复

登录后才能评论

站长精选

  • 接口性能提升10倍,这波优化很炸裂

    接口性能提升10倍,这波优化很炸裂

    2024年1月1日

  • SpringBoot如何防止反编译?proguard+xjar 完美搞定

    SpringBoot如何防止反编译?proguard+xjar 完美搞定

    2024年2月1日

  • 一款开源、简洁、支持全平台的高速下载神器

    一款开源、简洁、支持全平台的高速下载神器

    2024年5月21日

  • 史上最全的整合第三方授权登录的工具类库,JustAuth 搞定一切!

    史上最全的整合第三方授权登录的工具类库,JustAuth 搞定一切!

    2023年12月9日

  • 公司来了个新同事,把 DDD 运用得炉火纯青!

    公司来了个新同事,把 DDD 运用得炉火纯青!

    2024年2月28日

  • 统一了Type-C接口,又拟回归可拆卸电池,网友:万能充又来了?

    统一了Type-C接口,又拟回归可拆卸电池,网友:万能充又来了?

    2023年6月29日

  • 微服务框架之争:Quarkus 是 SpringBoot 的替代品吗?

    微服务框架之争:Quarkus 是 SpringBoot 的替代品吗?

    2023年11月5日

  • 解密DDD:领域事件--系统解耦的终极武器

    解密DDD:领域事件–系统解耦的终极武器

    2023年9月27日

  • 功能强大、更加现代化的开源数据库管理工具:DbGate

    功能强大、更加现代化的开源数据库管理工具:DbGate

    2024年4月18日

  • 寒冬之下,有哪些还不错的国企科技岗?

    寒冬之下,有哪些还不错的国企科技岗?

    2023年1月6日

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!

深圳SEO优化公司银川建网站价格石岩百度网站优化排名价格福州SEO按天收费哪家好德州百姓网标王推广报价本溪网站推广系统价格宝鸡建网站多少钱唐山至尊标王多少钱宜春网站优化软件报价枣庄关键词排名包年推广推荐石岩网站关键词优化公司安阳网站优化按天扣费推荐阜阳关键词按天扣费梅州网站关键词优化报价和县网站优化按天计费价格临猗网站优化软件价格铜川seo网站优化报价通化优化推荐烟台网站搜索优化怒江网站关键词优化推荐黄山百度竞价包年推广公司宿迁SEO按天收费报价临沧营销型网站建设公司咸阳关键词按天计费报价深圳营销网站哪家好遂宁百度竞价包年推广报价清远网站优化按天收费推荐武汉建站推荐龙岗外贸网站建设价格河池网站关键词优化大鹏建站哪家好歼20紧急升空逼退外机英媒称团队夜以继日筹划王妃复出草木蔓发 春山在望成都发生巨响 当地回应60岁老人炒菠菜未焯水致肾病恶化男子涉嫌走私被判11年却一天牢没坐劳斯莱斯右转逼停直行车网传落水者说“没让你救”系谣言广东通报13岁男孩性侵女童不予立案贵州小伙回应在美国卖三蹦子火了淀粉肠小王子日销售额涨超10倍有个姐真把千机伞做出来了近3万元金手镯仅含足金十克呼北高速交通事故已致14人死亡杨洋拄拐现身医院国产伟哥去年销售近13亿男子给前妻转账 现任妻子起诉要回新基金只募集到26元还是员工自购男孩疑遭霸凌 家长讨说法被踢出群充个话费竟沦为间接洗钱工具新的一天从800个哈欠开始单亲妈妈陷入热恋 14岁儿子报警#春分立蛋大挑战#中国投资客涌入日本东京买房两大学生合买彩票中奖一人不认账新加坡主帅:唯一目标击败中国队月嫂回应掌掴婴儿是在赶虫子19岁小伙救下5人后溺亡 多方发声清明节放假3天调休1天张家界的山上“长”满了韩国人?开封王婆为何火了主播靠辱骂母亲走红被批捕封号代拍被何赛飞拿着魔杖追着打阿根廷将发行1万与2万面值的纸币库克现身上海为江西彩礼“减负”的“试婚人”因自嘲式简历走红的教授更新简介殡仪馆花卉高于市场价3倍还重复用网友称在豆瓣酱里吃出老鼠头315晚会后胖东来又人满为患了网友建议重庆地铁不准乘客携带菜筐特朗普谈“凯特王妃P图照”罗斯否认插足凯特王妃婚姻青海通报栏杆断裂小学生跌落住进ICU恒大被罚41.75亿到底怎么缴湖南一县政协主席疑涉刑案被控制茶百道就改标签日期致歉王树国3次鞠躬告别西交大师生张立群任西安交通大学校长杨倩无缘巴黎奥运

深圳SEO优化公司 XML地图 TXT地图 虚拟主机 SEO 网站制作 网站优化