优化登录逻辑。

This commit is contained in:
2026-08-12 10:57:37 +08:00
parent a1d57d6b44
commit 043bf8773b
5 changed files with 52 additions and 44 deletions
+12
View File
@@ -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")
+19 -22
View File
@@ -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
+9 -13
View File
@@ -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
});
+2 -1
View File
@@ -50,7 +50,8 @@
{{if index .PageSet "users"}}{{template "admin_modal_user" .}}{{end}}
{{if index .PageSet "themes"}}{{template "admin_modal_theme" .}}{{end}}
<script src="/static/admin/app.js?v=19"></script>
<script>window.__currentUser = {{.CurrentUser}};</script>
<script src="/static/admin/app.js?v=20"></script>
</body>
</html>
{{end}}
+10 -8
View File
@@ -27,10 +27,15 @@
</div>
<script>
// 已登录则直接进入后台
if (localStorage.getItem('token')) {
location.replace('/admin');
}
// 已登录则直接进入后台(通过 Cookie 自动认证,调用接口验证有效性)
(async function () {
try {
var res = await fetch('/api/auth/me');
if (res.ok) {
location.replace('/admin');
}
} catch (e) { /* 网络异常继续显示登录页 */ }
})();
function showError(msg) {
const el = document.getElementById('login-error');
@@ -55,10 +60,7 @@
});
const data = await res.json();
if (res.ok) {
localStorage.setItem('token', data.token);
localStorage.setItem('user', JSON.stringify(data.user));
// 菜单由登录接口返回,缓存供前端参考(页面菜单以服务端模板渲染为准)
localStorage.setItem('menus', JSON.stringify(data.menus || []));
// 服务端已通过 Set-Cookie 写入 JWT,直接跳转即可
location.href = '/admin';
} else {
showError(data.error || '登录失败');