三方登錄(微博爲例)

三方登錄介紹

  • 三方登錄流程(以微博爲例)

1)前端獲取認證code

1. 在Vue頁面加載時動態發送請求獲取微博授權url
2. django收到請求的url後,通過微博應用ID(client_id)和回調地址(redirect_uri)動態生成授權url返回給Vue
3. 當用戶點擊上面的url進行掃碼,授權成功會跳轉我們的回調界面並附加code參數
4. Vue獲取到微博返回的code後,會將code發送給django後端(上面的redirect_uri)

2)獲取微博access_token

後端獲取code後,結合client_id、client_secret、redirect_uri參數進行傳遞
獲取微博access_token

3)獲取微博用戶基本信息並保存到數據庫

使用獲得的access_token調用獲取用戶基本信息的接口,獲取用戶第三方平臺的基本信息
用戶基本信息保存到數據庫,然後關聯本地用戶,然後將用戶信息返回給前端

4)生成token給Vue

django後端藉助微博認證成功後,可以使用JWT生成token,返回給Vue
Vue將token存儲到localStorage中,以便用戶訪問其他頁面進行身份驗證

oauth認證原理

  • OAuth是一個開放標準,允許用戶讓第三方應用訪問該用戶在某一網站上存儲的私密的資源,而無需將用戶名和密碼提供給第三方應用
  • OAuth允許用戶提供一個令牌,而不是用戶名和密碼來訪問他們存放在特定服務提供者的數據
  • 這個code如果能出三方換取到數據就證明這個用戶是三方真實的用戶

爲什麼使用三方登錄

  • 服務方希望用戶註冊, 而用戶懶得填註冊時的各種信息(主要是爲了保證用戶的唯一性,各種用戶名已佔用,密碼格式限制)
  • 而像微信, QQ, 微博等幾乎每個人都會安裝的應用中用戶肯定會在其中某一個應用中已經註冊過,證明該用戶在已經註冊的應用中的唯一性
  • 第三方登錄的實質就是在授權時獲得第三方應用提供的代表了用戶在第三方應用中的唯一性的openid.並將openid儲存在第三方服務控制的本地儲存

第三方登錄與本地登錄的關聯(三種情況)

1)情況1: 本地未登錄,第一次登錄第三方

此時相當於註冊,直接把第三方信息拉取來並註冊成本地用戶就可以了,並建立本地用戶與第三方用戶(openid)的綁定關係

2)情況2:本地未登錄,再次登錄第三方

此時用戶已註冊,獲取到openid後直接找出對應的本地用戶即可

3)情況3:本地登錄,並綁定第三方

這個只要將獲取到的openid綁定到本地用戶就可以了

微博申請應用

微博申請應用參考:https://cloud.tencent.com/developer/article/1441425
官方微博接入文檔:https://open.weibo.com/wiki/Connect/login

  • 前端Vue
vue init webpack webssh
npm install --save axios

src\router\index.js 添加路由

import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '@/components/HelloWorld'
import Login from '@/components/Login'
import WeiboCallback from '@/components/WeiboCallback'
import UserBind from '@/components/UserBind'

Vue.use(Router)

export default new Router({
  mode: 'history',
  routes: [
    { path: '/', name: 'HelloWorld', component: HelloWorld },
    { path: '/login', name: 'Login', component: Login },  // 登錄頁面
    { path: '/weibo_callback', name: 'WeiboCallback', component: WeiboCallback },  // 通過空頁面發送code給後端
    { path: '/userbind', name: 'UserBind', component: UserBind },  // 將本地用戶與第三方用戶綁定
  ]
})

src\components\Login.vue 登錄頁面

<template>
  <div>
    <a :href="weibo_url" class="weibo_login">微博</a>
  </div>
</template>

<style>

</style>

<script>
  import axios from "axios";
  export default {
    data: function(){
      return {
        weibo_url: ''  // 動態從後端獲取的微博掃碼URL
      }
    },
    mounted(){
      this.get_weibo_url()
    },
    methods: {
      get_weibo_url: function(){
        // http://127.0.0.1:8000/api/weibo_url/
        axios({
          url: 'http://127.0.0.1:8000/api/weibo_url/',
          method: 'get'
        }).then(res=>{
          this.weibo_url = res.data.weibo_url
        })
      }
    }
  };
