文字点击验证码是一种用户友好且安全性较高的验证方式,用户需要点击图片中指定的文字来完成验证。以下是实现方法:
inhere/captcha
扩展包composer require inhere/captcha
namespace App\Http\Controllers;
use Inhere\Captcha\ClickCaptcha;
use Illuminate\Support\Facades\Session;
class CaptchaController extends Controller
{
public function generate()
{
$captcha = new ClickCaptcha([
'width' => 300,
'height' => 200,
'clickCount' => 3, // 需要点击的文字数量
'fontSize' => 18,
'fontFile' => public_path('fonts/msyh.ttf') // 字体文件路径
]);
$data = $captcha->generate();
// 存储验证数据到session
Session::put('click_captcha', [
'code' => $data['code'],
'points' => $data['points']
]);
return response()->json([
'image' => $data['image'], // base64编码的图片
'tips' => $data['tips'] // 提示文字如"请点击图中的'苹果'"
]);
}
public function validateCaptcha(Request $request)
{
$captchaData = Session::get('click_captcha');
$userPoints = $request->input('points');
if (!$captchaData || !$this->validatePoints($captchaData['points'], $userPoints)) {
return response()->json(['success' => false, 'message' => '验证失败']);
}
Session::forget('click_captcha');
return response()->json(['success' => true]);
}
protected function validatePoints($originalPoints, $userPoints, $tolerance = 10)
{
if (count($originalPoints) !== count($userPoints)) {
return false;
}
foreach ($originalPoints as $i => $point) {
$distance = sqrt(pow($point['x'] - $userPoints[$i]['x'], 2) +
pow($point['y'] - $userPoints[$i]['y'], 2));
if ($distance > $tolerance) {
return false;
}
}
return true;
}
}
Route::get('/click-captcha/generate', [CaptchaController::class, 'generate']);
Route::post('/click-captcha/validate', [CaptchaController::class, 'validateCaptcha']);
<div id="captcha-container">
<img id="captcha-image" src="" alt="点击验证码">
<p id="captcha-tips">p>
div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const captchaImage = document.getElementById('captcha-image');
const captchaTips = document.getElementById('captcha-tips');
const clickPoints = [];
// 获取验证码
fetch('/click-captcha/generate')
.then(response => response.json())
.then(data => {
captchaImage.src = data.image;
captchaTips.textContent = data.tips;
});
// 点击事件
captchaImage.addEventListener('click', function(e) {
const rect = this.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// 添加点击标记
const marker = document.createElement('div');
marker.style.position = 'absolute';
marker.style.left = (x - 5) + 'px';
marker.style.top = (y - 5) + 'px';
marker.style.width = '10px';
marker.style.height = '10px';
marker.style.backgroundColor = 'red';
marker.style.borderRadius = '50%';
this.parentNode.appendChild(marker);
// 记录点击位置
clickPoints.push({x, y});
// 如果点击次数足够,提交验证
if (clickPoints.length >= 3) { // 与后端设置的clickCount一致
fetch('/click-captcha/validate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({ points: clickPoints })
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('验证成功');
} else {
alert('验证失败,请重试');
location.reload();
}
});
}
});
});
script>
namespace App\Services;
class TextClickCaptcha
{
public function generate()
{
$words = ['苹果', '香蕉', '橙子', '西瓜', '葡萄', '菠萝', '芒果'];
$selected = array_rand(array_flip($words), 3);
$target = $selected[array_rand($selected)];
$image = imagecreatetruecolor(300, 200);
$bgColor = imagecolorallocate($image, 255, 255, 255);
$textColor = imagecolorallocate($image, 0, 0, 0);
imagefilledrectangle($image, 0, 0, 300, 200, $bgColor);
$positions = [];
foreach ($selected as $word) {
$x = rand(20, 250);
$y = rand(20, 170);
$positions[$word] = ['x' => $x, 'y' => $y];
imagettftext($image, 18, 0, $x, $y, $textColor,
public_path('fonts/msyh.ttf'), $word);
}
ob_start();
imagepng($image);
$imageData = ob_get_clean();
imagedestroy($image);
return [
'image' => 'data:image/png;base64,' . base64_encode($imageData),
'target' => $target,
'positions' => $positions
];
}
}
namespace App\Http\Controllers;
use App\Services\TextClickCaptcha;
use Illuminate\Support\Facades\Session;
class CaptchaController extends Controller
{
public function generate(TextClickCaptcha $captcha)
{
$data = $captcha->generate();
Session::put('text_click_captcha', [
'target' => $data['target'],
'positions' => $data['positions']
]);
return response()->json([
'image' => $data['image'],
'tips' => "请点击图中的'{$data['target']}'"
]);
}
public function validateCaptcha(Request $request)
{
$captchaData = Session::get('text_click_captcha');
$clickPosition = $request->input('position');
$targetPosition = $captchaData['positions'][$captchaData['target']];
$distance = sqrt(pow($targetPosition['x'] - $clickPosition['x'], 2) +
pow($targetPosition['y'] - $clickPosition['y'], 2));
if ($distance > 20) { // 允许20像素的误差
return response()->json(['success' => false]);
}
Session::forget('text_click_captcha');
return response()->json(['success' => true]);
}
}
<div id="captcha-container">
<img id="captcha-image" src="" alt="点击验证码">
<p id="captcha-tips">p>
div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const captchaImage = document.getElementById('captcha-image');
const captchaTips = document.getElementById('captcha-tips');
fetch('/click-captcha/generate')
.then(response => response.json())
.then(data => {
captchaImage.src = data.image;
captchaTips.textContent = data.tips;
captchaImage.addEventListener('click', function(e) {
const rect = this.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
fetch('/click-captcha/validate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({ position: {x, y} })
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('验证成功');
} else {
alert('验证失败,请重试');
location.reload();
}
});
});
});
});
script>
以上实现可以根据项目需求进行调整,第一种方法使用现成的扩展包更加稳定和安全,第二种方法则提供了更大的灵活性。