我的网站搭建(第23天) 第三方登录:QQ

  • 发布时间:2016年7月27日 14:08
  • 作者:杨仕航

6月底的时候,我就想加上用QQ登录的功能。跑去QQ互联查看官方文档(一下被文档淹没了),注册了QQ互联、查了其他人写的代码、填了不少坑,终于写出来了。(工作也比较忙,实际开发差不多几天时间)。坑填好了,写出来,让更少人掉到坑里面。

一开始设想是用QQ登录之后,获取QQ邮箱。用这个QQ邮箱作为账号名登录或者注册。

在开发的过程中,发现获取不到QQ号,只能获取一个OpenID的东西。最后采取存储这个OpenID并绑定对应账号的方式。

所以需要创建对应的模型,即创建一个应用管理第三方登录。QQ登录功能开发流程如下图(结合你的网站设计可能不太一样):


第1步、QQ互联注册网站应用

打开QQ互联,进入管理中心。注册一下应用开发者,并添加网站应用。这里比较简单,需要注意的地方是网站信息。

网站地址需要验证一下,按照提示处理即可。

对于刚接触这个第三方登录的新手,最难理解的就是回调地址。回调地址不要填网站域名。

理解回调地址需要了解一下OAuth协议。

在你的网站页面里面,打开授权页面(这个授权页面不是回调地址)。在授权页面里面,登录QQ并确认授权。

授权之后,会得到一个授权码。回调地址就是用于接收这个授权码。

授权码以GET的方式返回,例如 http://yshblog.com/oauth/qq_check?code=xxxxxxxxxxxxx

通过这种方式,可以获取授权码,所以需要提供一个地址。这个地址先写一个暂时没有的地址,后面开发的时候,再给这个地址写对应的响应方法。


第2步、放置QQ按钮

这个QQ按钮是提供QQ登录的入口。从腾讯提供的QQ按钮下载放到你的登录页面即可。

不用看帮助文档里面的什么前端代码。这些用不上,只需要加一个固定的访问链接,再重定向即可。

我前端页面此处的代码如下:

<div>
    <span>其它账号登录:</span>
    <a href="{% url 'qq_login' %}" title="QQ登录">
        <img src="/static/img/connect/logo_qq.png" alt="QQ登录">
    </a>
</div>

qq_login链接在下面第3步创建oauth应用里面设置。


第3步、创建oauth应用

怎么创建应用就不细说了,这是基本功。我创建的应用名为oauth,打算后面把微博登录、Github登录什么都放到这里。

创建完成之后,打开models.py文件,编写模型:

#coding:utf-8
from django.db import models
from django.contrib.auth.models import User

class OAuth_ex(models.Model):
    """User models ex"""
    user = models.ForeignKey(User)   #和User关联的外键
    qq_openid = models.CharField(max_length = 64) #QQ的关联OpenID

    def __unicode__(self):
        return u'<%s>' % (self.user)

该模型用于存储QQ登录返回的OpenID值。这个OpenID值是用QQ号一一对应。腾讯不给得到真实QQ号可能是出于保护隐私的考虑。


再打开urls.py文件,先写一下需要哪些链接地址:

from django.conf.urls import include, url

#http://localhost:8000/oauth/
#start with 'oauth/'
urlpatterns = [
    url(r'qq_login', 'oauth.views.qq_login', name='qq_login'),
    url(r'qq_check', 'oauth.views.qq_check', name='qq_check'),

    url(r'bind_email', 'oauth.views.bind_email', name='bind_email'),
]

qq_login和qq_check前面有说到,分别是打开授权页面和回调地址。

bind_email是绑定邮箱的页面。

大致思路是授权之后,得到OpenID。判断这个OpenID是否存在数据库中。若存在,则直接登录对应的用户即可;若不存在,则打开这个绑定邮箱页面,绑定对应的用户。

同时,在总的urls路由中,加入这个应用路由。(总路由在和工程名一样的文件夹中的urls.py文件。我比较喜欢这种方式,对urls管理比较清晰)

urlpatterns = [
    ...
    url(r'^oauth/',include('oauth.urls')),
]

这个路由控制,大家根据自己的工程自己写即可。


第4步、开发OAuth登录功能

该部分写的代码有点多,纠结怎么写博文才能比较好的表达式思路。先采取拆散代码讲解,最后再给出完整代码。

