From ae92cd2441e3553341724ea014b390cb6c516480 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Wed, 24 Jun 2026 08:15:15 +0800 Subject: [PATCH] =?UTF-8?q?cvl=5Fcsrf=5Ftoken=20=20=E6=98=AF=E6=9C=89?= =?UTF-8?q?=E4=BA=86,=20=E4=BD=86=E6=98=AF=20acf=5Fccn=20=E8=BF=98?= =?UTF-8?q?=E6=B2=A1=E6=9C=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/douyu/login.py | 59 +++++++++++++++++++++++- 对比 ck.txt | 106 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 对比 ck.txt diff --git a/core/douyu/login.py b/core/douyu/login.py index e824da0..8fc62c7 100644 --- a/core/douyu/login.py +++ b/core/douyu/login.py @@ -54,6 +54,8 @@ class DouyuLogin: VERIFY_API = "https://passport.douyu.com/wgapi/member/passport/remotelogin/verify" LOGIN_CALLBACK_API = "https://www.douyu.com/api/passport/login" WEBLOGIN_API = "https://msg.douyu.com/webLogin" + CSRF_API = "https://www.douyu.com/japi/carnival/nc/common/generateCsrf" + CSRF_REFERER = "https://www.douyu.com/pages/live-peace-handbook/web/shop?ditchname=pass0&roomId=9263298" LOGIN_REFERER = ( "https://passport.douyu.com/index/login?" "passport_reg_callback=PASSPORT_REG_SUCCESS_CALLBACK&" @@ -732,7 +734,62 @@ class DouyuLogin: except Exception as e: logger.warning(f"WebLogin请求失败(不影响登录): {e}") - # 收集所有Cookie + # 登录成功后访问斗鱼 Web CSRF 接口,补齐 cvl_csrf_token;响应里的 acf_ccn 也会进入 session cookie jar。 + self._generate_csrf_cookie() + + return self._format_cookie_string() + + def _generate_csrf_cookie(self) -> str: + """ + 访问斗鱼 generateCsrf 接口,从 Set-Cookie 中同步 cvl_csrf_token。 + + requests.Session 会自动接收响应中的 Set-Cookie,所以这里不只保存 + cvl_csrf_token;接口额外下发的 acf_ccn 等 Cookie 也会保留到最终 CK。 + """ + logger.info("补齐CSRF Cookie...") + headers = { + 'Accept': 'application/json, text/plain, */*', + 'Accept-Language': 'zh-CN,zh;q=0.9,en-US;q=0.6,en;q=0.5', + 'Origin': 'https://www.douyu.com', + 'Referer': self.CSRF_REFERER, + 'Sec-Fetch-Dest': 'empty', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Site': 'same-origin', + # 覆盖登录接口的默认表单头,尽量贴近浏览器抓包。 + 'Content-Type': None, + 'X-Requested-With': None, + } + response = self._request('post', self.CSRF_API, headers=headers) + response.raise_for_status() + + body = response.text.strip() + if not body: + raise ValueError("生成CSRF失败: 响应为空") + + try: + payload = response.json() + except json.JSONDecodeError as exc: + preview = body[:200].replace("\n", "\\n") + raise ValueError(f"生成CSRF失败: 响应不是有效 JSON: {preview}") from exc + + if payload.get('error') != 0: + raise ValueError(f"生成CSRF失败: {payload.get('msg', '未知错误')}") + + cookies = self.session.cookies.get_dict() + csrf_token = cookies.get('cvl_csrf_token', '') + if not csrf_token: + raise ValueError("生成CSRF失败: 响应没有 cvl_csrf_token") + + if cookies.get('acf_ccn'): + logger.info("已同步 acf_ccn Cookie") + else: + logger.warning("generateCsrf 响应未同步 acf_ccn Cookie") + + logger.info("CSRF Cookie已补齐") + return csrf_token + + def _format_cookie_string(self) -> str: + """把当前 session 中的 Cookie 格式化为可导出的 CK 字符串。""" cookies = self.session.cookies.get_dict() # 格式化Cookie字符串 diff --git a/对比 ck.txt b/对比 ck.txt new file mode 100644 index 0000000..f107d0e --- /dev/null +++ b/对比 ck.txt @@ -0,0 +1,106 @@ + +目标 ck +cvl_csrf_token=62083f188904477e9bc1c5a70537145c; acf_ccn=8b025ffcbf403d5e0d880ad6327bb3cf; PHPSESSID=86k3denvoc1j16rl1fsv5l2mc2; acf_auth=5a8eUzBoDc8uyLasCP7LXaO9a2vUK%2FDaNFwYi0MVdUFMpUcMBfsoZfifJZKCQC8WgJuKq7WB78VZrRlRXnZUSt87TZM0tLNdnplqtuL87c7idAqo%2Fal57ls; acf_jwt_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJtZDUifQ.eyJ1aWQiOjk0MjQxNjQ1MSwiY3QiOjAsInN1YiI6InN0IiwiYXVkIjpbImR5Iiwibm9uZSJdLCJiaXoiOjEsImx0a2lkIjo2OTE2MTY4NSwic3RrIjoiNGZkNDY5ZDE2OWY3NzM0YiIsImV4cCI6MTc4MjMyMjc2OSwiaWF0IjoxNzgxNzE3OTcwLCJrZXkiOiJkeS1qd3QtbWQ1In0.NDc0YzMyNjQ0OGIzNTkxYzkxNGQ1NDk0Y2U0NTk1NTU; acf_dmjwt_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJtZDUifQ.eyJ1aWQiOjk0MjQxNjQ1MSwiY3QiOjAsInN1YiI6InN0IiwiYXVkIjpbImRtIiwibm9uZSJdLCJiaXoiOjEsImx0a2lkIjo2OTE2MTY4NSwic3RrIjoiNGZkNDY5ZDE2OWY3NzM0YiIsImV4cCI6MTc4MjMyMjc2OSwiaWF0IjoxNzgxNzE3OTcwLCJrZXkiOiJkeS1qd3QtbWQ1In0.NWM1ZDgwZjE1N2ZhNTE3NDUxNmQ4ZjljMzI5ODQ0ZjM; dy_auth=29bfEdmmHp50M9Zr2gxQ3%2F2AJD2DAdCK6I%2FoX92U5n13i64R%2B%2FywJZf%2FqG4%2BCJvbUQmFaUSu3dFbJj4%2Be1gU1DpW6ligJYaJLnNuVbmKMeZCtayBmGtns6A; wan_auth37wan=cf514c616a0fzsvhClbvRbju0sSZb4aDqHQQabpd66aLoVky4bvRRXYDfYMX3iSUNiDuh%2BHt%2B20QXW0f5JLZ6lBtOW8LIIwb31iX8OiZurGB0aztSKA; acf_uid=942416451; acf_username=942416451; acf_nickname=%E7%94%A8%E6%88%B74512197651; acf_own_room=0; acf_groupid=1; acf_phonestatus=1; acf_avatar=https%3A%2F%2Fapic.douyucdn.cn%2Fupload%2Favatar%2Fdefault%2F18_; acf_ct=0; acf_ltkid=69161685; acf_biz=1; acf_stk=4fd469d169f7734b; acf_devid=9869cce23d84da0fd8b1cd20162b3def + + + + + +====== +自己跑的 +dy_accounts_main=; dy_auth=0208CrOVxM5sDK9%2F5WrkVdZhuB4HSkgVod%2F7g6BhzIwaz0KjIUNW%2FkwQP2aNlqlgUCmz40FFwmZlH%2FV%2BKs%2FIfw9NgitHrJUx8w%2F8H1H2Dati3ezwI%2BOR6WYLeQ; wan_auth37wan=36a436a51f20daPv%2B2XfNSB6x4uSc4S%2Bu1ygA0cT%2FjaoFSAvJcFQtC6dlTLGIzT2xhSM8Al1gmJumyROhJOrnC5NFtcWbzxzPwbJEetjQKR58fRcTCcj; LTP0=eyJhbGciOiJtZDUiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOlsicGFzc3BvcnQiXSwiY3QiOjAsImN0aW1lIjoxNzgyMjMwOTMwLCJleHAiOjE3OTc5OTg5MzgsImtleSI6ImR5LWp3dC1tZDUiLCJsdGsiOiIzOWU4ZDZhYzBiZGYwNTEzIiwibHRraWQiOjcxODUwNjE0LCJzdWIiOiJsdCIsInVpZCI6MTAzMDAyMzc0MH0.MjQ3NGQzNWQ1MjkyOGQ1NzUxZjA1OTNiYzAxMmE5MWY; last_login_way=nickname; PHPSESSID=8a7ldg51tuapcngdkdice6pab0; acf_auth=dc71BJkxslSfnGLHIcCLvMmxXDfqt72pB0VW53Z30z7RZfS5NSZRGFvrNShEuX6k0YxPv%2B71Cnhl6O%2FTRQYciZ9l7RhRMqs0ykAhm%2Ftb1ulawonh0V2hoBmU0w; acf_jwt_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJtZDUifQ.eyJ1aWQiOjEwMzAwMjM3NDAsImN0IjowLCJzdWIiOiJzdCIsImF1ZCI6WyJkeSIsIm5vbmUiXSwiYml6IjoxLCJsdGtpZCI6NzE4NTA2MTQsInN0ayI6IjliNDEyMGE2MTFkMDA3NjAiLCJleHAiOjE3ODI4MzU3MzAsImlhdCI6MTc4MjIzMDkzMCwia2V5IjoiZHktand0LW1kNSJ9.ZmE0Y2IwMDIzMmZhZTQzZGIzMmMzNmY2ZWY1NGNjNjQ; acf_dmjwt_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJtZDUifQ.eyJ1aWQiOjEwMzAwMjM3NDAsImN0IjowLCJzdWIiOiJzdCIsImF1ZCI6WyJkbSIsIm5vbmUiXSwiYml6IjoxLCJsdGtpZCI6NzE4NTA2MTQsInN0ayI6IjliNDEyMGE2MTFkMDA3NjAiLCJleHAiOjE3ODI4MzU3MzAsImlhdCI6MTc4MjIzMDkzMCwia2V5IjoiZHktand0LW1kNSJ9.M2QyMWMxYjI2NjY5OTlkMGYyOGVlYTU3ZmRlZTg4YzE; acf_uid=1030023740; acf_username=1030023740; acf_nickname=%E7%94%A8%E6%88%B74965059115; acf_own_room=0; acf_groupid=1; acf_phonestatus=1; acf_avatar=https%3A%2F%2Fapic.douyucdn.cn%2Fupload%2Favatar%2Fdefault%2F24_; acf_ct=0; acf_ltkid=71850614; acf_biz=1; acf_stk=9b4120a611d00760; acf_devid=a72ea6ef2f1f6c51a823d212390ce718 + + + + +dy_accounts_main=; dy_auth=7febuXzoBCnE7TEoIP28mhEf2OXn6l%2FSwL56eAJA1LYlNzSpDua7PtYlV8gxpxxlPOCFRYwTRw%2BDh6X19MJetWfzOTasWoyu1kxEMg5uitOBAWm5uI%2BqS3M; wan_auth37wan=66e4873d1a6dU7jaNtlupCZ78lHwB7SzAUisHHNHyQlnH3heDOvP4b4CkEPOxy%2FBqT3HGEMra%2FAeflM6D6hfrMpJfXJGoNYwCF08uCnTsYWDiGGHWVE; LTP0=eyJhbGciOiJtZDUiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOlsicGFzc3BvcnQiXSwiY3QiOjAsImN0aW1lIjoxNzgyMjU5OTI5LCJleHAiOjE3OTgwMjc5NDQsImtleSI6ImR5LWp3dC1tZDUiLCJsdGsiOiIwOWJlYWZlZTM2MDBhMjBkIiwibHRraWQiOjU1ODI4NjkxLCJzdWIiOiJsdCIsInVpZCI6NzIyMDUwMDMxfQ.YjVlY2FjNTZkNzIxYjhjMDRjODZhNzE0ZDA3MmQ3OWE; last_login_way=nickname; PHPSESSID=oquhqrc9b6bkfvldj6n7mh9tg2; acf_auth=50c0Hnuwt0o08C%2FaUYAUoiY8QbryaO2fmxkd%2Frn7uktr5WqvmLxNHcpHAcEcgZVmmr29iqA2YTs1adLO%2B4R4NI7Hm8EgLqiKsTw8s152W32rEA9UN7JEL3w; acf_jwt_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJtZDUifQ.eyJ1aWQiOjcyMjA1MDAzMSwiY3QiOjAsInN1YiI6InN0IiwiYXVkIjpbImR5Iiwibm9uZSJdLCJiaXoiOjEsImx0a2lkIjo1NTgyODY5MSwic3RrIjoiMDJhMzM5MjE5YWQxYTdhNSIsImV4cCI6MTc4Mjg2NDcyOSwiaWF0IjoxNzgyMjU5OTI5LCJrZXkiOiJkeS1qd3QtbWQ1In0.YWNlM2FkYTliYjEwMjk5NzdjMDA0Zjc2NGJkNTEwNmQ; acf_dmjwt_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJtZDUifQ.eyJ1aWQiOjcyMjA1MDAzMSwiY3QiOjAsInN1YiI6InN0IiwiYXVkIjpbImRtIiwibm9uZSJdLCJiaXoiOjEsImx0a2lkIjo1NTgyODY5MSwic3RrIjoiMDJhMzM5MjE5YWQxYTdhNSIsImV4cCI6MTc4Mjg2NDcyOSwiaWF0IjoxNzgyMjU5OTI5LCJrZXkiOiJkeS1qd3QtbWQ1In0.ZDljYzlkNTZjNzNhYzMwZjAzNzY5NjQxZmJhM2FmODk; acf_uid=722050031; acf_username=722050031; acf_nickname=%E7%94%A8%E6%88%B76893423879; acf_own_room=0; acf_groupid=1; acf_phonestatus=1; acf_avatar=https%3A%2F%2Fapic.douyucdn.cn%2Fupload%2Favatar%2Fdefault%2F19_; acf_ct=0; acf_ltkid=55828691; acf_biz=1; acf_stk=02a339219ad1a7a5; acf_devid=d5d7050d31074c949bd5394f1241000c; cvl_csrf_token=347aeed3a7974d668de371c1a3213c18 + + + +======== +我看了 [对比 ck.txt](/Users/yml/codes/douyu_login_py/对比%20ck.txt),结论是:两组 CK 的核心登录字段都有,但不是同一个账号/同一次登录态;自己跑出来的 CK 少了目标里的 2 个字段,多了 3 个字段。 + +**字段差异** + +目标 CK 独有: + +```text +cvl_csrf_token +acf_ccn +``` + +自己跑的 CK 独有: + +```text +dy_accounts_main= +LTP0 +last_login_way=nickname +``` + +其中 `dy_accounts_main=` 是空值,实际意义可能不大;`LTP0` 是 passport 侧的长期 token,目标 CK 里没有。 + +**相同字段但值不同** + +这些字段两边都有,但值都不一样: + +```text +PHPSESSID +acf_auth +dy_auth +wan_auth37wan +acf_jwt_token +acf_dmjwt_token +acf_uid +acf_username +acf_nickname +acf_avatar +acf_ltkid +acf_stk +acf_devid +``` + +账号信息也不同: + +```text +目标 uid: 942416451 +自己 uid: 1030023740 + +目标 nickname: 用户4512197651 +自己 nickname: 用户4965059115 +``` + +**JWT 载荷对比** + +`acf_jwt_token` / `acf_dmjwt_token` 的结构是一致的,区别主要是账号、`ltkid`、`stk`、签发时间、过期时间: + +```text +目标: +uid = 942416451 +ltkid = 69161685 +stk = 4fd469d169f7734b +iat = 2026/6/18 01:39:30 +exp = 2026/6/25 01:39:29 + +自己: +uid = 1030023740 +ltkid = 71850614 +stk = 9b4120a611d00760 +iat = 2026/6/24 00:08:50 +exp = 2026/7/1 00:08:50 +``` + +`acf_jwt_token` 的 `aud` 都是 `["dy", "none"]`,`acf_dmjwt_token` 的 `aud` 都是 `["dm", "none"]`,这部分没问题。 + +**关键判断** + +自己跑出来的 CK 看起来是完整登录态,但和目标 CK 的“形态”不完全一样:主要缺 `cvl_csrf_token` 和 `acf_ccn`。如果某些接口依赖 CSRF 或风控相关 cookie,这两个缺失可能会导致请求失败;如果只是普通登录态校验,核心字段已经都有。 + + + + + + + + + +