基于 OAuth 2.0 的网站 QQ 第三方登录接口实现


基于 OAuth 2.0 的网站 QQ 第三方登录接口实现

本文章以Django框架网站为例,官方帮助文档腾讯开放平台 OPEN.QQ.COM

1. QQ互联创建应用

1.注册并登录QQ互联

QQ互联官网首页

2. 填写应用信息

应用管理 -> 网站应用 -> 创建应用

3. 域名和回调地址

回调接口(Callback URL)在第三方登录(如 QQ 登录、微信登录)、支付系统(如支付宝、微信支付)等场景中扮演关键角色,其核心作用是实现跨系统的数据传递和流程控制

以 QQ 登录为例,回调接口的工作流程如下:

  1. 用户发起登录:用户在你的网站点击 “QQ 登录” 按钮。
  2. 跳转至 QQ 授权页:你的应用引导用户到 QQ 的授权页面(携带你的应用 ID、请求权限等信息)。
  3. 用户授权:用户在 QQ 页面上确认授权(如允许你的应用获取昵称、头像等信息)。
  4. QQ 回调通知:QQ 平台根据你预先配置的回调 URL,将用户重定向回你的网站,并携带授权临时票据(code)和防 CSRF 参数(state)。
  5. 接下来就是通过回调接口取得的数据到其他接口实现一个完整的获取用户信息的流程,后面会详细讲。

social_django默认的回调地址格式为 {your_domain}/complete/{backend_name}/,对于 QQ 登录,backend_name 一般是 qq

所以建议回调地址格式如下:

https://example.com/social/complete/qq

4. 创建完成

完成应用的创建后会等待审核,在此期间我们需要将得到APP ID以及APP Key进行下一步配置,实现登录跳转到QQ

2. 项目配置

文章仅以Django项目作为演示参考,提供思路,如使用其他框架自行百度。

1. 安装依赖

安装社交认证依赖,使用 social-auth-app-django 库处理OAuth流程:

pip install social-auth-app-django

2. 配置settings.py

INSTALLED_APPS = [
    // ... existing code ...
    'social_django',  # 添加社交认证应用
    // ... existing code ...
]

# 社交认证配置
AUTHENTICATION_BACKENDS = [
    'social_core.backends.qq.QQOAuth2',  # QQ登录后端
    'django.contrib.auth.backends.ModelBackend',  # Django默认认证后端
]

# QQ互联应用信息
SOCIAL_AUTH_QQ_KEY = '你的APP ID'
SOCIAL_AUTH_QQ_SECRET = '你的APP Key'
SOCIAL_AUTH_QQ_REDIRECT_URI = 'https://example.com/complete/qq'  # 回调URL,以创建应用时填写的内容为准

SOCIAL_AUTH_LOGIN_REDIRECT_URL = '/' # 登录成功后重定向URL
SOCIAL_AUTH_LOGIN_ERROR_URL = '/login-error/'  # 登录错误重定向
SOCIAL_AUTH_USER_MODEL = 'auth.User'  # 指定用户模型

3. 修改urls.py

# 添加社交认证URL路由:
urlpatterns = [
    // ... existing code ...
    path('', include('social_django.urls', namespace='social')),  # 添加社交认证URL
    // ... existing code ...
]

4. 添加登录按钮

添加登录按钮并把连接设置为QQ登录链接,尽量不要把登录按钮藏得太深,审核人找不到登录按钮会直接审核不通过 图标下载地址:视觉素材下载 — QQ互联WIKI

<!-- 简单的示例 -->
<a href="{% url 'social:begin' 'qq' %}">登录</a>   

5. 数据库迁移

执行迁移命令创建社交认证所需数据表

python manage.py makemigrations
python manage.py migrate

3. 成功跳转

恭喜你!点击登录按钮成功跳转到QQ页面,然后我们就只需要等待QQ官方的审核通过了。

如果审核通过会收到邮箱通知,并且QQ互联的应用管理中项目审核状态会是通过。

4. 用户数据处理

1. 授权登录流程

授权码code

(1) 当用户成功授权登录后,会重定向到指定的回调地址,也就是url中的redirect_uri参数,并且url还会附带一个授权码code参数,用于获取后续的访问令牌access_token,以及一个state参数防止CSRF攻击。