为了管理好OAuth,在oauth应用的文件夹下创建oauth_client.py文件。把相关的OAuth操作方法集成在一起。编辑oauth_client.py文件:

#coding:utf-8
import json
import urllib, urllib2, urlparse

class OAuth_QQ():
    def __init__(self, client_id, client_key, redirect_uri):
        self.client_id = client_id
        self.client_key = client_key
        self.redirect_uri = redirect_uri

    def get_auth_url(self):
        """获取授权页面的网址"""
        params = {'client_id': self.client_id,
                  'response_type': 'code',
                  'redirect_uri': self.redirect_uri,
                  'scope': 'get_user_info',
                  'state': 1}
        url = 'https://graph.qq.com/oauth2.0/authorize?%s' % urllib.urlencode(params)
        return url

创建一个类,需要申请QQ登录的APP_ID、APP_KEY和回调地址。这些都是固定的,我把这几个常量放入到settings.py中。settings.py添加如下常量,具体的值请在你的申请页面查找:

#OAuth设置
QQ_APP_ID = 'xxxxx'
QQ_KEY = 'xxxxxxxxxxxxxxxxxx'
QQ_RECALL_URL = 'http://yshblog.com/oauth/qq_check'

回到OAuth_QQ类,现里面有个get_auth_url方法。该方法是获取打开授权页面的链接地址。(可参考官方帮助,写得不够清晰)


接着,在编辑oauth应用的views.py文件,加入qq_login对应的响应方法:

#coding:utf-8
from django.http import HttpResponseRedirect
from django.conf import settings
from oauth_client import OAuth_QQ

#http://yshblog.com/oauth/qq_login
def qq_login(request):
    oauth_qq = OAuth_QQ(settings.QQ_APP_ID, settings.QQ_KEY, settings.QQ_RECALL_URL)

    #获取 得到Authorization Code的地址
    url = oauth_qq.get_auth_url()
    #重定向到授权页面
    return HttpResponseRedirect(url)

到这里为止,就完成了点击QQ登录按钮,跳转到授权页面。

登录授权之后,授权页面会自动跳转到我们设置的回调地址。例如 http://yshblog.com/oauth/qq_check?code=xxxxxxxxxxxxx

我们可以获取这个地址上面的GET参数。先假设我们可以顺利获取到,继续完善OAuth_QQ类。拿到这个授权码之后,需要用该码获取腾讯的access_token通行令牌。(认证步骤有点多,好麻烦)


打开oauth_client.py文件,在OAuth_QQ类添加如下方法:

def get_access_token(self, code):
    """根据code获取access_token"""
    params = {'grant_type': 'authorization_code',
              'client_id': self.client_id,
              'client_secret': self.client_key,
              'code': code,
              'redirect_uri': self.redirect_uri}
    url = 'https://graph.qq.com/oauth2.0/token?%s' % urllib.urlencode(params)

    #访问该网址,获取access_token
    response = urllib2.urlopen(url).read()
    result = urlparse.parse_qs(response, True)

    access_token = str(result['access_token'][0])
    self.access_token = access_token
    return access_token

该方法使用了urllib2,在服务器后台访问对应的链接,获取access_token,并返回该值。因为我后续不需要用access_token做其他动作,直接一次性获取QQ昵称和OpenID。所以不用记录这个通行令牌的有效期。


得到这个access_token之后,就可以做其他事了。首先需要获取授权用户的OpenID,本来这里我想获取QQ号的,而腾讯不允许。只好退而求次,获取并保存OpenID。可参考官方文档

继续给这个OAuth_QQ添加获取OpenID的方法和使用OpenID获取QQ基本信息的方法:

def get_open_id(self):
    """获取QQ的OpenID"""
    params = {'access_token': self.access_token}
    url = 'https://graph.qq.com/oauth2.0/me?%s' % urllib.urlencode(params)

    response = urllib2.urlopen(url).read()
    v_str = str(response)[9:-3] #去掉callback的字符
    v_json = json.loads(v_str)

    openid = v_json['openid']
    self.openid = openid
    return openid

def get_qq_info(self):
    """获取QQ用户的资料信息"""
    params = {'access_token': self.access_token,
              'oauth_consumer_key': self.client_id,
              'openid': self.openid}
    url = 'https://graph.qq.com/user/get_user_info?%s' % urllib.urlencode(params)

    response = urllib2.urlopen(url).read()
    return json.loads(response)