</script>

src\components\WeiboCallback.vue 通過空頁面發送code給後端

<template>
    <p>跳轉中....</p>
</template>


<script>
import axios from 'axios'
export default {
    mounted(){
        this.get_code()
    },
    methods: {
        get_code: function(){
            let code = this.$route.query.code  // 獲取微博的驗證code
            console.log(code)
            axios({
                url:'http://127.0.0.1:8000/api/weibo_back/?code=' + code,
                method: 'get'
            }).then(res=>{
                console.log(res)
                if (res.data.code == 200) {
                    console.log('成功')
                    localStorage.username = res.data.username
                    localStorage.user_id = res.data.user_id
                    localStorage.token = res.data.token
                    window.location.href = '/'
                }
                if (res.data.code == 201) {
                    console.log('失敗')  // 如果用戶未綁定,跳轉到綁定頁面
                    localStorage.access_token = res.data.response
                    window.location.href = '/userbind'
                }
            })
        }
    }
}
</script>

src\components\UserBind.vue 將本地用戶與第三方用戶綁定

<template>
  <div>
      <form @submit.prevent="send_bind_info">
        <p>
          <label>輸入賬號:</label>
          <input type="account" name="account" id="account" v-model="account">
        </p>
        <p>
          <label>輸入密碼:</label>
          <input type="password" name="pwd" id="pwd" v-model="password">
        </p>
        <p>
          <input type="submit" value="注 冊" name>
        </p>
      </form>
    </div>
  </div>
</template>

<style>

</style>

<script>
document.title = "綁定頁面";
import axios from "axios";
export default {
  // axios-> access_token
  data: function() {
    return {
      password: "",
      account: "",
    };
  },
  methods: {
    send_bind_info: function() {
      let post_data = new FormData();
      let access_token = localStorage.access_token;
      post_data.append("password", this.password);
      post_data.append("account", this.account);
      post_data.append("access_token", access_token);
      axios({
        url: "http://127.0.0.1:8000/api/bind_user/",
        method: "post",
        data: post_data
      }).then(res => {});
    }
  }
};
</script>
  • django後端
# requirements.txt
Django==2.0.4
djangorestframework==3.9.2
djangorestframework-jwt==1.11.0
django-cors-headers==3.0.2

settings.py 配置使用JWT、corsheaders、rest_framework

INSTALLED_APPS = [
    'rest_framework.authtoken',
    'rest_framework', 
    'users',
    'corsheaders',
]


MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',
    # 'django.middleware.common.CommonMiddleware',
]




''' 配置jwt驗證 '''
REST_FRAMEWORK = {
    # 身份認證
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework.authentication.BasicAuthentication',
    ),
}
import datetime
JWT_AUTH = {
    'JWT_AUTH_HEADER_PREFIX': 'JWT',
    'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1),
    'JWT_RESPONSE_PAYLOAD_HANDLER':'users.views.jwt_response_payload_handler',  # 重新login登錄返回函數
}
AUTH_USER_MODEL='users.User'  # 指定使用users APP中的 model User進行驗證


'''配置cors'''
CORS_ALLOW_CREDENTIALS = True
CORS_ORIGIN_ALLOW_ALL = True


'''微博相關配置信息'''
WEIBO_APP_KEY = '3516473472'
WEIBO_APP_SECRET = '7862ee35a0dc6f0345d0464dc34f14fc'
WEIBO_FUNC_BACK = 'http://127.0.0.1:8080/weibo_callback' # VUE的回調

urls.py 總路由

from django.contrib import admin
from django.urls import path,include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('users.urls')),
]

users/urls.py

