fix: 统一 CSRF token 获取方式,修复 token 刷新后缓存失效

- utils.js getCSRFToken() 改为从 cookie 优先读取,meta 标签作为后备
- show.html 5处移除 csrfToken 缓存变量,改用 getCSRFToken() 动态获取
- studio/posts.html/drafts.html 移除 {{ .CSRFToken }} 模板硬编码
- studio-editor.js 移除本地 getCsrfToken(),改用共享 getCSRFToken()
- settings/index.html messages/index.html 改用 getCSRFToken()
- admin posts.js/comments.js 移除本地/死代码 CSRF 获取,统一用共享方法
This commit is contained in:
2026-06-02 18:47:22 +08:00
parent a83e0840d7
commit abe1388f8c
9 changed files with 24 additions and 42 deletions

View File

@ -133,12 +133,10 @@
if (link) { if (link) {
// 有链接的通知:使用 fetch keepalive 确保页面跳转前标记已读不会丢失 // 有链接的通知:使用 fetch keepalive 确保页面跳转前标记已读不会丢失
e.preventDefault(); e.preventDefault();
var csrfMeta = document.querySelector('meta[name="csrf-token"]');
var csrfToken = csrfMeta ? csrfMeta.getAttribute('content') : '';
fetch('/api/messages/' + id + '/read', { fetch('/api/messages/' + id + '/read', {
method: 'POST', method: 'POST',
keepalive: true, keepalive: true,
headers: { 'X-CSRF-Token': csrfToken } headers: { 'X-CSRF-Token': getCSRFToken() }
}); });
card.classList.remove('unread'); card.classList.remove('unread');
var badge = card.querySelector('.msg-badge'); var badge = card.querySelector('.msg-badge');

View File

@ -187,12 +187,11 @@
if (!btn) return; if (!btn) return;
btn.addEventListener('click', function() { btn.addEventListener('click', function() {
if (!confirm('确认提交审核?审核通过后将公开发布。')) return; if (!confirm('确认提交审核?审核通过后将公开发布。')) return;
var csrf = document.querySelector('meta[name="csrf-token"]');
fetch('/api/posts/' + btn.dataset.id + '/submit', { fetch('/api/posts/' + btn.dataset.id + '/submit', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'X-CSRF-Token': csrf ? csrf.getAttribute('content') : '' 'X-CSRF-Token': getCSRFToken()
} }
}) })
.then(function(r) { return r.json(); }) .then(function(r) { return r.json(); })
@ -217,8 +216,6 @@
if (!btn || !overlay) return; if (!btn || !overlay) return;
var postId = btn.dataset.id; var postId = btn.dataset.id;
var csrfMeta = document.querySelector('meta[name="csrf-token"]');
var csrfToken = csrfMeta ? csrfMeta.getAttribute('content') : '';
function showOverlay() { function showOverlay() {
overlay.style.display = 'flex'; overlay.style.display = 'flex';
@ -256,7 +253,7 @@
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken 'X-CSRF-Token': getCSRFToken()
}, },
body: JSON.stringify({ post_id: parseInt(postId), amount: amount }) body: JSON.stringify({ post_id: parseInt(postId), amount: amount })
}) })
@ -297,8 +294,6 @@
if (!container || !likeBtn || !dislikeBtn) return; if (!container || !likeBtn || !dislikeBtn) return;
var postId = container.dataset.postId; var postId = container.dataset.postId;
var csrfMeta = document.querySelector('meta[name="csrf-token"]');
var csrfToken = csrfMeta ? csrfMeta.getAttribute('content') : '';
// 页面加载时获取当前反应状态 // 页面加载时获取当前反应状态
fetch('/api/posts/' + postId + '/reaction') fetch('/api/posts/' + postId + '/reaction')
@ -321,7 +316,7 @@
likeBtn.disabled = true; likeBtn.disabled = true;
fetch('/api/posts/' + postId + '/like', { fetch('/api/posts/' + postId + '/like', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken } headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': getCSRFToken() }
}) })
.then(function(r) { .then(function(r) {
if (r.status === 401) { window.location.href = '/auth/login?redirect=/posts/' + postId; return null; } if (r.status === 401) { window.location.href = '/auth/login?redirect=/posts/' + postId; return null; }
@ -343,7 +338,7 @@
dislikeBtn.disabled = true; dislikeBtn.disabled = true;
fetch('/api/posts/' + postId + '/dislike', { fetch('/api/posts/' + postId + '/dislike', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken } headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': getCSRFToken() }
}) })
.then(function(r) { .then(function(r) {
if (r.status === 401) { window.location.href = '/auth/login?redirect=/posts/' + postId; return null; } if (r.status === 401) { window.location.href = '/auth/login?redirect=/posts/' + postId; return null; }
@ -377,15 +372,14 @@
if (!favBtn || !container) return; if (!favBtn || !container) return;
var postId = container.dataset.postId; var postId = container.dataset.postId;
var csrfMeta = document.querySelector('meta[name="csrf-token"]');
var csrfToken = csrfMeta ? csrfMeta.getAttribute('content') : '';
var isFavorited = false; var isFavorited = false;
var currentFolderId = null; var currentFolderId = null;
var loading = false; var loading = false;
function api(method, url, data) { function api(method, url, data) {
var opts = { method: method, headers: { 'Content-Type': 'application/json' } }; var opts = { method: method, headers: { 'Content-Type': 'application/json' } };
if (csrfToken) opts.headers['X-CSRF-Token'] = csrfToken; var token = getCSRFToken();
if (token) opts.headers['X-CSRF-Token'] = token;
if (data) opts.body = JSON.stringify(data); if (data) opts.body = JSON.stringify(data);
return fetch(url, opts).then(function(r) { return fetch(url, opts).then(function(r) {
if (r.status === 401) { window.location.href = '/auth/login?redirect=/posts/' + postId; throw new Error('unauthorized'); } if (r.status === 401) { window.location.href = '/auth/login?redirect=/posts/' + postId; throw new Error('unauthorized'); }
@ -469,7 +463,7 @@
// 无弹窗回退(未登录跳转) // 无弹窗回退(未登录跳转)
function handleNoModal() { function handleNoModal() {
if (!csrfToken) { if (!getCSRFToken()) {
window.location.href = '/auth/login?redirect=/posts/' + postId; window.location.href = '/auth/login?redirect=/posts/' + postId;
return; return;
} }
@ -571,7 +565,7 @@
// 事件:点击收藏按钮 → 打开弹窗 // 事件:点击收藏按钮 → 打开弹窗
favBtn.addEventListener('click', function() { favBtn.addEventListener('click', function() {
if (!csrfToken) { window.location.href = '/auth/login?redirect=/posts/' + postId; return; } if (!getCSRFToken()) { window.location.href = '/auth/login?redirect=/posts/' + postId; return; }
showModal(); showModal();
}); });
@ -627,15 +621,14 @@
var commentInput = document.getElementById('commentInput'); var commentInput = document.getElementById('commentInput');
var commentSubmitBtn = document.getElementById('commentSubmitBtn'); var commentSubmitBtn = document.getElementById('commentSubmitBtn');
var mentionDropdown = document.getElementById('mentionDropdown'); var mentionDropdown = document.getElementById('mentionDropdown');
var csrfMeta = document.querySelector('meta[name="csrf-token"]');
var csrfToken = csrfMeta ? csrfMeta.getAttribute('content') : '';
var currentPage = 1; var currentPage = 1;
var totalPages = 1; var totalPages = 1;
// ====== 工具函数 ====== // ====== 工具函数 ======
function api(method, url, data) { function api(method, url, data) {
var opts = { method: method, headers: { 'Content-Type': 'application/json' } }; var opts = { method: method, headers: { 'Content-Type': 'application/json' } };
if (csrfToken) opts.headers['X-CSRF-Token'] = csrfToken; var token = getCSRFToken();
if (token) opts.headers['X-CSRF-Token'] = token;
if (data) opts.body = JSON.stringify(data); if (data) opts.body = JSON.stringify(data);
return fetch(url, opts).then(function(r) { return r.json(); }); return fetch(url, opts).then(function(r) { return r.json(); });
} }
@ -1031,7 +1024,7 @@
fetch('/api/comments/upload-image', { fetch('/api/comments/upload-image', {
method: 'POST', method: 'POST',
headers: { 'X-CSRF-Token': csrfToken }, headers: { 'X-CSRF-Token': getCSRFToken() },
body: formData body: formData
}) })
.then(function(r) { return r.json(); }) .then(function(r) { return r.json(); })

View File

@ -1032,9 +1032,6 @@
var createBtn = document.getElementById('createFolderBtn'); var createBtn = document.getElementById('createFolderBtn');
if (!listEl) return; if (!listEl) return;
var csrfMeta = document.querySelector('meta[name="csrf-token"]');
var csrfToken = csrfMeta ? csrfMeta.getAttribute('content') : '';
function esc(s) { function esc(s) {
var d = document.createElement('div'); var d = document.createElement('div');
d.textContent = s || ''; d.textContent = s || '';
@ -1043,7 +1040,8 @@
function api(method, url, data) { function api(method, url, data) {
var opts = { method: method, headers: { 'Content-Type': 'application/json' } }; var opts = { method: method, headers: { 'Content-Type': 'application/json' } };
if (csrfToken) opts.headers['X-CSRF-Token'] = csrfToken; var token = getCSRFToken();
if (token) opts.headers['X-CSRF-Token'] = token;
if (data) opts.body = JSON.stringify(data); if (data) opts.body = JSON.stringify(data);
return fetch(url, opts).then(function (r) { return r.json(); }); return fetch(url, opts).then(function (r) { return r.json(); });
} }

View File

@ -65,7 +65,7 @@ function submitPost(id) {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'X-CSRF-Token': '{{.CSRFToken}}' 'X-CSRF-Token': getCSRFToken()
} }
}) })
.then(r => r.json()) .then(r => r.json())
@ -85,7 +85,7 @@ function deletePost(id) {
method: 'DELETE', method: 'DELETE',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'X-CSRF-Token': '{{.CSRFToken}}' 'X-CSRF-Token': getCSRFToken()
} }
}) })
.then(r => r.json()) .then(r => r.json())

View File

@ -86,7 +86,7 @@ function deletePost(id) {
method: 'DELETE', method: 'DELETE',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'X-CSRF-Token': '{{.CSRFToken}}' 'X-CSRF-Token': getCSRFToken()
} }
}) })
.then(r => r.json()) .then(r => r.json())

View File

@ -1,17 +1,13 @@
/** /**
* Studio Editor — 创作中心 Vditor 编辑器 * Studio Editor — 创作中心 Vditor 编辑器
* 基于 editor.js使用 /api/studio/posts 端点 * 基于 editor.js使用 /api/studio/posts 端点
* CSRF token 由 shared/utils.js 的 getCSRFToken() 统一提供
*/ */
let vditor = null let vditor = null
let draftTimer = null let draftTimer = null
let submitting = false let submitting = false
function getCsrfToken() {
var match = document.cookie.match(/(?:^|;\s*)mlb_csrf=([^;]*)/)
return match ? match[1] : ''
}
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const container = document.getElementById('vditor') const container = document.getElementById('vditor')
if (!container) return if (!container) return
@ -186,7 +182,7 @@ async function handleSubmit(e) {
method, method,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'X-CSRF-Token': getCsrfToken(), 'X-CSRF-Token': getCSRFToken(),
}, },
body: JSON.stringify({ title, body }), body: JSON.stringify({ title, body }),
}) })