这里有一个坑,腾讯返回OpenID和QQ基本信息的内容格式都不一样。


再回头编辑views.py,添加回调地址的处理方法:

#coding:utf-8
from django.http import HttpResponseRedirect
from django.core.urlresolvers import reverse #url逆向解析

from django.contrib.auth.models import User
from django.contrib.auth import logout,authenticate,login
from django.conf import settings

from oauth_client import OAuth_QQ
from oauth.models import OAuth_ex
from oauth.forms import BindEmail

import time

def qq_check(request):
    """登录之后,会跳转到这里。需要判断code和state"""
    request_code = request.GET.get('code')
    oauth_qq = OAuth_QQ(settings.QQ_APP_ID, settings.QQ_KEY, settings.QQ_RECALL_URL)

    #获取access_token
    access_token = oauth_qq.get_access_token(request_code)
    time.sleep(0.05)	#稍微休息一下,避免发送urlopen的10060错误
    open_id = oauth_qq.get_open_id()    

    #检查open_id是否存在
    qqs = OAuth_ex.objects.filter(qq_openid = open_id)
    if qqs:
        #存在则获取对应的用户,并登录
        user = qqs[0].user
        
        #设置backend,绕开authenticate验证
        setattr(user, 'backend', 'django.contrib.auth.backends.ModelBackend')
        
        login(request, user)
        return HttpResponseRedirect('/')
    else:
        #不存在,则跳转到绑定邮箱页面
        infos = oauth_qq.get_qq_info()    #获取用户信息
        url = '%s?open_id=%s&nickname=%s' % (reverse('bind_email'), open_id, infos['nickname'])
        return HttpResponseRedirect(url)

这里有两个坑在等着大家。

按照思路,授权之后,调整到处理授权结果的页面。获取授权码之后,用get_access_token方法得到access_token。

再用access_token获取OpenID。第一个坑出现了,若不加time.sleep(0.05)休息一下的话,会得到urlopen 10060错误。

获取到open_id之后,再判断一下数据库中是否存在。若存在,则已经关联对应的用户了,直接登录该用户。

第二个坑跳出来了,login方法登录需要先进行authenticate方法验证。authenticate方法验证需要知道用户名和密码,而此处的密码是无法获取到明文密码。找了很多资料,发现authenticate方法得到的user对象和普通的user对象多了一个backend参数。手动设置一下这个参数,则可以顺利使用login方法登录该用户。

若open_id不存在,则跳转到绑定邮箱的页面。该页面需要知道open_id和QQ昵称(为什么需要QQ昵称,下一步会提到)。通过GET方式,把这两个参数写在链接上即可传递过去。


这里还需要提一下,本地调试的方法。因为授权之后是调整到部署之后的网站上,而部署的网站还没开发响应的代码,无法响应对应的地址。查到可以修改host方法进行本地调试,不过有点麻烦。可以这样:先本地打开授权页面授权,得到一个回调地址。回调地址上有授权码,如下图:

然后,复制授权码,手动写上对应的本地回调地址,替换其中的code参数值。如下图:

即可实现本地调试。


第5步、绑定用户

上面提到若open_id在数据库中不存在,则打开绑定用户页面。该页面我设计成form表单,在oauth应用下新建forms.py文件。如下代码:

#coding:utf-8
from django import forms
from django.core.exceptions import ValidationError
from django.contrib.auth.models import User
from django.contrib.auth import authenticate

from oauth.models import OAuth_ex

class BindEmail(forms.Form):
    """bind the openid to email"""
    qq_openid = forms.CharField(widget=forms.HiddenInput(attrs={'id':'qq_openid'}))
    qq_nickname = forms.CharField(widget=forms.HiddenInput(attrs={'id':'qq_nickname'}))
    email = forms.EmailField(label=u'注册邮箱', 
        widget=forms.EmailInput(attrs={'class':'form-control', 'id':'email','placeholder':u'请输入您注册用的邮箱'}))
    pwd = forms.CharField(label=u'用户密码', max_length=36,
        widget=forms.PasswordInput(attrs={'class':'form-control', 'id':'pwd','placeholder':u'若尚未注册过,该密码则作为用户密码'}))

    #验证邮箱
    def clean_email(self):
        qq_openid = self.cleaned_data.get('qq_openid')
        email = self.cleaned_data.get('email')
        users = User.objects.filter(email = email)
        
        if users:
            #判断是否被绑定了
            if OAuth_ex.objects.filter(user = users[0]):
                raise ValidationError(u'该邮箱已经被绑定了')
        return email

    #验证密码
    def clean_pwd(self):
        email = self.cleaned_data.get('email')
        pwd = self.cleaned_data.get('pwd')

        users = User.objects.filter(email = email)
        if users:
            #若用户存在,判断密码是否正确
            user = authenticate(username=email, password=pwd)
            if user is not None:
                return pwd
            else:
                return ValidationError(u'密码不正确,不能绑定')

