GoAdmin框架实现的后台应用遇到的鉴权跳转问题

前言

GoAdmin是一个基于 golang 面向生产的数据可视化管理平台搭建框架,可以让你使用简短的代码在极短时间内搭建起一个管理后台,虽然我也会去修改和新增一些功能页面,但是它给我的感觉总像一个黑盒子,大概是我对Web开发的知识了解甚少,总是照葫芦画瓢的完成一些工作,最近在鉴权和会话过期的处理上遇到了一点点问题,直接GET访问页面在会话过期时会自动跳转到登录页面,而点击按钮提交表单时如果已经会话过期了却没有任何反应,我觉得是少设置了一些东西,绕了一大圈才算勉强解决,但这应该不是最优的方案,先记录一下,后续找到好的解决方案再优化。

问题的发现与解决

使用GoAdmin搭建的后台网站,如果长时间停留在一个页面,点击菜单中的其他页面时会自动跳转到登录页面,但是点击页面表单上的提交按钮却没有反应,这种情况会让人迷惑,不知道刚刚这个操作到底有没有执行成功,要解决这个问题就先查代码吧?实际上时候发现,这时点击提交按钮时报了一个js错误,只有在浏览器的控制台才能看到,起初没发现也是走了一些弯路。

一开始我认为两种情况一个表现正常、一个表现不正常是因为GET和POST的缘故,所以我对比了两种请求处理从注册路由开始的逻辑代码,尽最大可能保持一样,但没有解决。

注册路由的方式,GoAdmin开源项目中有相关代码

1
2
3
4
authRoute.POST("/menu/edit", admin.guardian.MenuEdit, admin.handler.EditMenu).Name("menu_edit")
authRoute.POST("/menu/order", admin.handler.MenuOrder).Name("menu_order")
authRoute.GET("/menu", admin.handler.ShowMenu).Name("menu")
authRoute.GET("/menu/edit/show", admin.handler.ShowEditMenu).Name("menu_edit_show")

我简单的理解就是,告诉框架当访问到前面这个路径的时候,依次调用后面的处理函数就行了,当然这也是我这两天跟踪代码时领取的,最后的 Name 函数是为这个路由命令,而我的应用不管是GET路由还是POST路由都使用了 auth.Middleware(admin.Conn) 这个鉴权函数,按道理说肯定都进行了鉴权处理,不应该有差异的,最开始有一些包装函数逻辑顺序的差异,我调整成一样的依旧不生效。

我使用断点,单步调试查看GET和POST的区别,但是没有发现差异,两者都返回了302状态码表示重定向,但是GET成功跳转了,而POST没有变化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// Middleware is the default auth middleware of plugins.
func Middleware(conn db.Connection) context.Handler {
return DefaultInvoker(conn).Middleware()
}

// DefaultInvoker return a default Invoker.
func DefaultInvoker(conn db.Connection) *Invoker {
return &Invoker{
prefix: config.Prefix(),
authFailCallback: func(ctx *context.Context) {
if ctx.Request.URL.Path == config.Url(config.GetLoginUrl()) {
return
}
if ctx.Request.URL.Path == config.Url("/logout") {
ctx.Write(302, map[string]string{
"Location": config.Url(config.GetLoginUrl()),
}, ``)
return
}
param := ""
if ref := ctx.Referer(); ref != "" {
param = "?ref=" + url.QueryEscape(ref)
}

u := config.Url(config.GetLoginUrl() + param)
_, err := ctx.Request.Cookie(DefaultCookieKey)
referer := ctx.Referer()

if (ctx.Headers(constant.PjaxHeader) == "" && ctx.Method() != "GET") ||
err != nil ||
referer == "" {
ctx.Write(302, map[string]string{
"Location": u,
}, ``)
} else {
msg := language.Get("login overdue, please login again")
ctx.HTML(http.StatusOK, `<script>
if (typeof(swal) === "function") {
swal({
type: "info",
title: "`+language.Get("login info")+`",
text: "`+msg+`",
showCancelButton: false,
confirmButtonColor: "#3c8dbc",
confirmButtonText: '`+language.Get("got it")+`',
})
setTimeout(function(){ location.href = "`+u+`"; }, 3000);
} else {
alert("`+msg+`")
location.href = "`+u+`"
}
</script>`)
}
},
permissionDenyCallback: func(rawCtx *context.Context) {
if rawCtx.Headers(constant.PjaxHeader) == "" && rawCtx.Method() != "GET" {
rawCtx.JSON(http.StatusForbidden, map[string]interface{}{
"code": http.StatusForbidden,
"msg": language.Get(errors.PermissionDenied),
})
} else {
page.SetPageContent(rawCtx, Auth(rawCtx), func(ctx interface{}) (types.Panel, error) {
return template2.WarningPanel(rawCtx, errors.PermissionDenied, template2.NoPermission403Page), nil
}, conn)
}
},
conn: conn,
}
}

