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}}
-
+
+