随笔 - 77  文章 - 0  评论 - 0  阅读 - 21363 

QQ登录

我们所说的第三方登录,是指用户可以不在本项目中输入密码,而直接通过第三方的验证,成功登录荏苒网。

要开发第三方登录功能,必须要成为QQ互联开发者后,创建应用,即获取当前项目与QQ互联的应用ID,创建应用的方法参考链接http://wiki.connect.qq.com/__trashed-2

申请创建开发应用以后, 需要经过人工审核,这个时间一般是一个工作日或者半个工作日.

审核的状态会影响我们开发者开发第三方登录,状态有三种:

1. 不通过和未审核的应用,是无法开发QQ第三方登录功能的.
2. 只有已通过的应用才可以开发

点击刚才申请的引用,进入到项目里面获取APPID和APP KEY.

QQ登录开发文档连接http://wiki.connect.qq.com/%E5%87%86%E5%A4%87%E5%B7%A5%E4%BD%9C_oauth2-0

QQ第三方登录的实现流程

 

创建模型类

创建一个新的应用oauth,用来实现QQ第三方认证登录。

cd renranapi/apps
python ../../manage.py startapp oauth

注册子应用 settings/dev.py,代码:

INSTALLED_APPS = [
   'django.contrib.admin',
   'django.contrib.auth',
   'django.contrib.contenttypes',
   'django.contrib.sessions',
   'django.contrib.messages',
   'django.contrib.staticfiles',

   'corsheaders',
   'xadmin',
   'crispy_forms',
   'reversion',
   'rest_framework',
   
   'users',
   'oauth',
]

 

在models.py中定义QQ身份(openid)与用户模型类User的关联关系

from django.db import models

# Create your models here.
from django.db import models
from renranapi.utils.models import BaseModel
from users.models import User
class OAuthUser(BaseModel):
   """
  登录用户数据
  """
   user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='用户')
   openid = models.CharField(max_length=64, verbose_name='openid', db_index=True)
   access_token = models.CharField(max_length=500,verbose_name="临时访问票据", help_text="有效期:3个月")
   refresh_token = models.CharField(max_length=500,verbose_name="刷新访问票据的token", help_text="当access_token以后,可以使用refresh_token来重新获取新的access_token")
   class Meta:
       db_table = 'rr_oauth_qq'
       verbose_name = 'QQ登录用户数据'
       verbose_name_plural = verbose_name

在utils/models.py,创建项目的公共模型

from django.db import models

class BaseModel(models.Model):
   """基本公共模型"""
   orders = models.IntegerField(default=0, null=True, blank=True, verbose_name="排序")
   is_show = models.BooleanField(default=True, verbose_name="是否展示")
   is_deleted = models.BooleanField(default=False, verbose_name="是否删除")
   created_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
   updated_time = models.DateTimeField(auto_now=True, verbose_name="更新时间")

   class Meta:
       abstract = True

 

进行数据库迁移

python manage.py makemigrations
python manage.py migrate

 

urllib使用说明

在后端接口中,我们需要向QQ服务器发送请求,查询用户的QQ信息,Python提供了标准模块urllib可以帮助我们发送http请求。

  • urllib.parse.urlencode(query)

    将query字典转换为url路径中的查询字符串

  • urllib.parse.parse_qs(qs)

    将qs查询字符串格式数据转换为python的字典

  • urllib.request.urlopen(url, data=None)

    发送http请求,如果data为None,发送GET请求,如果data不为None,发送POST请求

    返回response响应对象,可以通过read()读取响应体数据,需要注意读取出的响应体数据为bytes类型

 

代码实现

  1. 在settings/dev.py中增加QQ登录相关配置

# QQ登录参数
QQ_APP_ID = '101403367'
QQ_APP_KEY = '93112df14c10d6fde74baa62f5de95ab'
QQ_REDIRECT_URL = 'http://www.moluo.net:8080/oauth_callback.html'
QQ_STATE = "/" # 用于保存登录成功后的跳转页面路径
  1. 在oauth子应用下,创建utils.py文件,编写QQ登录的辅助类

from urllib.parse import urlencode, parse_qs
from urllib.request import urlopen
from django.conf import settings
import logging

logger = logging.getLogger('django')


class OAuthQQ(object):
   """
  QQ认证辅助工具类
  """
   def __init__(self, app_id=None, app_key=None, redirect_uri=None, state=None):
       self.app_id = app_id or settings.QQ_APP_ID
       self.app_key = app_key or settings.QQ_APP_KEY
       self.redirect_url = redirect_uri or settings.QQ_REDIRECT_URL
       self.state = state or settings.QQ_STATE  # 用于保存登录成功后的跳转页面路径

   def get_auth_url(self):
       """
      获取qq登录的网址
      :return: url网址
      """
       params = {
           'response_type': 'code',
           'client_id': self.app_id,
           'redirect_uri': self.redirect_url,
           'state': self.state,
           'scope': 'get_user_info',
      }
       url = 'https://graph.qq.com/oauth2.0/authorize?' + urlencode(params)
       return url

视图代码:

from rest_framework.views import APIView
from .utils import OAuthQQ
from rest_framework.response import Response
class OAuthQQAPIView(APIView):
   def get(self, request):
       """生成QQ登录的地址"""
       state = request.query_params.get('state') # 客户端指定的状态
       oauth = OAuthQQ(state=state)
       url = oauth.get_auth_url()
       print(url)
       return Response(url)

路由,代码:

# 子应用代码:
from django.urls import path
from . import views
urlpatterns = [
   path("qq/url/", views.OAuthQQAPIView.as_view() ),
]

# 总路由:
path('oauth/', include("oauth.urls")),

 

客户端请求获取QQ第三方登录地址

修改Login.vue,,在methods中增加qq_login方法

<template>
   <div class="sign">
   <div class="logo"><router-link to="/"><img src="/static/image/nav-logo.png" alt="Logo"></router-link></div>
   <div class="main">


<h4 class="title">
 <div class="normal-title">
   <router-link class="active" to="/user/login">登录</router-link>
   <b</b>
   <router-link id="js-sign-up-btn" class="" to="/user/register">注册</router-link>
 </div>
</h4>
<div class="js-sign-in-container">
 <form id="new_session" action="" method="post">
     <div class="input-prepend restyle js-normal">
       <input placeholder="手机号或邮箱" type="text" v-model="username" id="session_email_or_mobile_number">
       <i class="iconfont ic-user"></i>
     </div>
   <!-- 海外登录登录名输入框 -->

   <div class="input-prepend">
     <input placeholder="密码" type="password" v-model="password" id="session_password">
     <i class="iconfont ic-password"></i>
   </div>
   <div class="remember-btn">
     <input type="checkbox" value="true" checked="checked" v-model="remember_me" id="session_remember_me"><span>记住我</span>
   </div>
   <div class="forget-btn">
     <a class="" data-toggle="dropdown" href="">登录遇到问题?</a>
   </div>
   <button class="sign-in-button" id="sign-in-form-submit-btn" type="button" @click="show_captcha">
     <span id="sign-in-loading"></span>
    登录
   </button>
</form>
 <!-- 更多登录方式 -->
 <div class="more-sign">
   <h6>社交帐号登录</h6>
   <ul>
 <li id="weibo-link-wrap" class="">
   <a class="weibo" id="weibo-link">
     <i class="iconfont ic-weibo"></i>
   </a>
 </li>
 <li><a id="weixin" class="weixin" target="_blank" href=""><i class="iconfont ic-wechat"></i></a></li>
 <li><a id="qq" class="qq" target="_blank" href="" @click.prevent="qq_login"><i class="iconfont ic-qq_connect"></i></a></li>
</ul>
 </div>
</div>

   </div>
 </div>
