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<String>) {
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 (显示验证码的测试页面)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<img id="img" src="https://cdn2.jianshu.io/assets/web/nav-logo-4c7bbafe27adc892f3046e6978459bac.png"
alt="" width="70" height="35"/>
</body>
<script>
setTimeout(function () {
document.getElementById("img").src = "data:image/jpg;base64,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"
},2000)
</script>
</html>
其它逻辑按部就班实现起来没有什么花头,不再一一赘述。
2.利用验证行为的特征区分人机差异的行为验证
典型案例:极验验证码
「行为验证」不单纯基于“问题-答案”的模式来区别人机,而是基于完成验证过程中的行为模式和行为特征,通过深度学习对行为数据进行高维分析,构建人机边界。
摘自 https://docs.geetest.com/install/overview/prodes/
具体实现原理和交互逻辑可以查看极验
官网。当然,极验行为验证的核心技术在于机器学习,这部分内容的实现在极验的后台,对使用者而言是不可见的。
3.其它验证
- 淘宝使用过的选择你曾经购买过的商品图片...
- 12306使用过的小朋友认图片(体验很差,被喷成狗屎)
- ......
网友评论