安全丨验证码

前言

如今,无论是移动端还是网页端,验证码随处可见,例如:手机号登录会有短信验证码、手机消费支付会有指纹验证、网页端发表评论也有验证、哔哩哔哩弹幕网会有算术验证、京东账号密码登录会有滑块拼图验证、谷歌登录也有图片识别验证、银行业务需要身份证或人脸认证。这众多的验证方式,目的是验证用户是人类还是机器,防止恶意注册、登录、发帖、领优惠券、投票等等;防止用户疯狂占用服务器资源;在用户更改密码、异地登录时,提高账号安全性...

验证码分类

随机字符串验证码

这是最常见的形式,就是随机生成由大写字母或小写字母或数字或汉字组成的字符串。优点:生成方便,识别难易度可调节。缺点:识别度不好掌握,因为可辨识度高了容易被机器识别,可辨识度低了会造成用户体验差(图像识别上机器已经在某种条件下超越了人类)。

实现

这是一个简单的 jsp 页面:

<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<head>
    <title>Java-随机字符串验证码</title>
    <style type="text/css">
        .code_a {
            color: #3498db;
            font-size: 12px;
            text-decoration: none;
            cursor: pointer;
        }
        
        #imgCode {
            cursor: pointer;
        }
    </style>
    <script type="text/javascript">
        function changeCode(){
            var imgCode = document.getElementById("imgCode");
            // 为了达到刷新的效果,需要每一次访问路径不同
            imgCode.src = "randomCodeServlet?" + Math.random();
        }
    </script>
</head>
<body>
   <form action="valiCodeServlet" method="post">
       <label>验证码:</label>
       <input type="text" id="inCode" name="inCode" />
       <%--    图片引用为 randomCodeServlet 的访问路径  --%>
       <img src="randomCodeServlet" align="center" id="imgCode" onclick="changeCode()" />
       <a class="code_a" onclick="changeCode()">看不清?换一张</a>
       <br />
       <input type="submit" value="登录" />
   </form>
   <!-- 错误信息提示 -->
   <div style="color: #e74c3c;">${error}</div>
</body>
</html>

生成验证码字符串的 randomCodeServlet 程序:

import javax.imageio.ImageIO;
import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.annotation.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Random;

// 使用 @webServlet 注解
@WebServlet(name = "randomCodeServlet", value = "/randomCodeServlet")
public class randomCodeServlet extends HttpServlet {

    // 因为会重复使用,所以定义为全局变量
    private int width = 80;
    private int height =30;
    private int fontSize = 12;
    private Random random = new Random();
    private String str = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";

    // 随机组合颜色
    private Color randomColor(){
        // 三基色范围 0-255
        int red = random.nextInt(256);
        int green = random.nextInt(256);
        int blue = random.nextInt(256);
        return new Color(red, green, blue);
    }

    // 随机组合字符串
    private String randomStr(int len){
        if (len < 4){
            len = 4;
        }
        // 更改图片的宽度以适应字符数量
        width = 5+fontSize*len;
        String code = "";
        // 增加难度-最少 4个
        for (int i = 0; i < len; i++){
            code += str.charAt(random.nextInt(str.length()));
        }
        return code;
    }

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        this.doPost(request, response);
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 1. 创建画板 参数:高度,宽度,图片样式
        BufferedImage bufImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        // 2. 创建画笔
        Graphics2D pen = (Graphics2D) bufImg.getGraphics();
        // 3. 生成随机内容
        String codeStr = randomStr(4);
            // 3.1. 保存生成的随机码
        request.getSession().setAttribute("valiCode", codeStr);
        // 4. 绘制内容
        // 4.1. 设置绘制区域
        pen.fillRect(0, 0, width, height);
        // 4.2. 设置字体
        pen.setFont(new Font("微软雅黑", Font.BOLD, fontSize));
        // 4.3. 按照一定顺序逐个绘制字符
        for (int x = 0; x < codeStr.length(); x++){
            // 每一个字体颜色都随机
            pen.setColor(randomColor());
            pen.drawString(codeStr.charAt(x)+"", 5+x*fontSize, (fontSize+height)/2);
        }
        // 4.4. 绘制噪音线 用于识别困难度
        for (int i = 0; i < 3; i++){
            pen.setColor(randomColor());
            // 设置线条的粗细 数字越大越粗
            pen.setStroke(new BasicStroke(1));
            pen.drawLine(random.nextInt(width/2), random.nextInt(height),
                    random.nextInt(width), random.nextInt(height));
        }
        // 5. 另存为图片发送
        ServletOutputStream out = response.getOutputStream();
        ImageIO.write(bufImg, "png", out);
        // 刷新缓存
        out.flush();
        out.close();
    }
}

