feat:capjs custom content

#399
This commit is contained in:
samwaf
2025-07-11 13:43:01 +08:00
parent 3d0e3bfd47
commit cde17ce482
5 changed files with 394 additions and 313 deletions

1
exedata/capjs/capversion Normal file
View File

@@ -0,0 +1 @@
0.0.6

View File

@@ -1,335 +1,357 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Cache-Control" content="no-store, no-cache, must-revalidate, max-age=0" /> <meta http-equiv="Cache-Control" content="no-store, no-cache, must-revalidate, max-age=0" />
<meta http-equiv="Pragma" content="no-cache" /> <meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" /> <meta http-equiv="Expires" content="0" />
<title id="page-title">进行验证</title> <title id="page-title">进行验证</title>
<style> <style>
html, body { html, body {
width: 100%; width: 100%;
height: 100%; height: 100%;
padding: 0; padding: 0;
margin: 0; margin: 0;
color: #3C3C3C; color: #3C3C3C;
background: #EBF3FB; background: #EBF3FB;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
min-height: 100vh; min-height: 100vh;
}
.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 0;
}
.lang-switch {
position: absolute;
top: 10px;
right: 10px;
background: #fff;
border: 1px solid #ddd;
border-radius: 4px;
padding: 5px 10px;
cursor: pointer;
font-size: 14px;
color: #3C3C3C;
}
.lang-switch:hover {
background: #f5f5f5;
}
.success-message {
color: #5eaa2f;
text-align: center;
font-size: 18px;
margin-top: 20px;
font-family: system, -apple-system, "BlinkMacSystemFont",
".SFNSText-Regular", "San Francisco", "Roboto", "Segoe UI",
"Helvetica Neue", "Lucida Grande", "Ubuntu", "arial", sans-serif;
}
.error-message {
color: #ed4630;
text-align: center;
font-size: 16px;
margin-top: 20px;
font-family: system, -apple-system, "BlinkMacSystemFont",
".SFNSText-Regular", "San Francisco", "Roboto", "Segoe UI",
"Helvetica Neue", "Lucida Grande", "Ubuntu", "arial", sans-serif;
}
/* Toast样式 */
.toast {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
padding: 12px 24px;
border-radius: 6px;
font-size: 14px;
z-index: 1000;
opacity: 0;
transition: opacity 0.3s ease;
}
.toast.show {
opacity: 1;
}
.toast.success {
background: #f0f9eb;
border: 1px solid #dcf9cc;
color: #5eaa2f;
box-shadow: 1px 1px 10px #e0e0e0;
}
.toast.error {
background: #fef0f0;
border: 1px solid #fcd6d6;
color: #ed4630;
box-shadow: 1px 1px 10px #e0e0e0;
}
</style>
</head>
<body>
<button id="lang-switch" class="lang-switch">English</button>
<div class="container">
<!-- 添加提示信息区域 -->
<div class="info-message" id="info-message">
<h2 id="info-title">安全验证</h2>
<p id="info-text">为了确保您的访问安全,请完成以下验证</p>
</div>
<cap-widget
id="cap"
data-cap-api-endpoint="/samwaf_captcha/"
data-cap-i18n-verifying-label="验证中..."
data-cap-i18n-initial-state="我是人类"
data-cap-i18n-solved-label="我是人类"
data-cap-i18n-error-label="错误"
></cap-widget>
</div>
<script>
// 语言资源
const i18n = {
'zh': {
title: '进行验证',
verifySuccess: '验证成功,页面即将刷新...',
verifyFail: '验证失败',
validationError: '验证异常',
switchLang: 'English',
// 添加提示信息的中文翻译
infoTitle: '安全验证',
infoText: '为了确保您的访问安全,请完成以下验证',
// Cap.js widget 多语言
capWidget: {
verifyingLabel: '验证中...',
initialState: '我是人类',
solvedLabel: '我是人类',
errorLabel: '错误'
}
},
'en': {
title: 'Verification',
verifySuccess: 'Verification successful, page will refresh...',
verifyFail: 'Verification failed',
validationError: 'Validation error',
switchLang: '中文',
// 添加提示信息的英文翻译
infoTitle: 'Security Verification',
infoText: 'To ensure the security of your access, please complete the following verification',
// Cap.js widget 多语言
capWidget: {
verifyingLabel: 'Verifying...',
initialState: "I'm a human",
solvedLabel: "I'm a human",
errorLabel: 'Error'
}
}
};
// 语言管理
const langManager = (function() {
// Cookie 操作函数
function setCookie(name, value, days) {
let expires = "";
if (days) {
const date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
expires = "; expires=" + date.toUTCString();
}
document.cookie = name + "=" + (value || "") + expires + "; path=/";
}
function getCookie(name) {
const nameEQ = name + "=";
const ca = document.cookie.split(';');
for(let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
}
return null;
}
// 检测浏览器语言
function detectBrowserLang() {
const browserLang = navigator.language || navigator.userLanguage;
return browserLang.toLowerCase().startsWith('zh') ? 'zh' : 'en';
}
let currentLang = getCookie('samwaf_lang') || detectBrowserLang();
function setLanguage(lang) {
currentLang = lang;
setCookie('samwaf_lang', lang, 30); // 保存30天
applyLanguage();
return currentLang;
}
function getCurrentLang() {
return currentLang;
}
function getText(key) {
return i18n[currentLang][key] || i18n['en'][key] || key;
}
function getCapWidgetText(key) {
return i18n[currentLang].capWidget[key] || i18n['en'].capWidget[key] || key;
}
function applyLanguage() {
// 更新页面标题
document.getElementById('page-title').textContent = getText('title');
// 更新语言切换按钮
document.getElementById('lang-switch').textContent = getText('switchLang');
// 更新提示信息的多语言内容
const infoTitle = document.getElementById('info-title');
const infoText = document.getElementById('info-text');
if (infoTitle) {
infoTitle.textContent = getText('infoTitle');
}
if (infoText) {
infoText.textContent = getText('infoText');
} }
.container { // 更新cap-widget的多语言属性
display: flex; const capWidget = document.getElementById('cap');
flex-direction: column; if (capWidget) {
align-items: center; capWidget.setAttribute('data-cap-i18n-verifying-label', getCapWidgetText('verifyingLabel'));
justify-content: center; capWidget.setAttribute('data-cap-i18n-initial-state', getCapWidgetText('initialState'));
padding: 40px 0; capWidget.setAttribute('data-cap-i18n-solved-label', getCapWidgetText('solvedLabel'));
capWidget.setAttribute('data-cap-i18n-error-label', getCapWidgetText('errorLabel'));
} }
}
.lang-switch { return {
position: absolute; setLanguage,
top: 10px; getCurrentLang,
right: 10px; getText,
background: #fff; getCapWidgetText,
border: 1px solid #ddd; applyLanguage
border-radius: 4px; };
padding: 5px 10px; })();
cursor: pointer;
font-size: 14px;
color: #3C3C3C;
}
.lang-switch:hover { // Toast提示函数
background: #f5f5f5; function showToast(message, type = 'success') {
} // 移除已存在的toast
const existingToast = document.querySelector('.toast');
if (existingToast) {
existingToast.remove();
}
.success-message { const toast = document.createElement('div');
color: #5eaa2f; toast.className = `toast ${type}`;
text-align: center; toast.textContent = message;
font-size: 18px; document.body.appendChild(toast);
margin-top: 20px;
font-family: system, -apple-system, "BlinkMacSystemFont",
".SFNSText-Regular", "San Francisco", "Roboto", "Segoe UI",
"Helvetica Neue", "Lucida Grande", "Ubuntu", "arial", sans-serif;
}
.error-message { // 显示toast
color: #ed4630; setTimeout(() => {
text-align: center; toast.classList.add('show');
font-size: 16px; }, 100);
margin-top: 20px;
font-family: system, -apple-system, "BlinkMacSystemFont",
".SFNSText-Regular", "San Francisco", "Roboto", "Segoe UI",
"Helvetica Neue", "Lucida Grande", "Ubuntu", "arial", sans-serif;
}
/* Toast样式 */ // 隐藏toast
.toast { setTimeout(() => {
position: fixed; toast.classList.remove('show');
top: 50%; setTimeout(() => {
left: 50%; if (toast.parentNode) {
transform: translate(-50%, -50%); toast.parentNode.removeChild(toast);
padding: 12px 24px; }
border-radius: 6px; }, 300);
font-size: 14px; }, 2000);
z-index: 1000; }
opacity: 0;
transition: opacity 0.3s ease;
}
.toast.show { // 初始化语言
opacity: 1; document.addEventListener('DOMContentLoaded', function() {
} langManager.applyLanguage();
});
.toast.success { // 语言切换按钮事件
background: #f0f9eb; document.getElementById('lang-switch').addEventListener('click', function() {
border: 1px solid #dcf9cc; const newLang = langManager.getCurrentLang() === 'zh' ? 'en' : 'zh';
color: #5eaa2f; langManager.setLanguage(newLang);
box-shadow: 1px 1px 10px #e0e0e0; // 刷新页面以重新初始化验证码
} window.location.reload();
});
.toast.error { // 动态获取当前页面的协议、域名和端口
background: #fef0f0; const currentProtocol = window.location.protocol; // http: 或 https:
border: 1px solid #fcd6d6; const currentHost = window.location.host; // 包含域名和端口
color: #ed4630; window.CAP_CUSTOM_WASM_URL = `${currentProtocol}//${currentHost}/samwaf_captcha/cap_wasm.min.js`;
box-shadow: 1px 1px 10px #e0e0e0; </script>
} <script src="/samwaf_captcha/widget.js"></script>
</style>
</head>
<body>
<button id="lang-switch" class="lang-switch">English</button>
<div class="container">
<cap-widget
id="cap"
data-cap-api-endpoint="/samwaf_captcha/"
data-cap-i18n-verifying-label="验证中..."
data-cap-i18n-initial-state="我是人类"
data-cap-i18n-solved-label="我是人类"
data-cap-i18n-error-label="错误"
></cap-widget>
</div>
<script> <script>
// 语言资源 const widget = document.querySelector("#cap");
const i18n = {
'zh': { widget.addEventListener("solve", function (e) {
title: '进行验证', const token = e.detail.token;
verifySuccess: '验证成功,页面即将刷新...', // console.log("Captcha solved!");
verifyFail: '验证失败', //console.log("Token:" + token); // Token is returned by the server
validationError: '验证异常',
switchLang: 'English', // Submit token to backend for validation
// Cap.js widget 多语言 validateToken(token);
capWidget: { });
verifyingLabel: '验证中...',
initialState: '我是人类', // Function to validate token with backend
solvedLabel: '我是人类', async function validateToken(token) {
errorLabel: '错误' try {
} const response = await fetch('/samwaf_captcha/validate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}, },
'en': { body: JSON.stringify({ token: token })
title: 'Verification', });
verifySuccess: 'Verification successful, page will refresh...',
verifyFail: 'Verification failed',
validationError: 'Validation error',
switchLang: '中文',
// Cap.js widget 多语言
capWidget: {
verifyingLabel: 'Verifying...',
initialState: "I'm a human",
solvedLabel: "I'm a human",
errorLabel: 'Error'
}
}
};
// 语言管理 if (!response.ok) {
const langManager = (function() { throw new Error(`HTTP error! status: ${response.status}`);
// Cookie 操作函数 }
function setCookie(name, value, days) {
let expires = "";
if (days) {
const date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
expires = "; expires=" + date.toUTCString();
}
document.cookie = name + "=" + (value || "") + expires + "; path=/";
}
function getCookie(name) { const result = await response.json();
const nameEQ = name + "=";
const ca = document.cookie.split(';');
for(let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
}
return null;
}
// 检测浏览器语言 if (result.success) {
function detectBrowserLang() { // 隐藏验证码组件
const browserLang = navigator.language || navigator.userLanguage; document.querySelector('#cap').style.display = 'none';
return browserLang.toLowerCase().startsWith('zh') ? 'zh' : 'en'; // 隐藏语言切换按钮
} document.getElementById('lang-switch').style.display = 'none';
let currentLang = getCookie('samwaf_lang') || detectBrowserLang(); // 显示成功消息
const successEl = document.createElement("div");
successEl.className = "success-message";
successEl.textContent = langManager.getText('verifySuccess');
document.querySelector(".container").appendChild(successEl);
function setLanguage(lang) { // 显示成功toast
currentLang = lang; showToast(langManager.getText('verifySuccess'), 'success');
setCookie('samwaf_lang', lang, 30); // 保存30天
applyLanguage();
return currentLang;
}
function getCurrentLang() { // 2秒后刷新页面
return currentLang;
}
function getText(key) {
return i18n[currentLang][key] || i18n['en'][key] || key;
}
function getCapWidgetText(key) {
return i18n[currentLang].capWidget[key] || i18n['en'].capWidget[key] || key;
}
function applyLanguage() {
// 更新页面标题
document.getElementById('page-title').textContent = getText('title');
// 更新语言切换按钮
document.getElementById('lang-switch').textContent = getText('switchLang');
// 更新cap-widget的多语言属性
const capWidget = document.getElementById('cap');
if (capWidget) {
capWidget.setAttribute('data-cap-i18n-verifying-label', getCapWidgetText('verifyingLabel'));
capWidget.setAttribute('data-cap-i18n-initial-state', getCapWidgetText('initialState'));
capWidget.setAttribute('data-cap-i18n-solved-label', getCapWidgetText('solvedLabel'));
capWidget.setAttribute('data-cap-i18n-error-label', getCapWidgetText('errorLabel'));
}
}
return {
setLanguage,
getCurrentLang,
getText,
getCapWidgetText,
applyLanguage
};
})();
// Toast提示函数
function showToast(message, type = 'success') {
// 移除已存在的toast
const existingToast = document.querySelector('.toast');
if (existingToast) {
existingToast.remove();
}
const toast = document.createElement('div');
toast.className = `toast ${type}`;
toast.textContent = message;
document.body.appendChild(toast);
// 显示toast
setTimeout(() => { setTimeout(() => {
toast.classList.add('show'); window.location.reload();
}, 100);
// 隐藏toast
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => {
if (toast.parentNode) {
toast.parentNode.removeChild(toast);
}
}, 300);
}, 2000); }, 2000);
} else {
// 显示失败toast
showToast(langManager.getText('verifyFail'), 'error');
} }
} catch (error) {
// 初始化语言 console.error('Validation error:', error);
document.addEventListener('DOMContentLoaded', function() { // 显示错误toast
langManager.applyLanguage(); showToast(langManager.getText('validationError') + ': ' + error.message, 'error');
}); }
}
// 语言切换按钮事件 </script>
document.getElementById('lang-switch').addEventListener('click', function() { </html>
const newLang = langManager.getCurrentLang() === 'zh' ? 'en' : 'zh';
langManager.setLanguage(newLang);
// 刷新页面以重新初始化验证码
window.location.reload();
});
// 动态获取当前页面的协议、域名和端口
const currentProtocol = window.location.protocol; // http: 或 https:
const currentHost = window.location.host; // 包含域名和端口
window.CAP_CUSTOM_WASM_URL = `${currentProtocol}//${currentHost}/samwaf_captcha/cap_wasm.min.js`;
</script>
<script src="/samwaf_captcha/widget.js"></script>
<script>
const widget = document.querySelector("#cap");
widget.addEventListener("solve", function (e) {
const token = e.detail.token;
// console.log("Captcha solved!");
//console.log("Token:" + token); // Token is returned by the server
// Submit token to backend for validation
validateToken(token);
});
// Function to validate token with backend
async function validateToken(token) {
try {
const response = await fetch('/samwaf_captcha/validate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ token: token })
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result.success) {
// 隐藏验证码组件
document.querySelector('#cap').style.display = 'none';
// 隐藏语言切换按钮
document.getElementById('lang-switch').style.display = 'none';
// 显示成功消息
const successEl = document.createElement("div");
successEl.className = "success-message";
successEl.textContent = langManager.getText('verifySuccess');
document.querySelector(".container").appendChild(successEl);
// 显示成功toast
showToast(langManager.getText('verifySuccess'), 'success');
// 2秒后刷新页面
setTimeout(() => {
window.location.reload();
}, 2000);
} else {
// 显示失败toast
showToast(langManager.getText('verifyFail'), 'error');
}
} catch (error) {
console.error('Validation error:', error);
// 显示错误toast
showToast(langManager.getText('validationError') + ': ' + error.message, 'error');
}
}
</script>
</html>

