鸿蒙APP接入QQ登录方法详解

574 阅读12分钟

今天,我们来聊聊如何接入QQ登录,对于新手来说,这确实有点难度,主要是搞不清楚流程,官方文档也是一言难尽。 官方文档地址如下: QQ互联WIKI。大家有兴趣的也可以去看一下官方文档。下面,我们用鸿蒙应用开发来进行讲解,其余开发平台都差不多。

第一步:官网注册应用

1、在QQ互联注册开发者身份和创建应用,这里不做详解。我主要讲解一下注册完成应用之后,在应用的基本信息那里,会有一个平台信息,如果平台信息不正确,应用发起请求的时候,QQ登录后台是无法验证的。 首先是BundleName1,这个是你的应用包名,储存在工程项目的AppScope\app.json5文件里面(前提是你的应用在AppGallery Connect创建应用并配置相应的文件,创建包名这个在AppGallery Connect会有详细的步骤,这里不做详解) image.png 如果没有配置包名和发布证书,应用是无法云调试的,也无法上架应用商城。AppGallery Connect-移动应用开发-全场景服务-华为开发者联盟

2、安装包签名,大家在ICP备案鸿蒙应用时,可以看到有App特征信息及其获取方式的教程,跟着教程的步骤就可以获取到MD5签名,也就是我们所需的安装包签名了。(因为开通QQ登录就要接入网络,所以必须要APP备案和域名备案)

image.png 3 开通AppLinking,鸿蒙官方文档也有很详细的开通AppLinking步骤:使用App Linking实现应用间跳转-拉起指定应用-应用间跳转-Stage模型开发指导-Ability Kit(程序框架服务)-应用框架 - 华为HarmonyOS开发者 image.png

第二步:配置QQ登录环境

QQ授权登录使用的是OAuth2.0标准,OAuth2.0共提供四种登录方式:授权码模式、隐式授权模式、密码模式、客户端授权模式。大家可以根据自己的业务来选择适合的方登录方式,本次我选择的是授权码模式。授权码模式的运作方式如图: image.png

当用户访问资源时,比如在这里我在鸿蒙应用中使用第三方登录功能,例如QQ登录,那么这里的资源就是用户的QQ昵称和头像等信息。此时第三方应用(鸿蒙应用)将发送请求到授权服务器(QQ)去获取授权,此时授权服务器(QQ)将返回一个界面给用户,用户需要登录到QQ,并同意授权网易云音乐获得某些信息(资源)。当用户同意授权后,授权服务器将返回一个授权码(Authorization Code)给第三方应用,此时第三方应用在通过client_id、client_secret(这是需要第三方应用在授权服务器去申请的)和授权码去获得Access Token和Refresh Token,此时授权码将失效。然后就是第三方应用通过Access Token去资源服务器请求资源了,资源服务器校验Access Token成功后将返回资源给第三方应用。

  1. 打开终端下载QQ登录所需的SDK:
ohpm i @tencent/qq-open-sdk

依赖安装完成之后,我们可以看到在oh-package.json5 文件中新增依赖库。

  1. 打开module.json5文件,在module节点下配置QQ登录的信息:

image.png

"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进行开发。

  1. 导入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;
  1. 创建并初始化QQ SDK API实例,建议新建一个类,在类里面编写逻辑。
export class AuthService {
  private static instance: AuthService;
  // QQ互联应用ID,需要在QQ互联平台申请
  private appId: number = xxxxxxx;
  // 创建QQ互联API实例(单例模式)
  private iQQOpenApi: IQQOpenApi;
  1. 接着,构造函数与单例初始化,只有初始化之后才能向QQ发起请求。(顺便输出日志)
private constructor() {
  // 初始化QQ SDK API实例
  this.iQQOpenApi = QQOpenApiFactory.createApi(this.appId);
  this.databaseService = DatabaseService.getInstance();
  hilog.info(DOMAIN, TAG, 'QQ SDK API初始化完成');
}
  1. 使用单例模式来获取实例,单例模式确保一个类仅有一个实例,并提供一个全局访问点。
// 单例模式获取实例
public static getInstance(): AuthService {
  if (!AuthService.instance) {
    AuthService.instance = new AuthService();
  }
  return AuthService.instance;
}
  1. 创建完实例之后,就可以正式请求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);
    }
  });
}

后面‘ // 保存用户信息到数据库’部分,需要自己搭建数据库,这里不做详解。

  1. 第二个类方法,获取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}`));
      });
  });
}
  1. 第三个类方法:把授权码发送给后端,接收后端返回的用户数据 代码是私有异步方法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框架来写接口和请求作为演示。

  1. 首先配置相关信息,导入库和配置参数。日志模块可以帮助我们实时关注程序运行情况,在真正的项目中,日志模块是必不可少的,而不只是简单的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后台配置一致,没有就不填
  1. 然后构造函数和接口,前端发送包含授权码(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登录的流程基本就结束了,如果文章有什么不足的地方,欢迎大家评论区留言