验证用户输入的验证码的 valiCodeServlet 程序:

import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.annotation.*;
import java.io.IOException;
import java.util.Locale;

// valiCodeServlet 用于验证验证码
@WebServlet(name = "valiCodeServlet", value = "/valiCodeServlet")
public class valiCodeServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        this.doPost(request, response);
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 1. 得到用户输入的验证码数据 转换为字符串 小写
        String inCode = request.getParameter("inCode").toString().toLowerCase();
            // 1.1。读取存进 session 的验证码
        String valiCode = request.getSession().getAttribute("valiCode").toString().toLowerCase();
        // 2. 验证是否正确
        if (inCode.equals(valiCode)){
            // 跳转到 index.jsp
            response.sendRedirect("index.jsp");
        }else {
            request.getSession().setAttribute("error", "验证码错误!请重新输入。");
            // 返回上一页
            String url = request.getHeader("Referer");
            response.sendRedirect(url);
        }
    }
}

部署到 Tomcat 后启动,在浏览器访问其路径即可。
其中,生成随机验证码的部分可以使用 jQuery 代替,用 canvas 标签代替 img 标签。
jsp 页面:

<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<head>
    <title>Java-随机字符串验证码V2</title>
    <style type="text/css">
        .code_b {
            color: #3498db;
            font-size: 12px;
            text-decoration: none;
            cursor: pointer;
        }

        #cvs {
            cursor: pointer;
        }
    </style>
    <script type="text/javascript" src="JS/randomV2.js" charset="UTF-8"></script>
    <script type="text/javascript">
        var valicode2;

        function changeCode() {
            var cvs = document.getElementById("cvs");
            valicode2 = drawCode(cvs);
        }

        // 验证用户输入
        function valiCode() {
            var inputCode = document.getElementById("inCode").value;
            // 可能都是空的 null
            if (inputCode.toLowerCase() === valicode2.toLowerCase()) {
                return true;
            } else {
                document.getElementById("err").innerHTML = "验证码错误!请重新输入。";
                // 更换验证码
                changeCode();
                // 为了不刷新页面
                return false;
            }
        }
        // 初始化就加载验证码
        window.onload = changeCode;
    </script>
</head>
<body>
<form action="index.jsp" method="post">
    <label>验证码:</label>
    <label for="inCode"></label><input type="text" id="inCode" name="inCode"/>
    <canvas id="cvs" onclick="changeCode()"></canvas>
    <a class="code_b" onclick="changeCode()">看不清?换一张</a>
    <br/>
    <input type="submit" value="登录" onclick="return valiCode()"/>
</form>
<!-- 错误信息提示 -->
<div style="color: #e74c3c;" id="err"></div>
</body>
</html>

生成随机验证码的 randomV2 JavaScript 程序:

// 这个是使用 JS 实现随机字符串验证码
// 定义全局变量
var width = 80;
const height = 24;
const fontSize = height - 6;
const strTxt = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";

// 自定义生成随机整数
function randomInt(max) {
    return Math.floor(Math.random() * 100000 % max);
}

// 生成随机长度的字符串验证码
function randomCode(len) {
    // 最少生成4个
    if (len < 4) {
        len = 4;
    }
    let code = "";
    for (let i = 0; i < len; i++) {
        code += strTxt.charAt(randomInt(strTxt.length));
    }
    return code;
}

// 生成随机颜色
function randomColor() {
    const red = randomInt(256);
    const green = randomInt(256);
    const blue = randomInt(256);
    return "rgb(" + red + "," + green + "," + blue + ")";
}