#! /usr/bin/env python
# -*- coding: utf-8 -*-
from django.urls import path,re_path
from . import views
urlpatterns = [
    re_path(r'register/',views.RegisterView.as_view(),name='register'),
    re_path(r'weibo_url/', views.WeiboUrl.as_view(), name='weibo_url'),
    re_path(r'weibo_back/',views.WeiboBack.as_view(),name='weibo_back'),
    re_path(r'bind_user/',views.BindUser.as_view(),name='bind_user'),
]

users/models.py

from django.db import models
from django.contrib.auth.models import AbstractUser


class User(AbstractUser):
    username = models.CharField(max_length=64, unique=True)
    password = models.CharField(max_length=255)
    phone = models.CharField(max_length=64,blank=True,null=True)


class SocialUser(models.Model):
    user = models.ForeignKey(User,on_delete=models.CASCADE,verbose_name='用戶')
    platfrom_type_choices = (
        (1,'web'),
        (2,'移動'),
    )
    platfrom_ID = models.IntegerField(choices=platfrom_type_choices,verbose_name='平臺類型')
    platfrom_choices = (
        (1,'QQ'),
        (2,'微博'),
        (3,'微信'),
    )
    platfrom_type = models.IntegerField(choices=platfrom_choices,verbose_name='社交平臺')

    uid = models.CharField(max_length=100,verbose_name='用戶ID')

    def __str__(self):
        return self.user.username

users/serializers.py

#! /usr/bin/env python
# -*- coding: utf-8 -*-
from rest_framework_jwt.settings import api_settings
from rest_framework import serializers
from users.models import User
from users.models import SocialUser


class UserSerializer(serializers.Serializer):
    username = serializers.CharField()
    password = serializers.CharField()
    phone = serializers.CharField(required=False,allow_blank=True)
    token = serializers.CharField(read_only=True)

    def create(self, data):
        user = User.objects.create(**data)
        user.set_password(data.get('password'))
        user.save()
        # 補充生成記錄登錄狀態的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)  # 創建token
        user.token = token
        return user


class SocialUserSerializer(serializers.Serializer):
    user_id = serializers.IntegerField()
    platfrom_ID = serializers.IntegerField()      # 平臺類型(web/移動)
    platfrom_type = serializers.IntegerField()    # 社交平臺(微博/微信/QQ)
    uid = serializers.IntegerField()              # 社交平臺唯一ID

    def create(self, validated_data):
        social_user = SocialUser.objects.create(**validated_data)
        return social_user

users/views.py

from django.shortcuts import render
import json
from rest_framework.views import APIView
from rest_framework.views import Response
from rest_framework.permissions import IsAuthenticated
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from urllib.parse import urlencode
import requests
from rest_framework_jwt.settings import api_settings
from django.db import transaction
from rest_framework import serializers
from django.core.exceptions import ValidationError

from weiboLogin import settings
from users import models
from users.serializers import UserSerializer
from users.serializers import SocialUserSerializer


# 用戶註冊
class RegisterView(APIView):
    def post(self, request, *args, **kwargs):
        serializer = UserSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=201)
        return Response(serializer.errors, status=400)


# 重寫用戶登錄返回函數
def jwt_response_payload_handler(token, user=None, request=None):
    '''
    :param token: jwt生成的token值
    :param user: User對象
    :param request: 請求
    '''
    return {
        'test':'aaaa',
        'token': token,
        'user': user.username,
        'userid': user.id
    }


# 生成前端跳轉到微博掃碼頁面的url
class WeiboUrl(APIView):
    '''
        生成微博的登陸頁面路由地址
        https://api.weibo.com/oauth2/authorize?       # 微博oauth認證地址
        client_id=4152203033&                         # 註冊開發者id
        response_type=code&
        redirect_uri=http://127.0.0.1:8080/weibo_callback/     # 獲取code後將code回調給後端地址
    '''
    def get(self, request):
        url = 'https://api.weibo.com/oauth2/authorize?'
        data = {
            'client_id': settings.WEIBO_APP_KEY,
            'response_type': 'code',
            'redirect_uri': settings.WEIBO_FUNC_BACK,
        }
        weibo_url = url + urlencode(data)
        # https://api.weibo.com/oauth2/authorize?client_id=4152203033&response_type=code&redirect_uri=http://127.0.0.1:8000/api/weibo_back/
        return Response({'weibo_url': weibo_url})