</template>

<script>
   export default {
       name: "Login",
       data(){
           return {
               username:"",
               password:"",
               remember_me: false,
          }
      },
       methods:{
           loginHandler(){
               // 1. 验证数据[账号密码]
               if(this.username.length<1 || this.password.length<1){
                   this.$message.error("对不起,账号或密码不能为空!");
                   return ;
              }

               // 2. 发送ajax请求服务端
               this.$axios.post(`http://api.renran.cn:8000/users/login/`,{
                   username: this.username,
                   password: this.password,
              }).then(response=>{ // javascript最新语法中, 可以使用箭头函数
                   // 3. 接收服务端返回的结果[jwt]
                   // 根据用户是否勾选了remember_me[记住我]来使用本地存储保存用户的jwt
                   if(this.remember_me){
                       // 永久存储[一天]
                       localStorage.user_token = response.data.token;
                       localStorage.user_name = response.data.username;
                       localStorage.user_id = response.data.id;
                       localStorage.user_nickname = response.data.nickname;
                       localStorage.user_avatar = response.data.avatar;
                       sessionStorage.removeItem("user_token");
                       sessionStorage.removeItem("user_name");
                       sessionStorage.removeItem("user_id");
                       sessionStorage.removeItem("user_nickname");
                       sessionStorage.removeItem("user_avatar");
                  }else{
                       // 临时存储
                       sessionStorage.user_token = response.data.token;
                       sessionStorage.user_name = response.data.username;
                       sessionStorage.user_id = response.data.id;
                       sessionStorage.user_nickname = response.data.nickname;
                       sessionStorage.user_avatar = response.data.avatar;
                       localStorage.removeItem("user_token");
                       localStorage.removeItem("user_name");
                       localStorage.removeItem("user_id");
                       localStorage.removeItem("user_nickname");
                       localStorage.removeItem("user_avatar");
                  }

                   // 登录跳转
                   this.$confirm(`${response.data.nickname},欢迎回到荏苒~`, '登录成功', {
                     confirmButtonText: '去个人中心',
                     cancelButtonText: '返回上一页',
                     type: 'success'
                  }).then(() => {
                     // 跳转到个人中心
                     this.$router.push("/user");
                  }).catch(() => {
                     // 跳转到上一页
                     this.$router.back(); // this.$router.go(-1);
                  });

              }).catch(error=>{
                   if(error.response){
                       if(error.response.status==400){
                           this.$message.error("提交数据有误,请检查您输入的账号密码是否正确!");
                      }
                  }else{
                       console.log(error);
                  }

              });

          },
           show_captcha(){
               // 显示验证码

               if(this.username.length<1 || this.password.length<1){
                   this.$message.error("对不起,账号或密码不能为空!");
                   return ;
              }

               var captcha1 = new TencentCaptcha(this.$settings.TC_captcha.app_id, res=>{
                   /*
                  * ret     Int     验证结果,0:验证成功。2:用户主动关闭验证码。
                    ticket String 验证成功的票据,当且仅当 ret = 0 时 ticket 有值。
                    appid   String 场景 ID。
                    bizState Any     自定义透传参数。
                    randstr String 本次验证的随机串,请求后台接口时需带上。
                  *
                  * */
                   if(res.ret === 0){
                     this.$axios.post(`${this.$settings.Host}/users/captcha/`,{
                         ret: res.ret,
                         ticket: res.ticket,
                         randstr: res.randstr,
                    }).then(response=>{
                         if(response.data.message && response.data.randstr === res.randstr){
                             // 验证成功
                             this.loginHandler();
                        }else{
                             this.$meesage.error("验证码验证失败!请重新操作验证码");
                             captcha1.destroy();
                        }
                    }).catch(error=>{
                         console.log("发生错误!", error);
                    })
                  }

              });
               captcha1.show();
          },
           qq_login(){
               // 跳转到QQ第三方登录页面
               this.$axios.get(`${this.$settings.Host}/oauth/qq/url/`,{
                   params: {
                       state: "/",
                  }
              }).then(response=>{
                   let url = response.data;
                   // 跳转到登录页面
                   location.href = url;
              }).catch(error=>{
                   this.$message.error("网络错误!无法使用QQ登录!");
              });
          }
      }
  }
</script>

然后用户此时就可以在页面中通过点击跳转到QQ第三方登录页面了。

 

BUG

如果实现上面代码以后QQ页面出现如下错误信息:

对不起,该网站尚未开通QQ帐号登录(错误码:100008)

则表示当前站点应用没有通过审核。这表示我们的代码没有问题了,但是注册的应用是没通过或者审核中的。

解决方案:使用以下QQ登录应用进行测试开发。

# 1. 在settings/dev.py文件中修改QQ第三方登录配置。
# QQ登录参数
   QQ_APP_ID = '101403367'
   QQ_APP_KEY = '93112df14c10d6fde74baa62f5de95ab'
   QQ_REDIRECT_URL = 'http://www.moluo.net:8080/oauth_callback.html'
   QQ_STATE = "/" # 用于保存登录成功后的跳转页面路径
   
# 2. 在/etc/hosts/中添加如下配置信息:
   # sudo vim /etc/hosts
   127.0.0.1   www.moluo.net

# 3. 在客户端项目中临时修改config/index.js中的host域名为:www.moluo.net
#   并重启客户端项目

# 4. 在api服务端项目的配置文件settings/dev.py中,添加www.moluo.net到CORS_ORIGIN_WHITELIST列表中,并重启服务端项目
    CORS_ORIGIN_WHITELIST = (
  'http://www.moluo.net:8080',
    )

经过上面的提供的操作,用户在QQ登录成功后,QQ会将用户重定向回我们配置的回调域网址,我们申请QQ登录开发资质时配置的回调地址为:http://www.renran.cn:8080/login/qq_callback或者http://www.moluo.net:8080/oauth_callback.html。接下来,我们要在客户端对提供一个QQCallBack组件页面,地址绑定为oauth_callback.html

<template>
   
</template>

<script>
   export default {
       name: "QQCallBack"
  }
</script>

<style scoped>

</style>

路由router/index.js中绑定路由

import Vue from 'vue'
import Router from 'vue-router'

Vue.use(Router);

import QQCallBack from "@/components/QQCallBack"


export default new Router({
 mode: "history",
 routes: [
// .....
    {
      name:"QQCallBack",
      path:"/oauth_callback.html",// 改成自己注册QQ登录时的地址,不是一定要.html,只是我当初不小心加了而已
      component: QQCallBack,
    },
]
})

 

接收地址栏上面的code参数转发到服务端,服务端再次请求QQ服务器提取访问QQ用户信息的access_token。

客户端代码:

<template>
   <div>
    QQ回调地址
   </div>
</template>

<script>
   export default {
       name: "QQCallBack",
       data(){

      },
       created(){
           this.get_user_info();
      },
       methods:{
           get_user_info(){

               // 转发code提供给服务端
               this.$axios.get(`${this.$settings.Host}/oauth/qq/info/`+location.search
              ).then(response=>{
                  //
                   console.log("ok");
              }).catch(error=>{
                  this.$message.error("网络错误!无法使用QQ第三方登陆");
              });
          }
      }
  }
</script>

<style scoped>

</style>

 

服务端在uauth/utils.py的OAuthQQ辅助类中新增获取access_token和根据access_token提取openid的方法:

from urllib.request import urlopen
from django.conf import settings
from urllib.parse import urlencode, parse_qs
import logging
import json

logger =logging.getLogger("django")

class OAuthQQError(Exception):
   pass

