CAPTCHA实际上是完全自动公开图灵测试的缩写,以区分计算机和人类。该技术与前文介绍的 "用令牌保护表单安全 "中的技术类似。不同的是,该标记不是存储在一个隐藏的表单输入字段中,而是呈现为一个自动攻击系统难以解读的图形。此外,验证码的目的与表单令牌略有不同:它旨在确认网络访问者是人类,而不是自动系统。
如何做...
1.CAPTCHA有几种方法:根据只有人类才会拥有的知识提出问题,文字技巧,以及需要解释的图形图像。
2. 图像方法为网络访问者提供了一个带有严重扭曲的字母和/或数字的图像。然而,这种方法可能很复杂,因为它依赖于GD扩展,而GD扩展可能不是在所有服务器上都能使用。GD扩展可能很难编译,并且严重依赖主机服务器上必须存在的各种库。
3. 文本方法是呈现一系列的字母或数字,并给网络访问者一个简单的指示,如请倒着输入。另一种变化是使用ASCII "艺术 "来形成字符,使人类的网络访问者能够解释。
4. 最后,你可能会有一个问题或答案的方法,问题如The head is attached to the body by what body part,有答案如Arm、Leg、Neck。这种方法的缺点是,自动攻击系统通过测试的几率只有1/3。
生成一个文本验证码
1.在这个例子中,我们将从文本方法开始,然后是图像方法。无论哪种情况,我们首先需要定义一个类来生成短语(并由网络访问者解码)。为此,我们定义了一个Application\Captcha\Phrase
类。我们还定义了短语生成过程中使用的属性和类常量。
复制 namespace Application\Captcha;
class Phrase
{
const DEFAULT_LENGTH = 5;
const DEFAULT_NUMBERS = '0123456789';
const DEFAULT_UPPER = 'ABCDEFGHJKLMNOPQRSTUVWXYZ';
const DEFAULT_LOWER = 'abcdefghijklmnopqrstuvwxyz';
const DEFAULT_SPECIAL =
'¬\`|!"£$%^&*()_-+={}[]:;@\'~#<,>.?/|\\';
const DEFAULT_SUPPRESS = ['O','l'];
protected $phrase;
protected $includeNumbers;
protected $includeUpper;
protected $includeLower;
protected $includeSpecial;
protected $otherChars;
protected $suppressChars;
protected $string;
protected $length;
2. 正如你所期望的那样,构造函数接受各种属性的值,并分配了默认值,因此无需指定任何参数即可创建实例。$include*
标志用于指示哪些字符集将出现在基础字符串中,而短语将从基础字符串中生成。例如,如果你希望只包含数字,$includeUpper
和$includeLower
都会被设置为FALSE
。提供$otherChars
是为了增加灵活性。最后,$suppressChars
代表一个将从基本字符串中删除的字符数组。默认情况下会删除大写字母O
和小写字母l
。
复制 public function __construct(
$length = NULL,
$includeNumbers = TRUE,
$includeUpper= TRUE,
$includeLower= TRUE,
$includeSpecial = FALSE,
$otherChars = NULL,
array $suppressChars = NULL)
{
$this->length = $length ?? self::DEFAULT_LENGTH;
$this->includeNumbers = $includeNumbers;
$this->includeUpper = $includeUpper;
$this->includeLower = $includeLower;
$this->includeSpecial = $includeSpecial;
$this->otherChars = $otherChars;
$this->suppressChars = $suppressChars
?? self::DEFAULT_SUPPRESS;
$this->phrase = $this->generatePhrase();
}
3. 然后我们定义了一系列的getter和setter,每个属性一个。请注意,为了节省空间,我们只显示前两个。
复制 public function getString()
{
return $this->string;
}
public function setString($string)
{
$this->string = $string;
}
// other getters and setters not shown
4. 接下来我们需要定义一个初始化基础字符串的方法。这由一系列简单的if语句组成,这些语句检查各种$include*
标志,并适当地追加到基础字符串中。最后,我们使用str_replace()
来删除$suppressChars
中代表的字符。
复制 public function initString()
{
$string = '';
if ($this->includeNumbers) {
$string .= self::DEFAULT_NUMBERS;
}
if ($this->includeUpper) {
$string .= self::DEFAULT_UPPER;
}
if ($this->includeLower) {
$string .= self::DEFAULT_LOWER;
}
if ($this->includeSpecial) {
$string .= self::DEFAULT_SPECIAL;
}
if ($this->otherChars) {
$string .= $this->otherChars;
}
if ($this->suppressChars) {
$string = str_replace(
$this->suppressChars, '', $string);
}
return $string;
}
最佳实践
把可能与数字混淆的字母去掉(即字母O可以与数字0混淆,小写的l可以与数字1混淆。
5. 现在我们准备好定义核心方法,生成验证码呈现给网站访问者的随机短语。我们设置一个简单的for()
循环,并使用新的PHP 7 random_int()
函数在基础字符串中跳转。
复制 public function generatePhrase()
{
$phrase = '';
$this->string = $this->initString();
$max = strlen($this->string) - 1;
for ($x = 0; $x < $this->length; $x++) {
$phrase .= substr(
$this->string, random_int(0, $max), 1);
}
return $phrase;
}
}
6. 现在我们将注意力从短语转移到产生文本验证码的类上。为此,我们首先定义一个接口,以便将来可以创建更多的CAPTCHA类,这些类都可以使用Application\Captcha\Phrase
。请注意,getImage()
将返回文本、文本艺术或实际图像,这取决于我们决定使用哪个类。
复制 namespace Application\Captcha;
interface CaptchaInterface
{
public function getLabel();
public function getImage();
public function getPhrase();
}
7. 对于文本验证码,我们定义了一个Application\Captcha/Reverse
类。之所以叫这个名字,是因为这个类产生的不仅仅是文本,而是反向的文本。__construct()
方法建立了一个Phrase
的实例。请注意,getImage()
返回的是反向的短语。
复制 namespace Application\Captcha;
class Reverse implements CaptchaInterface
{
const DEFAULT_LABEL = 'Type this in reverse';
const DEFAULT_LENGTH = 6;
protected $phrase;
public function __construct(
$label = self::DEFAULT_LABEL,
$length = self:: DEFAULT_LENGTH,
$includeNumbers = TRUE,
$includeUpper = TRUE,
$includeLower = TRUE,
$includeSpecial = FALSE,
$otherChars = NULL,
array $suppressChars = NULL)
{
$this->label = $label;
$this->phrase = new Phrase(
$length,
$includeNumbers,
$includeUpper,
$includeLower,
$includeSpecial,
$otherChars,
$suppressChars);
}
public function getLabel()
{
return $this->label;
}
public function getImage()
{
return strrev($this->phrase->getPhrase());
}
public function getPhrase()
{
return $this->phrase->getPhrase();
}
}
生成一个图像验证码
1.形象的方法,你可以很好地想象,要复杂得多。短语的生成过程是一样的。主要的区别是,我们不仅需要将短语印在图形上,还需要对每个字母进行不同的变形,并以随机点的形式引入噪声。
2. 我们定义了一个Application\Captcha\Image
类,实现了验证码接口。该类的常量和属性不仅包括生成短语所需要的,还包括生成图像所需要的。
复制 namespace Application\Captcha;
use DirectoryIterator;
class Image implements CaptchaInterface
{
const DEFAULT_WIDTH = 200;
const DEFAULT_HEIGHT = 50;
const DEFAULT_LABEL = 'Enter this phrase';
const DEFAULT_BG_COLOR = [255,255,255];
const DEFAULT_URL = '/captcha';
const IMAGE_PREFIX = 'CAPTCHA_';
const IMAGE_SUFFIX = '.jpg';
const IMAGE_EXP_TIME = 300; // seconds
const ERROR_REQUIRES_GD = 'Requires the GD extension + '
. ' the JPEG library';
const ERROR_IMAGE = 'Unable to generate image';
protected $phrase;
protected $imageFn;
protected $label;
protected $imageWidth;
protected $imageHeight;
protected $imageRGB;
protected $imageDir;
protected $imageUrl;
3. 构造函数需要接受所有生成短语所需的参数,如前几步所述。此外,我们还需要接受生成图像所需的参数。两个必选参数是$imageDir
和$imageUrl
。第一个是图形将被写入的地方。第二个是基础URL,之后我们将附加生成的文件名。提供$imageFont
是为了防止我们提供TrueType字体,这将产生一个更安全的验证码。否则,我们只能使用默认字体,引用著名电影中的一句台词,这可不是什么好事。
复制 public function __construct(
$imageDir,
$imageUrl,
$imageFont = NULL,
$label = NULL,
$length = NULL,
$includeNumbers = TRUE,
$includeUpper= TRUE,
$includeLower= TRUE,
$includeSpecial = FALSE,
$otherChars = NULL,
array $suppressChars = NULL,
$imageWidth = NULL,
$imageHeight = NULL,
array $imageRGB = NULL
)
{
4. 接下来,仍然在构造函数中,我们检查 imagecreatetruecolor
函数是否存在。如果返回的是false`,我们就知道GD扩展不可用。否则,我们为属性分配参数,生成短语,删除旧图片,写出验证码图形。
复制 if (!function_exists('imagecreatetruecolor')) {
throw new \Exception(self::ERROR_REQUIRES_GD);
}
$this->imageDir = $imageDir;
$this->imageUrl = $imageUrl;
$this->imageFont = $imageFont;
$this->label = $label ?? self::DEFAULT_LABEL;
$this->imageRGB = $imageRGB ?? self::DEFAULT_BG_COLOR;
$this->imageWidth = $imageWidth ?? self::DEFAULT_WIDTH;
$this->imageHeight= $imageHeight ?? self::DEFAULT_HEIGHT;
if (substr($imageUrl, -1, 1) == '/') {
$imageUrl = substr($imageUrl, 0, -1);
}
$this->imageUrl = $imageUrl;
if (substr($imageDir, -1, 1) == DIRECTORY_SEPARATOR) {
$imageDir = substr($imageDir, 0, -1);
}
$this->phrase = new Phrase(
$length,
$includeNumbers,
$includeUpper,
$includeLower,
$includeSpecial,
$otherChars,
$suppressChars);
$this->removeOldImages();
$this->generateJpg();
}
5. 删除旧图片的过程是极其重要的,否则我们最终会发现目录中充满了过期的验证码图片!我们使用DirectoryIterator
类扫描指定的目录,并检查访问时间。我们使用DirectoryIterator
类来扫描指定的目录并检查访问时间。我们计算一个旧的图像文件是当前时间减去IMAGE_EXP_TIME
指定的值。
复制 public function removeOldImages()
{
$old = time() - self::IMAGE_EXP_TIME;
foreach (new DirectoryIterator($this->imageDir)
as $fileInfo) {
if($fileInfo->isDot()) continue;
if ($fileInfo->getATime() < $old) {
unlink($this->imageDir . DIRECTORY_SEPARATOR
. $fileInfo->getFilename());
}
}
}
6. 我们现在准备好进入正题。首先,我们将$imageRGB
数组分割成$red
、$green
和$blue
。我们使用核心的imagecreatetruecolor()
函数来生成指定宽度和高度的基础图形。我们使用RGB值对背景进行着色。
复制 public function generateJpg()
{
try {
list($red,$green,$blue) = $this->imageRGB;
$im = imagecreatetruecolor(
$this->imageWidth, $this->imageHeight);
$black = imagecolorallocate($im, 0, 0, 0);
$imageBgColor = imagecolorallocate(
$im, $red, $green, $blue);
imagefilledrectangle($im, 0, 0, $this->imageWidth,
$this->imageHeight, $imageBgColor);
7. 接下来,我们根据图像的宽度和高度定义x和y边距。然后我们初始化变量,用于将短语写到图形上。然后,我们循环次数与短语的长度相匹配。
复制 $xMargin = (int) ($this->imageWidth * .1 + .5);
$yMargin = (int) ($this->imageHeight * .3 + .5);
$phrase = $this->getPhrase();
$max = strlen($phrase);
$count = 0;
$x = $xMargin;
$size = 5;
for ($i = 0; $i < $max; $i++) {
8. 如果指定了$imageFont
,我们就能够用不同的大小和角度来书写每个字符。我们还需要根据大小调整x轴(即水平)值。
复制 if ($this->imageFont) {
$size = rand(12, 32);
$angle = rand(0, 30);
$y = rand($yMargin + $size, $this->imageHeight);
imagettftext($im, $size, $angle, $x, $y, $black,
$this->imageFont, $phrase[$i]);
$x += (int) ($size + rand(0,5));
9. 否则,我们就只能使用默认字体。我们使用最大的5号字体,因为较小的字体是不可读的。我们通过交替使用imagechar()
和imagecharup()
来提供低水平的失真,imagechar()
可以正常写入图像,而imagecharup()
可以横向写入图像。
复制 } else {
$y = rand(0, ($this->imageHeight - $yMargin));
if ($count++ & 1) {
imagechar($im, 5, $x, $y, $phrase[$i], $black);
} else {
imagecharup($im, 5, $x, $y, $phrase[$i], $black);
}
$x += (int) ($size * 1.2);
}
} // end for ($i = 0; $i < $max; $i++)
10. 接下来我们需要以随机点的形式添加噪声。这是必要的,以便使图像更难被自动系统检测到。同时建议你也添加代码来画几条线。
复制 $numDots = rand(10, 999);
for ($i = 0; $i < $numDots; $i++) {
imagesetpixel($im, rand(0, $this->imageWidth),
rand(0, $this->imageHeight), $black);
}
11. 然后,我们使用我们的老朋友md5(
)创建一个随机的图像文件名,参数为日期和一个0到9999的随机数。请注意,我们可以安全地使用md5()
,因为我们并没有试图隐藏任何秘密信息;我们只是想快速生成一个唯一的文件名。为了节省内存,我们也擦掉了图像对象。
复制 $this->imageFn = self::IMAGE_PREFIX
. md5(date('YmdHis') . rand(0,9999))
. self::IMAGE_SUFFIX;
imagejpeg($im, $this->imageDir . DIRECTORY_SEPARATOR
. $this->imageFn);
imagedestroy($im);
12. 整个构造都在try/catch
块中。如果出现错误或异常,我们会记录消息并采取适当的行动。
复制 } catch (\Throwable $e) {
error_log(__METHOD__ . ':' . $e->getMessage());
throw new \Exception(self::ERROR_IMAGE);
}
}
13. 最后,我们定义接口所需的方法。注意,getImage()
会返回一个HTML <img>
标签,然后可以立即显示。
复制 public function getLabel()
{
return $this->label;
}
public function getImage()
{
return sprintf('<img src="%s/%s" />',
$this->imageUrl, $this->imageFn);
}
public function getPhrase()
{
return $this->phrase->getPhrase();
}
}
如何运行...
一定要定义本事例中所讨论的类,总结在下表中。
Application\Captcha\Phrase
Generating a text CAPTCHA
Application\Captcha\CaptchaInterface
Application\Captcha\Reverse
Application\Captcha\Image
Generating an image CAPTCHA
接下来,定义一个名为chap_12_captcha_text.php
的调用程序,实现文本验证码。你首先需要设置自动加载并使用相应的类。
复制 <?php
require __DIR__ . '/../Application/Autoload/Loader.php';
Application\Autoload\Loader::init(__DIR__ . '/..');
use Application\Captcha\Reverse;
之后,一定要开始会话。你也会使用适当的措施来保护会话。为了节省空间,我们只展示一个简单的措施,session_regenerate_id()
。
复制 session_start();
session_regenerate_id();
接下来,你可以定义一个创建验证码的函数;检索短语、标签和图像(在本例中,反向文本);并将值存储在会话中。
复制 function setCaptcha(&$phrase, &$label, &$image)
{
$captcha = new Reverse();
$phrase = $captcha->getPhrase();
$label = $captcha->getLabel();
$image = $captcha->getImage();
$_SESSION['phrase'] = $phrase;
}
现在是初始化变量和确定登录状态的好时机。
复制 $image = '';
$label = '';
$phrase = $_SESSION['phrase'] ?? '';
$message = '';
$info = 'You Can Now See Super Secret Information!!!';
$loggedIn = $_SESSION['isLoggedIn'] ?? FALSE;
$loggedUser = $_SESSION['user'] ?? 'guest';
然后,您可以检查是否已按下登录按钮。如果是,检查是否输入了验证码短语。如果没有,则初始化一条消息,通知用户他们需要输入验证码短语。
复制 if (!empty($_POST['login'])) {
if (empty($_POST['captcha'])) {
$message = 'Enter Captcha Phrase and Login Information';
如果存在验证码短语,检查它是否与会话中存储的内容相匹配。如果不匹配,则继续进行表单无效的处理。否则,按照其他方式处理登录。在本例中,您可以使用硬编码的用户名和密码来模拟登录。
复制 } else {
if ($_POST['captcha'] == $phrase) {
$username = 'test';
$password = 'password';
if ($_POST['user'] == $username
&& $_POST['pass'] == $password) {
$loggedIn = TRUE;
$_SESSION['user'] = strip_tags($username);
$_SESSION['isLoggedIn'] = TRUE;
} else {
$message = 'Invalid Login';
}
} else {
$message = 'Invalid Captcha';
}
}
你可能还想添加注销选项的代码,就像保护PHP会话事例中描述的那样。
复制 } elseif (isset($_POST['logout'])) {
session_unset();
session_destroy();
setcookie('PHPSESSID', 0, time() - 3600);
header('Location: ' . $_SERVER['REQUEST_URI'] );
exit;
}
然后你可以运行setCaptcha()
。
复制 setCaptcha($phrase, $label, $image);
最后,别忘了视图逻辑,在这个例子中,呈现的是一个基本的登录表单。在表单标签里面,你需要添加视图逻辑来显示验证码和标签。
复制 <tr>
<th><?= $label; ?></th>
<td><?= $image; ?><input type="text" name="captcha" /></td>
</tr>
下面是结果输出。
为了演示如何使用图片验证码,请将chap_12_captcha_text.php
中的代码复制到cha_12_captcha_image.php
中。我们定义了常量来表示验证码图片的目录位置. (一定要创建这个目录!)否则,自动加载和使用语句的结构是相似的。注意,我们还定义了一个TrueType字体。不同之处用粗体表示。
复制 <?php
define('IMAGE_DIR', __DIR__ . '/captcha');
define('IMAGE_URL', '/captcha');
define('IMAGE_FONT', __DIR__ . '/FreeSansBold.ttf');
require __DIR__ . '/../Application/Autoload/Loader.php';
Application\Autoload\Loader::init(__DIR__ . '/..');
use Application\Captcha\Image;
session_start();
session_regenerate_id();
字体可能受到版权、商标、专利或其他知识产权法律的保护。如果您使用的字体没有得到授权,您和您的客户可能会被追究法律责任。使用开放源码的字体,或者是在网络服务器上可以使用的字体,你得有一个有效的许可证。
当然,在setCaptcha()
函数中,我们使用Image
类代替Reverse
。
复制 function setCaptcha(&$phrase, &$label, &$image)
{
$captcha = new Image(IMAGE_DIR, IMAGE_URL, IMAGE_FONT);
$phrase = $captcha->getPhrase();
$label = $captcha->getLabel();
$image = $captcha->getImage();
$_SESSION['phrase'] = $phrase;
return $captcha;
}
变量初始化与上一个脚本相同,登录处理与上一个脚本相同。
复制 $image = '';
$label = '';
$phrase = $_SESSION['phrase'] ?? '';
$message = '';
$info = 'You Can Now See Super Secret Information!!!';
$loggedIn = $_SESSION['isLoggedIn'] ?? FALSE;
$loggedUser = $_SESSION['user'] ?? 'guest';
if (!empty($_POST['login'])) {
// etc. -- identical to chap_12_captcha_text.php
即使是视图逻辑也是一样的,因为我们使用的是getImage(
),在图片验证码的情况下,它直接返回可用的HTML。下面是使用TrueType字体的输出。
更多...
如果你不愿意使用前面的代码来生成自己的内部验证码,有很多库可以使用。大多数流行的框架都有这种能力。例如,Zend Framework有其Zend\Captcha
组件类。还有reCAPTCHA,它通常是作为一个服务调用的,在这个服务中,你的应用程序调用一个外部网站,这个网站为你生成验证码和令牌。一个好的地方是http://www.captcha.net/ 网站。
关于将字体作为知识产权加以保护的更多信息,请参阅https://en.wikipedia.org/wiki/Intellectual_property_protection_of_typefaces。