From b24dee2ef9b829f2450508d6f46285f8cd663d9c Mon Sep 17 00:00:00 2001 From: jdysya <1912377458@qq.com> Date: Sun, 16 Feb 2025 21:46:23 +0800 Subject: [PATCH] =?UTF-8?q?feat(auth):=20=E5=AE=9E=E7=8E=B0=E9=A6=96?= =?UTF-8?q?=E9=A1=B5=E8=B7=AF=E7=94=B1=E7=9A=84=E8=AE=A4=E8=AF=81=E9=80=BB?= =?UTF-8?q?=E8=BE=91=E5=B9=B6=E4=BC=98=E5=8C=96=E9=9D=99=E6=80=81=E8=B5=84?= =?UTF-8?q?=E6=BA=90=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在客户端添加首页路由的认证逻辑,提高用户体验 - 优化静态资源服务配置,增强安全性 - 添加 URL 解析和保存功能,为后续的路由认证做准备 - 调整中间件顺序和配置,提升应用性能和安全性 --- doc/src/.vuepress/client.ts | 25 +++++++++++ gateway/config/urls.go | 42 ++++++++++++++++++ gateway/config/urls.json | 50 ++++++++++++++++++++++ gateway/main.go | 38 ++++++++++++----- gateway/middleware/route_auth.go | 31 ++++++++++++++ gateway/middleware/static_auth.go | 71 +++++++++++++++++++++++++++++++ gateway/utils/html_parser.go | 65 ++++++++++++++++++++++++++++ 7 files changed, 311 insertions(+), 11 deletions(-) create mode 100644 gateway/config/urls.go create mode 100644 gateway/config/urls.json create mode 100644 gateway/middleware/route_auth.go create mode 100644 gateway/middleware/static_auth.go create mode 100644 gateway/utils/html_parser.go diff --git a/doc/src/.vuepress/client.ts b/doc/src/.vuepress/client.ts index 278d069..9cbce52 100644 --- a/doc/src/.vuepress/client.ts +++ b/doc/src/.vuepress/client.ts @@ -4,6 +4,31 @@ import UserInfo from "./components/UserInfo.vue"; export default defineClientConfig({ enhance({ app, router }) { app.component("UserInfo", UserInfo); + + // 仅在客户端执行 + if (typeof window !== 'undefined') { + router.beforeEach(async (to, from, next) => { + // 检查是否是首页 + if (from.fullPath === '/' && to.fullPath !== '/') { + try { + // 复用 UserInfo 组件中相同的接口检查登录状态 + const response = await fetch('/api/user/info') + if (!response.ok) { + // 未登录状态,使用非SPA的路由跳转方式 + window.location.href = to.fullPath + return + } + } catch (error) { + console.error('检查登录状态失败:', error) + // 发生错误时同上 + window.location.href = to.fullPath + return + } + } + + next() + }) + } }, setup() { // 这里可以添加全局的设置 diff --git a/gateway/config/urls.go b/gateway/config/urls.go new file mode 100644 index 0000000..fb10ec1 --- /dev/null +++ b/gateway/config/urls.go @@ -0,0 +1,42 @@ +package config + +import ( + "encoding/json" + "os" + "path/filepath" +) + +// URLConfig 存储所有URL配置 +type URLConfig struct { + URLs []string `json:"urls"` +} + +// SaveURLsToFile 将URLs保存到文件 +func SaveURLsToFile(urls []string, filename string) error { + config := URLConfig{ + URLs: urls, + } + + data, err := json.MarshalIndent(config, "", " ") + if err != nil { + return err + } + + return os.WriteFile(filename, data, 0644) +} + +// GetProtectedURLs 从文件中读取需要保护的URLs +func GetProtectedURLs() ([]string, error) { + configPath := filepath.Join("config", "urls.json") + data, err := os.ReadFile(configPath) + if err != nil { + return nil, err + } + + var config URLConfig + if err := json.Unmarshal(data, &config); err != nil { + return nil, err + } + + return config.URLs, nil +} diff --git a/gateway/config/urls.json b/gateway/config/urls.json new file mode 100644 index 0000000..12787d8 --- /dev/null +++ b/gateway/config/urls.json @@ -0,0 +1,50 @@ +{ + "urls": [ + "/assets/index.html-6M4WaJGn.js", + "/assets/10.html-Cox3wLdF.js", + "/assets/index.html-iJcgfwSj.js", + "/assets/intro.html-QyfAbIVc.js", + "/assets/relation.html-Dqaw_w5f.js", + "/assets/simple.html-gWbsxbST.js", + "/assets/tomb.html-PQqy8bGV.js", + "/assets/address.html-CNNc9o0U.js", + "/assets/branch_list.html-vb7nrwTF.js", + "/assets/build_draft.html-AXYMqBRq.js", + "/assets/catalogue.html-FX5AkOsY.js", + "/assets/ink_presequence.html-BYSWwNUl.js", + "/assets/intro.html-BmWXutnI.js", + "/assets/origin_narrate.html-qihX0gD2.js", + "/assets/index.html-DTsg6_Mb.js", + "/assets/thank_support.html-Bec3hAfD.js", + "/assets/third_intro.html-DCYwR1dS.js", + "/assets/third_renew_record.html-Bw0dPE0f.js", + "/assets/whole_intro.html-Dxm36Rz4.js", + "/assets/yu_ink_intro.html-8081fdp9.js", + "/assets/余氏创修名目.html-UBd1EHMQ.js", + "/assets/续修宗谱序.html-CC03qlYI.js", + "/assets/西分祖一公总序.html-IqyDmuhg.js", + "/assets/长塘湾支祖序.html-D138eKRX.js", + "/assets/10.html-9er2Kamf.js", + "/assets/address.html-DGl_p7BY.js", + "/assets/banqiao_tomb_record.html-ZndFRXyq.js", + "/assets/catalogue.html-C390bTAm.js", + "/assets/hongshun_record.html-DPlvV_FX.js", + "/assets/intro.html-D09epTqh.js", + "/assets/jiangxi-find-origin.html-DYa2pI1G.js", + "/assets/meeting-record.html-DwOgR3Y5.js", + "/assets/proposal.html-B0yl1179.js", + "/assets/qitou_build_tomb_record.html-w1QQPg58.js", + "/assets/index.html-B8MyB7Nx.js", + "/assets/rebuild-origin-tomb.html-D6GwIgqw.js", + "/assets/rebuild-temple.html-yg6_cE67.js", + "/assets/rebuild.html-Bksfc_AQ.js", + "/assets/renew_family_disciplines.html-C6HeQ_LO.js", + "/assets/rule.html-C6rqyb74.js", + "/assets/simple.html-Co37wF1v.js", + "/assets/thank_support.html-Cb7EtcXi.js", + "/assets/the_fourth_postscript.html-S4JLTXyo.js", + "/assets/the_fourth_public.html-CNkGkbSJ.js", + "/assets/tianjue-intro.html-D1lSLpRz.js", + "/assets/404.html-CDvZn73T.js" + ] +} \ No newline at end of file diff --git a/gateway/main.go b/gateway/main.go index ebc2a34..08562fa 100644 --- a/gateway/main.go +++ b/gateway/main.go @@ -6,6 +6,7 @@ import ( "gateway/middleware" "gateway/models" "gateway/utils" + "path/filepath" "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" @@ -14,10 +15,23 @@ import ( ) func main() { - // 初始化日志 utils.InitLogger() + // 解析static/index.html中的URLs并立即写入 + hrefs, err := utils.ExtractHrefs("static/index.html") + if err != nil { + utils.Logger.Fatalf("解析HTML文件失败: %v", err) + } + + // 保存URLs到config目录 + configPath := filepath.Join("config", "urls.json") + if err := config.SaveURLsToFile(hrefs, configPath); err != nil { + utils.Logger.Fatalf("保存URLs失败: %v", err) + } + + utils.Logger.Info("成功提取并保存URLs") + // 初始化数据库 db, err := gorm.Open("sqlite3", "family.db") if err != nil { @@ -29,14 +43,15 @@ func main() { // 初始化 Gin 引擎 r := gin.Default() - // 配置静态文件服务 - r.Static("/assets", "./static/assets") - r.LoadHTMLGlob("templates/*") - - // 配置 Session 中间件 + // 配置 Session 中间件(移到最前面) store := config.InitSessionStore() r.Use(sessions.Sessions("mysession", store)) + // 配置静态文件服务,添加认证中间件 + r.Use(middleware.StaticAuthMiddleware()) + r.Static("/static", "./static") + r.LoadHTMLGlob("templates/*") + // 路由配置 r.GET("/login", handlers.GetLogin) r.POST("/login", handlers.PostLogin(db)) @@ -44,14 +59,15 @@ func main() { r.POST("/register", handlers.PostRegister(db)) r.GET("/api/user/info", handlers.GetUserInfo(db)) - // 权限校验中间件 - r.Use(middleware.AuthRequired()) - - // 添加用户信息API - // 文档页面路由 r.GET("/", handlers.ServeIndex(db)) + // 权限校验中间件 + //r.Use(middleware.AuthRequired()) + + // 添加路由认证中间件 + r.Use(middleware.RouteAuthMiddleware()) + // 在权限校验中间件后添加退出路由 r.GET("/logout", handlers.Logout) diff --git a/gateway/middleware/route_auth.go b/gateway/middleware/route_auth.go new file mode 100644 index 0000000..a556b19 --- /dev/null +++ b/gateway/middleware/route_auth.go @@ -0,0 +1,31 @@ +package middleware + +import ( + "net/url" + "path/filepath" + "strings" + + "github.com/gin-contrib/sessions" + "github.com/gin-gonic/gin" +) + +func RouteAuthMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + requestPath := c.Request.URL.Path + + // 如果路径没有后缀(不是静态资源)或以斜杠结尾,且不在白名单中,需要认证 + if filepath.Ext(requestPath) == "" || strings.HasSuffix(requestPath, "/") { + session := sessions.Default(c) + user := session.Get("user") + if user == nil { + returnURL := c.Request.URL.String() + encodedURL := url.QueryEscape(returnURL) + c.Redirect(302, "/login?return_url="+encodedURL) + c.Abort() + return + } + } + + c.Next() + } +} diff --git a/gateway/middleware/static_auth.go b/gateway/middleware/static_auth.go new file mode 100644 index 0000000..ac8240a --- /dev/null +++ b/gateway/middleware/static_auth.go @@ -0,0 +1,71 @@ +package middleware + +import ( + "gateway/config" + "net/url" + "path/filepath" + "strings" + + "github.com/gin-contrib/sessions" + "github.com/gin-gonic/gin" +) + +func StaticAuthMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + // 获取受保护的URL列表 + protectedURLs, err := config.GetProtectedURLs() + if err != nil { + c.AbortWithStatus(500) + return + } + + // 检查当前请求路径是否在受保护列表中 + requestPath := c.Request.URL.Path + + // 如果是登录相关的资源,直接放行 + if strings.Contains(c.Request.Referer(), "/login") || + strings.Contains(c.Request.Referer(), "/register") { + c.Next() + return + } + + // 检查是否是静态资源 + if strings.HasPrefix(requestPath, "/") { + needAuth := false + + // 检查是否是HTML文件 + if filepath.Ext(requestPath) == ".html" { + needAuth = true + } else { + // 检查是否在受保护列表中 + for _, protectedURL := range protectedURLs { + if requestPath == protectedURL { + needAuth = true + break + } + } + } + + // 如果需要认证,检查用户是否已登录 + if needAuth { + session := sessions.Default(c) + user := session.Get("user") + if user == nil { + if filepath.Ext(requestPath) == ".html" { + // HTML 文件重定向到登录页面 + returnURL := c.Request.URL.String() + encodedURL := url.QueryEscape(returnURL) + c.Redirect(302, "/login?return_url="+encodedURL) + } else { + // 非 HTML 文件返回 401 未授权状态码 + c.AbortWithStatus(401) + } + c.Abort() + return + } + } + } + + c.Next() + } +} diff --git a/gateway/utils/html_parser.go b/gateway/utils/html_parser.go new file mode 100644 index 0000000..2a52f2b --- /dev/null +++ b/gateway/utils/html_parser.go @@ -0,0 +1,65 @@ +package utils + +import ( + "os" + "strings" + + "golang.org/x/net/html" +) + +// ExtractHrefs 从HTML文件中提取所有rel="prefetch"的link标签的href属性 +func ExtractHrefs(filename string) ([]string, error) { + file, err := os.Open(filename) + if err != nil { + return nil, err + } + defer file.Close() + + doc, err := html.Parse(file) + if err != nil { + return nil, err + } + + var hrefs []string + var f func(*html.Node) + f = func(n *html.Node) { + if n.Type == html.ElementNode && n.Data == "link" { + var href, rel string + // 获取href和rel属性 + for _, a := range n.Attr { + switch a.Key { + case "href": + href = a.Val + case "rel": + rel = a.Val + } + } + // 如果是prefetch链接且href包含.html,添加到结果中 + if rel == "prefetch" && href != "" && strings.Contains(href, ".html") { + hrefs = append(hrefs, href) + } + } + for c := n.FirstChild; c != nil; c = c.NextSibling { + f(c) + } + } + f(doc) + + // 去重 + return removeDuplicates(hrefs), nil +} + +// removeDuplicates 移除重复的URL +func removeDuplicates(urls []string) []string { + seen := make(map[string]bool) + result := []string{} + + for _, url := range urls { + if !seen[url] { + seen[url] = true + result = append(result, url) + } + } + + return result +}