该表单有4个字段。open_id和QQ昵称用hidden字段隐藏。另外邮箱和密码是验证对应用户的。

接着,在views.py继续编辑,添加表单处理的对应方法:

#coding:utf-8
from django.http import HttpResponse,HttpResponseRedirect
from django.shortcuts import render_to_response, render
from django.core.urlresolvers import reverse #url逆向解析

from django.contrib.auth.models import User
from django.contrib.auth import logout,authenticate,login
from django.conf import settings

from oauth_client import OAuth_QQ
from oauth.models import OAuth_ex
from oauth.forms import BindEmail

from user_ex.views import get_active_code, send_active_email
import time

def bind_email(request):
    open_id = request.GET.get('open_id')
    nickname = request.GET.get('nickname')
    data = {}

    data['form_title'] = u'绑定用户'
    data['submit_name'] = u' 确定 '
    data['form_tip'] = u'Hi, <span class="label label-info"><img src="/static/img/connect/logo_qq.png">%s</span>!您已登录。请绑定用户,完成QQ登录' % nickname

    if request.method == 'POST':
        #表单提交
        form = BindEmail(request.POST)
        
        #验证是否合法
        if form.is_valid():
            #判断邮箱是否注册了
            qq_openid = form.cleaned_data['qq_openid']
            qq_nickname = form.cleaned_data['qq_nickname']
            email = form.cleaned_data['email']
            pwd = form.cleaned_data['pwd']

            users = User.objects.filter(email = email)
            if users:
                #用户存在,则直接绑定
                user = users[0]
                if not user.first_name:
                    user.first_name = qq_nickname    #更新昵称
                    user.save()
                data['message'] = u'绑定账号成功,绑定到%s”' % email
            else:
                #用户不存在,则注册,并发送激活邮件
                user=User(username=email, email=email)
                user.first_name = qq_nickname #使用QQ昵称作为昵称
                user.set_password(pwd)
                user.is_active=False
                user.save()

                #发送激活邮件
                try:
                    active_code=get_active_code(email)
                    send_active_email(email,active_code)
                except:
                    pass

                data['message'] = u'绑定账号成功,绑定到%s。<br>并发送激活邮件到该邮箱”' % email

            #绑定用户并
            oauth_ex = OAuth_ex(user = user, qq_openid = qq_openid)
            oauth_ex.save()
            
            #登录用户
            user = authenticate(username=email, password=pwd)
            if user is not None:
                login(request, user)

            #页面提示
            data['goto_url'] = '/'
            data['goto_time'] = 3000
            data['goto_page'] = True
            
            return render_to_response('message.html',data)
    else:
        #正常加载
        form = BindEmail(initial={
            'qq_openid': open_id,
            'qq_nickname': nickname,
            })
    data['form'] = form
    return render(request, 'form.html', data)

此处的处理逻辑是判断邮箱是否注册了,若注册了就判断密码是否正确,正确就绑定;不正确就提示。

若没有注册,则直接使用邮箱和密码注册用户,发送激活码。(发送激活邮件可参考《我的网站搭建(第15天) 注册认证》)

注册的时候,我是采用邮箱作为账号。昵称可以直接使用QQ昵称。当然这里还需要把QQ昵称显示出来,要不然申请QQ登录审核通不过。

这里使用到两个模版form.html和message.html。

form.html是通用的表单模版:

{% extends "base.html" %}

{% block title %}杨仕航的博客{% endblock %}