View File

@ -3,8 +3,6 @@
var currentPage = 1; var currentPage = 1;
var currentKeyword = ''; var currentKeyword = '';
var totalPages = 1; var totalPages = 1;
var csrfMeta = document.querySelector('meta[name="csrf-token"]');
var csrfToken = csrfMeta ? csrfMeta.getAttribute('content') : '';
var searchInput = document.getElementById('commentSearch'); var searchInput = document.getElementById('commentSearch');
var searchBtn = document.getElementById('commentSearchBtn'); var searchBtn = document.getElementById('commentSearchBtn');

View File

@ -1,8 +1,5 @@
(function() { (function() {
function getCSRFToken() { // CSRF token 由 shared/utils.js 的 getCSRFToken() 统一提供
var meta = document.querySelector('meta[name="csrf-token"]');
return meta ? meta.getAttribute('content') : '';
}
function api(url, method, body) { function api(url, method, body) {
return fetch(url, { return fetch(url, {

View File

@ -4,11 +4,13 @@
'use strict'; 'use strict';
/** /**
* 从 <meta name="csrf-token"> 读取 CSRF 令牌 * 获取 CSRF 令牌 — 优先从 cookie 读取与服务端验证源一致token 刷新后保持最新),
* 由服务端 CSRF 中间件注入到页面模板的 <meta> 标签 * meta 标签作为后备(部分页面可能未设置 cookie
* @returns {string} CSRF 令牌,未找到时返回空字符串 * @returns {string} CSRF 令牌,未找到时返回空字符串
*/ */
function getCSRFToken() { function getCSRFToken() {
var match = document.cookie.match(/(?:^|;\s*)mlb_csrf=([^;]*)/);
if (match && match[1]) return match[1];
var meta = document.querySelector('meta[name="csrf-token"]'); var meta = document.querySelector('meta[name="csrf-token"]');
return meta ? meta.getAttribute('content') : ''; return meta ? meta.getAttribute('content') : '';
} }