// 生成随机图片
function drawCode(canvas) {
    // 得到随机字符串
    const resCode = randomCode(4);
    console.log(resCode.toString());
    width = 5 + fontSize * resCode.length;
    // 判断浏览器是否满足 canvas可用:Internet Explorer 8 以及更早的版本不支持 <canvas> 元素
    if (canvas != null) {
        if (canvas.getContext && canvas.getContext("2d")) {
            // 设置显示区域大小
            canvas.style.width = width;
            // 设置画笔的高度宽度
            canvas.setAttribute("width", width);
            canvas.setAttribute("height", height);
            // 得到画笔
            const pen = canvas.getContext("2d");
            // 绘制背景
            pen.fillStyle = "rgb(255,255,255)";
            pen.fillRect(0, 0, width, height);
            // 设置绘制字符串的垂直对齐方式 :top middle bottom
            pen.textBaseline = "top";
            // 绘制内容
            for (let i = 0; i < resCode.length; i++) {
                pen.fillStyle = randomColor();
                // px 要加大
                pen.font = "bold" + (fontSize + 100 +randomInt(5)) + "px Arial";
                // 参数:要绘制的字符、字符的横坐标、字符的纵坐标
                pen.fillText(resCode.charAt(i), 5 + fontSize * i, height / 2 + randomInt(5));
            }
            // 绘制噪音线
            for (let j = 0; j < 3; j++) {
                // 起点
                pen.moveTo(randomInt(width) / 2, randomInt(height));
                // 终点
                pen.lineTo(randomInt(width), randomInt(height));
                // 随机颜色
                pen.strokeStyle = randomColor();
                // 线条粗细
                pen.lineWidth = 2;
                pen.stroke();
            }
            return resCode;
        }else {
            console.log("Canvas.getContext方法不可用!!!");
        }
    }else {
        console.log("canvas对象为【null】!!!");
    }
}

算术验证码

算术验证码与随机字符串相比,区别在于:需要用户对显示的表达式进行计算。主要将“生成随机字符”替换为“随机生成两个整数和运算符”。
jsp 页面:

<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<head>
    <title>Java-算术验证码V2</title>
    <style type="text/css">
        .code_b {
            color: #3498db;
            font-size: 12px;
            text-decoration: none;
            cursor: pointer;
        }
        
        #cvs {
            cursor: pointer;
        }
    </style>
    <%-- 更改引用的 js文件 --%>
    <script type="text/javascript" src="JS/arithmeticV2.js" charset="UTF-8"></script>
    <script>
        var valicode;
        function changeCode(){
            var cvs = document.getElementById("cvs");
            valicode = drawCode(cvs);
        }
        // 验证用户输入
        function valiCode(){
            var inputCode = document.getElementById("inCode").value;
            // JS返回的结果是一个 int ,所以转型
            if (inputCode.toString() === valicode.toString()){
                return true;
            }else {
                document.getElementById("err").innerHTML = "验证码错误!请重新输入。";
                // 更换验证码
                changeCode();
                // 为了不刷新页面
                return false;
            }
        }
        // 初始化就加载验证码
        window.onload = changeCode;
    </script>
</head>
<body>
   <form action="index.jsp" method="post">
       <label>验证码:</label>
       <input type="text" id="inCode" name="inCode" />
       <canvas id="cvs" onclick="changeCode()"></canvas>
       <a class="code_b" onclick="changeCode()">看不清?换一张</a>
       <br />
       <input type="submit" value="登录" onclick="return valiCode()" />
   </form>
   <!-- 错误信息提示 -->
   <div style="color: #e74c3c;" id="err"></div>
</body>
</html>

对应的 JavaScript 程序:

// 这个是使用 JQuery实现算术验证码
// 定义全局变量
var width = 80;
var height = 24;
var fontSize = height - 6;
var Str = "+-×÷";

// 自定义生成随机整数:最大值不超过 max
function randomInt(max) {
    return Math.floor(Math.random() * 100000 % max);
}

// 生成随机长度的字符串验证码
function randomCode() {
    var one = randomInt(100);
    var two = randomInt(100);
    var operator = Str.charAt(randomInt(Str.length));

    return "" + one + operator + two + "=";
}

// 生成随机颜色
function randomColor() {
    var red = randomInt(256);
    var green = randomInt(256);
    var blue = randomInt(256);
    return "rgb(" + red + "," + green + "," + blue + ")";
}

