简介:面向移动应用开发者的QQ第三方登录测试资源,以QQLoginDemo示例工程为核心,帮助读者在安卓应用中快速接入QQ账号登录与内容分享,覆盖从开放平台申请应用标识和密钥、初始化开发工具包、配置回调链接、验证用户身份到保存登录信息的完整链路。压缩包共一千九百八十五个文件,大小约二十四点九五兆字节,包含大量界面图片、界面布局与配置文件、数据文件、编译产物、第三方依赖库、系统接口定义文件以及可直接安装的安装包,目录结构接近真实安卓工程,便于开发者对照源码排查集成问题。该资源已有五百七十七人浏览学习,适合正在接入QQ登录或希望研究腾讯开放平台接口调用细节的初中级开发者。通过研读工程中的关键配置、登录回调处理和分享调用逻辑,可以快速复现整个登录流程,并将相关经验迁移到自有应用开发中。
1. 一个半小时从零调通 QQ 第三方登录:这个 Demo 到底解决什么问题
某开发者要给自己维护的内部运营平台加一个“QQ 一键登录”入口,按官方文档建应用、配回调、写跳转,忙了大半个下午,最后卡在redirect_uri参数与回调地址不一致这一步。后来同事丢来这套“QQ登录测试”Demo,十几分钟就跑通了完整链路,才发现自己缺的不是文档,而是一条能直接参考的最小实现。
这个 Demo 不是生产级 SDK,而是一个把 QQ 第三方登录的 OAuth2 授权码流程完整跑通的示例工程:前端发起授权跳转、后端接收回调、换取 access_token、解析 openid、拉取用户资料、建立本地登录态,六步全都有。适合两类人:一是第一次接第三方登录、想先看完整请求链路的新手;二是要给自己系统接入 QQ 登录、需要一份能照着改的参考代码的开发者。接下来按“看结构 → 读核心代码 → 跑起来 → 避坑 → 二次开发”的顺序拆开讲。
2. 项目结构与技术选型:先看懂这个 Demo 的骨架
2.1 技术选型:为什么是 Spring Boot + 原生页面
这套 Demo 选 Spring Boot 2.x 做后端,前端就一个 HTML 页面加一个按钮,没有任何前端框架。原因很简单:第三方登录的核心逻辑全在服务端——回调接收、token 交换、用户信息解析、会话写入,前端只负责“跳转到 QQ 授权页”和“接收用户点击后的回调”。用 Spring Boot 写这几个接口只要几十行代码,而且内置 Tomcat,本地启动一条命令就完事,对“跑通链路”这个目标来说是最短路径。
不选前后端分离的原因也在这:分离架构得额外处理跨域、token 传递,这些和 QQ 登录本身没关系,反而让新手分不清“到底是登录挂了还是前端请求挂了”。Demo 的价值是把链路讲清楚,不是展示工程化能力。
2.2 目录结构逐层拆解
工程结构很小,核心代码集中在五个文件里,适合逐个打开读:
qq-login-demo ├── pom.xml # Maven 依赖,只有 web / thymeleaf 两个核心包 ├── src │ ├── main │ │ ├── java │ │ │ └── com │ │ │ └── demo │ │ │ └── qqlogin │ │ │ ├── QQLoginApplication.java # Spring Boot 启动类 │ │ │ ├── config │ │ │ │ └── QqOAuthConfig.java # 应用配置读取:appId / appKey / 回调地址 │ │ │ ├── controller │ │ │ │ ├── AuthController.java # 发起授权跳转 + 处理回调 │ │ │ │ └── UserController.java # 模拟受保护资源,验证登录态 │ │ │ ├── service │ │ │ │ ├── QqOAuthService.java # 换取 token / 解析 openid / 拉取用户信息 │ │ │ │ └── UserService.java # 本地用户映射与登录态写入 │ │ │ └── model │ │ │ └── QqUserInfo.java # QQ 用户信息实体 │ │ └── resources │ │ ├── application.yml # 配置 appId / appKey / redirectUri │ │ ├── static │ │ │ └── index.html # 登录按钮页面 │ │ └── templates │ │ └── login_success.html # 登录成功后的展示页 │ └── test └── README.md # 启动说明这个结构的核心设计是“配置、控制、逻辑”三层分离:QqOAuthConfig把 appId 这类敏感配置集中放在application.yml里,换应用只需要改配置;controller层只有跳转和回调两个接口,不掺业务;QqOAuthService单拎出来管所有 HTTP 外部请求,这也是后面二次开发时最值得保留的部分。
2.3 核心配置项:app_id、app_key、redirect_uri 都配在哪里
打开application.yml,整套配置就四行关键内容:
server: port: 8080 qq: oauth: app-id: 替换为你的APP_ID app-key: 替换为你的APP_KEY redirect-uri: http://localhost:8080/callback authorize-url: https://graph.qq.com/oauth2.0/authorize token-url: https://graph.qq.com/oauth2.0/token openid-url: https://graph.qq.com/oauth2.0/me user-info-url: https://graph.qq.com/user/get_user_infoapp-id和app-key是在 QQ 互联平台创建应用后拿到的凭证,对应 OAuth2 协议里的client_id和client_secret。redirect-uri是用户授权成功后回调到本服务的地址,必须和你创建应用时填写的回调地址完全一致,否则授权接口直接拒绝。后面四个 url 是 QQ 开放平台提供的固定端点,分别负责授权、换 token、解析 openid、拿用户资料。这个 Demo 把它们收敛到配置里,而不是硬编码在 Service 代码中,调试或换环境时只改这一处就够了。
参数说明:authorize-url是发起授权的地址,用户浏览器直接访问它;token-url是服务端用code换access_token的地址,只能从后端发起,不能暴露到前端;openid-url和user-info-url都需要带 access_token 请求,返回的是用户在这个应用下的唯一标识和公开资料。理解这四个地址的分工,基本就理解了第三方登录的服务端逻辑。
3. 核心流程拆解:一次 QQ 登录从跳转到回调用了几步
3.1 授权码模式与 QQ 互联的差异点
QQ 登录走的是 OAuth2 授权码模式(Authorization Code),标准流程是四步:引导用户到授权页 → 用户同意后回调带code→ 后端用code换access_token→ 用 token 换取用户信息。腾讯系的实现里有三个和标准 OAuth2 不太一样的细节,踩过的人基本都在这翻车。
第一,QQ 的access_token换回来之后不能直接查用户资料,得先调openid-url解析出这个用户在当前应用下的唯一openid,再拿openid + access_token调用户信息接口。第二,openid-url返回的内容不是干净 JSON,而是一段 JSONP 风格的回调包裹,解析时得多做一层处理。第三,用户取消授权时回调里也带code,但错误信息塞在 query 参数里,要在同一入口处理“成功”和“用户主动取消”两种返回。
3.2 第一步:前端发起授权跳转
用户在index.html点登录按钮,前端不直接调后端接口,而是把浏览器导向后端生成的一个“发起授权”地址。这样做的目的是把跳转参数拼装逻辑收在服务端,前端只认一个固定链接。
@GetMapping("/login") public String login(HttpSession session) { String state = UUID.randomUUID().toString().replace("-", ""); session.setAttribute("qq_login_state", state); String redirectUri = URLEncoder.encode(config.getRedirectUri(), StandardCharsets.UTF_8); String authorizeUrl = config.getAuthorizeUrl() + "?response_type=code" + "&client_id=" + config.getAppId() + "&redirect_uri=" + redirectUri + "&state=" + state; return "redirect:" + authorizeUrl; }参数说明:response_type=code表示走授权码模式,这是固定写法;client_id填应用 ID;redirect_uri必须经过 URL 编码,否则包含特殊字符时授权地址解析会乱掉;state是防 CSRF 的随机串,生成后先存进 session,等回调时再校验。这个参数是授权码模型里最容易漏的一环——不校验 state,攻击者可以构造回调地址伪装用户登录。
代码里session.setAttribute("qq_login_state", state)是关键一步:用户从授权页跳回来时,回调接口从 session 里取出这个值,和回调 URL 上带的state做比对。一致才继续,不一致直接拒绝。
3.3 第二步:回调接口接收 code
用户授权成功后,QQ 开放平台把浏览器重定向到redirect-uri,并拼上code和state。回调接口的逻辑分三段:校验 state、用 code 换 token、跳转到成功页或错误页。
@GetMapping("/callback") public String callback(String code, String state, HttpSession session) { String savedState = (String) session.getAttribute("qq_login_state"); if (savedState == null || !savedState.equals(state)) { return "redirect:/error?msg=state校验失败"; } if (code == null || code.isEmpty()) { return "redirect:/error?msg=用户取消授权"; } QqUserInfo user = qqOAuthService.loginWithQq(code); if (user == null) { return "redirect:/error?msg=登录失败"; } userService.login(user, session); return "redirect:/user"; }这段代码看起来简单,但几个return分支都是实际跑流程时会遇到的场景:state 不一致可能是伪回调,code 为空多半是用户在授权页点了拒绝。session在这里同时承担了 state 的暂存地和登录态容器,所以整个回调接口没有额外引入分布式缓存——这也是 Demo 为什么适合新手读的原因,每个状态都有明确的存放位置。
qqOAuthService.loginWithQq(code)是整条链路的核心调用,进入 Service 层之后会连续发起两次 HTTP 请求,逻辑全在 3.4 节。
3.4 第三步:Service 层完成 token 换取与 openid 解析
QqOAuthService是这套 Demo 里最值得逐行读的类。它用RestTemplate发起外部请求,第一步先把回调带回来的code换成access_token。
public QqUserInfo loginWithQq(String code) { String tokenUrl = config.getTokenUrl() + "?grant_type=authorization_code" + "&client_id=" + config.getAppId() + "&client_secret=" + config.getAppKey() + "&code=" + code + "&redirect_uri=" + URLEncoder.encode(config.getRedirectUri(), StandardCharsets.UTF_8); String tokenResponse = restTemplate.getForObject(tokenUrl, String.class); Map<String, String> tokenMap = parseKeyValuePairs(tokenResponse); String accessToken = tokenMap.get("access_token"); if (accessToken == null) { return null; } String openId = fetchOpenId(accessToken); if (openId == null) { return null; } return fetchUserInfo(accessToken, openId); }grant_type=authorization_code是固定值,表示用授权码换令牌;client_secret是应用密钥,只能出现在服务端,一旦暴露在浏览器端就等于把自己的登录凭证交给了用户。重点看parseKeyValuePairs这个方法:QQ 的 token 接口返回的不是 JSON,而是access_token=xxx&expires_in=7776000&refresh_token=yyy这种 key-value 串,不能直接反序列化成对象,得用字符串拆解。
openid的解析同样特殊。QQ 的me接口返回类似callback( {"client_id":"appId","openid":"用户唯一标识"} )的格式,比正常 JSON 多了一层函数包裹,解析时先按括号切分再取 JSON 部分。
private String fetchOpenId(String accessToken) { String url = config.getOpenidUrl() + "?access_token=" + accessToken; String response = restTemplate.getForObject(url, String.class); String json = response.substring(response.indexOf("(") + 1, response.lastIndexOf(")")); JsonNode node = objectMapper.readTree(json); return node.get("openid").asText(); }参数说明:access_token直接拼在 query 上即可,QQ 的 openid 接口不要求 Authorization 头;返回的字符串掐头去尾后才是标准 JSON。拿到openid之后,fetchUserInfo用openid + access_token两个参数请求用户信息接口,返回nickname、figureurl_qq_2(头像)等字段,组装成QqUserInfo对象返回。
3.5 第四步:本地用户映射与登录态写入
外部授权流程走完后,要解决的问题变成了“怎么让这个 QQ 用户变成我们自己系统的用户”。UserService负责这层映射。
public void login(QqUserInfo qqUser, HttpSession session) { UserEntity user = userRepository.findByOpenid(qqUser.getOpenid()); if (user == null) { user = new UserEntity(); user.setOpenid(qqUser.getOpenid()); user.setNickname(qqUser.getNickname()); user.setAvatar(qqUser.getAvatar()); userRepository.save(user); } session.setAttribute("login_user", user); }设计思路是“以 openid 为绑定键”:第一次用 QQ 登录的用户,按openid建一条本地记录;第二次再来就直接查出来,更新登录时间即可。openid在全网是唯一的,而且同一个应用下同一个用户永远不变,不会出现用户换昵称导致身份丢失的问题。注意这里不建议把openid直接当主键用,因为它是外部系统的 ID,与平台耦合太紧,本地应该有自己的自增主键,openid 只做查询索引。
登录态写入 session 后,UserController里的受保护接口只需检查 session 里的login_user是否存在。整个 Demo 的链路到这里闭环:授权跳转、回调处理、令牌交换、资料查询、本地映射、会话建立,六步完整走了一遍。
4. 本地运行与调试指南:四步把 Demo 跑起来
4.1 环境准备与开放平台应用注册
要跑通这个 Demo,本地需要 JDK 8+ 和 Maven 3.6+。项目没有依赖外部数据库,用户映射用了一个简单的内存仓储,也就是重启后本地用户数据会丢,但这不影响验证登录链路——每次登录都会按 openid 重新创建用户记录。
去 QQ 互联平台创建一个“网站应用”,拿到APP ID和APP KEY。创建应用时要填两个关键信息:网站地址和回调地址。回调地址必须填http://localhost:8080/callback,这和application.yml里的redirect-uri必须一字不差。很多人在这一步填了http://127.0.0.1:8080/callback,或者多写了结尾的斜杠,授权时就会报redirect_uri is not valid。
拿到凭证后填进application.yml,然后执行:
mvn spring-boot:run启动完成后访问http://localhost:8080,浏览器会看到一个只有一个按钮的页面,点它就会跳到 QQ 的授权页。如果这步能跳转成功,说明应用注册和配置项都已经就位。
4.2 本地回调地址的接入思考
第三方登录平台普遍对回调地址有校验规则,本地调试最常遇到的问题是“回调地址域名校验失败”。这里说一个常见的递进排查思路:先确认application.yml里的redirect-uri和 QQ 互联后台填的回调地址字符串完全一致;再确认授权 URL 里拼的redirect_uri经过了 URL 编码;如果本地配的是localhost,浏览器地址栏出现“无法连接”时,先确认 8080 端口没有被占用。
授权回调这类流程还面临一个“内外网地址不一致”的细节:开发时你在自己的机器上用localhost,但接入真实生产应用后回调地址会被替换成线上域名。所以 Demo 在application.yml里把 redirect-uri 做成配置项,而不是在AuthController里硬编码,换环境只改一处。
4.3 启动服务并完整走一遍授权流程
服务启动后,打开浏览器访问http://localhost:8080,按 F12 打开开发者工具的网络面板,把“Preserve log”打开。然后点登录按钮,观察整个跳转过程:
- 第一个请求是
/login,响应是 302 重定向,能明确看到Location头指向 QQ 的授权地址。 - 跳转到授权页后登录自己的 QQ,同意授权。
- 浏览器重定向回
http://localhost:8080/callback?code=xxx&state=yyy。 - 这个回调请求返回 302,再跳转到
/user。
整个过程在开发者工具里能清晰看到三次 302,链路就算跑通了。建议每次测试都重新生成state,因为授权页登录后回调里的state是全新生成的,重复用同一个授权 URL 跳转反而会触发 state 校验失败。
4.4 怎么确认每一步接口真的成功了
如果登录流程没跑通,最直接的办法是不依赖前端页面,用命令行模拟服务端的令牌交换过程。先在浏览器里拿一次授权跳转后的回调 URL,把code复制出来,然后手动发一次 token 请求:
curl "https://graph.qq.com/oauth2.0/token?grant_type=authorization_code&client_id=你的APP_ID&client_secret=你的APP_KEY&code=回调里的code&redirect_uri=http://localhost:8080/callback"注意这里参数要和代码里完全一致,包括 redirect_uri 的编码形式。返回如果是以access_token=开头,说明 code 有效;如果返回40001之类的错误码,看错误码描述就能定位——绝大多数情况是回调地址不匹配或 code 过期。
code是一次性的,用 curl 试过一次之后,这个 code 就失效了,必须回浏览器重新走一遍授权流程拿新的。这是个容易让人误判的地方:第一次 curl 成功,第二次直接复制同一段命令再跑就报错,以为是接口问题,其实是 code 只能用一次。
5. 避坑记录:QQ 登录接入最常见的 5 个问题
5.1 redirect_uri 校验失败
现象:跳到授权页后直接报redirect_uri is not valid。
原因:QQ 互联后台填写的回调地址和代码里redirect-uri的值不一致。这个校验是纯字符串精确匹配,http://localhost:8080/callback和http://localhost:8080/callback/都会被判定为不同地址。
解决:不要凭记忆填,直接到application.yml里复制redirect-uri的值,粘贴到开放平台后台。改完后台配置后不需要重启本地服务,但已经生成的授权链接要重新发起一次。
5.2 state 校验失败导致反复重定向
现象:点击授权跳转 QQ 登录后,回调回来一直走到 error 页面,提示 state 不一致。
原因:用户打开了两个登录页面,或者手动刷新了授权页。每个/login请求都会生成一个新的 state 并覆盖 session 里旧值,旧授权页回调带的是旧 state。
解决:不要开多个登录页,一次只测一个授权流程;如果在授权页停留时间过长,回到应用后重新点登录按钮。生产环境下建议把 state 的过期时间设为 5 分钟,超时后直接拒绝回调。
5.3 openid 解析不出 JSON
现象:Service 层报UnrecognizedToken或字符串截取越界。
原因:QQ 的me接口返回的是callback( {...} )包裹格式,不是裸 JSON。直接拿objectMapper.readTree解析会失败,而很多开发者拿着标准 OAuth2 的教程套,默认就当成标准 JSON 处理。
解决:先打印原始返回内容确认格式,再按substring掐掉callback(前缀和结尾的)。Demo 中fetchOpenId已经实现了这个处理,照抄即可。
5.4 用户取消授权也走到 token 交换
现象:用户没输密码直接点拒绝,回调接口用空 code 去换 token,返回错误。
原因:授权码模式下用户拒绝时回调还是会被触发,这时候 URL 上没有code但有错误描述。代码没判断 code 是否为空,直接往下执行 HTTP 请求。
解决:回调接口开头先判空。代码里已经有这个分支:
if (code == null || code.isEmpty()) { return "redirect:/error?msg=用户取消授权"; }这让用户取消授权时立刻回到错误页,而不是等 token 接口超时再失败。
5.5 换 token 时接口报 100010
现象:grant_type=authorization_code执行后返回错误码 100010,描述为“code 无效或已过期”。
原因:最常见的是 code 被重复使用。回调接口和手动 curl 调试共用了同一个 code,第一次成功后第二次必然失效。另一种可能是两次请求间隔太久,授权码自身有效期大部分平台只有 10 分钟左右,超过期限再调试必然失败。
解决:不要手动刷新回调 URL。每次调试都要从点登录按钮开始,抓到新 code 后立刻处理。另外确认系统时间没有偏移,服务器时间错位会导致签名时间校验失败,但主要问题基本都是 code 重复消费。
6. 二次开发思路:把这个登录 Demo 改造成自己系统的登录模块
6.1 第一步:抽象一个 ThirdPartyAuthProvider 接口
现在QqOAuthService把 QQ 的 token 交换、openid 解析、用户信息拉取全写在一个类里,接口路径直接写在方法里。如果以后要加“微信登录”或“其他开放平台登录”,最舒服的做法是先抽出一个统一接口:
public interface ThirdPartyAuthProvider { String buildAuthorizeUrl(String state); ThirdPartyUser authenticate(String code); }buildAuthorizeUrl负责拼授权链接,authenticate负责用 code 换用户信息。QqOAuthService变成它的一个实现类,把已有的fetchOpenId、fetchUserInfo方法挪进去。新接入其他平台时写一个新的实现类,在配置里加一个loginType参数,AuthController根据loginType动态选择 Provider。
这个改动看着简单,却把“种类繁多的第三方平台差异”隔离在了一层。之后的回调接口、用户映射逻辑完全不用动,接新平台就只是新增一个实现类的事情。
6.2 第二步:把 session 登录态换成持久化会话
Demo 里登录态存在HttpSession,单机调试没问题,部署到多实例时 session 不共享,用户在 A 机器登录、请求打到 B 机器就掉线。常见的做法是换成 JWT,回调成功后将用户 ID 放进 token,前端保存,每次请求通过过滤器解析。改造时保留现有login方法做用户绑定,把session.setAttribute("login_user", user)换成签发 token 返回前端。
有几种常用方案对比如下:
| 方案 | 适用阶段 | 优势 | 注意点 |
|---|---|---|---|
| HttpSession | 单机 Demo | 改动小,开箱即用 | 多实例失效 |
| JWT | 前后端分离 | 无状态,跨实例共享 | 需要管理密钥与过期时间 |
| Redis + Session | 多实例部署 | 集中式会话,可设置过期策略 | 多一个中间件运维成本 |
如果只是想先把登录接到现有内部系统里,JWT 是性价比最高的选择。不用新增 Redis 节点,spring-boot-starter-security 自带的 filter 链就能完成 token 校验。
6.3 从回调框架到多端登录
顺着上面的接口抽象再往下走,就能很自然地扩展出扫码登录或小程序登录。不同端的差异集中在“授权发起方式”和“用户信息字段映射”两点。扫码登录只是把buildAuthorizeUrl换成生成二维码内容,回调流程完全一样;小程序登录则是用code换 session 信息,再拿 session 里的 openid 走同样的用户绑定逻辑。这意味着前面抽象出的ThirdPartyAuthProvider在接入不同端时依然适用,只是实现类的内部逻辑不同。
这套 Demo 本身没有做这些扩展,但它把“第三方登录的本质——外部认证 + 本地绑定”这条线留得很干净,顺着AuthController → QqOAuthService → UserService这条调用链往上做加法,每一层都留有明确的位置。
我自己后来接第二个第三方平台时,就严格按照 Provider 接口重新写了一个实现类,测试时先 mock 掉外部 HTTP 调用,再单测本地用户绑定逻辑。从那以后,每次接新的第三方登录,我都强制先走一遍“授权跳转 → 模拟 code → curl 验 token → 本地绑定”四步,确认外部链路没问题才写业务代码。这条习惯帮我少踩了好几个回调地址不匹配的坑,希望帮到你。
本文还有配套的精品资源,点击获取