View File

@@ -1 +1 @@
0.0.6 1.0.1

View File

@@ -77,6 +77,14 @@ type CaptchaConfig struct {
ChallengeSize int `json:"challengeSize,omitempty"` // Size of each challenge in bytes (default: 32) ChallengeSize int `json:"challengeSize,omitempty"` // Size of each challenge in bytes (default: 32)
ChallengeDifficulty int `json:"challengeDifficulty,omitempty"` // Difficulty level (default: 4) ChallengeDifficulty int `json:"challengeDifficulty,omitempty"` // Difficulty level (default: 4)
ExpiresMs int `json:"expiresMs,omitempty"` // Expiration time in milliseconds (default: 600000) ExpiresMs int `json:"expiresMs,omitempty"` // Expiration time in milliseconds (default: 600000)
InfoTitle struct {
En string `json:"en,omitempty"` // English title
Zh string `json:"zh,omitempty"` // Chinese title
} `json:"infoTitle,omitempty"` // Multi-language info title
InfoText struct {
En string `json:"en,omitempty"` // English text
Zh string `json:"zh,omitempty"` // Chinese text
} `json:"infoText,omitempty"` // Multi-language info text
} `json:"cap_js_config"` } `json:"cap_js_config"`
} }
@@ -97,6 +105,14 @@ func ParseCaptchaConfig(captchaJSON string) CaptchaConfig {
config.CapJsConfig.ChallengeDifficulty = 4 // 默认难度级别4 config.CapJsConfig.ChallengeDifficulty = 4 // 默认难度级别4
config.CapJsConfig.ExpiresMs = 600000 // 默认过期时间600秒(10分钟) config.CapJsConfig.ExpiresMs = 600000 // 默认过期时间600秒(10分钟)
// 初始化InfoTitle默认值
config.CapJsConfig.InfoTitle.Zh = "安全验证"
config.CapJsConfig.InfoTitle.En = "Security Verification"
// 初始化InfoText默认值
config.CapJsConfig.InfoText.Zh = "为了确保您的访问安全,请完成以下验证"
config.CapJsConfig.InfoText.En = "To ensure the security of your access, please complete the following verification"
// 如果JSON不为空则解析覆盖默认值 // 如果JSON不为空则解析覆盖默认值
if captchaJSON != "" { if captchaJSON != "" {
err := json.Unmarshal([]byte(captchaJSON), &config) err := json.Unmarshal([]byte(captchaJSON), &config)

View File

@@ -19,6 +19,7 @@ import (
"github.com/wenlng/go-captcha/v2/base/option" "github.com/wenlng/go-captcha/v2/base/option"
"github.com/wenlng/go-captcha/v2/click" "github.com/wenlng/go-captcha/v2/click"
"go.uber.org/zap" "go.uber.org/zap"
"io/ioutil"
"log" "log"
"net/http" "net/http"
"os" "os"
@@ -512,8 +513,49 @@ func (s *CaptchaService) ShowCaptchaHomePage(w http.ResponseWriter, r *http.Requ
// 从指定目录加载index.html // 从指定目录加载index.html
http.ServeFile(w, r, utils.GetCurrentDir()+"/data/captcha/index.html") http.ServeFile(w, r, utils.GetCurrentDir()+"/data/captcha/index.html")
} else if configStruct.EngineType == "capJs" { } else if configStruct.EngineType == "capJs" {
// 从指定目录加载index.html // 读取HTML模板文件
http.ServeFile(w, r, utils.GetCurrentDir()+"/data/capjs/index.html") htmlPath := utils.GetCurrentDir() + "/data/capjs/index.html"
htmlContent, err := ioutil.ReadFile(htmlPath)
if err != nil {
http.Error(w, "Failed to load page", http.StatusInternalServerError)
return
}
// 准备替换的数据
htmlStr := string(htmlContent)
// 替换中文提示信息
zhInfoTitle := configStruct.CapJsConfig.InfoTitle.Zh
zhInfoText := configStruct.CapJsConfig.InfoText.Zh
if zhInfoTitle == "" {
zhInfoTitle = "安全验证"
}
if zhInfoText == "" {
zhInfoText = "为了确保您的访问安全,请完成以下验证"
}
// 替换英文提示信息
enInfoTitle := configStruct.CapJsConfig.InfoTitle.En
enInfoText := configStruct.CapJsConfig.InfoText.En
if enInfoTitle == "" {
enInfoTitle = "Security Verification"
}
if enInfoText == "" {
enInfoText = "To ensure the security of your access, please complete the following verification"
}
// 使用strings.Replace替换HTML中的静态文本
htmlStr = strings.Replace(htmlStr, "infoTitle: '安全验证',", fmt.Sprintf("infoTitle: '%s',", zhInfoTitle), 1)
htmlStr = strings.Replace(htmlStr, "infoText: '为了确保您的访问安全,请完成以下验证',", fmt.Sprintf("infoText: '%s',", zhInfoText), 1)
htmlStr = strings.Replace(htmlStr, "infoTitle: 'Security Verification',", fmt.Sprintf("infoTitle: '%s',", enInfoTitle), 1)
htmlStr = strings.Replace(htmlStr, "infoText: 'To ensure the security of your access, please complete the following verification',", fmt.Sprintf("infoText: '%s',", enInfoText), 1)
// 同时替换HTML中的默认显示文本
htmlStr = strings.Replace(htmlStr, "<h2 id=\"info-title\">安全验证</h2>", fmt.Sprintf("<h2 id=\"info-title\">%s</h2>", zhInfoTitle), 1)
htmlStr = strings.Replace(htmlStr, "<p id=\"info-text\">为了确保您的访问安全,请完成以下验证</p>", fmt.Sprintf("<p id=\"info-text\">%s</p>", zhInfoText), 1)
// 输出修改后的HTML
w.Write([]byte(htmlStr))
} }
} }