class OAuthQQ(object):
   """QQ第三方登录的辅助类"""
   def __init__(self, app_id=None, app_key=None, redirect_uri=None, state=None):
       self.app_id = app_id or settings.QQ_APP_ID     # 应用ID
       self.app_key = app_key or settings.QQ_APP_KEY  # 应用秘钥 
       self.redirect_url = redirect_uri or settings.QQ_REDIRECT_URL # 回调域名
       self.state = state or settings.QQ_STATE  # 用于保存登录成功后的跳转页面路径

   def get_auth_url(self):
       """生成QQ第三方登录的链接"""
       params = {
           'response_type': 'code', # 授权类型
           'client_id': self.app_id,
           'redirect_uri': self.redirect_url, # 回调域名
           'state': self.state,     # 自定义状态
           'scope': 'get_user_info', # 可选,可以不填
      }

       url = 'https://graph.qq.com/oauth2.0/authorize?' + urlencode(params)

       return url

   def get_access_token(self,code):
       """通过授权码获取临时票据access_token"""
       params = {
           'grant_type': 'authorization_code',
           'client_id': self.app_id,
           'client_secret': self.app_key,
           'redirect_uri': self.redirect_url,
           'code': code,
      }
       # urlencode 把字典转换成查询字符串的格式
       url = 'https://graph.qq.com/oauth2.0/token?' + urlencode(params)
       try:
           response = urlopen(url)
           response_data = response.read().decode()
           # parse_qs 把查询字符串格式的内容转换成字典[注意:转换后的字典,值是列表格式]
           data = parse_qs(response_data)
           access_token = data.get('access_token')[0]
       except:
           logger.error('code=%s msg=%s' % (data.get('code'), data.get('msg')))
           raise OAuthQQError

       return access_token

   def get_open_id(self,access_token):
       """根据access_token获取openID"""
       url = 'https://graph.qq.com/oauth2.0/me?access_token=' + access_token
       try:
           response = urlopen(url)
           response_data = response.read().decode()
           data = json.loads(response_data[10:-4])
           openid = data.get('openid')
       except:
           logger.error('code=%s msg=%s' % (data.get('code'), data.get('msg')))
           raise OAuthQQError

       return openid

   def get_qq_user_info(self, access_token, openid):
       params = {
           'access_token': access_token,
           'oauth_consumer_key': self.app_id,
           'openid': openid,
      }
       url = 'https://graph.qq.com/user/get_user_info?' + urlencode(params)
       try:
           response = urlopen(url)
           response_data = response.read().decode()
           data = json.loads(response_data)
           return data
       except:
           logger.error('code=%s msg=%s' % (data.get('code'), data.get('msg')))
           raise OAuthQQError

视图调用辅助类完成获取QQ用户信息的逻辑:

from rest_framework.views import APIView
from .utils import OAuthQQ,OAuthQQError
from rest_framework.response import Response
from .models import OAuthUser

class OAuthQQAPIView(APIView):
   def get(self, request):
       """生成QQ登录的地址"""
       state = request.query_params.get('state') # 客户端指定的状态
       oauth = OAuthQQ(state=state)
       url = oauth.get_auth_url()
       print(url)
       return Response(url)

from itsdangerous import TimedJSONWebSignatureSerializer as Serializer, BadData
from django.conf import settings
from renranapi.settings import constants
class QQInfoAPIView(APIView):
   def get(self,request):
       """获取QQ用户的信息"""
       # 1. 获取客户端转发过来的QQ登录授权码
       code = request.query_params.get("code")
       state = request.query_params.get('state')  # 客户端指定的状态
       if not code:
           return Response("QQ登录异常!请重新尝试登录!")

       oauth = OAuthQQ(state=state)
       try:
           # 2. 根据授权码到QQ服务器获取access_token
           access_token = oauth.get_access_token(code)
           # 3. 根据access_token获取用户信息[openID]
           openid = oauth.get_open_id(access_token)
           # 3.1 获取用户信息
           user_info = oauth.get_qq_user_info(access_token,openid)
           # 4F65D2442D41B0D1639FD7FC14123B11

       except OAuthQQError:
           return Response("QQ登录异常!获取授权信息失败!请重新尝试登录!")

       # 4. 根据openID到数据库中查询用户判断是否属于第一次使用QQ登录
       try:
           oauth_qq_user = OAuthUser.objects.get(openid=openid)
           # 查找到对应的用户记录,证明用户创建我们网站的账号并且已经关联了QQ的openID
           user = oauth_qq_user.user
           # 生成jwt登录token
           from rest_framework_jwt.settings import api_settings
           jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
           jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER

           payload = jwt_payload_handler(user)
           token = jwt_encode_handler(payload)
           print(token)
           user_info = {
               "token":token,
               "id":user.id,
               "username":user.username,
               "avatar":user.avatar.url,
               "nickname":user.nickname,
          }
           return Response({"user_info": user_info,"status": 1})
       except OAuthUser.DoesNotExist:
           # 查找不到对应的用户记录,用户属于第一次使用QQ登录
           # 使用itsdangrous对数据进行加密
           serializer = Serializer(settings.SECRET_KEY, constants.DATA_SIGNATURE_EXPIRE)
           data = serializer.dumps({"openid": openid}).decode()
           return Response({
               "avatar": user_info.get("figureurl_qq_1"),
               "nickname": user_info.get("nickname"),
               "data": data,
               "status": 0,
          })

路由:

from django.urls import path
from . import views
urlpatterns = [
   path("qq/url/", views.OAuthQQAPIView.as_view() ),
   path("qq/info/", views.QQInfoAPIView.as_view() ),
]

客户端实现用户已经拥有平台账号而且绑定了QQ账号的情况.

<template>
   <div class="sign">
   <div class="logo"><a href="/"><img src="/static/image/nav-logo.png" alt="Logo"></a></div>
   <div class="main">


     <h4 class="title">
       <div class="normal-title">
               <router-link class="active" to="/user/login">登录</router-link>
               <b</b>
               <router-link id="js-sign-up-btn" to="/user/register">注册</router-link>
       </div>
     </h4>
     <div class="js-sign-in-container">
       <form id="new_session" action="" method="post">
           <div class="input-prepend restyle js-normal">
             <input placeholder="登录账号或手机号或邮箱" type="text" v-model="username" id="session_email_or_mobile_number">
             <i class="iconfont ic-user"></i>
           </div>
           <!-- 海外登录登录名输入框 -->

           <div class="input-prepend">
             <input placeholder="密码" type="password" v-model="password" id="session_password">
             <i class="iconfont ic-password"></i>
           </div>
           <div class="remember-btn">
             <input type="checkbox" value="true" checked="checked" v-model="remember_me" id="session_remember_me"><span>记住我</span>
           </div>
           <div class="forget-btn">
             <router-link to="/find_password">通过邮箱找回密码?</router-link>
           </div>
           <button class="sign-in-button" id="sign-in-form-submit-btn" type="button" @click.prevent="show_captcha">
             <span id="sign-in-loading"></span>登录
           </button>
       </form>
       <!-- 更多登录方式 -->
       <div class="more-sign">
         <h6>社交帐号登录</h6>
         <ul>
       <li id="weibo-link-wrap" class="">
         <a class="weibo" id="weibo-link">
           <i class="iconfont ic-weibo"></i>
         </a>
       </li>
       <li><a id="weixin" class="weixin" target="_blank" href=""><i class="iconfont ic-wechat"></i></a></li>
       <li><a id="qq" class="qq" target="_blank" href="" @click.prevent="qq_login"><i class="iconfont ic-qq_connect"></i></a></li>
     </ul>
       </div>
     </div>

   </div>
 </div>
</template>