访问令牌access_token

(2) 第一步的数据处理完成后会携带code、自身的 appidappkey 以及回调地址redirect_uri发送到 QQ 的令牌接口https://graph.qq.com/oauth2.0/token。请求成功后会返回数据格式类似access_token=具体令牌值&expires_in=有效期秒数&refresh_token=刷新令牌值,其中access_token用于标识用户在该应用的登录状态与授权信息,expires_in表示access_token的有效期,refresh_token可用于更新access_token。此时已经代表获得授权,可以获取用户信息。

用户标识openid

(3) 获取access_token后,请求https://graph.qq.com/oauth2.0/me?access_token=具体令牌值接口,获取 OpenID。返回数据格式通常为callback({"client_id": "应用appid", "openid": "具体OpenID值"}),每个 QQ 用户的 OpenID 都是唯一的,可用于与第三方应用中的用户账号进行绑定。

用户基本信息

(4) 使用access_tokenopenid访问用户信息接口https://graph.qq.com/user/get_user_info,可获得用户的基本信息。返回的数据为JSON格式,示例如下:

{
    "ret":0,
    "msg":"",
    "is_lost":0,
    "nickname":"张三",
    "gender":"男",
    "gender_type":2,
    "province":"广东",
    "city":"深圳",
    "year":"1990",
    "figureurl":"http://thirdqq.qlogo.cn/ek_qqapp/AQRsxaz34jyd69994KbrsIQq2rjfrRo1ku4CfGgzuzP92R4PDthjGWTxOjXt0CYjU/40", 
    "figureurl_1":"http://thirdqq.qlogo.cn/ek_qqapp/AQRsxaz34jyd69994K8qrIcH9eblNCuqCsVibrsIQq2rfGgzuzP92R4PDthjGWTxOjOXAXt0CYjU/40", 
    "figureurl_2":"http://thirdqq.qlogo.cn/ek_qqapp/AQRsxaz34jyd69994CuyOftqrIcH9evibrsIQq2rjfrRo1ku4CfGgzuzP92OjOXAXt0CYjU/100", 
    "figureurl_qq_1":"http://thirdqq.qlogo.cn/ek_qqapp/AQRsxaevyGJyNFicLZLKLR4CiblNCuqCsVibrsIQq2rjfrRo1ku4CfGgzuz4PDthjGWTxO0CYjU/40",
    "figureurl_qq_2":"http://thirdqq.qlogo.cn/ek_qqapp/AQRsxCsVibrsIQq2rjfrRo1ku4CfGgzuzP92R4PDthjGWTxXAXt0CYjU/100",
    "figureurl_qq":"http://thirdqq.qlogo.cn/ek_qq9UftqrIcH9evyGJyq2rjfrRo1ku4CfGgzuzP92R4PDthjGWTx0CYjU/0",
    "is_yellow_vip":"0",
    "vip":"0",
    "yellow_vip_level":"0",
    "level":"0",
    "is_yellow_year_vip":"0"
}
参数说明 描述
ret 返回码
msg 如果 ret < 0,会有相应的错误信息提示,返回数据全部用 UTF-8 编码
is_lost 判断是否有数据丢失。如果应用不使用 cache,不需要关心此参数。0 或者不返回:没有数据丢失,可以缓存。1:有部分数据丢失或错误,不要缓存
nickname 用户在 QQ 空间的昵称
figureurl 大小为 30×30 像素的 QQ 空间头像 URL
figureurl_1 大小为 50×50 像素的 QQ 空间头像 URL
figureurl_2 大小为 100×100 像素的 QQ 空间头像 URL
figureurl_qq_1 大小为 40×40 像素的 QQ 头像 URL
figureurl_qq_2 大小为 100×100 像素的 QQ 头像 URL。需要注意,不是所有的用户都拥有 QQ 的 100×100 的头像,但 40×40 像素则是一定会有
gender 性别。如果获取不到则默认返回 “男”
gender_type 性别类型。默认返回 2
province
city
year
constellation 星座
is_yellow_vip 标识用户是否为黄钻用户
yellow_vip_level 黄钻等级
is_yellow_year_vip 是否为年费黄钻用户

