mirror of
https://github.com/tnb-labs/panel.git
synced 2026-09-01 14:55:12 +08:00
feat: 安全加固
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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 } }),
|
||||
// 创建用户
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 } : {},
|
||||
|
||||
Reference in New Issue
Block a user