<script>
   export default {
       name: "QQCallBack",
       data(){
         return {

        }
      },
       created() {
         this.get_qq_user_info();
      },
       methods:{
         get_qq_user_info(){
           // 获取QQ登录用户的信息
           this.$axios.get(`${this.$settings.Host}/oauth/qq/info/`,{
             params:{
               code: this.$route.query.code
            }
          }).then(response=>{
             let data = response.data;
             if(data.status){
               let info = data.user_info;
               // 登录成功
               localStorage.removeItem("user_token");
               localStorage.removeItem("user_id");
               localStorage.removeItem("user_name");
               localStorage.removeItem("user_avatar");
               localStorage.removeItem("user_nickname");
               sessionStorage.user_token = info.token;
               sessionStorage.user_id = info.id;
               sessionStorage.user_name = info.username;
               sessionStorage.user_avatar = info.avatar;
               sessionStorage.user_nickname = info.nickname;

               this.$confirm('登录成功, 欢迎回来!', '提示', {
                 confirmButtonText: '返回首页',
                 cancelButtonText: '返回上一页',
                 type: 'success'
              }).then(() => {
                 this.$router.push("/");
              }).catch(() => {
                 this.$router.go(-1);
              });

            }

          }).catch(error=>{
             this.$message.error("QQ登录异常!");
          })
        }
      }
  }
</script>

接下来,我们实现用户没有绑定QQ账号的情况.

提供页面

<template>
   <div class="sign">
   <div class="logo"><a href="/"><img src="/static/image/nav-logo.png" alt="Logo"></a></div>
   <div class="main">


     <h4 class="title">
       <div class="normal-title">
           <a :class="status==1?'active':''" @click="status=1">已有账号</a>
           <b</b>
           <a :class="status==2?'active':''" @click="status=2">没有账号</a>
       </div>
     </h4>
     <div class="js-sign-in-container" v-if="status==1">
       <form action="" method="post">
           <div class="input-prepend restyle js-normal">
             <input placeholder="登录账号或手机号或邮箱" type="text" v-model="username">
             <i class="iconfont ic-user"></i>
           </div>
           <div class="input-prepend">
             <input placeholder="密码" type="password" v-model="password">
             <i class="iconfont ic-password"></i>
           </div>
           <div class="forget-btn">
             <router-link to="/find_password">通过邮箱找回密码?</router-link>
           </div>
           <button class="sign-in-button" type="button" @click.prevent="show_captcha">
             <span></span>登录
           </button>
       </form>
     </div>
     <div class="js-sign-in-container" v-if="status==2">
       <form class="new_user" id="new_user" action="" accept-charset="UTF-8" method="post">
         <div class="input-prepend restyle">
             <input placeholder="你的昵称" type="text" value="" v-model="nickname" id="user_nickname">
           <i class="iconfont ic-user"></i>
         </div>
           <div class="input-prepend restyle no-radius js-normal">
               <input placeholder="手机号" type="tel" v-model="mobile" id="user_mobile_number">
             <i class="iconfont ic-phonenumber"></i>
           </div>
         <div class="input-prepend restyle no-radius security-up-code js-security-number" v-if="is_show_sms_code">
             <input type="text" v-model="sms_code" id="sms_code" placeholder="手机验证码">
           <i class="iconfont ic-verify"></i>
           <a tabindex="-1" class="btn-up-resend js-send-code-button"  href="javascript:void(0);" id="send_code" @click.prevent="send_sms">{{sms_code_text}}</a>
         </div>
         <input type="hidden" name="security_number" id="security_number">
         <div class="input-prepend">
           <input placeholder="设置密码" type="password" v-model="password" id="user_password">
           <i class="iconfont ic-password"></i>
         </div>
         <input type="submit" name="commit" value="注册" class="sign-up-button" id="sign_up_btn" @click.prevent="registerHandler">
         <p class="sign-up-msg">点击 “注册” 即表示您同意并愿意遵守荏苒<br> <a target="_blank" href="">用户协议</a> 和 <a target="_blank" href="">隐私政策</a> 。</p>
       </form>
     </div>
   </div>
 </div>
</template>

<script>
   export default {
       name: "QQCallBack",
       data(){
         return {
           status: 1, // 当前用户是否拥有了平台账号
           username: "",
           password: "",
           nickname:"",
           mobile:"",
           sms_code:"",
           sms_code_text:"发送验证码",
           is_show_sms_code:false,
           openid:"",
        }
      },
       created() {
         this.get_qq_user_info();
      },
       watch: {
         mobile() {
           if (/^1[3-9]\d{9}$/.test(this.mobile)) {
             this.is_show_sms_code = true;
          } else {
             this.is_show_sms_code = false;
          }
        }
      },
       methods:{
         get_qq_user_info(){
           // 获取QQ登录用户的信息
           this.$axios.get(`${this.$settings.Host}/oauth/qq/info/`,{
             params:{
               code: this.$route.query.code
            }
          }).then(response=>{
             let data = response.data;
             if(data.status){
               let info = data.user_info;
               // 登录成功
               localStorage.removeItem("user_token");
               localStorage.removeItem("user_id");
               localStorage.removeItem("user_name");
               localStorage.removeItem("user_avatar");
               localStorage.removeItem("user_nickname");
               sessionStorage.user_token = info.token;
               sessionStorage.user_id = info.id;
               sessionStorage.user_name = info.username;
               sessionStorage.user_avatar = info.avatar;
               sessionStorage.user_nickname = info.nickname;

               this.$confirm('登录成功, 欢迎回来!', '提示', {
                 confirmButtonText: '返回首页',
                 cancelButtonText: '返回上一页',
                 type: 'success'
              }).then(() => {
                 this.$router.push("/");
              }).catch(() => {
                 this.$router.go(-1);
              });

            }else{
               console.log(data);
            }

          }).catch(error=>{
             this.$message.error("QQ登录异常!");
          })
        },
         send_sms(){
               if( !/1[1-9]{2}\d{8}/.test(this.mobile) ){
                 return false;
              }

               // 如果 sms_code_text 不是文本,而是数字,则表示当前手机号码还在60秒的发送短信间隔内
               if(this.sms_code_text != "发送验证码"){
                 return false;
              }

               // 发送短信
               let _this = this;
               this.$axios.get(`${_this.$settings.Host}/users/sms/${_this.mobile}/`).then(response=>{
                 // 显示发送短信以后的文本倒计时
                 let time = 60;
                 let timer = setInterval(()=>{
                   --time;
                   if(time <=1){
                     // 如果倒计时为0,则关闭当前定时器
                     _this.sms_code_text = "发送验证码";
                     clearInterval(timer);
                     time = 60;
                  }else{
                       _this.sms_code_text = time+"秒";
                  }
                },1000);

              }).catch(error=>{
                 console.log(error);
              })
          },
         show_captcha(){

             // 判断手机号或者密码是否为空!
             if(this.username.length<1 || this.password.length<1){
               return false; // 阻止代码继续往下执行
            }

             // 验证码
             let self = this;
             // 生成一个验证码对象
             var captcha1 = new TencentCaptcha(this.$settings.TC_captcha.app_id, function(res) {
               console.log(res);
               // res(未通过验证)= {ret: 1, ticket: null}

               // ticket 验证成功的票据,当且仅当ret=0时ticket有值
               // res(验证成功) = {ret: 0, ticket: "String", randstr: "String"}
               if (res.ret === 0) {
                 // 随机码
                 // api服务端校验验证码的结果
                 self.$axios.get(`${self.$settings.Host}/users/captcha/`,{
                   params:{
                     ticket: res.ticket,
                     randstr: res.randstr,
                  }
                }).then(response=>{
                   // 进行登录处理
                   self.loginhander();
                }).catch(error=>{
                   self.$message.error("验证码校验错误!");
                })
              }
            });

             // 显示验证码
             captcha1.show();
          },
         loginhander(){
             this.$axios.post(this.$settings.Host+"/oauth/qq/login/",
              {
                 "username":this.username,
                 "password":this.password,
                                                                  "openid": this.openid,
              }).then(response=>{
                   // 使用浏览器本地存储保存token
                   if (this.remember_me) {
                     // 记住登录
                     sessionStorage.removeItem("user_token");
                     sessionStorage.removeItem("user_id");
                     sessionStorage.removeItem("user_name");
                     sessionStorage.removeItem("user_avatar");
                     sessionStorage.removeItem("user_nickname");
                     localStorage.user_token = response.data.token;
                     localStorage.user_id = response.data.id;
                     localStorage.user_name = response.data.username;
                     localStorage.user_avatar = response.data.avatar;
                     localStorage.user_nickname = response.data.nickname;
                  } else {
                     // 未记住登录
                     localStorage.removeItem("user_token");
                     localStorage.removeItem("user_id");
                     localStorage.removeItem("user_name");
                     localStorage.removeItem("user_avatar");
                     localStorage.removeItem("user_nickname");
                     sessionStorage.user_token = response.data.token;
                     sessionStorage.user_id = response.data.id;
                     sessionStorage.user_name = response.data.username;
                     sessionStorage.user_avatar = response.data.avatar;
                     sessionStorage.user_nickname = response.data.nickname;
                  }

                   this.$confirm('登录成功, 欢迎回来!', '提示', {
                     confirmButtonText: '返回首页',
                     cancelButtonText: '返回上一页',
                     type: 'success'
                  }).then(() => {
                     this.$router.push("/");
                  }).catch(() => {
                     this.$router.go(-1);
                  });

              }).catch(error=>{
                   this.$message.error("登录失败!账号或密码错误!");
                   this.username = "";
                   this.password = "";
              })
          },
      }
  }