两种方式函数最终都走到了下面这个分支,结果却不相同

1
2
3
4
5
6
7
if (ctx.Headers(constant.PjaxHeader) == "" && ctx.Method() != "GET") ||
err != nil ||
referer == "" {
ctx.Write(302, map[string]string{
"Location": u,
}, ``)
}

没办法,这时我只能打开浏览器按下F12开始调试,渐渐发现了一些端倪,无论是点击菜单还是点击提交按钮,都收到了302也进行了跳转,但是点提交按钮这种情况收到302后虽然跳转成功了,但是页面没有切换,赶紧问了一下ChatGPT,他告诉我很可能是Ajax的锅。

Ajax究竟是个啥,虽然之前听过,但是我却说不上来,继续搜索吧。

AJAX(Asynchronous JavaScript and XML)是一种用于创建快速动态网页的技术。通过使用 AJAX,网页能够与服务器进行异步交互,而无需重新加载整个页面。AJAX 允许在后台与服务器进行通信,并使用 JavaScript 更新网页的部分内容。

使用 AJAX 的优势很多,比如:

  • 提高用户体验:AJAX 允许在后台加载数据,从而使网页更流畅,用户不需要等待整个页面重新加载。
  • 减少服务器负载:只请求所需的数据,而不是重新加载整个页面,降低了数据传输量。
  • 更快的响应时间:通过部分更新页面内容,用户能够更快地看到更新结果。

常见的 AJAX 应用场景如下:

  • 表单提交:在用户提交表单时,AJAX 可以异步提交数据,避免页面刷新。
  • 动态内容加载:在用户滚动或点击时加载更多内容,例如无限滚动。
  • 实时数据更新:如聊天应用、股票价格更新等实时信息。

这里的表单提交我确实使用了AJAX,这就导致它没有正确处理302状态码,我当时是这么想的,也是朝着这个方向努力的

找到页面上中设置Ajax回调函数的逻辑

1
2
3
4
5
6
7
8
9
panel.EnableAjaxData(types.AjaxData{
SuccessTitle: "success",
DisableJump: true,
})

aform := components.Form().
SetId("cmd-form").
SetUrl("/admin/cmd").
SetAjax(panel.AjaxSuccessJS, panel.AjaxErrorJS)

我开始拿 panel.AjaxErrorJS 开刀,我分析了前面的代码,以为这个字段是空的,准备替换成如下代码:

1
2
3
4
5
6
7
8
9
10
11
function(data) {
if (data.status === 302) {
// 处理重定向
const redirectUrl = data.headers.get('Location');
if (redirectUrl) {
window.location.href = redirectUrl;
}
} else {
consle.log(data.responseText);
}
}

很遗憾不太好用,有时好使有时不好使,并且页面一直在上方显示着加载中的进度条,没办法继续问ChatGPT吧,有一项相关提醒:

错误处理逻辑:确保在 error 函数中正确处理请求失败的情况。如果没有适当地处理错误,进度条可能会一直显示。

所以我又看了一下控制台,发现错误提醒,告诉我没有函数名:

1
Uncaught SyntaxError: Function statements require a function name

所以我改成了下面这样

1
2
3
4
5
function alala(data) {
if (data.status === 302) {
...
}
}

但这次一点也不好使了,我查看页面源码一看,原来变成了下面这样,能好使才怪了

1
2
3
4
5
6
7
function (data) {
function alala(data) {
if (data.status === 302) {
...
}
}
}

