一.成为QQ互联的开发者
注册链接:http://wiki.connect.qq.com/%E6%88%90%E4%B8%BA%E5%BC%80%E5%8F%91%E8%80%85
等个3-5 天的工作日审核,审核通过就称为一个开发者了
二.创建应用
在应用管理中创建一个应用,得到APPID
填写相关资料,审核通过得到APPKEY,记录之前的回调地址
三.项目实现
浏览器--> 服务器 -- > QQ服务器
1.浏览器用Vue的axios方法,GET请求服务器获取登录QQ服务器的网址(浏览器-->服务器)
2.服务器拼接url路径返回给浏览器(服务器-->浏览器)
3.浏览器用response接收到的url路径访问QQ服务器。(浏览器-->QQ服务器)
4.QQ服务器返回code值,浏览器截取code值返回给服务器(QQ服务器-->浏览器,浏览器-->服务器)
5.服务器获取浏览器发送过来的code值,拼接字符串重新访问QQ服务器(服务器-->QQ服务器)
6.QQ服务器返回access_token给服务器(QQ服务器-->服务器)
7.服务器获取响应中的access_token值,拼接字符串给QQ服务器(服务器-->QQ服务器)
8.QQ服务器返回openid(用户唯一身份标识)给服务器(QQ服务器-->服务器)
QQ登录开发文档链接:http://wiki.connect.qq.com/%E5%87%86%E5%A4%87%E5%B7%A5%E4%BD%9C_oauth2-0
前端代码放置QQ按钮,访问服务器
HTML
<div class="third_party">
<a @click="qq_login" class="qq_login">QQ</a>
<a href="#" class="weixin_login">微信</a>
<a href="/register.html" class="register_btn">立即注册</a>
</div>
JS
qq_login: function(){
var state = this.get_query_string('next') || '/';
axios.get(this.host + '/oauth/qq/statues/?state=' + state, {
responseType: 'json'
})
.then(response => {
location.href = response.data.auth_url;
})
.catch(error => {
console.log(error.response.data);
})
},
通过前端axios请求GET方法访问服务器,服务器返回拼接好的URL
class QQAuthURLView(APIView):
"""
请求方式: GET /oauth/qq/statues/
"""
def get(self, request):
# 生成auth_url
# https://graph.qq.com/oauth2.0/authorize?xxx=xxx
# 请求参数请包含如下内容:
# response_type 必须 授权类型,此值固定为“code”。
# client_id 必须 申请QQ登录成功后,分配给应用的appid。
# redirect_uri 必须 成功授权后的回调地址,必须是注册appid时填写的主域名下的地址,建议设置为网站首页或网站的用户中心。注意需要将url进行URLEncode。
# state 必须 client端的状态值。用于第三方应用防止CSRF攻击,成功授权后回调时会原样带回。请务必严格按照流程检查用户与state参数状态的绑定。
# scope 可选 scope=get_user_info
state = request.query_params.get('state')
# 1. base_url
# GET www.baidu.com/a.html?a=xxx&b=xxx
# ? 是为了将路径和参数进行分割
base_url = 'https://graph.qq.com/oauth2.0/authorize?'
# 2. 将参数放在字典中
params = {
'response_type': 'code',
'client_id': settings.QQ_APP_ID,
'redirect_uri': settings.QQ_REDIRECT_URL,
'state': 'test',
}
# 3.
# 将query字典转换为url路径中的查询字符串
auth_url = base_url + urlencode(params)
return Response({'auth_url': auth_url})
前端根据返回的auth_url的response访问QQ服务器,服务器返回code给回调函数页面,前端获取code并将其传给服务器
JS
mounted: function(){
// 从路径中获取qq重定向返回的code
var code = this.get_query_string('code');
axios.get(this.host + '/oauth/qq/users/?code=' + code, {
responseType: 'json',
})
.then(response => {
})
.catch(error => {
console.log(error.response.data);
alert('服务器异常');
})
},
服务器接收到code,拼接字符串用urlopen访问QQ服务器
class QQTokenView(APIView):
def get(self, request):
# PC网站:https://graph.qq.com/oauth2.0/token
# GET
# grant_type 必须 授权类型,在本步骤中,此值为“authorization_code”。
# client_id 必须 申请QQ登录成功后,分配给网站的appid。
# client_secret 必须 申请QQ登录成功后,分配给网站的appkey。
# code 必须 上一步返回的authorization
# redirect_uri 必须 与上面一步中传入的redirect_uri保持一致。
code = request.query_params.get('code')
if code is None:
return Response(status=status.HTTP_400_BAD_REQUEST)
# PC网站:https://graph.qq.com/oauth2.0/token
# GET
# grant_type 必须 授权类型,在本步骤中,此值为“authorization_code”。
# client_id 必须 申请QQ登录成功后,分配给网站的appid。
# client_secret 必须 申请QQ登录成功后,分配给网站的appkey。
# code 必须 上一步返回的authorization
# redirect_uri 必须 与上面一步中传入的redirect_uri保持一致。
base_url = 'https://graph.qq.com/oauth2.0/token?'
params = {
'grant_type': 'authorization_code',
'client_id': settings.QQ_APP_ID,
'client_secret': settings.QQ_APP_KEY,
'code': code,
'redirect_uri': settings.QQ_REDIRECT_URL,
}
url = base_url + urlencode(params)
response = urlopen(url)
data = response.read().decode()
# print(data)
acecess_data = parse_qs(data)
token = acecess_data.get('access_token')[0]
print(token)
return token
获取到QQ服务器返回的access_token值
服务器在有access_token拼接字符串访问QQ服务器得到openid
def get_openid_by_token(self, token):
"""
PC网站:https://graph.qq.com/oauth2.0/me
2 请求方法
GET
3 请求参数
请求参数请包含如下内容:
参数 是否必须 含义
access_token 必须 在Step1中获取到的access token。
"""
# 1. base_url
base_url = 'https://graph.qq.com/oauth2.0/me?'
# 2. 参数
params = {
'access_token': token
}
# 3. url
url = base_url + urlencode(params)
# 4. 根据url获取数据
response = urlopen(url)
data = response.read().decode()
# print(data)
# 5. 解析数据
# 因为它返回的数据 不是 字典类型,我们要想获取 字典数据,需要对这个字符串进行截取
# 'callback( {"client_id":"101474184","openid":"483C55DADEF65CC5735695CBC262F979"} );'
try:
openid_data = json.loads(data[10:-4])
except Exception:
raise Exception('数据获取错误')
# print(openid_data)
return openid_data['openid']
在视图函数中添加:
try:
qq_user = OAuthQQuser.objects.get(openid=openid)
except OAuthQQuser.DoesNotExist:
# 2.没有绑定过需要将openid和user信息绑定
access_token = OAuthQQuser.generic_token_by_openid(openid)
return Response({'access_token': access_token})
由于服务器需要向openid值,但openid是非常重要的信息,不能泄露,所以需要使用itsdangerous生成激活token
1.安装:pip install itsdangerous
2.生成用户激活token的方法封装在OAuthQQUser模型类中
- Serializer()生成序列化器,传入混淆字符串和过期时间
- dumps()生成openid加密后的token,传入封装openid的字典
- 返回token字符串
-
loads()解出token字符串,得到用户id明文
class OAuthQQUser(BaseModel):
"""
QQ登录用户数据
"""
...
@staticmethod
def generate_save_user_token(openid):
serializer = Serializer(settings.SECRET_KEY, expires_in=3600)
token = serializer.dumps({'openid': openid})
return token.decode()
@staticmethod
def openid_by_token(access_token):
serializer = Serializer(settings.SECRET_KEY, 3600)
try:
result = serializer.loads(access_token)
except BadData:
return None
return result.get('openid')
然后开始流程的下半部分:
接上面第8步后
9.首先判断用户是不是第一次使用QQ登录,如果不是,返回用户信息和token值,如果是,生成一个token值,返回前端
# 1.根据openid来判断用户是否存在
try:
qq_user = OAuthQQuser.objects.get(openid=openid)
except OAuthQQuser.DoesNotExist:
# 2.没有绑定过需要将openid和user信息绑定
access_token = OAuthQQuser.generic_token_by_openid(openid)
return Response({'access_token': access_token})
else:
# 3.绑定过,直接返回登录的token
from rest_framework_jwt.settings import api_settings
# 补充生成记录登录状态的token
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
payload = jwt_payload_handler(qq_user.user)
token = jwt_encode_handler(payload)
return Response({
'token': token,
'user_id': qq_user.user.id,
'username': qq_user.user.username,
})
10.如果是第一次使用QQ登录,需要绑定用户信息,进入绑定用户信息页面
前端代码:
mounted: function(){
// 从路径中获取qq重定向返回的code
var code = this.get_query_string('code');
axios.get(this.host + '/oauth/qq/users/?code=' + code, {
responseType: 'json',
})
.then(response => {
if (response.data.user_id){
// 用户已绑定
sessionStorage.clear();
localStorage.clear();
localStorage.user_id = response.data.user_id;
localStorage.username = response.data.username;
localStorage.token = response.data.token;
// 从路径中取出state,引导用户进入登录成功之后的页面
var state = this.get_query_string('state');
location.href = state;
} else {
// 用户未绑定
this.access_token = response.data.access_token;
this.generate_image_code();
this.is_show_waiting = false;
}
})
.catch(error => {
console.log(error.response.data);
alert('服务器异常');
})
},
这里可以看出,location.href = state;前面获取code传到前端的state即是成功绑定信息的返回路径
业务逻辑:
- 用户需要填写手机号、密码、图片验证码、短信验证码
- 如果用户未在美多商城注册过,则会将手机号作为用户名为用户创建一个美多账户,并绑定用户
- 如果用户已在美多商城注册过,则检验密码后直接绑定用户
后端接口设计
请求方式: POST /oauth/qq/user/
请求参数:
参数 | 类型 | 是否必须 | 说明 |
---|---|---|---|
mobile | str | 是 | 手机号 |
password | str | 是 | 密码 |
sms_code | str | 是 | 短信验证码 |
access_token | str | 是 | 凭据 (包含openid) |
返回数据:
返回值 | 类型 | 是否必须 | 说明 |
---|---|---|---|
token | str | 是 | JWT token |
user_id | int | 是 | 用户id |
username | str | 是 | 用户名 |
def post(self,request):
""" 明确你的需求,分析已知条件, 根据已知条件,创建实现的步骤
1. 前段应该将 短信,密码和手机号 以及 access_token(openid)的信息 传递给我们
2. 后端接受到数据之后,对数据进行校验
3. user信息??? 我们根据手机号来判断
4. 我们需要将 openid 和 user信息保存(绑定)起来
"""
serializer = QQTokenSerializer(data=request.data)
serializer.is_valid()
user = serializer.save()
from rest_framework_jwt.settings import api_settings
# 补充生成记录登录状态的token
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)
return Response({
'token': token,
'user_id': user.id,
'username': user.username,
})
用一个序列化器去判断前端传过来的页面,最后返回给前端token值,user_id,username
11.判断字段
def validate(self, attrs):
access_token = attrs.get('access_token')
openid = OAuthQQuser.openid_by_token(access_token)
if openid is None:
raise serializers.ValidationError('openid发生错误')
attrs['openid'] = openid
mobile = attrs['mobile']
redis_conn = get_redis_connection('code')
redis_code = redis_conn.get('sms_%s' % mobile)
if redis_code is None:
raise serializers.ValidationError('短信验证码已过期')
sms_code = attrs.get('sms_code')
if redis_code.decode() != sms_code:
raise serializers.ValidationError('短信验证码不一致')
try:
user = User.objects.get(mobile=mobile)
except User.DoesNotExist:
pass
else:
password = attrs.get('password')
if not user.check_password(password):
raise serializers.ValidationError('密码输入错误')
attrs['user'] = user
return attrs
1.根据openid判断用户是否绑定过
2.根据mobile和sms_code判断短信验证码是否正确
3.根据mobile判断用户是否已经注册
4.根据password判断已经注册的用户密码是否正确
5.如果没有注册,需要创建一个新用户绑定,重写create方法
def create(self, validated_data):
# 1. 获取用户信息
user = validated_data.get('user')
# 2. 判断用户信息是否存在
if user is None:
# 不存在就创建
user = User.objects.create(
username=validated_data.get('mobile'),
password=validated_data.get('password'),
mobile=validated_data.get('mobile')
)
# 密码还是明文
user.set_password(validated_data['password'])
user.save()
OAuthQQuser.objects.create(
user=user,
openid=validated_data.get('openid')
)
return user
补全前端代码:
on_submit: function(){
this.check_pwd();
this.check_phone();
this.check_sms_code();
if(this.error_password == false && this.error_phone == false && this.error_sms_code == false) {
axios.post(this.host + '/oauth/qq/users/', {
password: this.password,
mobile: this.mobile,
sms_code: this.sms_code,
access_token: this.access_token
}, {
responseType: 'json',
})
.then(response => {
// 记录用户登录状态
sessionStorage.clear();
localStorage.clear();
localStorage.token = response.data.token;
localStorage.user_id = response.data.user_id;
localStorage.username = response.data.username;
location.href = this.get_query_string('state');
})
.catch(error=> {
if (error.response.status == 400) {
this.error_sms_code_message = error.response.data.message;
this.error_sms_code = true;
} else {
console.log(error.response.data);
}
})
}
}
使用postman测试时,由于无法保存token信息,需要在请求头加入Authorization:JWT空格+token