亿企赢逆向手记(四):会话的隐藏状态与多公司组切换
xsrf-token 回填、companyId/accountId/ucAccountId 身份三件套、多公司组切换,串起「按企业名找客户」的完整搜索链路。
「亿企赢逆向手记」第四篇。登录只是入场券,业务请求还依赖一批看不见的会话状态。本文把
xsrf-token、companyId/accountId/ucAccountId、以及多公司组切换逐个接口讲透——含「按企业名找客户」的完整搜索链路。自己账号自动化,技术学习向。
登录成功、cookie 也拿到了,直接打采集接口却回 403 或空数据——这是逆向登录态站点最常见的「明明登录了却用不了」。原因是业务请求依赖的不只是登录 cookie,还有一批随会话流转的隐藏状态。亿企赢这里有三类。
一、xsrf-token:cookie 搬到 header
标准的 double-submit-cookie 防护:服务端在 cookie 里种 xsrf-token,前端每次请求把它读出来塞进同名请求头。
xsrf = self.final_cookies.get('xsrf-token', '') or self.final_cookies.get('XSRF-TOKEN', '')
headers = {
**self.headers_common,
'Content-Type': 'application/json;charset=UTF-8',
'X-Requested-With': 'XMLHttpRequest',
'xsrf-token': xsrf, # cookie 里的值原样回填到头
}
两个坑:① 大小写两种都兜(xsrf-token / XSRF-TOKEN),不同接口/时期种下的键大小写可能不一致。② 你不需要伪造它——服务端只校验「header 里的值 == cookie 里的值」,不关心值是什么、谁生成的,所以只要忠实地从 cookie 搬到 header。漏这一步的人,通常是压根没意识到要回填。
二、身份三件套:companyId / accountId / ucAccountId
登录回跳时 companyId、accountId 随 cookie 下发(第一篇)。它们代表「你现在以哪个公司、哪个账套的身份操作」,几乎每个采集接口的请求体里要显式带:
json_data = {
'count': True, 'pageIndex': page_no, 'pageSize': 200,
'customerId': customer_id,
'accountId': self.journalIdList, # = cookie.accountId
'platformId': self.companyId, # = cookie.companyId
}
还有一个更隐蔽的:搜索客户前,要手动把 accountId 的值再注入成另一个 cookie 键 ucAccountId:
self.final_cookies["ucAccountId"] = self.journalIdList
这种「同一个值要在 cookie 和 body 各出现一次、甚至换个键名重复出现」的设计,只能对着抓包一处处比对补齐。稳妥习惯:把登录后所有 cookie 全量打印,对照真实请求,逐个确认哪些字段被消费、以什么键名被消费。
三、多公司组:切上下文才能看到不同客户
一个账号可能挂在多个「公司组」下,不同组能看到的客户不同。会话同时只处在一个组里,要看别的组得先切过去:
| 方法 | 端点 | 作用 |
|---|---|---|
| GET | xactivity/activity/group/getGroupList |
列出所有公司组(返回 id / name) |
| POST | xactivity/activity/group/changeGroup |
body {groupId},切换;返回新 companyId |
| GET | xactivity/activity/login/getLoginSession |
查当前会话在哪个组 |
changeGroup 是有副作用的请求,切完要把返回的新 companyId 同步回本地:
body = resp.json()['body']
self.final_cookies['companyId'] = body.get('companyId')
self.companyId = body.get('companyId')
print(f"已切换到 {body.get('companyName')} (companyId={body.get('companyId')})")
一个语义坑:getGroupList 返回的 id 和 getLoginSession 返回的「当前 companyId」是同一套 id 空间,所以能用「组 id == 当前 companyId」判断「这个组是不是我已经在的组」,避免重复切换:
current = self.get_current_company_id() # getLoginSession → body.companyId
for group in group_list:
if group['id'] == current:
continue # 当前组已找过,跳过
self.change_group(group['id'])
...
把三类状态串起来:按企业名找客户
搜索用两个接口兜底(不同账套类型走不同接口),请求体是这样:
period = 上个月的 "YYYYMM" # 财税数据按账期,默认查上月
# 接口 A
POST /xmanage-finance/manage/v2/finance/queryCustomerWithAccountSet
{'period': period, 'includeSubordinate': True, 'key': 企业名,
'relationshipType': 'JOURNAL', 'pageNo': 1, 'pageSize': 100}
# 接口 B
POST /xmanage-finance/manage/v2/finance/queryCustomer
{'period': period, 'includeSubordinate': True, 'key': 企业名,
'relationshipType': 'JOURNAL', 'pageNo': 1, 'pageSize': 100,
'statusList': [{'templateColumn':'checkPaperTicket'},
{'templateColumn':'backReceipt'}],
'finished': None, 'journalIdList': [self.journalIdList]}
命中判据是 customerFullName == 企业名(全名精确匹配,避免模糊命中错的客户)。命中后拿到 customerId / accountSetId,再切入账套:
PUT /financeweb/finance/account/session/accountSet?_rnd_=<random>
data = {'customerId', 'customerName', 'customerShortName',
'accountSetId', 'platform': 'yqdz'}
→ body.token # 账套级 token,后续该客户的接口带它
于是完整搜索逻辑是「当前组找不到就遍历所有组、逐个 changeGroup 再找」——把三类状态(防护 token、身份 id、组上下文)全对齐了,才能稳定定位到任意一个客户。
小结
登录态站点的隐藏状态,逆向时按三类逐一排查:
- 防护类(xsrf-token):cookie → header 原样搬运,大小写都兜,别漏;
- 身份类(companyId / accountId / ucAccountId):藏 cookie,被 body 显式消费,甚至换键名重复注入;
- 上下文类(公司组):会话同时只在一个上下文,切换是有副作用的请求,切完同步本地状态,用 id 相等判断避免重复切。
三类对齐,「登录了却用不了」基本解决。下一篇收尾——真正跑起来之后的工程化:分页收尾、断点续传、异常熔断。
评论 0