</script>    

客户端接收服务端因为没有绑定QQ账号而返回的QQ用户信息,里面隐藏了一个data标识openid

<template>
   <div class="sign">
   <div class="logo"><a href="/"><img src="/static/image/nav-logo.png" alt="Logo"></a></div>
   <div class="main">


     <h4 class="title">
       <div class="normal-title">
           <a :class="status==1?'active':''" @click="status=1">已有账号</a>
           <b</b>
           <a :class="status==2?'active':''" @click="status=2">没有账号</a>
       </div>
     </h4>
     <div class="js-sign-in-container" v-if="status==1">
       <form action="" method="post">
           <div class="input-prepend restyle js-normal">
             <input placeholder="登录账号或手机号或邮箱" type="text" v-model="username">
             <i class="iconfont ic-user"></i>
           </div>
           <div class="input-prepend">
             <input placeholder="密码" type="password" v-model="password">
             <i class="iconfont ic-password"></i>
           </div>
           <div class="forget-btn">
             <router-link to="/find_password">通过邮箱找回密码?</router-link>
           </div>
           <button class="sign-in-button" type="button" @click.prevent="show_captcha">
             <span></span>登录
           </button>
       </form>
     </div>
     <div class="js-sign-in-container" v-if="status==2">
       <form class="new_user" id="new_user" action="" accept-charset="UTF-8" method="post">
         <div class="input-prepend restyle">
             <input placeholder="你的昵称" type="text" value="" v-model="qq_nickname" id="user_nickname">
           <i class="iconfont ic-user"></i>
         </div>
           <div class="input-prepend restyle no-radius js-normal">
               <input placeholder="手机号" type="tel" v-model="mobile" id="user_mobile_number">
             <i class="iconfont ic-phonenumber"></i>
           </div>
         <div class="input-prepend restyle no-radius security-up-code js-security-number" v-if="is_show_sms_code">
             <input type="text" v-model="sms_code" id="sms_code" placeholder="手机验证码">
           <i class="iconfont ic-verify"></i>
           <a tabindex="-1" class="btn-up-resend js-send-code-button"  href="javascript:void(0);" id="send_code" @click.prevent="send_sms">{{sms_code_text}}</a>
         </div>
         <input type="hidden" name="security_number" id="security_number">
         <div class="input-prepend">
           <input placeholder="设置密码" type="password" v-model="password" id="user_password">
           <i class="iconfont ic-password"></i>
         </div>
         <input type="submit" name="commit" value="注册" class="sign-up-button" id="sign_up_btn" @click.prevent="registerHandler">
         <p class="sign-up-msg">点击 “注册” 即表示您同意并愿意遵守荏苒<br> <a target="_blank" href="">用户协议</a> 和 <a target="_blank" href="">隐私政策</a> 。</p>
       </form>
     </div>
   </div>
 </div>
</template>

<script>
   export default {
       name: "QQCallBack",
       data(){
         return {
           status: 1, // 当前用户是否拥有了平台账号
           username: "",
           password: "",
           nickname:"",
           mobile:"",
           sms_code:"",
           sms_code_text:"发送验证码",
           is_show_sms_code:false,
           openid:"",
           qq_avatar: "",
           qq_nickname: "",
        }
      },
       created() {
         this.get_qq_user_info();
      },
       watch: {
         mobile() {
           if (/^1[3-9]\d{9}$/.test(this.mobile)) {
             this.is_show_sms_code = true;
          } else {
             this.is_show_sms_code = false;
          }
        }
      },
       methods:{
         get_qq_user_info(){
           // 获取QQ登录用户的信息
           this.$axios.get(`${this.$settings.Host}/oauth/qq/info/`,{
             params:{
               code: this.$route.query.code
            }
          }).then(response=>{
             let data = response.data;
             if(data.status){
               let info = data.user_info;
               // 登录成功
               localStorage.removeItem("user_token");
               localStorage.removeItem("user_id");
               localStorage.removeItem("user_name");
               localStorage.removeItem("user_avatar");
               localStorage.removeItem("user_nickname");
               sessionStorage.user_token = info.token;
               sessionStorage.user_id = info.id;
               sessionStorage.user_name = info.username;
               sessionStorage.user_avatar = info.avatar;
               sessionStorage.user_nickname = info.nickname;

               this.$confirm('登录成功, 欢迎回来!', '提示', {
                 confirmButtonText: '返回首页',
                 cancelButtonText: '返回上一页',
                 type: 'success'
              }).then(() => {
                 this.$router.push("/");
              }).catch(() => {
                 this.$router.go(-1);
              });

            }else{
               this.status = 2;
               this.openid = data.data;
               this.qq_avatar = data.avatar;
               this.qq_nickname = data.nickname;
            }

          }).catch(error=>{
             this.$message.error("QQ登录异常!");
          })
        },
         send_sms(){
               if( !/1[1-9]{2}\d{8}/.test(this.mobile) ){
                 return false;
              }

               // 如果 sms_code_text 不是文本,而是数字,则表示当前手机号码还在60秒的发送短信间隔内
               if(this.sms_code_text != "发送验证码"){
                 return false;
              }

               // 发送短信
               let _this = this;
               this.$axios.get(`${_this.$settings.Host}/users/sms/${_this.mobile}/`).then(response=>{
                 // 显示发送短信以后的文本倒计时
                 let time = 60;
                 let timer = setInterval(()=>{
                   --time;
                   if(time <=1){
                     // 如果倒计时为0,则关闭当前定时器
                     _this.sms_code_text = "发送验证码";
                     clearInterval(timer);
                     time = 60;
                  }else{
                       _this.sms_code_text = time+"秒";
                  }
                },1000);

              }).catch(error=>{
                 console.log(error);
              })
          },
         show_captcha(){

             // 判断手机号或者密码是否为空!
             if(this.username.length<1 || this.password.length<1){
               return false; // 阻止代码继续往下执行
            }

             // 验证码
             let self = this;
             // 生成一个验证码对象
             var captcha1 = new TencentCaptcha(this.$settings.TC_captcha.app_id, function(res) {
               console.log(res);
               // res(未通过验证)= {ret: 1, ticket: null}

               // ticket 验证成功的票据,当且仅当ret=0时ticket有值
               // res(验证成功) = {ret: 0, ticket: "String", randstr: "String"}
               if (res.ret === 0) {
                 // 随机码
                 // api服务端校验验证码的结果
                 self.$axios.get(`${self.$settings.Host}/users/captcha/`,{
                   params:{
                     ticket: res.ticket,
                     randstr: res.randstr,
                  }
                }).then(response=>{
                   // 进行登录处理
                   self.loginhander();
                }).catch(error=>{
                   self.$message.error("验证码校验错误!");
                })
              }
            });

             // 显示验证码
             captcha1.show();
          },
         loginhander(){
             this.$axios.post(this.$settings.Host+"/oauth/qq/login/",
              {
                 "username":this.username,
                 "password":this.password,
                 "openid": this.openid,
              }).then(response=>{
                   // 使用浏览器本地存储保存token
                   if (this.remember_me) {
                     // 记住登录
                     sessionStorage.removeItem("user_token");
                     sessionStorage.removeItem("user_id");
                     sessionStorage.removeItem("user_name");
                     sessionStorage.removeItem("user_avatar");
                     sessionStorage.removeItem("user_nickname");
                     localStorage.user_token = response.data.token;
                     localStorage.user_id = response.data.id;
                     localStorage.user_name = response.data.username;
                     localStorage.user_avatar = response.data.avatar;
                     localStorage.user_nickname = response.data.nickname;
                  } else {
                     // 未记住登录
                     localStorage.removeItem("user_token");
                     localStorage.removeItem("user_id");
                     localStorage.removeItem("user_name");
                     localStorage.removeItem("user_avatar");
                     localStorage.removeItem("user_nickname");
                     sessionStorage.user_token = response.data.token;
                     sessionStorage.user_id = response.data.id;
                     sessionStorage.user_name = response.data.username;
                     sessionStorage.user_avatar = response.data.avatar;
                     sessionStorage.user_nickname = response.data.nickname;
                  }

                   this.$confirm('登录成功, 欢迎回来!', '提示', {
                     confirmButtonText: '返回首页',
                     cancelButtonText: '返回上一页',
                     type: 'success'
                  }).then(() => {
                     this.$router.push("/");
                  }).catch(() => {
                     this.$router.go(-1);
                  });

              }).catch(error=>{
                   this.$message.error("登录失败!账号或密码错误!");
                   this.username = "";
                   this.password = "";
              })
          },
      }
  }