{% block content %}
    {#通用表单页面#}
    <div class="row">
        <div class="col-md-6 col-md-offset-3" >
            <div id="panel_form" class="panel panel-default">
            <div class="panel-body">
            <h3 class="form_title">{{form_title}}</h3>
            <p>{{form_tip|safe}}</p>
            <form class="main_form" method='post'>
                {%csrf_token%}
                {% for field in form %}
                    {# 区分是否是hidden字段 #}
                    {% if field.is_hidden %}
                        {{ field }}
                    {% else %}
                        <div class="input-group">
                            <label class="input-group-addon" for="{{ field.id_for_label }}">
                                {{ field.label }}
                            </label>
                            {{ field }}
                        </div>
                        {# 错误提示信息 #}
                        <p class="text-danger text-right">
                            {{ field.errors.as_text }}
                        </p>
                    {%endif%}
                {%endfor%}

                <div class="text-right">
                    <input class="btn btn-primary" id="btn_submit" type="submit" value="{{submit_name}}"/>
                </div>
            </form>
            </div>
            </div>
        </div>
    </div>
{% endblock %}

{% block extra_footer %}
    <style type="text/css">
        .form_title{
            margin-bottom: 1em;
            padding-bottom: 0.5em;
            border-bottom: 1px #ccc solid;
        }
        .main_form div{
            margin-top:1em;
        }
        #btn_submit{
            margin-top: 1em;
        }
        #panel_form{
            margin-top: 2em;
            margin-bottom: 3em;
        }
    </style>
{% endblock %}

message.html是显示消息用的模版:

{% extends "base.html" %}

{% block title %}杨仕航的博客{% endblock %}

{% block content %}
    {#显示消息的页面#}
    <div class="row">
        <center><h4 style="line-height:300%;">{{message|safe}}</h4></center>
    </div>
{% endblock %}

{% block extra_footer %}
    <script type="text/javascript">
        {#页面调整控制#}
        {% if goto_page %}
            $(function(){
                window.setTimeout(function(){ 
                    window.location = '{{goto_url}}';
                },{{goto_time}});
            });
        {% endif %}
    </script>
{% endblock %}

绑定用户的界面如下:

写完代码之后,本地测试可以通过。最后再部署到服务器并在QQ互联提交审核。一般审核要1~2天左右。若审核不通过,又不明白审核说明,就直接找客服问问


-----<我是分割线,下面是view.py和oauth_client.py完整的代码>-----

views.py:

#coding:utf-8
from django.http import HttpResponse,HttpResponseRedirect
from django.shortcuts import render_to_response, render
from django.core.urlresolvers import reverse #url逆向解析

from django.contrib.auth.models import User
from django.contrib.auth import logout,authenticate,login
from django.conf import settings

from oauth_client import OAuth_QQ
from oauth.models import OAuth_ex
from oauth.forms import BindEmail

from user_ex.views import get_active_code, send_active_email
import time

#http://yshblog.com/oauth/qq_login
def qq_login(request):
    oauth_qq = OAuth_QQ(settings.QQ_APP_ID, settings.QQ_KEY, settings.QQ_RECALL_URL)

    #获取 得到Authorization Code的地址
    url = oauth_qq.get_auth_url()
    #重定向到授权页面
    return HttpResponseRedirect(url)

def qq_check(request):
    """登录之后,会跳转到这里。需要判断code和state"""
    request_code = request.GET.get('code')
    oauth_qq = OAuth_QQ(settings.QQ_APP_ID, settings.QQ_KEY, settings.QQ_RECALL_URL)

    #获取access_token
    access_token = oauth_qq.get_access_token(request_code)
    time.sleep(0.05)	#稍微休息一下,避免发送urlopen的10060错误
    open_id = oauth_qq.get_open_id()    

    #检查open_id是否存在
    qqs = OAuth_ex.objects.filter(qq_openid = open_id)
    if qqs:
        #存在则获取对应的用户,并登录
        user = qqs[0].user
        
        #设置backend,绕开authenticate验证
        setattr(user, 'backend', 'django.contrib.auth.backends.ModelBackend')
        
        login(request, user)
        return HttpResponseRedirect('/')
    else:
        #不存在,则跳转到绑定邮箱页面
        infos = oauth_qq.get_qq_info()    #获取用户信息
        url = '%s?open_id=%s&nickname=%s' % (reverse('bind_email'), open_id, infos['nickname'])
        return HttpResponseRedirect(url)

def bind_email(request):
    open_id = request.GET.get('open_id')
    nickname = request.GET.get('nickname')
    data = {}

    data['form_title'] = u'绑定用户'
    data['submit_name'] = u' 确定 '
    data['form_tip'] = u'Hi, <span class="label label-info"><img src="/static/img/connect/logo_qq.png">%s</span>!您已登录。请绑定用户,完成QQ登录' % nickname

    if request.method == 'POST':
        #表单提交
        form = BindEmail(request.POST)
        
        #验证是否合法
        if form.is_valid():
            #判断邮箱是否注册了
            qq_openid = form.cleaned_data['qq_openid']
            qq_nickname = form.cleaned_data['qq_nickname']
            email = form.cleaned_data['email']
            pwd = form.cleaned_data['pwd']

            users = User.objects.filter(email = email)
            if users:
                #用户存在,则直接绑定
                user = users[0]
                if not user.first_name:
                    user.first_name = qq_nickname    #更新昵称
                    user.save()
                data['message'] = u'绑定账号成功,绑定到%s”' % email
            else:
                #用户不存在,则注册,并发送激活邮件
                user=User(username=email, email=email)
                user.first_name = qq_nickname #使用QQ昵称作为昵称
                user.set_password(pwd)
                user.is_active=False
                user.save()

                #发送激活邮件
                try:
                    active_code=get_active_code(email)
                    send_active_email(email,active_code)
                except:
                    pass

                data['message'] = u'绑定账号成功,绑定到%s。<br>并发送激活邮件到该邮箱”' % email

            #绑定用户并
            oauth_ex = OAuth_ex(user = user, qq_openid = qq_openid)
            oauth_ex.save()
            
            #登录用户
            user = authenticate(username=email, password=pwd)
            if user is not None:
                login(request, user)

            #页面提示
            data['goto_url'] = '/'
            data['goto_time'] = 3000
            data['goto_page'] = True
            
            return render_to_response('message.html',data)
    else:
        #正常加载
        form = BindEmail(initial={
            'qq_openid': open_id,
            'qq_nickname': nickname,
            })
    data['form'] = form
    return render(request, 'form.html', data)

oauth_client.py:

#coding:utf-8
import json
import urllib, urllib2, urlparse

class OAuth_QQ():
    def __init__(self, client_id, client_key, redirect_uri):
        self.client_id = client_id
        self.client_key = client_key
        self.redirect_uri = redirect_uri

    def get_auth_url(self):
        """获取授权页面的网址"""
        params = {'client_id': self.client_id,
                  'response_type': 'code',
                  'redirect_uri': self.redirect_uri,
                  'scope': 'get_user_info',
                  'state': 1}
        url = 'https://graph.qq.com/oauth2.0/authorize?%s' % urllib.urlencode(params)
        return url

    def get_access_token(self, code):
        """根据code获取access_token"""
        params = {'grant_type': 'authorization_code',
                  'client_id': self.client_id,
                  'client_secret': self.client_key,
                  'code': code,
                  'redirect_uri': self.redirect_uri}
        url = 'https://graph.qq.com/oauth2.0/token?%s' % urllib.urlencode(params)

        #访问该网址,获取access_token
        response = urllib2.urlopen(url).read()
        result = urlparse.parse_qs(response, True)

        access_token = str(result['access_token'][0])
        self.access_token = access_token
        return access_token

    def get_open_id(self):
        """获取QQ的OpenID"""
        params = {'access_token': self.access_token}
        url = 'https://graph.qq.com/oauth2.0/me?%s' % urllib.urlencode(params)

        response = urllib2.urlopen(url).read()
        v_str = str(response)[9:-3] #去掉callback的字符
        v_json = json.loads(v_str)

        openid = v_json['openid']
        self.openid = openid
        return openid

    def get_qq_info(self):
        """获取QQ用户的资料信息"""
        params = {'access_token': self.access_token,
                  'oauth_consumer_key': self.client_id,
                  'openid': self.openid}
        url = 'https://graph.qq.com/user/get_user_info?%s' % urllib.urlencode(params)

        response = urllib2.urlopen(url).read()
        return json.loads(response)

上一篇:Python2和3兼容写法

下一篇:树莓派控制LED闪烁和呼吸

评论列表

杨仕航

杨仕航

接下来,再试试新浪微博和github的第三方登录

2016-07-27 14:22 回复

新的评论

清空

猜你喜欢

  • 猜测中,请稍等...