From 043bf8773b3060abf64e6932c51bd553c48f4b7a Mon Sep 17 00:00:00 2001 From: Zhang Chao Date: Wed, 12 Aug 2026 10:57:37 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E7=99=BB=E5=BD=95=E9=80=BB?= =?UTF-8?q?=E8=BE=91=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- handlers/admin_view.go | 12 ++++++++++++ middleware/auth.go | 41 +++++++++++++++++++---------------------- static/admin/app.js | 22 +++++++++------------- static/admin/index.html | 3 ++- static/admin/login.html | 18 ++++++++++-------- 5 files changed, 52 insertions(+), 44 deletions(-) diff --git a/handlers/admin_view.go b/handlers/admin_view.go index 2b798bf..6427d1f 100644 --- a/handlers/admin_view.go +++ b/handlers/admin_view.go @@ -1,6 +1,7 @@ package handlers import ( + "encoding/json" "html/template" "log/slog" "net/http" @@ -21,6 +22,7 @@ type AdminPageData struct { Menus []AdminMenuGroup // 当前角色可见菜单(登录接口同源) PageSet map[string]bool // 菜单中的页面标识集合,模板据此渲染对应页面框架 DisplayName string // 侧边栏显示的用户名 + CurrentUser template.JS // JSON 序列化的当前用户信息(id/username/nickname/role),注入前端 } var ( @@ -92,10 +94,20 @@ func AdminView(c *gin.Context) { if displayName == "" { displayName = user.Username } + + // 将用户基本信息注入前端,避免依赖 localStorage + currentUserJSON, _ := json.Marshal(map[string]interface{}{ + "id": user.ID, + "username": user.Username, + "nickname": user.Nickname, + "role": user.Role, + }) + data := AdminPageData{ Menus: menus, PageSet: menuPageSet(menus), DisplayName: displayName, + CurrentUser: template.JS(currentUserJSON), } c.Header("Content-Type", "text/html; charset=utf-8") diff --git a/middleware/auth.go b/middleware/auth.go index e69a3b8..5d09756 100644 --- a/middleware/auth.go +++ b/middleware/auth.go @@ -10,24 +10,32 @@ import ( "github.com/golang-jwt/jwt/v5" ) +// tokenFromContext 统一从 Cookie 或 Authorization 头提取 JWT token +// 优先读 Cookie(服务端渲染 + 同域 AJAX 自动携带),回退到 Authorization 头 +func tokenFromContext(c *gin.Context) string { + if tokenString, err := c.Cookie("token"); err == nil && tokenString != "" { + return tokenString + } + authHeader := c.GetHeader("Authorization") + if authHeader != "" { + parts := strings.SplitN(authHeader, " ", 2) + if len(parts) == 2 && parts[0] == "Bearer" { + return parts[1] + } + } + return "" +} + // JWT 认证中间件 func JWTAuth() gin.HandlerFunc { return func(c *gin.Context) { - authHeader := c.GetHeader("Authorization") - if authHeader == "" { + tokenString := tokenFromContext(c) + if tokenString == "" { c.JSON(http.StatusUnauthorized, gin.H{"error": "缺少认证令牌"}) c.Abort() return } - parts := strings.SplitN(authHeader, " ", 2) - if len(parts) != 2 || parts[0] != "Bearer" { - c.JSON(http.StatusUnauthorized, gin.H{"error": "认证格式错误"}) - c.Abort() - return - } - - tokenString := parts[1] claims := &handlers.Claims{} token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) { @@ -61,20 +69,9 @@ func AdminRequired() gin.HandlerFunc { } // OptionalAuth 可选认证中间件(用于某些既支持游客又支持登录用户的接口/页面) -// 优先读 Authorization 头,其次回退到 Cookie(服务端渲染的前台页面依赖 Cookie) func OptionalAuth() gin.HandlerFunc { return func(c *gin.Context) { - tokenString := "" - authHeader := c.GetHeader("Authorization") - if authHeader != "" { - parts := strings.SplitN(authHeader, " ", 2) - if len(parts) == 2 && parts[0] == "Bearer" { - tokenString = parts[1] - } - } - if tokenString == "" { - tokenString, _ = c.Cookie("token") - } + tokenString := tokenFromContext(c) if tokenString == "" { c.Next() return diff --git a/static/admin/app.js b/static/admin/app.js index 4ef2db8..815492c 100644 --- a/static/admin/app.js +++ b/static/admin/app.js @@ -4,23 +4,21 @@ const API_BASE = '/api'; // 当前登录用户 let currentUser = null; -// 跳转到独立登录页 +// 跳转到登录页 function goLogin() { - localStorage.removeItem('token'); - localStorage.removeItem('user'); - localStorage.removeItem('menus'); currentUser = null; location.replace('/admin/login'); } // 页面初始化(登录已移至独立页面 /admin/login,菜单与页面框架由服务端模板按权限渲染) +// 认证完全依赖 HttpOnly Cookie,不再使用 localStorage 存储 token document.addEventListener('DOMContentLoaded', () => { - const token = localStorage.getItem('token'); - if (!token) { + // 从服务端注入的用户信息初始化 + currentUser = window.__currentUser || null; + if (!currentUser) { goLogin(); return; } - currentUser = JSON.parse(localStorage.getItem('user') || '{}'); const usernameEl = document.getElementById('username'); if (usernameEl) { usernameEl.textContent = currentUser.nickname || currentUser.username; @@ -97,12 +95,11 @@ async function refreshCurrentUser() { const fresh = data.data || {}; // 角色发生变化时重新加载页面,让服务端按新角色重新渲染菜单 if (currentUser && fresh.role && currentUser.role !== fresh.role) { - localStorage.setItem('user', JSON.stringify(fresh)); + currentUser = Object.assign({}, currentUser, fresh); location.reload(); return; } currentUser = Object.assign({}, currentUser, fresh); - localStorage.setItem('user', JSON.stringify(currentUser)); const usernameEl = document.getElementById('username'); if (usernameEl) { usernameEl.textContent = currentUser.nickname || currentUser.username; @@ -170,11 +167,10 @@ async function logout() { goLogin(); } -// 获取请求头 +// 获取请求头(认证通过 HttpOnly Cookie 自动携带,只需 Content-Type) function getHeaders() { return { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer ' + localStorage.getItem('token') + 'Content-Type': 'application/json' }; } @@ -1458,7 +1454,7 @@ async function uploadImageFile(file) { const res = await fetch(`${API_BASE}/upload`, { method: 'POST', // 注意:上传 FormData 时不能手动设置 Content-Type,浏览器会自动处理 boundary - headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }, + // 认证通过 HttpOnly Cookie 自动携带 body: formData }); diff --git a/static/admin/index.html b/static/admin/index.html index cb25e13..9624179 100644 --- a/static/admin/index.html +++ b/static/admin/index.html @@ -50,7 +50,8 @@ {{if index .PageSet "users"}}{{template "admin_modal_user" .}}{{end}} {{if index .PageSet "themes"}}{{template "admin_modal_theme" .}}{{end}} - + + {{end}} diff --git a/static/admin/login.html b/static/admin/login.html index 4b87c4a..06f84cd 100644 --- a/static/admin/login.html +++ b/static/admin/login.html @@ -27,10 +27,15 @@