</script>

客户端发送ajax请求服务端绑定QQ用户

<template>
   <div class="sign">
   <div class="logo"><a href="/"><img src="/static/image/nav-logo.png" alt="Logo"></a></div>
   <div class="main">


     <h4 class="title">
       <div class="normal-title">
           <a :class="status==1?'active':''" @click="status=1">已有账号</a>
           <b</b>
           <a :class="status==2?'active':''" @click="status=2">没有账号</a>
       </div>
     </h4>
     <div class="js-sign-in-container" v-if="status==1">
       <form action="" method="post">
           <div class="input-prepend restyle js-normal">
             <input placeholder="登录账号或手机号或邮箱" type="text" v-model="username">
             <i class="iconfont ic-user"></i>
           </div>
           <div class="input-prepend">
             <input placeholder="密码" type="password" v-model="password">
             <i class="iconfont ic-password"></i>
           </div>
           <div class="forget-btn">
             <router-link to="/find_password">通过邮箱找回密码?</router-link>
           </div>
           <button class="sign-in-button" type="button" @click.prevent="show_captcha">
             <span></span>登录
           </button>
       </form>
     </div>
     <div class="js-sign-in-container" v-if="status==2">
       <form class="new_user" id="new_user" action="" accept-charset="UTF-8" method="post">
         <div class="input-prepend restyle">
             <input placeholder="你的昵称" type="text" value="" v-model="qq_nickname" id="user_nickname">
           <i class="iconfont ic-user"></i>
         </div>
           <div class="input-prepend restyle no-radius js-normal">
               <input placeholder="手机号" type="tel" v-model="mobile" id="user_mobile_number">
             <i class="iconfont ic-phonenumber"></i>
           </div>
         <div class="input-prepend restyle no-radius security-up-code js-security-number" v-if="is_show_sms_code">
             <input type="text" v-model="sms_code" id="sms_code" placeholder="手机验证码">
           <i class="iconfont ic-verify"></i>
           <a tabindex="-1" class="btn-up-resend js-send-code-button"  href="javascript:void(0);" id="send_code" @click.prevent="send_sms">{{sms_code_text}}</a>
         </div>
         <input type="hidden" name="security_number" id="security_number">
         <div class="input-prepend">
           <input placeholder="设置密码" type="password" v-model="password" id="user_password">
           <i class="iconfont ic-password"></i>
         </div>
         <input type="submit" name="commit" value="注册" class="sign-up-button" id="sign_up_btn" @click.prevent="show_captcha">
         <p class="sign-up-msg">点击 “注册” 即表示您同意并愿意遵守荏苒<br> <a target="_blank" href="">用户协议</a> 和 <a target="_blank" href="">隐私政策</a> 。</p>
       </form>
     </div>
   </div>
 </div>
</template>

