资讯动态

Spring Boot实现QQ第三方登录:OAuth2授权码模式Demo全流程解析

发布时间:2026/10/16 22:23:43 来源:尧图企业网站定制
简介面向移动应用开发者的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_typecode client_id config.getAppId() redirect_uri redirectUri state state; return redirect: authorizeUrl; }参数说明response_typecode表示走授权码模式这是固定写法client_id填应用 IDredirect_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?msgstate校验失败; } 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_typeauthorization_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); MapString, 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_typeauthorization_code是固定值表示用授权码换令牌client_secret是应用密钥只能出现在服务端一旦暴露在浏览器端就等于把自己的登录凭证交给了用户。重点看parseKeyValuePairs这个方法QQ 的 token 接口返回的不是 JSON而是access_tokenxxxexpires_in7776000refresh_tokenyyy这种 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?codexxxstateyyy。这个回调请求返回 302再跳转到/user。整个过程在开发者工具里能清晰看到三次 302链路就算跑通了。建议每次测试都重新生成state因为授权页登录后回调里的state是全新生成的重复用同一个授权 URL 跳转反而会触发 state 校验失败。4.4 怎么确认每一步接口真的成功了如果登录流程没跑通最直接的办法是不依赖前端页面用命令行模拟服务端的令牌交换过程。先在浏览器里拿一次授权跳转后的回调 URL把code复制出来然后手动发一次 token 请求curl https://graph.qq.com/oauth2.0/token?grant_typeauthorization_codeclient_id你的APP_IDclient_secret你的APP_KEYcode回调里的coderedirect_urihttp://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_typeauthorization_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 → 本地绑定”四步确认外部链路没问题才写业务代码。这条习惯帮我少踩了好几个回调地址不匹配的坑希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