A-A+

公司会议室预定系统token逻辑分析

2025年01月22日 09:36 学习笔记 暂无评论 共4791字 (阅读81 views次)

【注意:此文章为博主原创文章!转载需注意,请带原文链接,至少也要是txt格式!】

突发奇想,想写一个抢单位会议室的定时程序,但是手头现在也没什么工具,所以就单纯用浏览器的F12然后分析一波。

随便找一个请求,然后看一下内容如下图:

images

请求代码如下:

GET /api/mxxxng/front/finxxxPic/13xxxx7 HTTP/2
Host: axxxxxp.cn
Sec-Ch-Ua-Platform: "Windows"
Sec-Ch-Ua: "Not A(Brand";v="8", "Chromium";v="132", "Google Chrome";v="132"
Sec-Ch-Ua-Mobile: ?0
Awp-Token: eyJ0eXxxidG9rZW4iOiJlMmUzNmMwOS1xxxGb_itI05JrCNLIwOKHvu1rewjg
X-Req-Pdt-Dst: 2
Accept: application/json, text/plain, */*
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36
Token: 4IzQ4fCUyfCoyfDAwMTxxxjeXNoYkxVbGZqNVA3a3NxTHpMc2VLcEpNOxxxHNqTzFDeHZwcFxxxm1GSlVNSDJtNTR6Ly9EbmJZaXlxx
X-Req-Zone-Id: Etc%2FGMT-8
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Priority: u=1, i
Connection: close

每次请求,必然都带Awp-TokenToken,主要目的就是分析Awp-TokenToken他们值的来源!

 

第一步 断点追踪

首先根据请求找到对应的请求点,如下图:

images

注意这里m.send(f || null) 但是到这里,所有的请求都已经准备就绪,包括加密数据都已经完成了,如下图:

images

 

所以下一步继续网上翻找,断点上移。这里可能需要一点一点的上移,凭经验,凭细心。因为有的时候加密的数据并不会搜索的到,所以只能不断的翻找,要有充足的耐心

上移一个断点后,如下图:

images

往上翻找后发现Awp-TokenToken还是已经得到了加密后的值,此刻就要继续向上翻找,一定要找到加密的点,这样才能知道如何加密的。

到这里我们继续向上找函数,如下图,这里我把此处最大化截屏,让大家能尽可能的多看上下文。一定注意其中的l(e),如下图:

images

由此看出跟进到了l函数里面,但是l的传参是e,这个时候的e函数里面headers已经存在加密的Awp-TokenToken了。不过也相对好追踪了,此刻需要大家注意,上图中箭头的地方,也就是②,这里函数前面有一个数字,是81924,我们全局搜索一下81924,看看它在哪里?

这里直接给图下图,同时做一个大概的分析:

images

 

如上图,已经标注的很详细了,那么下面就简单了,我们来跟进分析_request这个封装的函数,代码如下:

 

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
_request(e, t) {
                let n, o;
                "string" == typeof e ? (t = t || {}).url = e : t = e || {};
                let {transitional: i, paramsSerializer: s, headers: b} = t = (0,
                c.Z)(this.defaults, t);
                void 0 !== i && p.Z.assertOptions(i, {
                    silentJSONParsing: u.transitional(u.boolean),
                    forcedJSONParsing: u.transitional(u.boolean),
                    clarifyTimeoutError: u.transitional(u.boolean)
                }, !1),
                null != s && (r.Z.isFunction(s) ? t.paramsSerializer = {
                    serialize: s
                } : p.Z.assertOptions(s, {
                    encode: u.function,
                    serialize: u.function
                }, !0)),
                t.method = (t.method || this.defaults.method || "get").toLowerCase();
                let d = b && r.Z.merge(b.common, b[t.method]);
                b && r.Z.forEach(["delete", "get", "head", "post", "put", "patch", "common"], e => {
                    delete b[e]
                }
                ),
                t.headers = l.Z.concat(d, b);
                let M = []
                  , f = !0;
                this.interceptors.request.forEach(function(e) {
                    if ("function" != typeof e.runWhen || !1 !== e.runWhen(t))
                        f = f && e.synchronous,
                        M.unshift(e.fulfilled, e.rejected)
                });
                let z = [];
                this.interceptors.response.forEach(function(e) {
                    z.push(e.fulfilled, e.rejected)
                });
                let O = 0;
                if (!f) {
                    let e = [a.Z.bind(this), void 0];
                    for (e.unshift.apply(e, M),
                    e.push.apply(e, z),
                    o = e.length,
                    n = Promise.resolve(t); O < o; )
                        n = n.then(e[O++], e[O++]);
                    return n
                }
                o = M.length;
                let h = t;
                for (O = 0; O < o; ) {
                    let e = M[O++]
                      , t = M[O++];
                    try {
                        h = e(h)
                    } catch (e) {
                        t.call(this, e);
                        break
                    }
                }
                try {
                    n = a.Z.call(this, h)
                } catch (e) {
                    return Promise.reject(e)
                }
                for (O = 0,
                o = z.length; O < o; )
                    n = n.then(z[O++], z[O++]);
                return n
            }

其实这里没用的代码特别多。我们只需要分析重点即可。这里可以通过一步一步的调试,找到重点部分即可。

这里通过在 let n, o;也就是函数最开始的地方就断点,然后一步一步走,然后其实上面的代码虽然长,但是核心代码如下图:

images

图中才是封装head头的整体部分。这里肯定就会涉及Awp-TokenToken。其实最核心的代码就是:

 

if (!f) {
                    let e = [a.Z.bind(this), void 0];
                    for (e.unshift.apply(e, M),
                    e.push.apply(e, z),
                    o = e.length,
                    n = Promise.resolve(t); O < o; ) //最核心的组装head头的代码在这里
                        n = n.then(e[O++], e[O++]); //在这里进行组装,要跟到这里面去!
                    return n
                }

那我们就单步跟进去看看,这里到底是怎么做的。

跟进去后如下图:

images

其实到这里就非常明显了。我们只需要了解(0,t.tn)()(0,t.vC)()这两个函数就OK了。这里t.tn返回的是M函数,t.vC返回的是h函数然后直接跟进去,如下图:

images

到这里就非常非常清晰了。

Awp-Token = sessionStorage.getItem("Awp-Token")
Token = sessionStorage.getItem("Ulp-Token")

这里有个知识点:sessionStorage 的存储范围是 当前的浏览器选项卡。每个选项卡都有自己独立的 sessionStorage 空间,即使是同一个页面在多个选项卡打开,它们的 sessionStorage 并不共享。

当选项卡或窗口关闭时,sessionStorage 会自动清空,不会保留数据。

images

这里就延申另一个问题了,既然这个值已经在标签的会话存储中了,那么到底是哪来的值???

 

那下一步我们删除这两个值,然后刷新页面看看。哪个请求会返回这两个值,或者说哪个请求写入了这两个值,具体是怎么写入的?

 

刷新页面后,具体请求链路如下:

images

第一个user-info请求已经提示“请重新登录”了。那么我们看第二个。

因为公司有统一登录系统,统一登录系统SSO是把token写在cookie中的。所以直接请求sso是会携带sso自己的cookie的,此处这里没有做详细分析,但是在最下面有个简短的分析。如下图:

images

因中间的调用环节异常复杂这里就不深入分析如何调用https://uxxx.sso.cn/api/web/auth/login_by_sso 简单点说就是{"code":2300,"msg":"请重新登录","info":null}然后程序根据2300,然后调用sso重新登录。传参是url=https%3A%2F%2Fhys.xxxx.cn&env=prod,只要sso请求head中cookie的token没有时效,那么页面会返回:

1
2
3
4
5
6
7
8
<noscript>
 
 
                <strong>Note:</strong> Since your browser does not support JavaScript,
                you must press the Continue button once to proceed.
 
 
        </noscript>
 

 

看代码也明白了,下一步页面会自动加载https://sso.xxxxx.cn/web/auth/sso_redirect,提交的参数也写的很明确,就是拿urlenvtoken请求这个地址。因为请求包很明确,这就不看了,我们注意这个请求的返回包如下图:

images

从上面的返回包明显可以看出,直接302跳转到https://sso.xxxx.cn/ulp-esso?redirect_param=base64-xxxx-xxx%3D这里解码这个redirect_param参数的base64值结果,然后完整的是https://sso.xxxx.cn/ulp-esso?redirect_param=https://hys.xxxx.cn/mms/?sysBranchName=AWP_MS&targetpath=cardCode&token=6CCxxxxxxxxx37 到这里基本就清晰了。然后我们可以看一下这个请求返回的是什么,如下图:

images

到这里其实也很清晰,看html就是按redirect_param中的url地址,带着token再跳过去。如下图:

images

然后系统再把这个token、ulpToken写入浏览器缓存就OK了。至此整个流程就算分析完毕。下面整理一个简单的图来概括一下。

images

 

后续写抢会议室的脚本也简单了,就是按上面的逻辑写python掉接口就OK。

===================

额外章节

===================

:这里对如何加载sso登录 https://sso.com/api/web/auth/login_by_sso 做一个简单的说明。

其实就是通过iframe然后传参并加载这个页面。

第一步:接口返回2300,然后如下图

images

第二步:返回到了之前说的异步request请求里面

images

第三步:从这个异步请求出去后,因为token过期了,所以进入过期请求处理函数

images

第四步:过期后,系统自动进入startSSO()函数,重新获取SSO登录态

images

第五步:通过SSO函数,最终创建iframe里面。如下图

images

第六步:创建并加载iframe页面,并加载页面。具体JS逻辑核心部分如下图(下面的图片特别大,所有图片都可以点击放大

images

具体加载了什么样式的页面,如下图:

images

布施恩德可便相知重

微信扫一扫打赏

支付宝扫一扫打赏

×

给我留言