// 生成随机图片
function drawCode(canvas) {
    var resCode = randomCode();
    width = 5 + fontSize * resCode.length;
    // 判断浏览器是否满足 canvas可用
    if (canvas != null) {
        if (canvas.getContext && canvas.getContext("2d")) {
            // 设置显示区域大小
            canvas.style.width = width;
            // 设置画笔的高度宽度
            canvas.setAttribute("width", width);
            canvas.setAttribute("height", height);
            // 得到画笔
            var pen = canvas.getContext("2d");
            // 绘制背景
            pen.fillStyle = "rgb(255,255,255)";
            pen.fillRect(0, 0, width, height);
            // 设置水平线的位置 :middle bottom
            pen.textBaseline = "top";
            // 绘制内容
            for (var i = 0; i < resCode.length; i++) {
                pen.fillStyle = randomColor();
                pen.font = "BOLD" + (fontSize + 500 + randomInt(3)) + "px 微软雅黑";
                pen.fillText(resCode.charAt(i), 5 + fontSize * i, height / 2 + randomInt(5));
            }
            // 绘制噪音线
            for (var j = 0; j < 3; j++) {
                // 起点
                pen.moveTo(randomInt(width) / 2, randomInt(height));
                // 终点
                pen.lineTo(randomInt(width), randomInt(height));
                // 随机颜色
                pen.strokeStyle = randomColor();
                // 线条粗细
                pen.lineWidth = 2;
                pen.stroke();
            }

            // 去掉等于号
            resCode = resCode.substr(0, resCode.length - 1);
            // 调用 eval()函数,它可计算某个字符串,并执行其中的的 JavaScript 代码。
            return eval(resCode);
        }
    }
}

在哔哩哔哩弹幕网,直播间领取领瓜子宝箱时需要算术验证。

图片滑块验证码

以哔哩哔哩弹幕网、京东为例,在注册用户时都使用了图片滑块验证码:


哔哩哔哩注册
京东注册

用 CSS 与 JavaScript 实现一个图片滑块验证码:

破解参考:CSDN

文字验证码

以哔哩哔哩弹幕网为例,文字验证码需要你按照一定的顺序或逻辑点击。


文字验证码1
文字验证码2
文字验证码3

手机短信验证码

最常用的就是这个了,个人项目中如果有需要的话一般去购买 API。


阿里云API市场
腾讯云API市场

验证码框架

自己实现的话,需要考虑很多,面面俱到;而使用框架的话,根据帮助文档进行配置即可,不需要考虑实现细节。

在 Servlet 中使用

  1. 下载驱动 jar 包,放到项目中的 lib 目录下;
  2. 在 web.xml 文件中配置验证码 Servlet(设置字体、颜色);
  3. 编写验证类;
  4. 前台页面调用并验证。

谷歌的 kaptcha

打开下载链接,下载后解压把得到的 kaptcha-2.3.2.jar 引入 Module 或 Libraries 中。
区别:Modules 下的 Dependencies 引入的依赖 jar 包,仅供当前 Module 模块使用;而 Libraries 下引入的是供整个Project 项目来使用的。
web.xml 中配置如下:

<!-- 使用 kaptcha 框架的 servlet -->
<servlet>
    <servlet-name>myKaServlet</servlet-name>
    <!-- 这个是 kaptcha 的 servlet,不是自己写的  -->
    <servlet-class>com.google.code.kaptcha.servlet.KaptchaServlet</servlet-class>
    <!-- 自定义属性值 -->
    <init-param>
        <!-- 去除边框 -->
        <param-name>kaptcha.border</param-name>
        <param-value>no</param-value>
    </init-param>
    <init-param>
        <!-- 字体颜色 -->
        <param-name>kaptcha.textproducer.font.color</param-name>
        <!-- 对应的RGB值,没有空格 -->
        <param-value>16,157,88</param-value>
    </init-param>
    <init-param>
        <!-- 图片宽度 -->
        <param-name>kaptcha.image.width</param-name>
        <param-value>250</param-value>
    </init-param>
    <init-param>
        <!-- 图片高度 -->
        <param-name>kaptcha.image.height</param-name>
        <param-value>50</param-value>
    </init-param>
    <init-param>
        <!-- 使用的字符集 -->
        <param-name>kaptcha.textproducer.char.string</param-name>
        <param-value>Aa1bZc2dYeXfW3VgUhTiSj4kRlQmP0O9NnMoL5pKqJrIsHt8Gu6vFwExDyzC7B</param-value>
    </init-param>
    <init-param>
        <!-- 验证码的长度:默认为 5 -->
        <param-name>kaptcha.textproducer.char.length</param-name>
        <param-value>6</param-value>
    </init-param>
    <init-param>
        <!-- 验证码的间隔:像素 -->
        <param-name>kaptcha.textproducer.char.space</param-name>
        <param-value>3</param-value>
    </init-param>
    <init-param>
        <!-- 字体大小 -->
        <param-name>kaptcha.textproducer.font.size</param-name>
        <param-value>40</param-value>
    </init-param>
    <init-param>
        <!-- 图片显示样式 -->
        <param-name>kaptcha.obscurificator.impl</param-name>
        <param-value>com.google.code.kaptcha.impl.ShadowGimpy</param-value>
    </init-param>
