This commit is contained in:
Vaica committed 2026-06-21 23:28:31 +08:00
1 parent 834d9d0425
commit 424355da7c
6 files changed
+220 -32

No files matched your search

+59 -15
View File
@@ -1,27 +1,57 @@
# Write Server Nginx 站点配置
# 反向代理配置 + Basic Auth 认证
# HTTP -> HTTPS 重定向
server {
listen 80;
server_name _; # 匹配所有域名和 IP
server_name post.usj.cc;
# Let's Encrypt 验证
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
# 其他请求重定向到 HTTPS
location / {
return 301 https://$host$request_uri;
}
}
# HTTPS 主站
server {
listen 443 ssl http2;
server_name post.usj.cc;
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
# 日志
access_log /var/log/nginx/write-server-access.log;
error_log /var/log/nginx/write-server-error.log;
# Basic Auth 认证(保护管理后台)
auth_basic "Write Server Admin";
auth_basic_user_file /etc/nginx/.htpasswd;
# 健康检查(不需要认证)
location /health {
auth_basic off;
proxy_pass http://write-server:8016/api/stats;
access_log off;
# 静态文件(登录页、Service Worker、htpasswd 验证)
location /sw.js {
alias /etc/nginx/sw.js;
add_header Content-Type application/javascript;
add_header Cache-Control "no-cache, no-store";
}
# 代理到 Write Server
location /login.html {
alias /etc/nginx/login.html;
add_header Cache-Control "no-cache, no-store";
}
location /auth/check {
auth_basic "Write Server";
auth_basic_user_file /etc/nginx/.htpasswd;
return 200 'ok';
add_header Content-Type text/plain;
}
# 所有其他请求通过 auth_request 验证
location / {
auth_request /auth/internal;
proxy_pass http://write-server:8016;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
@@ -32,14 +62,28 @@ server {
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
# 超时设置
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 120s;
client_max_body_size 50m;
error_page 401 = @login;
}
location = /auth/internal {
internal;
proxy_pass http://write-server:8016/api/stats;
proxy_set_header Authorization $http_authorization;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
}
location @login {
internal;
add_header WWW-Authenticate "";
return 302 /login.html;
}
# 禁止访问隐藏文件
location ~ /\. {
deny all;
access_log off;
+104
View File
@@ -0,0 +1,104 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>写作后台 - 登录</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{min-height:100vh;display:flex;align-items:center;justify-content:center;background:#fcfaf5;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
.login-card{background:#fff;border-radius:16px;box-shadow:0 4px 24px rgba(34,61,102,.08);padding:48px 40px;width:100%;max-width:380px}
h1{font-size:20px;color:#1d3d6c;margin-bottom:4px;font-weight:700}
.sub{font-size:13px;color:#999;margin-bottom:32px}
.field{margin-bottom:20px}
.field label{display:block;font-size:13px;color:#223d66;margin-bottom:6px;font-weight:500}
.field input{width:100%;padding:10px 14px;border:1.5px solid #e4dfd2;border-radius:8px;font-size:14px;color:#223d66;background:#fcfaf5;outline:none;transition:border .2s}
.field input:focus{border-color:#1d3d6c}
.btn{width:100%;padding:12px;background:#1d3d6c;color:#fff;border:none;border-radius:8px;font-size:15px;font-weight:600;cursor:pointer;transition:opacity .2s;margin-top:4px}
.btn:hover{opacity:.85}
.btn:active{opacity:.7}
.btn:disabled{opacity:.5;cursor:not-allowed}
.error{background:#fef2f2;color:#dc2626;font-size:13px;padding:10px 14px;border-radius:8px;margin-bottom:16px;display:none}
</style>
</head>
<body>
<div class="login-card">
<h1>写作后台</h1>
<p class="sub">优世界 · Blog Admin</p>
<div class="error" id="err">用户名或密码错误</div>
<form id="form">
<div class="field">
<label>用户名</label>
<input type="text" id="user" autocomplete="username" autofocus required>
</div>
<div class="field">
<label>密码</label>
<input type="password" id="pass" autocomplete="current-password" required>
</div>
<button class="btn" type="submit" id="btn">登 录</button>
</form>
</div>
<script>
document.getElementById('form').addEventListener('submit', async function(e) {
e.preventDefault();
var btn = document.getElementById('btn');
var err = document.getElementById('err');
btn.disabled = true;
btn.textContent = '登录中...';
err.style.display = 'none';
var u = document.getElementById('user').value;
var p = document.getElementById('pass').value;
var tok = btoa(u + ':' + p);
try {
// 验证凭据
var res = await fetch('/auth/check', {
headers: { 'Authorization': 'Basic ' + tok }
});
if (!res.ok) {
throw new Error('auth failed');
}
// 存储凭据
localStorage.setItem('wauth', tok);
// 注册 Service Worker
if ('serviceWorker' in navigator) {
var reg = await navigator.serviceWorker.register('/sw.js');
// 等待 SW 激活
if (reg.installing) {
await new Promise(function(resolve) {
reg.installing.addEventListener('statechange', function() {
if (this.state === 'activated') resolve();
});
});
}
// 发送 token 给 SW
var sw = reg.active || reg.waiting || reg.installing;
if (sw) {
sw.postMessage({ type: 'SET_TOKEN', token: tok });
}
// 确保 SW 控制当前页面
if (navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.postMessage({ type: 'SET_TOKEN', token: tok });
}
}
// 跳转到主页
location.href = '/';
} catch (err2) {
err.style.display = 'block';
btn.disabled = false;
btn.textContent = '登 录';
}
});
// 已登录则直接跳转
if (localStorage.getItem('wauth')) {
location.href = '/';
}
</script>
</body>
</html>
+42
View File
@@ -0,0 +1,42 @@
const CACHE = 'v1';
const LOGIN = '/login.html';
self.addEventListener('install', () => self.skipWaiting());
self.addEventListener('activate', e => e.waitUntil(self.clients.claim()));
self.addEventListener('fetch', e => {
const req = e.request;
if (req.url.includes('/login.html') || req.url.includes('/auth/')) {
e.respondWith(fetch(req));
return;
}
e.respondWith((async () => {
const token = self.__auth_token;
if (!token) {
// 尝试从 IndexedDB/localStorage 读取(SW 中无法直接访问 localStorage)
// 依赖客户端页面消息传递 token
return fetch(req);
}
const headers = new Headers(req.headers);
headers.set('Authorization', 'Basic ' + token);
const res = await fetch(new Request(req, { headers }));
if (res.status === 401) {
return Response.redirect(LOGIN, 302);
}
return res;
})());
});
// 接收页面发来的 token
self.addEventListener('message', e => {
if (e.data && e.data.type === 'SET_TOKEN') {
self.__auth_token = e.data.token;
}
if (e.data && e.data.type === 'CLEAR_TOKEN') {
self.__auth_token = null;
}
});