1.“问题-答案”模式的图片验证
常见的图片验证有字母数字组合
、简单算术题
、简单选择题
等等,这种类型的验证码的可行性在于人类能够轻松识别、判断,而机器比较难区分,但随着人工智能和机器学习的不断发展,这类验证码越来越容易破解了,不过生成图片的时候做一定程度的干扰(比如字符重叠、增加干扰线条、使用非纯色背景、甚至采用gif)可以加大破解成本。
1.1 工作流程(以登录验证为例,其它业务场景同样适用)
1.进入登录页,客户端向服务端发送预登录
请求;
2.服务端生成随机字符串verify_code
(通常为字母+数字组合),并以字符串为内容生成图片verify_image
,同时创建一个唯一的verify_key
与字符串绑定在服务端存储(可以设置有效期),将verify_image
和verify_key
一并作为预登录的响应结果返回客户端;
3.用户登录,客户端向服务端发送登录
请求,以用户输入的账号、密码、验证码input_code
以及预登录请求得到verify_key
作为请求参数;
4.服务端接收登录请求后检查参数是否完整,若参数完整,则根据verify_key
获取存储的verify_code
与input_code
进行比较(无论是否一致均删除此条记录,保证一个验证码只能验证一次),完全一致则继续执行账号、密码验证,否则直接登录失败。
1.2 代码实现
VerifyCode.kt (服务端生成图片)
import java.awt.BasicStroke
import java.awt.Color
import java.awt.Font
import java.awt.Graphics2D
import java.awt.image.BufferedImage
import java.io.File
import java.io.IOException
import java.io.OutputStream
import java.util.Random
import javax.imageio.ImageIO
import javax.imageio.stream.FileImageOutputStream
/**
* 验证码工具类
* 代码来自 https://blog.csdn.net/yangxuwang888/article/details/81431705
*/
class VerifyCode {
private val w = 70
private val h = 35
private val r = Random()
// {"宋体", "华文楷体", "黑体", "华文新魏", "华文隶书", "微软雅黑", "楷体_GB2312"}
private val fontNames = arrayOf("宋体", "华文楷体", "黑体", "微软雅黑", "楷体_GB2312")
private val codes = "23456789abcdefghjkmnopqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ"
private val bgColor = Color(255, 255, 255)
// 向图片中画4个字符
/**
* 首字符的基线位于用户空间的 (x, h-5) 位置处
* 原点在左上角,X轴递增的方向是从左向右;Y轴是从上到下
* 在提供的坐标位于基线上最左边字符的情况下,可以从右到左呈现字形
* h-5表示y轴方向,向上偏移了5
*/
val image: BufferedImage
get() {
val image = createImage()
val g2 = image.graphics as Graphics2D
val sb = StringBuilder()
for (i in 0..3) {
val s = randomChar() + ""
sb.append(s)
val x = i.toFloat() * 1.0f * w.toFloat() / 4
g2.font = randomFont()
g2.color = randomColor()
g2.drawString(s, x, (h - 5).toFloat())
}
text = sb.toString()
drawLine(image)
return image
}
private fun randomColor(): Color {
val red = r.nextInt(150)
val green = r.nextInt(150)
val blue = r.nextInt(150)
return Color(red, green, blue)
}
private fun randomFont(): Font {
val index = r.nextInt(fontNames.size)
val fontName = fontNames[index]
val style = r.nextInt(4)
val size = r.nextInt(5) + 24
return Font(fontName, style, size)//指定字体名称、样式和点大小,创建一个新 Font。
}
//画干扰的线条
private fun drawLine(image: BufferedImage) {
val num = 3//画三条
val g2 = image.graphics as Graphics2D
for (i in 0 until num) {
val x1 = r.nextInt(w)
val y1 = r.nextInt(h)
val x2 = r.nextInt(w)
val y2 = r.nextInt(h)
g2.stroke = BasicStroke(1.5f)
g2.color = Color.BLUE
g2.drawLine(x1, y1, x2, y2)
}
}
private fun randomChar(): Char {
val index = r.nextInt(codes.length)
return codes[index]
}
private fun createImage(): BufferedImage {
val image = BufferedImage(w, h, BufferedImage.TYPE_INT_RGB)
val g2 = image.graphics as Graphics2D
g2.color = this.bgColor
g2.fillRect(0, 0, w, h)
return image
}
companion object {
var text: String? = null
@Throws(IOException::class)
fun output(image: BufferedImage, out: OutputStream) {
ImageIO.write(image, "JPEG", out)
}
}
}
/*
fun main(args: Array) {
for (i in 1..10) {
val code = VerifyCode()
val image = code.image
//获取图片
ImageIO.write(image, "jpg", FileImageOutputStream(File("/tmp/$i.jpg")))
//获取图片中的文字
println(VerifyCode.text)
}
}
*/
TestController.kt (基于SpringMVC实现的用于测试的API)
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
import javax.imageio.ImageIO
import javax.servlet.http.HttpServletResponse
import sun.misc.BASE64Encoder
import java.io.ByteArrayOutputStream
@RestController
class TestController {
@Autowired
private lateinit var response: HttpServletResponse
/**
* 直接返回图片
*/
@GetMapping("/test")
fun test() {
val code = VerifyCode()
val image = code.image
//获取图片
ImageIO.write(image, "jpg", response.outputStream)
//获取图片中的文字
println(VerifyCode.text)
}
/**
* 将图片base64编码返回
*/
@GetMapping("/test2")
fun test2(): String {
val code = VerifyCode()
val image = code.image
val byteArrayOutputStream = ByteArrayOutputStream()//io流
ImageIO.write(image, "jpg", byteArrayOutputStream)//写入流中
val bytes = byteArrayOutputStream.toByteArray()//转换成字节
val encoder = BASE64Encoder()
var base64 = encoder.encodeBuffer(bytes).trim()//转换成base64串
base64 = base64.replace("\n".toRegex(), "").replace("\r".toRegex(), "")//删除 \r\n
return base64
}
}
test.html (显示验证码的测试页面)
Title
其它逻辑按部就班实现起来没有什么花头,不再一一赘述。
2.利用验证行为的特征区分人机差异的行为验证
典型案例:极验验证码
「行为验证」不单纯基于“问题-答案”的模式来区别人机,而是基于完成验证过程中的行为模式和行为特征,通过深度学习对行为数据进行高维分析,构建人机边界。
摘自 https://docs.geetest.com/install/overview/prodes/
具体实现原理和交互逻辑可以查看极验
官网。当然,极验行为验证的核心技术在于机器学习,这部分内容的实现在极验的后台,对使用者而言是不可见的。
3.其它验证
- 淘宝使用过的选择你曾经购买过的商品图片...
- 12306使用过的小朋友认图片(体验很差,被喷成狗屎)
- ......