2. 接口处理

在进行上述授权登录流程时,social-auth-app-django库中自带的认证后端会处理这一流程,需要在settings.py中配置后端。

AUTHENTICATION_BACKENDS = [
    'social_core.backends.qq.QQOAuth2',  # qq认证后端
    'django.contrib.auth.backends.ModelBackend', #  Django 内置的默认身份验证后端
]

但是我个人在进行实现的时候在access_token的处理上出现了问题。在网上搜索发现QQ的access_token 获取方式有些特殊,QQ 返回的是 URL 参数格式(如callback?access_token=...),而非标准 JSON。所以我写了一个自定义认证后端,qq.py(注意要在一个包下创建):

# qq.py
from social_core.backends.qq import QQOAuth2
from social_core.exceptions import AuthFailed
import requests
import json

class CustomQQOAuth2(QQOAuth2):
    def auth_complete(self, *args, **kwargs):
        """完成认证流程,获取access_token和用户信息"""
        # 直接从请求参数中获取授权码
        code = self.strategy.request_data().get('code', '')
        if not code:
            raise AuthFailed(self, '缺少授权码')

        # 验证state参数
        self.validate_state()

        # 构建请求access_token的参数
        token_params = {
            'client_id': self.setting('KEY'),
            'client_secret': self.setting('SECRET'),
            'code': code,
            'redirect_uri': self.get_redirect_uri(self.data),
            'grant_type': 'authorization_code'
        }

        # 请求access_token
        response = requests.post(
            self.ACCESS_TOKEN_URL,
            data=token_params,
            headers={'User-Agent': 'Mozilla/5.0'},
            verify=True,
            proxies={}
        )

        # 打印原始响应用于调试
        print(f"access_token响应内容: {response.text}")

        # 处理QQ返回的非JSON格式响应
        try:
            # 尝试解析JSON
            response_data = response.json()
        except ValueError:
            # 如果不是JSON格式,按URL参数解析
            response_data = {}
            for param in response.text.split('&'):
                if '=' in param:
                    key, value = param.split('=', 1)
                    response_data[key] = value

        # 检查是否有错误
        if 'error' in response_data:
            error_msg = response_data.get('error_description', '认证失败')
            print(f"获取access_token失败: {error_msg}")
            raise AuthFailed(self, error_msg)

        # 确保包含access_token
        if 'access_token' not in response_data:
            print(f"响应中不包含access_token: {response_data}")
            raise AuthFailed(self, '获取access_token失败')

        # 获取用户信息(包含完整的用户数据和openid)
        user_data = self.user_data(response_data['access_token'])

        # 完成认证 - 关键修改:将user_data作为response传递
        kwargs.update({
            'backend': self,
            'response': user_data,  # 传递完整的用户数据
            'details': self.get_user_details(user_data),
            'user': kwargs.get('user')
        })

        return self.strategy.authenticate(*args, **kwargs)

    def user_data(self, access_token, *args, **kwargs):
        """获取用户信息"""
        # 获取openid
        openid_response = requests.get(
            'https://graph.qq.com/oauth2.0/me',
            params={'access_token': access_token},
            headers={'User-Agent': 'Mozilla/5.0'},
            verify=True,
            proxies={}
        )

        # 打印原始响应用于调试
        print(f"openid响应状态码: {openid_response.status_code}")
        print(f"openid响应内容: {openid_response.text}")

        # 解析openid响应(JSONP格式)
        openid_data = {}
        try:
            content = openid_response.text.strip()

            # 处理带错误码的响应
            if 'error' in content:
                error_data = json.loads(content)
                raise AuthFailed(self, f"QQ API错误: {error_data.get('error_description')}")

            # 处理标准JSONP格式
            if content.startswith('callback('):
                content = content.split('(', 1)[1].rsplit(')', 1)[0]
                openid_data = json.loads(content)
            else:
                openid_data = json.loads(content)

            # 添加调试日志
            print(f"[DEBUG] 解析后的openid数据: {openid_data}")

        except json.JSONDecodeError as e:
            print(f"[ERROR] JSON解析失败: {e.doc}")
            raise AuthFailed(self, '无效的QQ API响应格式')

        if 'openid' not in openid_data:
            print(f"响应中不包含openid: {openid_data}")
            raise AuthFailed(self, '获取用户ID失败')

        # 获取用户详细信息
        user_info_response = requests.get(
            'https://graph.qq.com/user/get_user_info',
            params={
                'access_token': access_token,
                'oauth_consumer_key': self.setting('KEY'),
                'openid': openid_data['openid']
            },
            headers={'User-Agent': 'Mozilla/5.0'},
            verify=True,
            proxies={}
        )

        # 打印用户信息响应
        print(f"用户信息响应状态码: {user_info_response.status_code}")
        print(f"用户信息响应内容: {user_info_response.text}")

        try:
            user_info = user_info_response.json()
            # 关键修改:添加access_token到返回数据中
            user_info['access_token'] = access_token
            user_info['openid'] = openid_data['openid']
            return user_info
        except ValueError:
            print(f"解析用户信息失败: {user_info_response.text}")
            raise AuthFailed(self, '获取用户信息失败')

    def get_user_details(self, response):
        """
        解析用户信息,映射到Django用户模型字段
        此方法返回的字段将用于创建/更新Django用户
        """
        return {
            'username': f"qq_{response['openid']}",  # 使用openid作为唯一用户名
            'first_name': response.get('nickname', ''),  # 昵称作为名字
            'email': '',  # QQ登录通常不返回邮箱
            'figureurl_qq': response.get('figureurl_qq'),
            'extra_data': {
                'nickname': response.get('nickname'),  # 保存昵称
                'avatar': response.get('figureurl_qq_1'),  # 保存头像URL
                'figureurl_qq': response.get('figureurl_qq'),  # 原始头像
                'gender': response.get('gender'),  # 保存性别
                'province': response.get('province'),  # 保存省份
                'city': response.get('city'),  # 保存城市
            }
        }