</servlet>

<servlet-mapping>
    <servlet-name>myKaServlet</servlet-name>
    <!-- 访问图片的路径 -->
    <url-pattern>/showKaCode</url-pattern>
</servlet-mapping>

<!-- 这个才是自己写的处理验证码的servlet,继承 HttpServlet -->
<servlet>
    <servlet-name>KaServlet</servlet-name>
    <servlet-class>com.yyt.VerificationCode.KaServlet</servlet-class>
</servlet>

<servlet-mapping>
    <servlet-name>KaServlet</servlet-name>
    <url-pattern>/KaServlet</url-pattern>
</servlet-mapping>

验证类 KaServlet 程序:

import com.google.code.kaptcha.Constants;
import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.annotation.*;
import java.io.IOException;

/**
 * 验证 kaptcha框架的验证码
 * @author yyt
 */
@WebServlet(name = "KaServlet", value = "/KaServlet")
public class KaServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        this.doPost(request, response);
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 1.得到图片验证码:KaptchaServlet会把验证码设置到session中,得到之后,转小写
        String valiCode = (String) request.getSession().getAttribute(Constants.KAPTCHA_SESSION_KEY);
        // java.lang.NullPointerException:因为web.xml文件配置出错了
        System.out.println(valiCode);
        // 转小写
        String temp = valiCode.toLowerCase();
        // 2.得到用户输入的验证码:也需要转小写
        String inputCode = request.getParameter("inCode").toLowerCase();
        // 3.判断是否相等
        if (inputCode.equals(temp)){
            // 跳转
            response.sendRedirect("index.jsp");
        }else {
            request.getSession().setAttribute("error", "验证码错误!请重新输入。");
            // 返回上一页
            String url = request.getHeader("Referer");
            response.sendRedirect(url);
        }
    }
}

前台页面 KaCode.jsp

<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<head>
    <title>谷歌kaptcha框架-验证码</title>
    <style type="text/css">
        .code_a {
            color: #3498db;
            font-size: 12px;
            text-decoration: none;
            cursor: pointer;
        }
        
        #imgCode {
            cursor: pointer;
        }
    </style>
    <script type="text/javascript">
        function changeCode(){
            var imgCode = document.getElementById("imgCode");
            // 为了达到刷新的效果,需要每一次访问路径不同
            imgCode.src = "showKaCode?" + Math.random();
        }
    </script>
</head>
<body>
   <form action="KaServlet" method="post">
       <label>验证码:</label>
       <input type="text" id="inCode" name="inCode" />
       <%--    图片路径为 web.xml 中 mapping 的 url-pattern 定义的访问路径  --%>
       <img src="showKaCode" align="center" id="imgCode" onclick="changeCode()" />
       <a class="code_a" onclick="changeCode()">看不清?换一张</a>
       <br />
       <input type="submit" value="登录" />
   </form>
   <!-- 错误信息提示 -->
   <div style="color: #e74c3c;">${error}</div>
</body>
</html>

开源的 EasyCaptcha

效果更加精美,验证码更加多样。

官方文档:EasyCaptcha

在 Spring Boot 中使用

一般通过 maven 方式引入依赖,然后在 Controller 中设置验证码的参数配置。

其他

除了这些验证码,还有一些验证码比较有意思,可以在这个网站看到。

  • 在验证码图片中,字母有多种颜色,例如红、蓝、粉,只需要输入粉色的字母作为验证码;
  • 在验证码图片的上方,有一行小符号,例如实心圆、三角形,只需要输入实心圆下面的字母作为验证码;
  • 验证码图片的背景图是一行比较模糊的字母,真正的验证码比较清晰,而且要求输入的验证码之间有空格。

讨厌点

有时候,一张验证码可以治疗好你的低血压。
例如,如果混用了不同的字体,那么数字 1、小写字母 l、大写字母 I 这三者;数字 0 与小写字母 o;数字 9、数字 6、小写字母 g ... 这几种真的很难分清楚。

参考

CSDN - kaptcha验证码使用
打赏
评论区
头像
文章目录