进一步分析我发现 panel.AjaxSuccessJSpanel.AjaxErrorJS 都不是空的,不能简单覆盖,需要做一步调整,这两个变量的初始化代码在 EnableAjaxData 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
func (f *FormPanel) EnableAjaxData(data AjaxData) *FormPanel {
f.Ajax = true
if f.AjaxSuccessJS == template.JS("") {
successMsg := modules.AorB(data.SuccessTitle != "", `"`+data.SuccessTitle+`"`, "data.msg")
errorMsg := modules.AorB(data.ErrorTitle != "", `"`+data.ErrorTitle+`"`, "data.msg")
jump := modules.AorB(data.SuccessJumpURL != "", `"`+data.SuccessJumpURL+`"`, "data.data.url")
text := modules.AorB(data.SuccessText != "", `text:"`+data.SuccessText+`",`, "")
wrongText := modules.AorB(data.ErrorText != "", `text:"`+data.ErrorText+`",`, "text:data.msg,")
jumpURL := ""
if !data.DisableJump {
if data.JumpInNewTab != "" {
jumpURL = `listenerForAddNavTab(` + jump + `, "` + data.JumpInNewTab + `");`
}
jumpURL += `$.pjax({url: ` + jump + `, container: '#pjax-container'});`
} else {
jumpURL = `
if (data.data && data.data.token !== "") {
$("input[name='__go_admin_t_']").val(data.data.token)
}`
}
f.AjaxSuccessJS = template.JS(`
if (typeof (data) === "string") {
data = JSON.parse(data);
}
if (data.code === 200) {
swal({
type: "success",
title: ` + successMsg + `,
` + text + `
showCancelButton: false,
confirmButtonColor: "#3c8dbc",
confirmButtonText: '` + language.Get("got it") + `',
}, function() {
$(".modal-backdrop.fade.in").remove();
` + jumpURL + `
` + data.SuccessJS + `
});
} else {
if (data.data && data.data.token !== "") {
$("input[name='__go_admin_t_']").val(data.data.token);
}
swal({
type: "error",
title: ` + errorMsg + `,
` + wrongText + `
showCancelButton: false,
confirmButtonColor: "#3c8dbc",
confirmButtonText: '` + language.Get("got it") + `',
})
}
`)
}
if f.AjaxErrorJS == template.JS("") {
errorMsg := modules.AorB(data.ErrorTitle != "", `"`+data.ErrorTitle+`"`, "data.responseJSON.msg")
error2Msg := modules.AorB(data.ErrorTitle != "", `"`+data.ErrorTitle+`"`, "'"+language.Get("error")+"'")
wrongText := modules.AorB(data.ErrorText != "", `text:"`+data.ErrorText+`",`, "text:data.msg,")
f.AjaxErrorJS = template.JS(`
if (data.responseText !== "") {
if (data.responseJSON.data && data.responseJSON.data.token !== "") {
$("input[name='__go_admin_t_']").val(data.responseJSON.data.token)
}
swal({
type: "error",
title: ` + errorMsg + `,
` + wrongText + `
showCancelButton: false,
confirmButtonColor: "#3c8dbc",
confirmButtonText: '` + language.Get("got it") + `',
})
} else {
swal({
type: "error",
title: ` + error2Msg + `,
` + wrongText + `
showCancelButton: false,
confirmButtonColor: "#3c8dbc",
confirmButtonText: '` + language.Get("got it") + `',
})
}
`)
}
return f
}

仿照着它的逻辑,在设置Ajax回调函数时我直接写function之中的内容,反正最后function和大括号它会自动生成,改完AjaxErrorJS后满心欢喜的测试,心想马上就解决了,但是发现居然没有什么效果,打开控制台一开又报错了

1
2
3
4
5
6
7
8
VM4407:2 Uncaught SyntaxError: Unexpected token '<', "
<!DOCTYPE "... is not valid JSON
at JSON.parse (<anonymous>)
at Object.success (eval at globalEval (all.min.8425540791.js:1:2618), <anonymous>:14:13)
at j (all.min.8425540791.js:1:27005)
at Object.fireWith [as resolveWith] (all.min.8425540791.js:1:27818)
at x (all.min.8425540791.js:1:75367)
at XMLHttpRequest.<anonymous> (all.min.8425540791.js:1:78879)

