优化登录逻辑。
This commit is contained in:
@@ -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
@@ -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
@@ -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
|
||||
});
|
||||
|
||||
|
||||
@@ -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
@@ -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 || '登录失败');
|
||||
|
||||
Reference in New Issue
Block a user