# 微博掃碼成功後攜帶code調用此接口,認證成功返回jwt token
class WeiboBack(APIView):
    '''
        通過回調連接,獲取access_token
        https://api.weibo.com/oauth2/access_token?
        client_id=YOUR_CLIENT_ID
        client_secret=YOUR_CLIENT_SECRET&
        grant_type=authorization_code&
        code=CODE
        redirect_uri=YOUR_REGISTERED_REDIRECT_URI
    '''
    def get(self, request):
        code = request.query_params.get('code')  # code爲微博微博認證的code
        data = {
            'client_id': settings.WEIBO_APP_KEY,
            'client_secret': settings.WEIBO_APP_SECRET,
            'grant_type': 'authorization_code',
            'code': code,
            'redirect_uri': settings.WEIBO_FUNC_BACK,
        }
        url = 'https://api.weibo.com/oauth2/access_token'
        response = requests.post(url=url, data=data).json()  # 拿取請求的返回結果
        # {'access_token': '2.00jqYNTGfgNAXEbd85e6c672uTGF8E',
        # 'remind_in': '157679999', 'expires_in': 157679999,
        # 'uid': '5928542965', 'isRealName': 'true'}
        print(response)
        uid = response.get('uid')  # uid是微博三方的唯一id
        if not uid:                # 獲取不到則爲微博code錯誤
            return Response({'code': 201, 'error': '三方授權失敗'})
        try:
            # 判斷當前UID是否存在與數據庫中,如果存在,代表用戶可以登陸的
            user = models.SocialUser.objects.get(uid=uid)
        except:
            # 如果不存在,代表這個用戶不能登陸,先得跳轉到綁定頁面,將本地用戶與微博用戶進行綁定
            return Response(
                {
                    'code': 201,
                    'response': json.dumps(response)
                }
            )
        else:  # 如果獲取到uid,就生成token返回給vue
            jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
            jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
            payload = jwt_payload_handler(user.user)  # 創建載荷
            token = jwt_encode_handler(payload)  # 創建token
            return Response({
                'code': 200,
                'token': token,
                'username': user.user.username,
                'user_id': user.user.id,
            })


# 將微博用戶綁定到本地用戶
class BindUser(APIView):
    def post(self, request):
        account = request.data.get('account')   # 綁定的賬號
        password = request.data.get('password')  # 綁定的密碼
        user_url = "https://api.weibo.com/2/users/show.json?access_token=%s&uid=%s"
        access_token = json.loads(request.data.get('access_token'))
        get_url = user_url % (access_token['access_token'], access_token['uid'])
        # {'access_token': '2.00jqYNTGfgNAXEbd85e6c672uTGF8E','uid': '5928542965', 'isRealName': 'true'}
        data = requests.get(url=get_url).json()  # 通過token獲取微博詳細信息
        if data.get('error'):
            return Response({
                'code': 201,
                'token': access_token,
            })
        else:
            try:
                serializer = UserSerializer(data={'username':account,'password':password})
                if serializer.is_valid():
                    # 創建本地用戶與微博用戶關聯
                    with transaction.atomic():
                        save_id = transaction.savepoint()  # 創建一個保存點
                        try:
                            user = serializer.save()
                            socialuser = SocialUserSerializer(data={
                                "user_id": user.id,
                                "platfrom_ID" : 1,
                                "platfrom_type": 2,
                                "uid" : data['id']
                            })
                            if socialuser.is_valid():
                                socialuser.save()
                                return Response({
                                    'code': 200,
                                    'token': user.token,
                                    'username': user.username,
                                    'user_id': user.id,
                                })
                            else:
                                transaction.savepoint_rollback(save_id)  # 出現異常退回到保存點
                        except ValidationError:
                            raise
                        except Exception as e:
                            transaction.savepoint_rollback(save_id)
                        finally:
                            # 提交事務
                            transaction.savepoint_commit(save_id)
            except Exception as e:
                print(e)
            return Response(status=400)

詳情鏈接參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章