<script>
   export default {
       name: "QQCallBack",
       data(){
         return {
           status: 1, // 当前用户是否拥有了平台账号
           username: "",
           password: "",
           nickname:"",
           mobile:"",
           sms_code:"",
           sms_code_text:"发送验证码",
           is_show_sms_code:false,
           openid:"",
           qq_avatar: "",
           qq_nickname: "",
        }
      },
       created() {
         this.get_qq_user_info();
      },
       watch: {
         mobile() {
           if (/^1[3-9]\d{9}$/.test(this.mobile)) {
             this.is_show_sms_code = true;
          } else {
             this.is_show_sms_code = false;
          }
        }
      },
       methods:{
         get_qq_user_info(){
           // 获取QQ登录用户的信息
           this.$axios.get(`${this.$settings.Host}/oauth/qq/info/`,{
             params:{
               code: this.$route.query.code
            }
          }).then(response=>{
             let data = response.data;
             if(data.status){
               let info = data.user_info;
               // 登录成功
               localStorage.removeItem("user_token");
               localStorage.removeItem("user_id");
               localStorage.removeItem("user_name");
               localStorage.removeItem("user_avatar");
               localStorage.removeItem("user_nickname");
               sessionStorage.user_token = info.token;
               sessionStorage.user_id = info.id;
               sessionStorage.user_name = info.username;
               sessionStorage.user_avatar = info.avatar;
               sessionStorage.user_nickname = info.nickname;

               this.$confirm('登录成功, 欢迎回来!', '提示', {
                 confirmButtonText: '返回首页',
                 cancelButtonText: '返回上一页',
                 type: 'success'
              }).then(() => {
                 this.$router.push("/");
              }).catch(() => {
                 this.$router.go(-1);
              });

            }else{
               this.status = 2;
               this.openid = data.data;
               this.qq_avatar = data.avatar;
               this.qq_nickname = data.nickname;
            }

          }).catch(error=>{
             this.$message.error("QQ登录异常!");
          })
        },
         send_sms(){
               if( !/1[1-9]{2}\d{8}/.test(this.mobile) ){
                 return false;
              }

               // 如果 sms_code_text 不是文本,而是数字,则表示当前手机号码还在60秒的发送短信间隔内
               if(this.sms_code_text != "发送验证码"){
                 return false;
              }

               // 发送短信
               let _this = this;
               this.$axios.get(`${_this.$settings.Host}/users/sms/${_this.mobile}/`).then(response=>{
                 // 显示发送短信以后的文本倒计时
                 let time = 60;
                 let timer = setInterval(()=>{
                   --time;
                   if(time <=1){
                     // 如果倒计时为0,则关闭当前定时器
                     _this.sms_code_text = "发送验证码";
                     clearInterval(timer);
                     time = 60;
                  }else{
                       _this.sms_code_text = time+"秒";
                  }
                },1000);

              }).catch(error=>{
                 console.log(error);
              })
          },
         show_captcha(){

             // 验证数据!
             if(this.status == 1){ // 用户选择了已有账号
               if(this.username.length<1 || this.password.length<1){
                 return false; // 阻止代码继续往下执行
              }
            }else{ // 用户选择了没有账号

               if( !/1[1-9]{2}\d{8}/.test(this.mobile) ){
                 return false;
              }

               if(this.sms_code.length<1 || this.nickname.length<1){
                 return false;
              }

            }

             // 验证码
             let self = this;
             // 生成一个验证码对象
             var captcha1 = new TencentCaptcha(this.$settings.TC_captcha.app_id, function(res) {
               if (res.ret === 0) {
                 // api服务端校验验证码的结果
                 self.$axios.get(`${self.$settings.Host}/users/captcha/`,{
                   params:{
                     ticket: res.ticket,
                     randstr: res.randstr,
                  }
                }).then(response=>{
                   // 进行登录处理
                   self.bind_account();
                }).catch(error=>{
                   self.$message.error("验证码校验错误!");
                })
              }
            });

             // 显示验证码
             captcha1.show();
          },
         bind_account(){
             // 绑定账号
             let data = {
                 "status": this.status, // 值为1,则属于已有平台账号绑定QQ,值为2,则属于新建用户绑定QQ
                 "username":this.username,
                 "password":this.password,
                 "openid": this.openid,
                 "mobile": this.mobile,
                 "sms_code": this.sms_code,
                 "nickname": this.qq_nickname,
                 "avatar": this.qq_avatar,
            };

             this.$axios.post(this.$settings.Host+"/oauth/qq/login/",data).then(response=>{
                   // 使用浏览器本地存储保存token
                   if (this.remember_me) {
                     // 记住登录
                     sessionStorage.removeItem("user_token");
                     sessionStorage.removeItem("user_id");
                     sessionStorage.removeItem("user_name");
                     sessionStorage.removeItem("user_avatar");
                     sessionStorage.removeItem("user_nickname");
                     localStorage.user_token = response.data.token;
                     localStorage.user_id = response.data.id;
                     localStorage.user_name = response.data.username;
                     localStorage.user_avatar = response.data.avatar;
                     localStorage.user_nickname = response.data.nickname;
                  } else {
                     // 未记住登录
                     localStorage.removeItem("user_token");
                     localStorage.removeItem("user_id");
                     localStorage.removeItem("user_name");
                     localStorage.removeItem("user_avatar");
                     localStorage.removeItem("user_nickname");
                     sessionStorage.user_token = response.data.token;
                     sessionStorage.user_id = response.data.id;
                     sessionStorage.user_name = response.data.username;
                     sessionStorage.user_avatar = response.data.avatar;
                     sessionStorage.user_nickname = response.data.nickname;
                  }

                   this.$confirm('登录成功, 欢迎回来!', '提示', {
                     confirmButtonText: '返回首页',
                     cancelButtonText: '返回上一页',
                     type: 'success'
                  }).then(() => {
                     this.$router.push("/");
                  }).catch(() => {
                     this.$router.go(-1);
                  });

              }).catch(error=>{
                   this.$message.error("登录失败!账号或密码错误!");
                   this.username = "";
                   this.password = "";
              })
          },
      }
  }
</script>

 

服务端提供绑定QQ用户的API接口

  1. 已有平台账号绑定QQ

from rest_framework.views import APIView
from .utils import OAuthQQ,OAuthQQError
from rest_framework.response import Response
from .models import OAuthUser

class OAuthQQAPIView(APIView):
   def get(self, request):
       """生成QQ登录的地址"""
       state = request.query_params.get('state') # 客户端指定的状态
       oauth = OAuthQQ(state=state)
       url = oauth.get_auth_url()
       print(url)
       return Response(url)

from itsdangerous import TimedJSONWebSignatureSerializer as Serializer, BadData
from django.conf import settings
from renranapi.settings import constants
class QQInfoAPIView(APIView):
   def get(self,request):
       """获取QQ用户的信息"""
       # 1. 获取客户端转发过来的QQ登录授权码
       code = request.query_params.get("code")
       state = request.query_params.get('state')  # 客户端指定的状态
       if not code:
           return Response("QQ登录异常!请重新尝试登录!")

       oauth = OAuthQQ(state=state)
       try:
           # 2. 根据授权码到QQ服务器获取access_token
           access_token = oauth.get_access_token(code)
           # 3. 根据access_token获取用户信息[openID]
           openid = oauth.get_open_id(access_token)
           # 3.1 获取用户信息
           user_info = oauth.get_qq_user_info(access_token,openid)
           print(user_info)
           # 4F65D2442D41B0D1639FD7FC14123B11

       except OAuthQQError:
           return Response("QQ登录异常!获取授权信息失败!请重新尝试登录!")

       # 4. 根据openID到数据库中查询用户判断是否属于第一次使用QQ登录
       try:
           oauth_qq_user = OAuthUser.objects.get(openid=openid)
           # 查找到对应的用户记录,证明用户创建我们网站的账号并且已经关联了QQ的openID
           user = oauth_qq_user.user
           # 生成jwt登录token
           from rest_framework_jwt.settings import api_settings
           jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
           jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER

           payload = jwt_payload_handler(user)
           token = jwt_encode_handler(payload)
           user_info = {
               "token":token,
               "id":user.id,
               "username":user.username,
               "avatar":user.avatar.url,
               "nickname":user.nickname,
          }
           return Response({"user_info": user_info,"status": 1})
       except OAuthUser.DoesNotExist:
           # 查找不到对应的用户记录,用户属于第一次使用QQ登录
           # 使用itsdangrous对数据进行加密
           serializer = Serializer(settings.SECRET_KEY, constants.DATA_SIGNATURE_EXPIRE)
           data = serializer.dumps({"openid": openid}).decode()
           return Response({
               "avatar": user_info.get("figureurl_qq_1"),
               "nickname": user_info.get("nickname"),
               "data": data,
               "status": 0,
          })


from rest_framework import status as http_status
from users.utils import UsernameMobileAuthBackend
from .models import OAuthUser

class BindQQUserAPIView(APIView):
   def post(self,request):
       """绑定QQ用户账号"""
       """
        "status": this.status, // 值为1,则属于已有平台账号绑定QQ,值为2,则属于新建用户绑定QQ
        "username":this.username,
        "password":this.password,
        "openid": this.openid,
        "mobile": this.mobile,
        "sms_code": this.sms_code,
        "nickname": this.qq_nickname,
        "avatar": this.qq_avatar,
      """
       status = request.data.get("status")
       if status != 1 and status != 2:
           return Response("绑定账号数据出错!", status=http_status.HTTP_400_BAD_REQUEST)

       if status == 1:
           """已有账号绑定QQ"""
           username = request.data.get("username")
           password = request.data.get("password")

           # 账号密码校验
           Authbackend = UsernameMobileAuthBackend()
           user = Authbackend.authenticate(request, username, password)
           if user is None:
               return Response("账号或密码错误!", status=http_status.HTTP_400_BAD_REQUEST)

           try:
               data = request.data.get("openid")
               serializer = Serializer(settings.SECRET_KEY, constants.DATA_SIGNATURE_EXPIRE)
               ret = serializer.loads(data)
               openid = ret.get("openid")
               OAuthUser.objects.create(user=user,openid=openid)
           except:
               return Response("账号绑定超时,请重新登录绑定!", status=http_status.HTTP_400_BAD_REQUEST)

           # 生成jwt登录token
           from rest_framework_jwt.settings import api_settings
           jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
           jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER

           payload = jwt_payload_handler(user)
           token = jwt_encode_handler(payload)
           user_info = {
               "token":token,
               "id":user.id,
               "username":user.username,
               "avatar":user.avatar.url,
               "nickname":user.nickname,
          }
           return Response({"user_info": user_info,"status": 1})

       else:
           """注册账号后绑定QQ"""
           pass