之后要修改settings中的认证后端配置:

AUTHENTICATION_BACKENDS = [
    # 'social_core.backends.qq.QQOAuth2',
    'myblog.back.qq.CustomQQOAuth2',  # 自定义认证后端,注意路径问题myblog.back.qq是我的项目路径,qq是我写的自定义认证后端文件
    'django.contrib.auth.backends.ModelBackend',
]

如果碰到了和我类似的问题,可以参考一下我的这个解决方案。

3. 存储用户数据

social-auth-app-django 会通过内置的模型和管道(pipeline)机制,自动处理用户信息,存储到数据库中。默认情况下是在名为social_auth_usersocialauth的表中,包含了id,provider,uid,user_id,created,modified,extra_data字段。其中管道需要在settings中进行配置:

# 添加以下内容
SOCIAL_AUTH_PIPELINE = [
    'social_core.pipeline.social_auth.social_details',  # 获取第三方用户信息
    'social_core.pipeline.social_auth.social_uid',      # 提取唯一标识(如OpenID)
    'social_core.pipeline.social_auth.auth_allowed',    # 验证是否允许登录
    'social_core.pipeline.social_auth.social_user',     # 查找或创建关联用户
    'social_core.pipeline.user.get_username',           # 生成用户名
    'social_core.pipeline.user.create_user',            # 创建新用户(首次登录)
    'social_core.pipeline.social_auth.associate_user',  # 关联用户与第三方标识
    'social_core.pipeline.social_auth.load_extra_data', # 加载额外信息到extra_data
    'social_core.pipeline.user.user_details',           # 更新用户详细信息
]

对于返回的extra_data字段的JSON数据,我们也可以在settings中进行自定义:

SOCIAL_AUTH_QQ_EXTRA_DATA = [
    'nickname',
    'figureurl_qq_1',
    'figureurl_qq_2',
    'figureurl_qq',
    'gender',
    'province',
    'city'
]

extra_data中包含了大量的用户个人信息,所以在后端操作数据时也需要从这里拿取数据,通过自定义的方式可以更个性的展示用户信息。比如我们为了让用户头像更加的清晰可以使用extra_data中的figureurl_qq,也就是用户原始头像,因为其他种类的头像像素太低,有些模糊。

0 条评论

发表评论

奇日克
5err
huhuha
哈哈哈