feat: 安全加固

This commit is contained in:
耗子
2026-05-26 00:21:08 +08:00
parent 5ef6074870
commit ef9789a6b2
6 changed files with 126 additions and 10 deletions
+29 -8
View File
@@ -100,15 +100,13 @@ func Entrance(t *gotext.Locale, conf *config.Config, session *sessions.Manager)
}
}
// 情况三:通过APIKey+入口路径访问,重写请求路径并跳过验证
if strings.HasPrefix(r.URL.Path, entrance) && r.Header.Get("Authorization") != "" {
// 只在设置了入口路径的情况下,才进行重写
if entrance != "/" {
if rctx := chi.RouteContext(r.Context()); rctx != nil {
rctx.RoutePath = strings.TrimPrefix(rctx.RoutePath, entrance)
r.URL.Path = strings.TrimPrefix(r.URL.Path, entrance)
}
// 情况三:通过 APIKey+入口路径访问 API,重写请求路径并跳过入口验证
apiPath, ok := trimEntranceAPIPath(r.URL.Path, entrance)
if ok && r.Header.Get("Authorization") != "" {
if rctx := chi.RouteContext(r.Context()); rctx != nil {
rctx.RoutePath = apiPath
}
r.URL.Path = apiPath
next.ServeHTTP(w, r)
return
}
@@ -132,6 +130,29 @@ func Entrance(t *gotext.Locale, conf *config.Config, session *sessions.Manager)
}
}
func trimEntranceAPIPath(path string, entrance string) (string, bool) {
if entrance == "/" {
if path == "/api" || strings.HasPrefix(path, "/api/") {
return path, true
}
return "", false
}
if path != entrance && !strings.HasPrefix(path, entrance+"/") {
return "", false
}
apiPath := strings.TrimPrefix(path, entrance)
if apiPath == "" {
return "", false
}
if apiPath != "/api" && !strings.HasPrefix(apiPath, "/api/") {
return "", false
}
return apiPath, true
}
func abortEntrance(w http.ResponseWriter, r *http.Request, conf *config.Config, locale string) {
errorType := conf.HTTP.EntranceError
+34
View File
@@ -0,0 +1,34 @@
package middleware
import "testing"
func TestTrimEntranceAPIPath(t *testing.T) {
tests := []struct {
name string
path string
entrance string
wantPath string
wantOK bool
}{
{name: "root api", path: "/api/user/info", entrance: "/", wantPath: "/api/user/info", wantOK: true},
{name: "root api exact", path: "/api", entrance: "/", wantPath: "/api", wantOK: true},
{name: "root api prefix confusion", path: "/apiary", entrance: "/", wantOK: false},
{name: "entrance api", path: "/secret/api/user/info", entrance: "/secret", wantPath: "/api/user/info", wantOK: true},
{name: "entrance api exact", path: "/secret/api", entrance: "/secret", wantPath: "/api", wantOK: true},
{name: "entrance page", path: "/secret/login", entrance: "/secret", wantOK: false},
{name: "entrance prefix confusion", path: "/secretx/api/user/info", entrance: "/secret", wantOK: false},
{name: "api prefix confusion", path: "/secret/apiary", entrance: "/secret", wantOK: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotPath, gotOK := trimEntranceAPIPath(tt.path, tt.entrance)
if gotOK != tt.wantOK {
t.Fatalf("ok = %v, want %v", gotOK, tt.wantOK)
}
if gotPath != tt.wantPath {
t.Fatalf("path = %q, want %q", gotPath, tt.wantPath)
}
})
}
}
+1 -1
View File
@@ -27,7 +27,7 @@ export default {
// 是否2FA
isTwoFA: (username: string): any => http.Get('/user/is_2fa', { params: { username } }),
// 获取用户信息
info: (): any => http.Get('/user/info'),
info: (config = {}): any => http.Get('/user/info', config),
// 获取用户列表
list: (page: number, limit: number): any => http.Get(`/users`, { params: { page, limit } }),
// 创建用户
+59
View File
@@ -0,0 +1,59 @@
import type { Router } from 'vue-router'
import user from '@/api/panel/user'
import { useUserStore } from '@/stores'
let verified = false
let verifying: Promise<boolean> | null = null
async function verifyLogin() {
const userStore = useUserStore()
if (verified && userStore.id) {
return true
}
if (verifying) {
return verifying
}
verifying = (async () => {
const loggedIn = await user.isLogin().send(true)
if (!loggedIn) {
userStore.$reset()
return false
}
const info = await user.info({ meta: { noAlert: true } }).send(true)
userStore.set(info)
verified = true
return true
})()
try {
return await verifying
} finally {
verifying = null
}
}
export function createAuthGuard(router: Router) {
router.beforeEach(async (to) => {
if (!to.matched.some((route) => route.meta?.requireAuth)) {
return true
}
try {
if (await verifyLogin()) {
return true
}
} catch {
verified = false
}
return {
path: '/login',
query: {
redirect: to.fullPath,
},
}
})
}
+2
View File
@@ -1,5 +1,6 @@
import type { Router } from 'vue-router'
import { createAuthGuard } from '@/router/guard/auth-guard'
import { createTabGuard } from '@/router/guard/tab-guard'
import { createAppInstallGuard } from './app-install-guard'
@@ -9,6 +10,7 @@ import { createPageTitleGuard } from './page-title-guard'
export function setupRouterGuard(router: Router) {
createPageLoadingGuard(router)
createPageTitleGuard(router)
createAuthGuard(router)
createTabGuard(router)
createAppInstallGuard(router)
}
+1 -1
View File
@@ -3,7 +3,7 @@ import { router } from '@/router'
export function toLogin() {
const currentRoute = unref(router.currentRoute)
const needRedirect =
!currentRoute.meta.requireAuth && !['/404', '/login'].includes(router.currentRoute.value.path)
currentRoute.meta.requireAuth && !['/404', '/login'].includes(router.currentRoute.value.path)
router.replace({
path: '/login',
query: needRedirect ? { ...currentRoute.query, redirect: currentRoute.path } : {},