路由:

from django.urls import path
from . import views
urlpatterns = [
   path("qq/url/", views.OAuthQQAPIView.as_view() ),
   path("qq/info/", views.QQInfoAPIView.as_view() ),
   path("qq/login/", views.BindQQUserAPIView.as_view() ),
]
  1. 注册平台账号绑定QQ

from rest_framework.views import APIView
from .utils import OAuthQQ,OAuthQQError
from rest_framework.response import Response
from .models import OAuthUser

class OAuthQQAPIView(APIView):
   def get(self, request):
       """生成QQ登录的地址"""
       state = request.query_params.get('state') # 客户端指定的状态
       oauth = OAuthQQ(state=state)
       url = oauth.get_auth_url()
       print(url)
       return Response(url)

from itsdangerous import TimedJSONWebSignatureSerializer as Serializer, BadData
from django.conf import settings
from renranapi.settings import constants
class QQInfoAPIView(APIView):
   def get(self,request):
       """获取QQ用户的信息"""
       # 1. 获取客户端转发过来的QQ登录授权码
       code = request.query_params.get("code")
       state = request.query_params.get('state')  # 客户端指定的状态
       if not code:
           return Response("QQ登录异常!请重新尝试登录!")

       oauth = OAuthQQ(state=state)
       try:
           # 2. 根据授权码到QQ服务器获取access_token
           access_token = oauth.get_access_token(code)
           # 3. 根据access_token获取用户信息[openID]
           openid = oauth.get_open_id(access_token)
           # 3.1 获取用户信息
           user_info = oauth.get_qq_user_info(access_token,openid)
           print(user_info)
           # 4F65D2442D41B0D1639FD7FC14123B11

       except OAuthQQError:
           return Response("QQ登录异常!获取授权信息失败!请重新尝试登录!")

       # 4. 根据openID到数据库中查询用户判断是否属于第一次使用QQ登录
       try:
           oauth_qq_user = OAuthUser.objects.get(openid=openid)
           # 查找到对应的用户记录,证明用户创建我们网站的账号并且已经关联了QQ的openID
           user = oauth_qq_user.user
           # 生成jwt登录token
           from rest_framework_jwt.settings import api_settings
           jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
           jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER

           payload = jwt_payload_handler(user)
           token = jwt_encode_handler(payload)
           user_info = {
               "token":token,
               "id":user.id,
               "username":user.username,
               "avatar":user.avatar.url,
               "nickname":user.nickname,
          }
           return Response({"user_info": user_info,"status": 1})
       except OAuthUser.DoesNotExist:
           # 查找不到对应的用户记录,用户属于第一次使用QQ登录
           # 使用itsdangrous对数据进行加密
           serializer = Serializer(settings.SECRET_KEY, constants.DATA_SIGNATURE_EXPIRE)
           data = serializer.dumps({"openid": openid}).decode()
           return Response({
               "avatar": user_info.get("figureurl_qq_1"),
               "nickname": user_info.get("nickname"),
               "data": data,
               "status": 0,
          })


from rest_framework import status as http_status
from users.utils import UsernameMobileAuthBackend
from .models import OAuthUser
from users.models import User

class BindQQUserAPIView(APIView):
   def post(self,request):
       """绑定QQ用户账号"""
       status = request.data.get("status")
       if status != 1 and status != 2:
           return Response("绑定账号数据出错!", status=http_status.HTTP_400_BAD_REQUEST)

       if status == 1:
           """已有账号绑定QQ"""
           username = request.data.get("username")
           password = request.data.get("password")

           # 账号密码校验
           Authbackend = UsernameMobileAuthBackend()
           user = Authbackend.authenticate(request, username, password)
           if user is None:
               return Response("账号或密码错误!", status=http_status.HTTP_400_BAD_REQUEST)

           try:
               data = request.data.get("openid")
               serializer = Serializer(settings.SECRET_KEY, constants.DATA_SIGNATURE_EXPIRE)
               ret = serializer.loads(data)
               openid = ret.get("openid")
               OAuthUser.objects.create(user=user,openid=openid)
           except:
               return Response("账号绑定超时,请重新登录绑定!", status=http_status.HTTP_400_BAD_REQUEST)

       else:
           """注册账号后绑定QQ"""
           """
            "password":this.password,
            "openid": this.openid,
            "mobile": this.mobile,
            "sms_code": this.sms_code,
            "nickname": this.qq_nickname,
            "avatar": this.qq_avatar,
          """
           password = request.data.get("password")
           openid_data = request.data.get("openid")
           mobile = request.data.get("mobile")
           sms_code = request.data.get("sms_code")
           nickname = request.data.get("nickname")
           avatar = request.data.get("avatar")
           # 判断数据是否完整
           if len(password) > 16 and len(password)<6:
               return Response("密码长度有误!", status=http_status.HTTP_400_BAD_REQUEST)

           # todo 判断手机格式是否正确
           # todo 判断手机验证码是否正确

           # 判断 昵称是否被占用
           try:
               User.objects.get(nickname=nickname)
               return Response("当前昵称已经被使用!", status=http_status.HTTP_400_BAD_REQUEST)
           except User.DoesNotExist:
               pass

           # todo 判断 手机是否被占用
           try:
               User.objects.get(mobile=mobile)
               return Response("当前手机已经被使用!", status=http_status.HTTP_400_BAD_REQUEST)
           except User.DoesNotExist:
               pass

           # 判断openid是否超时或者被篡改
           try:
               serializer = Serializer(settings.SECRET_KEY, constants.DATA_SIGNATURE_EXPIRE)
               ret = serializer.loads(openid_data)
               openid = ret.get("openid")
           except:
               return Response("账号绑定超时,请重新登录绑定!", status=http_status.HTTP_400_BAD_REQUEST)

           # 判断openid是否绑定了别的账户
           try:
               OAuthUser.objects.get(openid=openid)
               return Response("当前QQ已经绑定别的账号,请使用账号进行登录!", status=http_status.HTTP_400_BAD_REQUEST)
           except OAuthUser.DoesNotExist:
               pass

           # 注册用户信息
           user = User.objects.create_user(username=mobile, mobile=mobile, nickname=nickname, password=password)
           # 新用户绑定QQ账号
           OAuthUser.objects.create(user=user, openid=openid)

       # 生成jwt登录token
       from rest_framework_jwt.settings import api_settings
       jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
       jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER

       payload = jwt_payload_handler(user)
       token = jwt_encode_handler(payload)
       user_info = {
           "token": token,
           "id": user.id,
           "username": user.username,
           "avatar": user.avatar.url,
           "nickname": user.nickname,
      }
       return Response({"user_info": user_info, "status": 1})

前面为了快速实现功能,我们只保存了用户的openid,接下来我们继续吧access_token,

作业: 给OauthUser模型新增3个字段,保存access_token, expires_in,refresh_token.

 

posted on   rider_yang  阅读(1672)  评论(0编辑  收藏  举报
编辑推荐:
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
点击右上角即可分享
微信分享提示