2328
This commit is contained in:
1 parent
834d9d0425
commit
424355da7c
6 files changed
+220
-32
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user