什么意思呢?分析过后其实是调用Ajax回调函数 AjaxSuccessJS 时,执行 data = JSON.parse(data); 这一句报了错,这里有点奇怪,起初以为302会调用到error的回调函数,现在一看跑到success函数中去了

大概分析原因如下:

302 是一个重定向状态码,表示所请求的资源临时位于不同的 URI。通常,响应中会包含一个 Location 头,指示新位置。当 AJAX 请求收到 302 状态码时,浏览器会处理重定向,通常会自动跟随新的 URL。

如果重定向的目标是一个 HTML 页面(如登录页面),浏览器会将该页面的内容返回给 AJAX 请求的 success 回调,而不是将其处理为错误。换句话说,AJAX 请求会成功接收到重定向目标的 HTML 内容,因为 HTTP 状态码 302 仍被视为“成功”请求的一部分(尽管它表示需要重定向)。

知道了这些就明白怎么去解决问题了,既然返回的是登录界面的HTML不是json数据,那么我就先判断,如果是登录界面的时候跳转就可以了,为了提升体验,可以加一个弹窗提示和延迟跳转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (data.includes("!DOCTYPE html") && data.includes("登录")) {
swal({
title: "会话已过期",
text: "请重新登录,即将跳转到登录页面……",
icon: "warning",
button: false,
timer: 2000
});
setTimeout(function() {
window.location.href = "/login";
}, 2000);
} else {
data = JSON.parse(data);
}

关于这个问题我又问了一下ChatGPT,得到的答案是这样的:

为什么浏览器已经收到了跳转而来的登录界面的内容,却没有跳转,而是继续保持在过期的页面呢

当浏览器发出 AJAX 请求并收到 302 重定向时,通常的行为是浏览器会自动向新的 URL 发起请求。但是,如果在 AJAX 请求的上下文中出现以下情况,可能导致浏览器没有自动跳转,而是显示重定向目标页面的内容,可能的原因如下:

  1. AJAX 请求的响应处理

    • 当 AJAX 请求收到 302 响应时,浏览器会自动请求新的 URL,但这个行为通常不影响当前页面的 URL。相反,它只是在 AJAX 请求的上下文中更新了响应内容。
    • 如果服务器返回的 302 重定向是指向一个 HTML 页面(如登录页面),并且这个 HTML 内容被返回到 AJAX 的 success 回调中,那么浏览器会将这个内容显示在当前页面,而不会自动进行 URL 更改。
  2. AJAX 的特性

    • AJAX 的目的就是异步加载数据,而不影响页面的整体加载。因此,即使 AJAX 请求收到重定向,浏览器的行为是将新的内容注入到当前页面中,而不是直接改变当前页面的 URL。
  3. 浏览器行为

    • 在某些情况下(尤其是在使用 JavaScript 进行页面更新时),浏览器可能不会进行页面跳转,因为页面的 JavaScript 处理了新的 HTML 内容。开发者可以在 AJAX 请求的 success 回调中自定义如何处理这种情况。

总结

  • GoAdmin是一个用 golang 编写的前后端一体的面向生产的数据可视化管理平台搭建框架
  • AJAX是一种用于创建快速动态网页的技术,能够实现与服务器进行异步交互,更新网页的部分内容
  • swal 是 SweetAlert 库的一个函数,用于创建漂亮的弹出框(模态窗口)以显示消息、警告、提示等
  • SweetAlert有1和2两个版本,SweetAlert 2 的返回值支持 Promise,可以使用 then() 方法
  • 当使用Ajax发送请求时入股遇到重定向返回给ajax回调函数success的数据是新页面的HTML内容

==>> 反爬链接,请勿点击,原地爆炸,概不负责!<<==

我饮人间二两酒,一饮无奈一饮空!我欠人间二两墨,一笔相思,一笔错!也许人生就是这样,不品人间酒,不知其中醉,不陷世间情,怎知其中泪。永远不用幻想着另一条路是美好的,那只是我们内心美化后的结果,经历过的才是真实~

2024-12-26 22:07:26

Albert Shi wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客