后端部分,使用node_js:
const express = require('express');
const mysql = require('mysql2/promise'); // 使用 promise 版本
const cors = require('cors');
const bodyParser = require('body-parser');
const rateLimit = require('express-rate-limit');
const app = express();
// 设置 trust proxy
app.set('trust proxy', 1);
// 配置请求频率限制
const limiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
message: '请求过于频繁,请稍后再试。',
standardHeaders: true,
legacyHeaders: false,
});
app.use(limiter);
app.use(cors({
origin: '*',
methods: ['GET', 'POST'],
allowedHeaders: ['Content-Type']
}));
app.use(bodyParser.json());
app.use(express.static('public'));
// 数据库连接配置
const dbConfig = {
host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || 3306,
user: process.env.DB_USER || 'root',
password: process.env.DB_PASSWORD || '',
database: process.env.DB_NAME || 'mydb',
connectTimeout: 60000,
acquireTimeout: 60000,
timeout: 60000,
connectionLimit: 10,
waitForConnections: true,
queueLimit: 0
};
// 创建连接池
const pool = mysql.createPool(dbConfig);
// 测试数据库连接
async function testConnection() {
try {
const connection = await pool.getConnection();
console.log('已成功连接到 MySQL 数据库');
connection.release();
} catch (err) {
console.error('数据库连接失败:', err);
setTimeout(testConnection, 5000);
}
}
// 初始测试连接
testConnection();
// API 路由
app.post('/api/get-article', async (req, res) => {
try {
const { articleId, password } = req.body;
// 输入验证
if (!articleId || !password) {
return res.status(400).json({
success: false,
error: '缺少必要参数'
});
}
// 验证 articleId 是否为有效的整数
if (!Number.isInteger(Number(articleId)) || articleId <= 0) {
return res.status(400).json({
success: false,
error: '无效的文章ID'
});
}
// 执行查询
const [rows] = await pool.execute(
'SELECT id, title, content FROM articles WHERE id = ? AND password = ?',
[articleId, password]
);
if (!rows || rows.length === 0) {
return res.status(401).json({
success: false,
error: '密码错误或文章不存在'
});
}
// 成功响应
res.json({
success: true,
article: {
id: rows[0].id,
title: rows[0].title,
content: rows[0].content
}
});
} catch (error) {
console.error('查询错误:', error);
res.status(500).json({
success: false,
error: '服务器内部错误'
});
}
});
//关闭连接池
process.on('SIGINT', async () => {
try {
await pool.end();
console.log('数据库连接池已关闭');
process.exit(0);
} catch (err) {
console.error('关闭连接池时出错:', err);
process.exit(1);
}
});
// 启动服务器
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`服务器运行在 http://localhost:${PORT}`);
});
数据库部分,使用mysql
-- 创建数据库
CREATE DATABASE IF NOT EXISTS blog_db;
USE blog_db;
-- 创建文章表
CREATE TABLE articles (
id INT PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
password VARCHAR(50) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 插入测试数据
INSERT INTO articles (title, content, password)
VALUES ('测试文章', '这是一篇受密码保护的文章内容。这里可以是很长的文章内容...', '1234');
前端部分(支持markdown代码渲染)在verifyPassword
函数中配置后端连接信息
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>加密文章访问</title>
<!-- 使用 ES6 模块方式加载 marked -->
<script type="module">
import { marked } from 'https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js';
window.marked = marked;
marked.setOptions({
breaks: true,
gfm: true,
headerIds: true,
mangle: false,
sanitize: false
});
</script>
<!-- 加载 highlight.js -->
<script src="https://cdn.jsdelivr.net/npm/highlight.js@11.8.0/lib/highlight.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js@11.8.0/styles/github.min.css">
<style>
/* 基础变量定义 */
:root {
--bg-color: inherit;
--bg-secondary: inherit;
--text-color: inherit;
--text-secondary: #476582;
--border-color: #eaecef;
--input-bg: #ffffff;
--focus-color: #3eaf7c;
--focus-shadow: rgba(62, 175, 124, 0.25);
--code-bg: #f6f8fa;
--code-block-bg: #282c34;
--code-block-color: #abb2bf;
--code-color: #476582;
--blockquote-color: #6a737d;
--blockquote-border: #42b983;
--table-alt-bg: #f6f8fa;
--link-color: #3eaf7c;
--error-bg: #fef0f0;
--error-color: #f56c6c;
--header-bg: linear-gradient(120deg, #155799, #159957);
--header-color: #fff;
--button-hover: #42d392;
--scrollbar-thumb: #c1c1c1;
--scrollbar-track: #f1f1f1;
--card-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
/* 全局样式重置 */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
/* 滚动条美化 */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--scrollbar-track);
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: var(--scrollbar-thumb);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
/* 基础样式 */
html {
font-size: 16px;
line-height: 1.6;
overflow-y: scroll;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell,
'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
width: 100%;
min-height: 100vh;
margin: 0;
padding: 0;
background-color: var(--bg-color);
color: var(--text-color);
}
/* 页面容器 */
.page-container {
width: 100%;
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* 页头样式 */
.header {
color: var(--header-color);
padding: clamp(2rem, 5vw, 4rem) clamp(1rem, 3vw, 2rem);
text-align: center;
margin-bottom: 1rem;
}
.header h1 {
font-size: clamp(1.5rem, 4vw, 2.5rem);
margin-bottom: 1rem;
font-weight: 600;
}
.header p {
font-size: clamp(1rem, 2vw, 1.25rem);
opacity: 0.9;
max-width: 800px;
margin: 0 auto;
}
/* 主容器样式 */
.container {
width: 100%;
max-width: min(90vw, 1200px);
min-width: min(300px, 90vw);
margin: 0 auto;
padding: clamp(1rem, 3vw, 2rem);
flex: 1;
}
/* 卡片样式 */
.card {
background: var(--bg-color);
border-radius: 8px;
box-shadow: var(--card-shadow);
padding: clamp(1rem, 3vw, 2rem);
margin-bottom: 1rem;
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
}
/* 密码输入区域样式 */
.password-section {
max-width: 600px;
margin: 0 auto;
text-align: center;
}
.password-section h2 {
font-size: clamp(1.25rem, 3vw, 1.75rem);
margin-bottom: 1.5rem;
color: var(--text-color);
margin-top: 0;
}
.password-form {
display: flex;
flex-direction: column;
gap: 1rem;
align-items: center;
}
/* 输入框样式 */
.input-group {
width: 100%;
max-width: 400px;
position: relative;
}
input[type="password"] {
width: 100%;
padding: 0.75rem 1rem;
font-size: 1rem;
border: 2px solid var(--border-color);
border-radius: 6px;
background: var(--input-bg);
color: var(--text-color);
transition: all 0.3s ease;
}
input[type="password"]:focus {
outline: none;
border-color: var(--focus-color);
box-shadow: 0 0 0 3px var(--focus-shadow);
}
/* 按钮样式 */
.btn {
padding: 0.75rem 2rem;
font-size: 1rem;
font-weight: 500;
color: white;
background: var(--focus-color);
border: none;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
min-width: 200px;
}
.btn:hover {
background: var(--button-hover);
transform: translateY(-1px);
}
.btn:active {
transform: translateY(0);
}
.btn:disabled {
background: var(--border-color);
cursor: not-allowed;
transform: none;
}
/* 错误消息样式 */
.error-message {
background: var(--error-bg);
color: var(--error-color);
padding: 0.75rem 1rem;
border-radius: 6px;
margin-top: 1rem;
font-size: 0.875rem;
display: none;
animation: fadeInUp 0.3s ease;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 文章内容区域样式 */
.article-content {
width: 100%;
max-width: 100%;
opacity: 0;
transform: translateY(20px);
transition: all 0.5s ease;
display: none;
}
.article-content.visible {
opacity: 1;
transform: translateY(0);
display: block;
}
.article-title {
font-size: clamp(1.5rem, 4vw, 2.5rem);
margin-bottom: 2rem;
color: var(--text-color);
text-align: center;
}
/* Markdown 内容样式 */
.markdown-content {
width: 100%;
max-width: 100%;
margin: 0 auto;
color: var(--text-color);
line-height: 1.8;
font-size: clamp(1rem, 1.1vw, 1.2rem);
}
.markdown-content h1,
.markdown-content h2,
.markdown-content h3,
.markdown-content h4,
.markdown-content h5,
.markdown-content h6 {
margin: 2rem 0 1rem;
line-height: 1.4;
color: var(--text-color);
font-weight: 600;
}
.markdown-content h1 { font-size: clamp(1.75rem, 2.5vw, 2rem); }
.markdown-content h2 { font-size: clamp(1.5rem, 2vw, 1.75rem); }
.markdown-content h3 { font-size: clamp(1.25rem, 1.8vw, 1.5rem); }
.markdown-content h4 { font-size: clamp(1.1rem, 1.6vw, 1.25rem); }
.markdown-content h5 { font-size: clamp(1rem, 1.4vw, 1.1rem); }
.markdown-content h6 { font-size: clamp(0.9rem, 1.2vw, 1rem); }
.markdown-content p {
margin: 1rem 0;
line-height: 1.8;
}
.markdown-content a {
color: var(--link-color);
text-decoration: none;
border-bottom: 1px solid transparent;
transition: all 0.3s ease;
}
.markdown-content a:hover {
border-bottom-color: var(--link-color);
}
.markdown-content code {
background: var(--code-bg);
color: var(--code-color);
padding: 0.2em 0.4em;
border-radius: 3px;
font-family: 'SF Mono', Monaco, Menlo, Consolas, 'Ubuntu Mono', monospace;
font-size: 0.9em;
}
.markdown-content pre {
background: var(--code-block-bg);
color: var(--code-block-color);
padding: 1rem 1.5rem;
border-radius: 6px;
overflow-x: auto;
margin: 1.5rem 0;
position: relative;
}
.markdown-content pre code {
background: transparent;
color: inherit;
padding: 0;
font-size: 0.9em;
line-height: 1.6;
}
.markdown-content blockquote {
margin: 1.5rem 0;
padding: 0.5rem 1.5rem;
border-left: 4px solid var(--blockquote-border);
background: var(--bg-secondary);
color: var(--blockquote-color);
border-radius: 0 6px 6px 0;
}
.markdown-content img {
max-width: 100%;
height: auto;
border-radius: 6px;
margin: 1.5rem 0;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.markdown-content table {
width: 100%;
margin: 1.5rem 0;
border-collapse: collapse;
border-spacing: 0;
display: block;
overflow-x: auto;
}
.markdown-content table th,
.markdown-content table td {
padding: 0.75rem 1rem;
border: 1px solid var(--border-color);
text-align: left;
}
.markdown-content table th {
background: var(--bg-secondary);
font-weight: 600;
}
.markdown-content table tr:nth-child(2n) {
background: var(--table-alt-bg);
}
.markdown-content ul,
.markdown-content ol {
padding-left: 1.5rem;
margin: 1rem 0;
}
.markdown-content li {
margin: 0.5rem 0;
}
.markdown-content hr {
height: 1px;
background: var(--border-color);
border: none;
margin: 2rem 0;
}
/* 响应式布局 */
@media screen and (max-width: 768px) {
.container {
padding: 1rem;
}
.card {
padding: 1.25rem;
}
.password-form {
padding: 0 1rem;
}
.btn {
width: 100%;
}
.markdown-content pre {
margin: 1rem -1.25rem;
border-radius: 0;
}
.markdown-content blockquote {
margin: 1rem -1.25rem;
border-radius: 0;
}
}
/* 打印样式 */
@media print {
.header, .password-section {
display: none;
}
.container {
max-width: none;
padding: 0;
}
.card {
box-shadow: none;
padding: 0;
}
.markdown-content {
font-size: 12pt;
}
.markdown-content pre,
.markdown-content code {
white-space: pre-wrap;
}
}
</style>
<div class="page-container">
<header class="header">
<h1>加密文章访问</h1>
<p>请输入密码以访问加密内容</p>
</header>
<main class="container">
<div class="card">
<div id="passwordSection" class="password-section">
<h2>验证访问密码</h2>
<div class="password-form">
<div class="input-group">
<input type="password" id="passwordInput" placeholder="请输入密码" autocomplete="off">
</div>
<button class="btn" onclick="verifyPassword()">验证密码</button>
<div id="errorMessage" class="error-message"></div>
</div>
</div>
<div id="articleContent" class="article-content">
<h1 id="articleTitle" class="article-title"></h1>
<div id="articleBody" class="markdown-content"></div>
</div>
</div>
</main>
</div>
<script>
// 更新主题和代码高亮
function updateTheme() {
const isDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
const highlightTheme = isDarkMode ?
'https://cdn.jsdelivr.net/npm/highlight.js@11.8.0/styles/atom-one-dark.min.css' :
'https://cdn.jsdelivr.net/npm/highlight.js@11.8.0/styles/github.min.css';
let highlightStylesheet = document.querySelector('link[data-highlight-theme]');
if (!highlightStylesheet) {
highlightStylesheet = document.createElement('link');
highlightStylesheet.setAttribute('rel', 'stylesheet');
highlightStylesheet.setAttribute('data-highlight-theme', 'true');
document.head.appendChild(highlightStylesheet);
}
highlightStylesheet.setAttribute('href', highlightTheme);
}
// 初始化主题
updateTheme();
// 监听系统主题变化
window.matchMedia('(prefers-color-scheme: dark)').addListener(updateTheme);
// 等待 marked 加载完成
window.addEventListener('load', () => {
if (typeof window.marked === 'undefined') {
console.error('marked 库加载失败');
return;
}
console.log('marked 库加载成功');
document.getElementById('passwordInput').focus();
});
function showError(message) {
const errorElement = document.getElementById('errorMessage');
errorElement.textContent = message;
errorElement.style.display = 'block';
if (errorElement.fadeTimeout) {
clearTimeout(errorElement.fadeTimeout);
}
errorElement.fadeTimeout = setTimeout(() => {
errorElement.style.display = 'none';
}, 3000);
if (message.includes('密码错误')) {
const passwordInput = document.getElementById('passwordInput');
passwordInput.value = '';
passwordInput.focus();
}
}
function showArticle(title, content) {
console.log('显示文章:', { title, content });
const passwordSection = document.getElementById('passwordSection');
passwordSection.style.opacity = '0';
passwordSection.style.transform = 'translateY(-20px)';
const header = document.querySelector('.header');
header.style.opacity = '0';
header.style.transform = 'translateY(-20px)';
setTimeout(() => {
passwordSection.style.display = 'none';
header.style.display = 'none';
// 设置标题
document.getElementById('articleTitle').textContent = title;
// 渲染 Markdown 内容
try {
const articleBody = document.getElementById('articleBody');
if (typeof window.marked === 'undefined') {
articleBody.textContent = content;
console.error('marked 库未加载,显示原始内容');
} else {
const htmlContent = window.marked.parse(content);
articleBody.innerHTML = htmlContent;
// 高亮代码块
articleBody.querySelectorAll('pre code').forEach((block) => {
hljs.highlightBlock(block);
});
}
} catch (error) {
console.error('Markdown 渲染错误:', error);
document.getElementById('articleBody').textContent = content;
}
const articleContent = document.getElementById('articleContent');
articleContent.style.display = 'block';
setTimeout(() => {
articleContent.classList.add('visible');
}, 10);
}, 300);
}
function verifyPassword() {
const password = document.getElementById('passwordInput').value;
const articleId = 1; // 这里可以根据需要修改文章 ID
const submitButton = document.querySelector('.btn');
submitButton.disabled = true;
submitButton.textContent = '验证中...';
//修改成自己的api
fetch('https://se.991198.xyz/api/get-article', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
mode: 'cors',
credentials: 'omit',
body: JSON.stringify({ articleId, password })
})
.then(response => {
if (response.status === 401) {
return response.json().then(data => {
throw new Error(data.error || '密码错误或文章不存在');
});
}
if (!response.ok) {
throw new Error(`服务器错误 (${response.status})`);
}
return response.json();
})
.then(data => {
if (data.success) {
console.log('接收到的数据:', data);
showArticle(data.article.title, data.article.content);
}
})
.catch(error => {
console.error('请求失败:', error);
if (error.message.includes('密码错误') || error.message.includes('文章不存在')) {
showError(error.message);
} else if (error.name === 'TypeError') {
showError('无法连接到服务器,请检查网络连接');
} else {
showError('服务器错误,请稍后重试');
}
})
.finally(() => {
submitButton.disabled = false;
submitButton.textContent = '验证密码';
});
}
// 添加回车键提交功能
document.getElementById('passwordInput').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
verifyPassword();
}
});
// 错误处理
window.onerror = function(msg, url, line, col, error) {
console.error('全局错误:', {
message: msg,
url: url,
line: line,
column: col,
error: error
});
};
</script>
效果展示:
密码:2025
加密文章访问
请输入密码以访问加密内容
评论区