简介:本资源为QQ第三方登录与分享功能的Android示例工程QQLoginDemo,面向需要集成QQ授权登录、提升应用注册转化率的移动开发者,适合具备一定Android基础、希望快速跑通完整授权链路的中级学习者。压缩包共1985个文件,约24.95MB,以966个png图片资源、742个xml布局与配置、131个json数据文件为主,另含57个class、24个jar、12个aidl及少量java、so、apk等,覆盖SDK依赖、界面资源与工程配置。项目完整演示了从QQ开放平台获取AppID与AppKey、导入SDK、配置URL Scheme、初始化、发起登录、处理回调到验证并保存用户信息的全流程,同时包含分享到QQ好友与QQ空间的接口调用示例。已有577人学习下载,可帮助开发者对照代码理解授权与分享的完整实现,减少集成踩坑。
1. QQ登录测试:为什么你的自动化脚本总在扫码环节翻车
做过第三方登录接入的工程师大多有类似经历:本地手工点一遍 QQ 登录顺风顺水,一旦写成自动化脚本跑进 CI,就开始出现二维码不刷新、回调地址对不上、授权码换 token 报 100016 这类问题。QQ登录测试这件事,表面是“点一下授权”,实际要覆盖的是 OAuth2.0 授权码模式下的完整链路——拉起授权页、扫码或账密登录、回调拿 code、后端换 access_token、再拿 openid 换用户信息。任何一环参数错位,整条链路就断在那里,而且报错信息往往只有一句“参数错误”,排查全靠经验。
这篇笔记面向的是需要把 QQ 登录接进自己系统、并且要写自动化测试保证它不回归的开发者。我会按“协议链路怎么走 → 测试环境怎么搭 → 用例怎么设计 → 参数怎么配 → 坑在哪”的顺序讲,中间给出可直接抄的 Python 请求代码和断言逻辑。读完你应该能独立搭出一套覆盖授权、回调、换 token、拉用户信息四个阶段的测试,并且知道每个失败码背后大概率是什么原因。
2. 先把 OAuth2.0 授权码链路拆开:QQ登录测试到底在测什么
QQ 互联的登录走的是标准 OAuth2.0 授权码模式(Authorization Code Grant),但它在标准之上加了自己的参数命名和签名规则,所以不能直接套用通用 OAuth 库。理解这条链路是设计测试用例的前提,否则你测的只是“页面能不能打开”,而不是“登录能不能成功”。
2.1 四个阶段与各自的输入输出
整条链路可以拆成四段,每段的输入输出都很明确:
| 阶段 | 发起方 | 关键输入 | 关键输出 | 测试关注点 |
|---|---|---|---|---|
| 拉起授权页 | 前端/浏览器 | client_id、redirect_uri、state | 授权页面 HTML | 参数拼接、state 防 CSRF |
| 用户授权 | 用户 | 扫码或账密 | 回调 URL 带 code | 回调地址白名单匹配 |
| 换 access_token | 后端 | code、client_secret | access_token、openid | 签名、code 一次性 |
| 拉用户信息 | 后端 | access_token、openid | 昵称、头像等 | token 有效期、scope |
测试的核心不是“页面长什么样”,而是这四段的参数传递是否严格一致。最常见的翻车点是 redirect_uri:授权页请求里写的和换 token 时写的必须完全一致,包括末尾斜杠和 URL 编码方式,差一个字符就报 redirect_uri 不合法。
2.2 为什么 state 参数必须测
state 是 OAuth2.0 里防 CSRF 的字段,QQ 互联要求原样回传。很多团队在开发阶段图省事把 state 写死成固定值,测试也就跟着写死,结果上线后遇到并发登录就出问题。测试用例里应该包含“state 随机生成 → 回调校验一致 → 不一致时拒绝”这一条,否则这个安全字段等于没接。
2.3 授权码的一次性特征
code 只能用一次,换过 token 后再用同一个 code 请求会直接失败。这个特性决定了测试用例不能复用同一个 code 跑多条断言,必须每条用例独立走一遍授权拿新 code。这一点在写自动化时特别容易踩,后面避坑章节会展开。
3. 搭一套可复现的 QQ登录测试环境:从回调地址到本地 mock
真正能跑起来的测试环境,难点不在代码,在于回调地址和网络可达性。QQ 互联要求回调地址是备案域名,本地 localhost 通常加不进白名单,所以测试环境要分两层:一层是能真实走通链路的联调环境,一层是纯逻辑的 mock 环境。
3.1 回调地址的三种处理方式
常见做法有三种,按落地成本排序:
- 内网穿透把本地端口映射到一个已备案的测试域名,真实走完整链路。适合联调阶段,但依赖外部服务,CI 里不稳定。
- 在测试域名下部署一个回调接收服务,测试脚本只负责构造授权 URL 和校验回调参数。适合 CI,稳定但需要一台常驻测试机。
- 完全 mock:把授权页返回和 token 接口响应都拦截下来,只测自己代码的参数拼接和解析逻辑。适合单元测试,跑得最快。
我一般会组合使用:单元测试用第三种,集成测试用第二种,上线前手工跑一遍第一种。
3.2 用 Python 构造授权 URL 并校验参数
下面这段代码负责生成授权 URL,并校验回调回来的参数是否合法。它是整个测试的入口,参数拼接错了后面全错。
import urllib.parse import secrets import hashlib # 模拟配置,实际使用时从环境变量读取 CLIENT_ID = "your_client_id" REDIRECT_URI = "https://test.example.com/callback" AUTHORIZE_ENDPOINT = "https://graph.qq.com/oauth2.0/authorize" def build_authorize_url(scope="get_user_info"): """构造授权页 URL,state 随机生成并返回以便回调校验""" state = secrets.token_hex(16) params = { "response_type": "code", "client_id": CLIENT_ID, "redirect_uri": REDIRECT_URI, "state": state, "scope": scope, } # urlencode 默认会把空格转成 +,这里用 quote_via 保证和 QQ 要求一致 query = urllib.parse.urlencode(params, quote_via=urllib.parse.quote) return f"{AUTHORIZE_ENDPOINT}?{query}", state def verify_callback(callback_url, expected_state): """校验回调 URL 中的 state 和 code""" parsed = urllib.parse.urlparse(callback_url) qs = urllib.parse.parse_qs(parsed.query) if qs.get("state", [None])[0] != expected_state: raise AssertionError("state 不匹配,疑似 CSRF") code = qs.get("code", [None])[0] if not code: raise AssertionError("回调未携带 code") return code逻辑说明:build_authorize_url用secrets.token_hex生成不可预测的 state,避免固定值带来的安全测试盲区。urlencode的quote_via参数是关键,默认的quote_plus会把空格编码成+,而 QQ 互联对 redirect_uri 的编码方式敏感,用quote更接近浏览器行为。verify_callback先校验 state 再取 code,顺序不能反,否则 state 校验就失去意义。
参数说明:scope决定后续能拿到哪些用户信息,get_user_info是最常用的一个。response_type固定为code,这是授权码模式的标志。
3.3 换 token 与拉用户信息的请求封装
拿到 code 之后,后端要发两次请求。第一次用 code 换 access_token 和 openid,第二次用 access_token 和 openid 拉用户信息。这两步的返回格式是key=value&key=value的文本,不是 JSON,解析时要注意。
import requests TOKEN_ENDPOINT = "https://graph.qq.com/oauth2.0/token" OPENID_ENDPOINT = "https://graph.qq.com/oauth2.0/me" USERINFO_ENDPOINT = "https://graph.qq.com/user/get_user_info" CLIENT_SECRET = "your_client_secret" def exchange_token(code): """用授权码换 access_token""" params = { "grant_type": "authorization_code", "client_id": CLIENT_ID, "client_secret": CLIENT_SECRET, "code": code, "redirect_uri": REDIRECT_URI, } resp = requests.get(TOKEN_ENDPOINT, params=params, timeout=10) resp.raise_for_status() # 返回形如 access_token=xxx&expires_in=7776000&refresh_token=yyy data = dict(item.split("=") for item in resp.text.split("&")) if "access_token" not in data: raise AssertionError(f"换 token 失败: {resp.text}") return data["access_token"] def get_openid(access_token): """用 access_token 换 openid,返回体是 callback( {...} ) 形式""" resp = requests.get(OPENID_ENDPOINT, params={"access_token": access_token}, timeout=10) text = resp.text.strip() # 去掉 callback( 和 ); json_str = text[text.index("(") + 1 : text.rindex(")")] import json data = json.loads(json_str) if "openid" not in data: raise AssertionError(f"获取 openid 失败: {text}") return data["openid"] def get_user_info(access_token, openid): """拉取用户信息""" params = { "access_token": access_token, "oauth_consumer_key": CLIENT_ID, "openid": openid, } resp = requests.get(USERINFO_ENDPOINT, params=params, timeout=10) data = resp.json() if data.get("ret") != 0: raise AssertionError(f"拉取用户信息失败: {data}") return data逻辑说明:exchange_token里手动解析key=value格式,因为 QQ 这个接口不返回 JSON。get_openid的返回体是 JSONP 风格,必须剥掉外层callback(...)才能解析,这是新手最容易卡住的地方。get_user_info用ret字段判断成功,ret=0才是正常,非 0 时msg字段会给出原因。
参数说明:grant_type固定authorization_code。oauth_consumer_key就是 client_id,命名不同但值一样,别填错。timeout建议设 10 秒,QQ 接口偶发慢响应,不设超时会让测试挂死。
4. QQ登录测试用例设计:从正常流到异常分支的覆盖清单
环境搭好之后,真正决定测试价值的是用例覆盖度。只测“能登录成功”等于没测,异常分支才是线上事故的高发区。
4.1 正常流的五条断言
一条完整的正常流用例应该包含五个断言点,缺一不可:
- 授权 URL 能被正确构造,且 state 非空。
- 回调 URL 携带 code,且 state 与请求时一致。
- 换 token 接口返回 access_token,且 expires_in 为正数。
- 换 openid 接口返回的 openid 非空。
- 拉用户信息接口 ret=0,且昵称字段存在。
这五条串起来才叫“登录成功”,只断言第 3 条就结束的测试,等于把后面两步的风险留给了线上。
4.2 必须覆盖的异常分支
下面这些分支建议每条都写成独立用例:
- code 为空或格式错误:换 token 应返回错误码,而不是抛异常。
- code 重复使用:第二次请求应失败,验证一次性特征。
- state 不匹配:回调校验应拒绝,验证 CSRF 防护生效。
- redirect_uri 与授权时不一致:换 token 应报 redirect_uri 不合法。
- access_token 过期或伪造:拉用户信息应返回非 0 的 ret。
这些用例的价值在于,它们能在你改配置、换域名、升级依赖时第一时间报警,而不是等用户反馈登录不了。
4.3 用参数化把用例数量压下来
异常分支多,但结构相似,用 pytest 的参数化能大幅减少重复代码:
import pytest @pytest.mark.parametrize("bad_code,expect_error", [ ("", True), ("invalid_code_123", True), ("expired_code", True), ]) def test_exchange_token_with_bad_code(bad_code, expect_error): """异常 code 应统一走失败分支""" try: exchange_token(bad_code) failed = False except AssertionError: failed = True assert failed == expect_error逻辑说明:参数化把“输入不同、预期相同”的用例合并成一个函数,新增异常场景只需加一行参数。expect_error显式写出预期,避免断言逻辑被改坏时测试还“通过”。
参数说明:bad_code覆盖空值、格式错、过期三类。实际项目中还可以加“已使用过的 code”,但需要先跑一次正常流拿到 code,属于有状态用例,建议单独放一个测试类。
5. QQ登录测试避坑:五个让脚本半夜报警的细节
这一章是我踩过的坑里挑出来最有代表性的五条,每条都按“现象 → 原因 → 解决”写,方便你对照排查。
5.1 回调地址末尾斜杠导致 redirect_uri 不合法
现象:授权页能打开,扫码后回调也回来了,但换 token 时报error=100010,提示 redirect_uri 不合法。
原因:授权请求里写的是https://test.example.com/callback,换 token 时写成了https://test.example.com/callback/,多了一个斜杠。QQ 互联对 redirect_uri 做的是字符串精确匹配,不做规范化。
解决:把 redirect_uri 抽成一个常量,授权和换 token 都引用同一个变量,从源头杜绝不一致。测试里加一条断言,校验两处使用的值相等。
5.2 code 被复用导致偶发失败
现象:本地跑测试全绿,CI 上偶发失败,报 code 已失效。
原因:测试用例之间共享了同一个 code,或者上一次运行的 code 被缓存下来复用。code 是一次性的,第二次用必然失败。
解决:每条用例独立走授权拿新 code,禁止跨用例传递。如果授权步骤太慢,可以在测试夹具里为每个用例单独申请,而不是整个测试类共用一个。
5.3 state 写死导致并发测试互相干扰
现象:并发跑多条登录用例时,回调校验随机失败。
原因:state 被写成了固定值,多条用例的回调互相覆盖,校验时拿到的是别人的 state。
解决:state 必须每次随机生成,并且和当前会话绑定。测试里用secrets.token_hex生成,回调校验时从当前用例的上下文取期望值,不要用全局变量。
5.4 把 JSONP 当 JSON 解析
现象:获取 openid 时报 JSON 解析错误,日志里看到返回体是callback( {"openid":"xxx"} );。
原因:QQ 的 openid 接口返回的是 JSONP 格式,直接resp.json()会失败。
解决:先剥掉外层callback(和);,再解析内部 JSON。注意返回体里可能有空格和换行,用strip()清理后再定位括号。
5.5 access_token 有效期没纳入断言
现象:测试通过,但上线后用户登录一段时间后拉不到信息。
原因:测试只断言了“能拿到 token”,没断言expires_in的合理性,也没测 token 过期后的行为。
解决:在正常流用例里断言expires_in大于 0 且接近预期值(QQ 一般是 7776000 秒)。另外单独写一条用例,用伪造的过期 token 请求用户信息,验证失败分支能正确处理。
6. 让 QQ登录测试真正防回归:把断言写进 CI 的进阶做法
前面讲的都是单次可跑的测试,但登录测试的价值在于“持续防回归”。我现在的习惯是把这套测试拆成两层:一层是纯逻辑的单元测试,跑在每次提交;一层是走真实链路的集成测试,跑在每日构建。单元测试用 mock 拦截 HTTP 请求,只验证参数拼接和解析逻辑,秒级完成;集成测试用测试账号真实走一遍,慢但可信。
具体做法上,我会把授权 URL 构造、回调校验、token 解析这三个纯函数单独抽出来,用responses或unittest.mock拦截请求,断言请求参数是否符合预期。比如下面这条,验证换 token 时 redirect_uri 和授权时完全一致:
import responses @responses.activate def test_token_request_uses_same_redirect_uri(): """断言换 token 请求里的 redirect_uri 与配置一致""" responses.add( responses.GET, TOKEN_ENDPOINT, body="access_token=abc&expires_in=7776000", status=200, ) exchange_token("fake_code") # 取出实际发出的请求,校验参数 actual = responses.calls[0].request.url assert urllib.parse.quote(REDIRECT_URI, safe="") in actual逻辑说明:responses库拦截了真实 HTTP 请求,让测试不依赖网络。responses.calls[0].request.url拿到实际发出的 URL,断言 redirect_uri 被正确编码后出现在其中。这样即使有人改了配置,测试也会立刻报警。
参数说明:safe=""表示所有特殊字符都编码,和 QQ 的要求对齐。如果你的 redirect_uri 里没有特殊字符,这行断言依然有效,只是编码前后一样。
集成测试那边,我会用一个专门的测试账号,把 access_token 缓存起来复用,避免每次跑都触发扫码。缓存要设过期时间,且过期后自动重新授权。这样每日构建既能覆盖真实链路,又不会因为频繁授权被限流。
最后说个我自己的教训:早期我图省事,把登录测试写成了“能打开授权页就算过”,结果一次域名切换后回调地址没同步更新,线上登录挂了半天才发现。从那以后我坚持一条原则——登录测试的断言必须落到“拿到用户信息”这一步,中间任何一步的“看起来正常”都不算数。希望帮到你。
本文还有配套的精品资源,点击获取