今天,我们来聊聊如何接入QQ登录,对于新手来说,这确实有点难度,主要是搞不清楚流程,官方文档也是一言难尽。 官方文档地址如下: QQ互联WIKI。大家有兴趣的也可以去看一下官方文档。下面,我们用鸿蒙应用开发来进行讲解,其余开发平台都差不多。
第一步:官网注册应用
1、在QQ互联注册开发者身份和创建应用,这里不做详解。我主要讲解一下注册完成应用之后,在应用的基本信息那里,会有一个平台信息,如果平台信息不正确,应用发起请求的时候,QQ登录后台是无法验证的。
首先是BundleName1,这个是你的应用包名,储存在工程项目的AppScope\app.json5文件里面(前提是你的应用在AppGallery Connect创建应用并配置相应的文件,创建包名这个在AppGallery Connect会有详细的步骤,这里不做详解)
如果没有配置包名和发布证书,应用是无法云调试的,也无法上架应用商城。AppGallery Connect-移动应用开发-全场景服务-华为开发者联盟
2、安装包签名,大家在ICP备案鸿蒙应用时,可以看到有App特征信息及其获取方式的教程,跟着教程的步骤就可以获取到MD5签名,也就是我们所需的安装包签名了。(因为开通QQ登录就要接入网络,所以必须要APP备案和域名备案)
3
开通AppLinking,鸿蒙官方文档也有很详细的开通AppLinking步骤:使用App Linking实现应用间跳转-拉起指定应用-应用间跳转-Stage模型开发指导-Ability Kit(程序框架服务)-应用框架 - 华为HarmonyOS开发者
第二步:配置QQ登录环境
QQ授权登录使用的是OAuth2.0标准,OAuth2.0共提供四种登录方式:授权码模式、隐式授权模式、密码模式、客户端授权模式。大家可以根据自己的业务来选择适合的方登录方式,本次我选择的是授权码模式。授权码模式的运作方式如图:
当用户访问资源时,比如在这里我在鸿蒙应用中使用第三方登录功能,例如QQ登录,那么这里的资源就是用户的QQ昵称和头像等信息。此时第三方应用(鸿蒙应用)将发送请求到授权服务器(QQ)去获取授权,此时授权服务器(QQ)将返回一个界面给用户,用户需要登录到QQ,并同意授权网易云音乐获得某些信息(资源)。当用户同意授权后,授权服务器将返回一个授权码(Authorization Code)给第三方应用,此时第三方应用在通过client_id、client_secret(这是需要第三方应用在授权服务器去申请的)和授权码去获得Access Token和Refresh Token,此时授权码将失效。然后就是第三方应用通过Access Token去资源服务器请求资源了,资源服务器校验Access Token成功后将返回资源给第三方应用。
- 打开终端下载QQ登录所需的SDK:
ohpm i @tencent/qq-open-sdk
依赖安装完成之后,我们可以看到在oh-package.json5 文件中新增依赖库。
- 打开module.json5文件,在module节点下配置QQ登录的信息:
"querySchemes": [
"https",
"qqopenapi"
],
3 找到Ability的skills节点,配置scheme。可参考以下代码:
"skills": [
{
"entities": [
"entity.system.browsable"
],
"actions": [
"ohos.want.action.viewData"
],
"uris": [
{
"scheme": "qqopenapi", // 接收 QQ 回调数据
"host": "", // 业务申请的互联 appId,如果填错会导致 QQ 无法回调
"pathRegex": "\b(auth|share)\b",
"linkFeature": "Login",
}
]
}
]
信息配置为以上内容,把信息配置完成就可以去编写代码逻辑了。
第三步:基于Promise的登录方式
前端逻辑
通过SDK向QQ发起登录请求。 官方提供了两种方式(基于回调和基于Promise)。这里我们选择的是基于Promise进行开发。
- 导入QQ登录相关类型、接口(按照自己的业务需求导入相应的模块,这里仅作示例)和定义常量,用于日志模块的输出
import Want from '@ohos.app.ability.Want';
import hilog from '@ohos.hilog';
import common from '@ohos.app.ability.common';
import { rcp } from '@kit.RemoteCommunicationKit'
// 导入QQ SDK相关类型和接口
import { QQOpenApiFactory, IQQOpenApi, AuthResult, AuthResultType, AuthReqOptions } from '@tencent/qq-open-sdk';
// 常量定义
const TAG = 'QQAuthService';
const DOMAIN = 0x00010000;
- 创建并初始化QQ SDK API实例,建议新建一个类,在类里面编写逻辑。
export class AuthService {
private static instance: AuthService;
// QQ互联应用ID,需要在QQ互联平台申请
private appId: number = xxxxxxx;
// 创建QQ互联API实例(单例模式)
private iQQOpenApi: IQQOpenApi;
- 接着,构造函数与单例初始化,只有初始化之后才能向QQ发起请求。(顺便输出日志)
private constructor() {
// 初始化QQ SDK API实例
this.iQQOpenApi = QQOpenApiFactory.createApi(this.appId);
this.databaseService = DatabaseService.getInstance();
hilog.info(DOMAIN, TAG, 'QQ SDK API初始化完成');
}
- 使用单例模式来获取实例,单例模式确保一个类仅有一个实例,并提供一个全局访问点。
// 单例模式获取实例
public static getInstance(): AuthService {
if (!AuthService.instance) {
AuthService.instance = new AuthService();
}
return AuthService.instance;
}
-
创建完实例之后,就可以正式请求QQ登录服务端来获取授权码了。我用三个类方法,第一个方法调用第二、第三个方法,第二个方法获取授权码,第三个方法是把授权码发给后端服务,并接收后端返回的用户数据。
第一个方法:
// QQ登录方法
public async login(context: common.UIAbilityContext): Promise<UserInfo> {
return new Promise<UserInfo>(async (resolve, reject) => {
hilog.info(DOMAIN, TAG, '开始登录流程');
try {
// 通过QQ SDK获取授权码
const authCode = await this.getQQAuthCode();
// 使用授权码换取用户信息
const userInfo = await this.exchangeTokenAndGetUserInfo(authCode);
// 保存用户信息到数据库
await this.saveUserToDatabase(userInfo);
this.currentUser = userInfo;
hilog.info(DOMAIN, TAG, `登录成功,用户: ${userInfo.nickname}`);
resolve(userInfo);
} catch (error) {
hilog.error(DOMAIN, TAG, `登录失败: ${error.message || error}`);
reject(error);
}
});
}
后面‘ // 保存用户信息到数据库’部分,需要自己搭建数据库,这里不做详解。
- 第二个类方法,获取QQ登录授权码,验证并返回授权码。 定义授权参数(授权范围为 “all”、不使用二维码、不强制网页登录、无网络超时限制),然后调用 QQ SDK 的 login 方法发起授权请求;接着通过 Promise 链式调用处理授权结果,若成功且获取到 authCode 则返回授权码,若用户取消授权或授权失败则抛出对应错误,同时捕获调用过程中可能出现的异常并包装为特定错误信息
// 获取QQ授权码
private async getQQAuthCode(): Promise<string> {
return new Promise<string>((resolve, reject) => {
const loginOptions: AuthReqOptions = {
scope: "all",
useQrCode: false,
forceWebLogin: false,
networkTimeout: 0
};
this.iQQOpenApi.login(loginOptions)
.then((result: AuthResult) => {
if (result.type === AuthResultType.Success) {
const authCode = result.authResponse?.authCode;
if (authCode) {
hilog.info(DOMAIN, TAG, `QQ授权成功,获取到authCode`);
resolve(authCode);
} else {
reject(new Error('未获取到授权码'));
}
} else if (result.type === AuthResultType.Cancel) {
reject(new Error('用户取消授权'));
} else {
reject(new Error(`授权失败: ${result.message}`));
}
})
.catch((error: Error) => {
reject(new Error(`QQ授权异常: ${error.message}`));
});
});
}
- 第三个类方法:把授权码发送给后端,接收后端返回的用户数据
代码是私有异步方法
exchangeTokenAndGetUserInfo,用于接收 QQ 授权码(authCode)并通过后端接口换取用户信息:首先定义后端接口 URL,创建网络会话(若会话创建失败则直接拒绝),构建包含授权码的 POST 请求数据;随后发送 POST 请求至后端,若响应状态码为 200 且存在响应体,则调用parseBackendResponse解析响应体获取用户信息并返回;若服务器响应异常(非 200 状态码)、网络请求失败或请求初始化出错,均会捕获对应错误并封装为具体错误信息后拒绝;最后通过finally块确保会话资源无论请求成功与否都会被关闭,同时通过日志记录关键流程节点(请求 URL、响应状态码、错误详情)以便调试追踪
// 使用授权码换取用户信息
private async exchangeTokenAndGetUserInfo(authCode: string): Promise<UserInfo> {
return new Promise<UserInfo>(async (resolve, reject) => {
try {
// 后端URL
const backendUrl = "你的后端接收授权码的地址";
hilog.info(DOMAIN, TAG, `向后端发送请求,URL: ${backendUrl}`);
const session = rcp.createSession();
if (!session) {
reject(new Error('会话创建失败'));
return;
}
const postData: rcp.RequestContent = {
fields: { 'code': authCode }
};
try {
const response = await session.post(backendUrl, postData);
hilog.info(DOMAIN, TAG, `后端响应状态码: ${response.statusCode}`);
if (response.statusCode === 200 && response.body) {
const userInfo = await this.parseBackendResponse(response.body);
resolve(userInfo);
} else {
reject(new Error(`服务器响应异常,状态码: ${response.statusCode}`));
}
} catch (err) {
hilog.error(DOMAIN, TAG, `网络请求失败: ${JSON.stringify(err)}`);
reject(new Error(`网络请求失败`));
} finally {
session.close();
}
} catch (error) {
hilog.error(DOMAIN, TAG, `请求初始化失败: ${error}`);
reject(new Error('请求初始化失败'));
}
});
}
后端服务
那后端是怎么请求呢?我这里使用的是python的flask框架来写接口和请求作为演示。
- 首先配置相关信息,导入库和配置参数。日志模块可以帮助我们实时关注程序运行情况,在真正的项目中,日志模块是必不可少的,而不只是简单的print打印。
from flask import Flask, request, jsonify
import requests
import json
import logging
from datetime import datetime
app = Flask(__name__)
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('qq_login_api.log', encoding='utf-8'),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
# 配置参数 - 替换为你自己的值
QQ_APP_ID = '' # 你的QQ应用ID
QQ_APP_KEY = '' # 你的QQ应用密钥
REDIRECT_URI = '' # 这里可以随便填,但必须和QQ后台配置一致,没有就不填
- 然后构造函数和接口,前端发送包含授权码(code)的 JSON 格式 POST 请求到
/qq/login接口后,服务端先提取授权码,第一步调用get_access_token函数向 QQ OAuth 接口请求获取访问令牌(access_token);第二步使用获取到的 access_token 通过get_openid函数获取用户唯一标识(openid);第三步再用 access_token 和 openid 通过get_user_info函数获取用户信息(如昵称、头像等);最终将 access_token、openid、昵称和头像 URL 整合为 JSON 响应返回给前端。具体代码如下:
# 获取 access_token
def get_access_token(auth_code):
logger.info(f"开始获取access_token,授权码: {auth_code[:10]}...")
token_url = "https://graph.qq.com/oauth2.0/token"
params = {
"grant_type": "authorization_code",
"client_id": QQ_APP_ID,
"client_secret": QQ_APP_KEY,
"code": auth_code,
"redirect_uri": REDIRECT_URI
}
logger.debug(f"请求URL: {token_url}")
logger.debug(f"请求参数: {dict(params, client_secret='***')}")
try:
response = requests.get(token_url, params=params, timeout=10)
logger.info(f"获取access_token响应状态码: {response.status_code}")
logger.debug(f"获取access_token响应内容: {response.text}")
if response.status_code != 200:
error_msg = f"获取access_token失败,状态码: {response.status_code}"
logger.error(error_msg)
return None, error_msg
except requests.exceptions.RequestException as e:
error_msg = f"请求access_token时发生网络错误: {str(e)}"
logger.error(error_msg)
return None, error_msg
data = {}
for item in response.text.split("&"):
if "=" in item:
key, value = item.split("=", 1) # 最多分割成两部分
data[key] = value
else:
logger.warning(f"忽略非法参数项: {item}")
access_token = data.get("access_token")
if not access_token:
error_msg = f"缺少 access_token 字段,原始响应: {response.text}"
logger.error(error_msg)
return None, error_msg
logger.info(f"成功获取access_token: {access_token[:10]}...")
return data, None
# 获取 openid
def get_openid(access_token):
logger.info(f"开始获取openid,access_token: {access_token[:10]}...")
openid_url = "https://graph.qq.com/oauth2.0/me"
params = {
"access_token": access_token
}
logger.debug(f"请求URL: {openid_url}")
logger.debug(f"请求参数: access_token={access_token[:10]}...")
try:
response = requests.get(openid_url, params=params, timeout=10)
logger.info(f"获取openid响应状态码: {response.status_code}")
logger.debug(f"获取openid响应内容: {response.text}")
if response.status_code != 200:
error_msg = f"获取openid失败,状态码: {response.status_code}"
logger.error(error_msg)
return None, error_msg
except requests.exceptions.RequestException as e:
error_msg = f"请求openid时发生网络错误: {str(e)}"
logger.error(error_msg)
return None, error_msg
try:
content = response.text.strip()
logger.debug(f"处理openid响应内容: {content}")
# 去除 callback( ... ); 包裹
start_idx = content.find("{")
end_idx = content.rfind("}") + 1
if start_idx == -1 or end_idx == -1:
error_msg = f"无法解析openid响应: {content}"
logger.error(error_msg)
return None, error_msg
json_str = content[start_idx:end_idx]
logger.debug(f"提取的JSON字符串: {json_str}")
data = json.loads(json_str)
openid = data["openid"]
logger.info(f"成功获取openid: {openid}")
return openid, None
except Exception as e:
error_msg = f"解析openid响应失败: {str(e)}"
logger.error(error_msg, exc_info=True)
return None, error_msg
# 获取用户信息
def get_user_info(access_token, openid):
logger.info(f"开始获取用户信息,openid: {openid}")
user_info_url = "https://graph.qq.com/user/get_user_info"
params = {
"access_token": access_token,
"oauth_consumer_key": QQ_APP_ID,
"openid": openid,
"format": "json"
}
logger.debug(f"请求URL: {user_info_url}")
logger.debug(f"请求参数: {dict(params, access_token=access_token[:10]+'...')}")
try:
response = requests.get(user_info_url, params=params, timeout=10)
logger.info(f"获取用户信息响应状态码: {response.status_code}")
logger.debug(f"获取用户信息响应内容: {response.text}")
if response.status_code != 200:
error_msg = f"获取用户信息失败,状态码: {response.status_code}"
logger.error(error_msg)
return None, error_msg
except requests.exceptions.RequestException as e:
error_msg = f"请求用户信息时发生网络错误: {str(e)}"
logger.error(error_msg)
return None, error_msg
try:
user_data = response.json()
logger.info(f"成功获取用户信息,昵称: {user_data.get('nickname', 'N/A')}")
logger.debug(f"用户信息详情: {user_data}")
return user_data, None
except Exception as e:
error_msg = f"JSON 解析失败: {str(e)}"
logger.error(error_msg, exc_info=True)
return None, error_msg
@app.route('/qq/login', methods=['POST'])
def qq_login():
request_id = datetime.now().strftime('%Y%m%d_%H%M%S_%f')
logger.info(f"[{request_id}] 收到QQ登录请求,来源IP: {request.remote_addr}")
try:
# 确保是 JSON 请求
if not request.is_json:
logger.warning(f"[{request_id}] 请求不是JSON格式")
return jsonify({"error": "Unsupported Media Type"}), 415
data = request.get_json()
logger.debug(f"[{request_id}] 请求数据: {data}")
# 从 fields 对象中提取 code
auth_code = data.get('fields', {}).get('code')
logger.info(f"[{request_id}] 成功提取授权码: {auth_code[:10] if auth_code else 'None'}...")
if not auth_code:
logger.error(f"[{request_id}] 缺少授权码")
return jsonify({"error": "缺少授权码"}), 400
# Step 1: 获取 access_token
logger.info(f"[{request_id}] 步骤1: 开始获取access_token")
token_data, error = get_access_token(auth_code)
if error:
logger.error(f"[{request_id}] 步骤1失败: {error}")
return jsonify({"error": error}), 500
access_token = token_data.get("access_token")
logger.info(f"[{request_id}] 步骤1成功: 获取到access_token")
# Step 2: 获取 openid
logger.info(f"[{request_id}] 步骤2: 开始获取openid")
openid, error = get_openid(access_token)
if error:
logger.error(f"[{request_id}] 步骤2失败: {error}")
return jsonify({"error": error}), 500
logger.info(f"[{request_id}] 步骤2成功: 获取到openid")
# Step 3: 获取用户信息
logger.info(f"[{request_id}] 步骤3: 开始获取用户信息")
user_info, error = get_user_info(access_token, openid)
if error:
logger.error(f"[{request_id}] 步骤3失败: {error}")
return jsonify({"error": error}), 500
logger.info(f"[{request_id}] 步骤3成功: 获取到用户信息")
# 构造返回数据
result = {
"access_token": access_token,
"openid": openid,
"nickname": user_info.get("nickname"),
"avatarUrl": user_info.get("figureurl_qq_1") or user_info.get("figureurl_1")
}
logger.info(f"[{request_id}] QQ登录成功,用户昵称: {result.get('nickname', 'N/A')}")
print(result)
logger.debug(f"[{request_id}] 返回结果: {dict(result, access_token=access_token[:10]+'...')}")
return jsonify(result)
except Exception as e:
logger.error(f"[{request_id}] 发生未处理的异常: {str(e)}", exc_info=True)
return jsonify({"error": f"服务器内部错误: {str(e)}"}), 500
if __name__ == '__main__':
logger.info("启动QQ登录API服务器,监听地址: 127.0.0.1:3000")
app.run(host='0.0.0.0', port=3000, debug=True)
到这里,整个QQ登录的流程基本就结束了,如果文章有什么不足的地方,欢迎大家评论区留言