前后端分离项目OAuth登录总结

最近在开发博客网站登录过程中,涉及到了多个前端对应一个后端的前后端分离项目如何使用OAuth完成第三方授权登录的问题,特此总结一篇文章,详细记录了完整的开发过程思路分析和具体的代码实现,大家需要相同的业务场景时可参考使用。

一、OAuth概述

1. OAuth功能

在这里插入图片描述

举个例子,当你想要上班摸鱼逛知乎时,但是你从来没有注册过知乎账号,而且你又嫌麻烦不愿意注册知乎账号,此时就可以使用第三方社交账号登录,例如使用QQ账号授权登录后,会自动登录知乎账号,并将自己的用户名、性别、头像等基本信息就会保存在知乎平台做账号绑定。

2. 什么是OAuth

首先说明一点,OAuth 不是一个API或者公共服务,而是一个验证授权的开放标准,只要授权方和被授权方遵守这个协议去写代码提供服务,那双方就是实现了OAuth模式。目前可以提供OAuth的平台有很多,他们都遵从这个标准,实现了自己的OAuth功能。虽然OAuth指定了明确的标准,但是各家的使用方式还是略有差异。
OAuth主要有OAuth 1.0、OAuth 1.0a、OAuth 2.0三个版本。OAuth 1.0a主要是修复了 OAuth 1.0的安全 问题,OAuth 2 是为了解决 OAuth 1.0a 过于复杂的问题。OAuth2.0 是目前广泛使用的版本,目前第三方平台也都是基于OAuth2标准开放服务。

3. OAuth流程

在这里插入图片描述

上述例子中的知乎就是客户端,QQ就是认证服务器,OAuth2.0就是客户端和认证服务器之间为了解决相互不信任而产生的一个授权协议。(要是相互信任那豆瓣直接读取QQ的数据库登录不就好了,搞这么费劲作甚)
整个流程分为以下三个阶段
① 用户点击QQ登录进入授权页面同意授权,登录完成后获取到code;
② 知乎网站请求QQ服务器,通过code换取授权access_token;
③ QQ通过网页授权access_token向知乎返回用户的基本信息。

二、项目开发思路

1. 整体流程思路

上面举例仅是简单的业务场景,前后端不分离项目开发思路。但是遇到多个前端对应一个后端的前后端分离项目,开发的流程思路还是略有差异。
完整的设计思路如下:

  • 首先在各个第三方登录平台创建应用,PC端和手机端各一个,是两个不同的key和secret。
  • 当用户点击第三方按钮登录时,前端传客户端类型和第三方平台给后端API接口,获取到应用的client id(也就是key)
  • 前端依据各个平台的请求地址格式,拼接成完整的url地址,其中包含前端的回调地址,并调转到第三方登录页
  • 用户登录完成后,会跳转到请求地址中指定的redirect_uri前端回调地址
  • 前端回调页获取到code参数后请求传参给后端。
  • 后端通过code参数请求第三方平台,换取token,然后使用token获取用户详细信息。
  • 后期根据openid判断用户是否已注册过(已注册——>直接登录;未注册——>获取用户信息并创建用户然后登录)返回给前端用户id和token。
  • 前端存储用户信息,并跳转到首页。至此,整个流程完成

2. 前端模块设计

前端的工作主要有两部分,分别是登录页和回调页
登录页放置第三方登录按钮,当用户点击登录后,后后端API接口传入登录平台、应用类型(桌面端还是移动端)两个参数,获取到client id,然后根据不同的第三方登录平台要求拼接URL地址,跳转到第三方登录页
回调页功能是当用户完成登录授权后,会跳转到回调页,从URL中获取到code参数,传递给后端。等待后端完成登录处理后,获取到用户id和token,并保存到local storage或者session storage中

3. 后端模块设计

后端的工作主要有两部分,分别是查询应用client id和完成用户登录
查询应用client id为一个接口,用于登录页请求。根据前端传入的登录平台、应用类型两个参数,返回应用的client id
用户登录为另一个API接口,用户回调页请求。用户传入code后,请求第三方平台OAuth接口,获取用户openid。然后判断当前用户是否已注册过账号(已注册——>直接登录;未注册——>获取用户信息并创建用户然后登录)并返回给前端用户id和token

三、应用创建与注意事项

1. 新浪微博

  • 相关地址

微博申请地址:https://open.weibo.com/
微博登录文档:https://open.weibo.com/wiki/Connect/login

  • 注意事项

新浪微博需要实名认证,只认证身份信息,不验证回调地址等信息是否正确。等审核通过再修改回调地址。在审核期间修改回调地址不生效。

2. 支付宝

  • 相关地址

支付宝申请地址https://open.alipay.com/
支付宝官方文档:https://opendocs.alipay.com/support/01rg6a

  • 注意事项

支付宝同样也需要实名认证,但也是只验证身份信息。但调用支付宝OAuth时需要使用支付宝sdk完成。
调用sdk建立连接是需要传入KEY、PRIVATE_KEY、PUBLIC_KEY三个参数,其中PUBLIC_KEY从应用信息——>接口加签方式中查看,PRIVATE_KEY是创建应用时上传的证书key
在这里插入图片描述

3. QQ

  • 相关地址

qq申请地址https://connect.qq.com/
qq文档:https://wiki.connect.qq.com/oauth2-0%e5%bc%80%e5%8f%91%e6%96%87%e6%a1%a3

  • 注意事项

QQ审核除了实名认证外,还审核应用的信息,记得应用名称填写备案号上的应用名称,并且要在应用中添加QQ登录按钮和跳转链接才能审核通过,审核通过后可以修改回调地址。

4. 百度

  • 相关地址

百度申请地址:http://developer.baidu.com/console#app/project
百度文档:https://openauth.baidu.com/doc/regdevelopers.html

5. github

  • 相关地址

github申请地址:https://github.com/settings/developers
github官方文档:https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps

6. 微软

  • 相关地址

微软申请地址:https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade
微软参考文档:https://docs.microsoft.com/zh-cn/graph/auth-v2-user

  • 注意事项

有个tenant参数。根据自己的账户权限,传入具体的值(文档示例和网上的资料都是common,但不一定适合自己的账户权限)
在这里插入图片描述

在这里插入图片描述

四、后端代码

1. 项目配置(settings.py)

将第三方平台创建的应用key和secret保存到setting配置中,后续函数处理时直接调用setting配置即可。

# 第三方登录
AUTH = {
    'WEIBO': {
        'PC': {
            'KEY': '110*****1',
            'SECRET': 'c37a*****c38f',
        },
        'M': {
            'KEY': '27*****52',
            'SECRET': 'b41*****2acd'
        }
    },
    'QQ': {
        'PC': {
            'KEY': '10*****3',
            'SECRET': 'c3*****d99',
        },
        'M': {
            'KEY': '1*****5',
            'SECRET': '32*****5fb'
        }
    },
    'PAY': {
        'PC': {
            'KEY': '2021*****589',
            'SECRET': '',
            'PRIVATE_KEY': 'M2*********M=',
            'PUBLIC_KEY': 'MI****QAB'
        },
        'M': {
            'KEY': '202****70',
            'SECRET': '',
            'PRIVATE_KEY': 'MI*****************KW4=',
            'PUBLIC_KEY': 'MI******************8IQAB'
        }
    },
    'GITHUB': {
        'PC': {
            'KEY': 'be2**************89',
            'SECRET': '5b1**************185',
        },
        'M': {
            'KEY': '92*********************8a',
            'SECRET': '154**************8982ab'
        }
    },
    'BAIDU': {
        'PC': {
            'KEY': '44T*******************lN',
            'SECRET': 'qEk***************VQTz2',
        },
        'M': {
            'KEY': 'hh4****************EaG',
            'SECRET': 'u1RV*************7okSbM'
        }
    },
    'MICROSOFT': {
        'PC': {
            'KEY': '61e*************84049',
            'SECRET': 'XBk***************P~wK',
        },
        'M': {
            'KEY': 'b966******************61f8',
            'SECRET': '59J**************iciGJrFr'
        }
    },
}

1. 模型(models.py)

模型这儿设计了三张表,分别是用户来源、用户信息、用户第三方登录用户ID。
用户来源表主要记录用户是第三方登录还是直接注册的,并提前在用户来源表中输入相关记录。与用户信息表source字段一对多关联。
用户信息表通过使用AbstractUser可以对Django内置的User进行扩展使用,添加一些用户自定义的属性字段。
第三方登录用户ID用于记录用户ID与第三方平台对应关系,判断用户是否已注册绑定过账号。

from django.contrib.auth.models import User, AbstractUser
from django.db import models
from django.conf import settings


class UserSource(models.Model):
    name = models.CharField('来源', max_length=100)

    class Meta:
        verbose_name = '注册用户来源'
        verbose_name_plural = verbose_name

    def __str__(self):
        return self.name


class UserInfo(AbstractUser):
    source = models.ForeignKey(UserSource, verbose_name='用户来源', on_delete=models.CASCADE, default=1)
    phone = models.CharField(verbose_name='手机号', max_length=20, blank=True, null=True)
    sex_choice = [('1', '男'), ('2', '女')]
    sex = models.CharField(verbose_name='性别', max_length=1, choices=sex_choice, default=1)
    web = models.URLField(verbose_name='个人网站', blank=True, null=True)
    signature = models.TextField(verbose_name='个性签名', max_length=200, default="这个人很懒,什么都没留下!")
    photo = models.URLField(verbose_name='头像', default='https://oss.cuiliangblog.cn/images/photo.jpg')
    area_code = models.CharField(verbose_name='地区编号', max_length=10, blank=True, null=True)
    area_name = models.CharField(verbose_name='地区名称', max_length=20, blank=True, null=True)
    birthday = models.DateField(verbose_name='生日', blank=True, null=True)
    is_flow = models.BooleanField('开启更新订阅', default=0)
    search = models.ManyToManyField(SearchKey, verbose_name='搜索记录')

    class Meta:
        verbose_name = '用户详细信息'
        verbose_name_plural = verbose_name

    def __str__(self):
        return self.username


class OAuthId(models.Model):
    user = models.ForeignKey(UserInfo, verbose_name='用户ID', on_delete=models.CASCADE, default=1)
    source = models.ForeignKey(UserSource, verbose_name='用户来源', on_delete=models.CASCADE, default=1)
    openid = models.CharField(max_length=100, verbose_name='用户OAuthID')

    class Meta:
        verbose_name = '第三方登录用户ID'
        verbose_name_plural = verbose_name

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

在这里插入图片描述

2. 路由(urls.py)

后端向前端提供两个API接口,一个是查询应用client id,另一个是处理登录完成后回调相关业务逻辑

from django.urls import path
from rest_framework import routers
from account import views

app_name = "account"
urlpatterns = [
    path('OAuthID/', views.OAuthIDAPIView.as_view()),
    # 获取第三方客户端ID
    path('OAuthCallback/', views.OAuthCallbackAPIView.as_view()),
    # 第三方登录后回调
]
router = routers.DefaultRouter()
urlpatterns += router.urls

在这里插入图片描述

在这里插入图片描述

3. 视图(views.py)

import json
import urllib.parse
from loguru import logger
from rest_framework import status, viewsets
from rest_framework.response import Response
from rest_framework.views import APIView
from public.tools import OAuth
from django.conf import settings


class OAuthIDAPIView(APIView):
    """
    获取第三方登录应用ID
    """

    @staticmethod
    def get(request):
        platform = request.query_params.get('platform')
        kind = request.query_params.get('kind')
        result = {'clientId': settings.AUTH[platform.upper()][kind.upper()]['KEY']}
        return Response(result, status=status.HTTP_200_OK)


class OAuthCallbackAPIView(APIView):
    """
    授权第三方登录后回调地址
    """

    @staticmethod
    def post(request):
        platform = request.data.get('platform')
        kind = request.data.get('kind')
        code = request.data.get('code')
        redirect_uri = request.data.get('redirect_uri')
        print(platform, code, redirect_uri, kind)
        auth = OAuth(platform, kind, code, redirect_uri)
        result = {}
        if platform == 'WEIBO':
            result = auth.weiboLogin()
        elif platform == 'QQ':
            result = auth.qqLogin()
        elif platform == 'PAY':
            result = auth.payLogin()
        elif platform == 'GITHUB':
            result = auth.githubLogin()
        elif platform == 'BAIDU':
            result = auth.baiduLogin()
        elif platform == 'MICROSOFT':
            result = auth.microsoftLogin()
        return Response(result, status=status.HTTP_200_OK)

4. OAuth类(tools.py)

import random
import datetime
import uuid
from urllib.parse import urlencode
from loguru import logger
import requests
from alipay.aop.api.request.AlipaySystemOauthTokenRequest import AlipaySystemOauthTokenRequest
from alipay.aop.api.request.AlipayUserInfoShareRequest import AlipayUserInfoShareRequest
from alipay.aop.api.constant.ParamConstants import *
from alipay.aop.api.response.AlipaySystemOauthTokenResponse import AlipaySystemOauthTokenResponse
from alipay.aop.api.response.AlipayUserInfoShareResponse import AlipayUserInfoShareResponse
from django.core.cache import cache
from django.core.mail import EmailMultiAlternatives
from django.conf import settings
from django.utils import timezone
import json
from account.models import UserInfo, UserSource, OAuthId
from rest_framework_simplejwt.tokens import RefreshToken
from alipay.aop.api.AlipayClientConfig import AlipayClientConfig
from alipay.aop.api.DefaultAlipayClient import DefaultAlipayClient
import traceback


class OAuth:
    """
    第三方登录
    """

    def __init__(self, platform, kind, code, redirect_uri):
        print(platform, kind, code, redirect_uri)
        self._client_key = settings.AUTH[platform][kind]['KEY']
        # 应用id
        self._client_secret = settings.AUTH[platform][kind]['SECRET']
        # 应用key
        self._code = code
        # 用户code
        self._redirect_uri = redirect_uri
        # 登录回调地址
        self.openid = ''
        # 用户第三方登录ID
        self.source_id = ''
        # 用户来源id
        self.user_id = ''
        # 用户id
        self.platform = platform
        # 第三方登录平台
        self.kind = kind
        # 前端类型(PC或M)

    def __checkUserRegister(self):
        """
        检查用户是否已注册
        :return:
        """
        print('开始检测用户是否已注册过')
        user = OAuthId.objects.filter(source_id=self.source_id, openid=self.openid)
        if user.count() != 0:
            self.user_id = user.first().user.id
            return True
        else:
            return False

    def __createUser(self, username, **kwargs):
        """
        创建新用户
        :param username: 用户名
        :param kwargs: 用户信息
        :return: None
        """
        print("开始创建新用户啦")
        while UserInfo.objects.filter(username=username):  # 防止用户名重复
            username = username + str(random.randrange(10))
        userinfo = {
            'source_id': self.source_id,
            'username': username,
            'password': str(uuid.uuid1())
        }
        for key, value in kwargs.items():
            userinfo[key] = value
        logger.info('存储信息:{}'.format(userinfo))
        print(userinfo)
        # 用户信息表插入数据
        new_user = UserInfo.objects.create_user(**userinfo)
        self.user_id = new_user.id
        # OAuthId表插入数据
        OAuthId.objects.create(user_id=self.user_id, source_id=self.source_id, openid=self.openid)

    def __userLogin(self):
        """
        用户登录签发token
        :return:
        """
        print("开始登录了")
        user = UserInfo.objects.get(id=self.user_id)
        user.last_login = timezone.now()
        user.save()
        refresh = RefreshToken.for_user(user)
        result = dict()
        result['token'] = str(refresh.access_token)
        result['userid'] = user.id
        result['username'] = user.username
        return result

    def weiboLogin(self):
        """
        微博登录
        :return:
        """
        print("微博登录了")
        self.source_id = UserSource.objects.get(name='微博').id
        # 获取用户access_token和uid
        access_token_url = 'https://api.weibo.com/oauth2/access_token?client_id={0}&client_secret={1}&grant_type=authorization_code&code={2}&redirect_uri={3}'.format(
            self._client_key, self._client_secret, self._code, self._redirect_uri)
        access_response = requests.post(access_token_url).json()
        print(access_response['access_token'], access_response['uid'])
        self.openid = access_response['uid']
        # 判断用户是否已注册过
        user = self.__checkUserRegister()
        if user:
            print('已注册过,直接登录')
            return self.__userLogin()
        else:
            # 获取用户信息
            userinfo_url = "https://api.weibo.com/2/users/show.json?access_token={0}&uid={1}".format(
                access_response['access_token'], access_response['uid'])
            userinfo_response = requests.get(userinfo_url).json()
            print(userinfo_response)
            logger.info('微博用户信息:{}'.format(userinfo_response))
            username = userinfo_response['name']
            signature = userinfo_response['description']
            photo = userinfo_response['avatar_large']
            web = userinfo_response['url']
            area_name = userinfo_response['location']
            if userinfo_response['gender'] == 'f':
                sex = 2
            else:
                sex = 1
            # 新建用户
            self.__createUser(username, signature=signature, photo=photo, web=web, area_name=area_name, sex=sex)
            # 用户登录
            return self.__userLogin()

    def qqLogin(self):
        """
        QQ登录
        """
        print("QQ登录了")
        self.source_id = UserSource.objects.get(name='qq').id
        # 获取用户access_token
        access_token_url = 'https://graph.qq.com/oauth2.0/token?client_id={0}&client_secret={1}&grant_type=authorization_code&code={2}&redirect_uri={3}&fmt=json'.format(
            self._client_key, self._client_secret, self._code, self._redirect_uri)
        access_response = requests.get(access_token_url).json()
        print(access_response['access_token'])
        # 使用Access Token获取用户的OpenID
        openID_url = 'https://graph.qq.com/oauth2.0/me?access_token={}&fmt=json'.format(access_response['access_token'])
        openID_response = requests.get(openID_url).json()
        print("openID:", openID_response['openid'])
        self.openid = openID_response['openid']
        # 判断用户是否已注册过
        user = self.__checkUserRegister()
        if user:
            print('已注册过,直接登录')
            return self.__userLogin()
        else:
            # 获取用户信息
            print('开始获取用户信息')
            userinfo_url = "https://graph.qq.com/user/get_user_info?access_token={0}&oauth_consumer_key={1}&openid={2}&fmt=json".format(
                access_response['access_token'], self._client_key, self.openid)
            userinfo_response = requests.get(userinfo_url).json()
            logger.info('QQ用户信息:{}'.format(userinfo_response))
            print(userinfo_response)
            username = userinfo_response['nickname']
            photo = userinfo_response['figureurl_2']
            area_name = userinfo_response['province'] + ' ' + userinfo_response['city']
            if userinfo_response['gender'] == '女':
                sex = 2
            else:
                sex = 1
            # 新建用户
            self.__createUser(username, photo=photo, area_name=area_name, sex=sex)
            # 用户登录
            return self.__userLogin()

    def baiduLogin(self):
        """
        百度账号登录
        """
        print("百度登录了")
        self.source_id = UserSource.objects.get(name='百度').id
        print(self.source_id)
        # 获取用户access_token
        access_token_url = 'https://openapi.baidu.com/oauth/2.0/token?grant_type=authorization_code&client_id={0}&client_secret={1}&code={2}&redirect_uri={3}'.format(
            self._client_key, self._client_secret, self._code, self._redirect_uri)
        access_response = requests.get(access_token_url).json()
        print(access_response['access_token'])
        # 使用Access Token获取用户信息
        userinfo_url = "https://openapi.baidu.com/rest/2.0/passport/users/getInfo?access_token={}".format(
            access_response['access_token'])
        userinfo_response = requests.get(userinfo_url).json()
        logger.info('百度用户信息:{}'.format(userinfo_response))
        print(userinfo_response)
        self.openid = userinfo_response['portrait']
        # 判断用户是否已注册过
        user = self.__checkUserRegister()
        if user:
            print('已注册过,直接登录')
            return self.__userLogin()
        else:
            # 获取用户信息
            username = userinfo_response['username']
            photo = 'https://himg.bdimg.com/sys/portrait/item/' + userinfo_response['portrait']
            # 新建用户
            self.__createUser(username, photo=photo)
            # 用户登录
            return self.__userLogin()

    def microsoftLogin(self):
        """
        微软账号登录
        """
        print("微软账号登录了")
        self.source_id = UserSource.objects.get(name='微软').id
        print(self.source_id)
        # 获取用户access_token
        access_token_headers = {
            'Content-Type': 'application/x-www-form-urlencoded'
        }
        access_token_data = {
            "client_id": self._client_key,
            "client_secret": self._client_secret,
            "code": self._code,
            "redirect_uri": self._redirect_uri,
            "grant_type": 'authorization_code',
            "scope": 'offline_access user.read'
        }
        access_token_url = 'https://login.microsoftonline.com/consumers/oauth2/v2.0/token'
        access_response = requests.post(access_token_url, headers=access_token_headers,
                                        data=urlencode(access_token_data)).json()
        print(access_response['access_token'])
        # 使用Access Token获取用户信息
        userinfo_headers = {
            "Authorization": 'Bearer ' + access_response['access_token'],
            "Host": 'graph.microsoft.com'
        }
        userinfo_url = "https://graph.microsoft.com/v1.0/me"
        userinfo_response = requests.get(userinfo_url, headers=userinfo_headers).json()
        logger.info('微软用户信息:{}'.format(userinfo_response))
        print(userinfo_response)
        self.openid = userinfo_response['id']
        # 判断用户是否已注册过
        user = self.__checkUserRegister()
        if user:
            print('已注册过,直接登录')
            return self.__userLogin()
        else:
            # 获取用户信息
            username = userinfo_response['displayName'].replace(" ", "")
            # 新建用户
            self.__createUser(username)
            # 用户登录
            return self.__userLogin()

    def __payToken(self, client):
        """
        支付宝通过code获取用户token
        :param client:
        :return:
        """
        # 构造请求参数对象
        request = AlipaySystemOauthTokenRequest()
        request.code = self._code
        request.grant_type = "authorization_code"
        response_content = None
        # 执行API调用
        try:
            response_content = client.execute(request)
        except Exception as e:
            print(traceback.format_exc(), e)
        if not response_content:
            print("failed execute")
        else:
            # 解析响应结果
            response = AlipaySystemOauthTokenResponse()
            response.parse_response_content(response_content)
            if response.is_success():
                # 如果业务成功,可以通过response属性获取需要的值
                auth_token = response.access_token
                self.openid = response.user_id
                return auth_token
            # 响应失败的业务处理
            else:
                # 如果业务失败,可以从错误码中可以得知错误情况,具体错误码信息可以查看接口文档
                print(response.code + "," + response.msg + "," + response.sub_code + "," + response.sub_msg)

    def __payUserInfo(self, client, token):
        """
        获取支付宝用户信息
        :return:
        """
        request = AlipayUserInfoShareRequest()
        # 添加auth_token
        udf_params = dict()
        udf_params[P_AUTH_TOKEN] = token
        request.udf_params = udf_params
        response_content = None
        # 执行API调用
        try:
            # 执行接口请求
            response_content = client.execute(request)
        except Exception as e:
            print(traceback.format_exc(), e)
        if not response_content:
            print("failed execute")
        else:
            response = AlipayUserInfoShareResponse()
            # 解析响应结果
            response.parse_response_content(response_content)
            # 响应成功的业务处理
            if response.is_success():
                # 如果业务成功,可以通过response属性获取需要的值
                # print(response)
                logger.info('支付宝用户信息:{}'.format(response))
                username = response.nick_name
                photo = response.avatar
                area_name = response.province + ' ' + response.city
                if response.gender == 'f':
                    sex = 2
                else:
                    sex = 1
                # 新建用户
                self.__createUser(username, photo=photo, area_name=area_name, sex=sex)
            # 响应失败的业务处理
            else:
                # 如果业务失败,可以从错误码中可以得知错误情况,具体错误码信息可以查看接口文档
                print(response.code + "," + response.msg + "," + response.sub_code + "," + response.sub_msg)

    def payLogin(self):
        """
        支付宝登录
        """
        print("支付宝登录了")
        self.source_id = UserSource.objects.get(name='支付宝').id
        # 实例化客户端
        alipay_client_config = AlipayClientConfig()
        alipay_client_config.server_url = 'https://openapi.alipay.com/gateway.do'
        alipay_client_config.app_id = self._client_key
        alipay_client_config.app_private_key = settings.AUTH[self.platform][self.kind]['PRIVATE_KEY']
        alipay_client_config.alipay_public_key = settings.AUTH[self.platform][self.kind]['PUBLIC_KEY']
        client = DefaultAlipayClient(alipay_client_config)
        # 获取用户token
        token = self.__payToken(client)
        # 判断用户是否已注册过
        user = self.__checkUserRegister()
        if user:
            print('已注册过,直接登录')
            return self.__userLogin()
        else:
            # 获取用户信息
            print('开始获取用户信息')
            self.__payUserInfo(client, token)
            return self.__userLogin()

    def __githubToken(self):
        """
        github获取用户token
        :return:
        """
        response = None
        headers = {
            'accept': 'application/json'
        }
        url = 'https://github.com/login/oauth/access_token?client_id={0}&client_secret={1}&code={2}'.format(
            self._client_key, self._client_secret, self._code)
        i = 0
        while i < 3:
            try:
                print("开始尝试获取token", timezone.localtime())
                response = requests.post(url, headers=headers, timeout=5).json()
                if response:
                    return response
            except requests.exceptions.RequestException:
                i += 1
        if response is None:
            print("获取token请求失败了")
            return False

    def __githubUserInfo(self, token):
        """
        github获取用户信息
        :return:
        """
        response = None
        headers = {
            'accept': 'application/json',
            'Authorization': 'token ' + token
        }
        url = 'https://api.github.com/user'
        i = 0
        while i < 3:
            try:
                print("开始尝试获取用户信息", timezone.localtime())
                response = requests.get(url, headers=headers, timeout=5).json()
                if response:
                    print(response)
                    logger.info('github用户信息:{}'.format(response))
                    return response
            except requests.exceptions.RequestException:
                i += 1
        if response is None:
            print("获取用户信息失败了")
            return False

    def githubLogin(self):
        """
        github登录
        :return:
        """
        print("github登录了")
        self.source_id = UserSource.objects.get(name='github').id
        # 获取用户access_token
        access_response = self.__githubToken()
        if access_response:
            print(access_response['access_token'])
            # 获取用户信息
            userinfo_response = self.__githubUserInfo(access_response['access_token'])
            if userinfo_response:
                print(userinfo_response)
                self.openid = userinfo_response['id']
                # 判断用户是否已注册过
                user = self.__checkUserRegister()
                if user:
                    print('已注册过,直接登录')
                    return self.__userLogin()
                else:
                    if userinfo_response['name']:
                        username = userinfo_response['name']
                    else:
                        username = userinfo_response['login']
                    signature = userinfo_response['bio']
                    photo = userinfo_response['avatar_url']
                    if userinfo_response['blog']:
                        web = userinfo_response['blog']
                    else:
                        web = userinfo_response['html_url']
                    area_name = userinfo_response['location']
                    # 新建用户
                    self.__createUser(username, signature=signature, photo=photo, web=web, area_name=area_name)
                    # 用户登录
                    return self.__userLogin()
        else:
            return False

五、前端代码

1. API地址封装

// 获取第三方登录ID
export function getOAuthID(platform) {
	return index.get('/account/OAuthID/' + '?platform=' + platform + '&kind=PC')
}
// 第三方授权登录后回调
export function postOAuthCallback(params) {
	return index.post('/account/OAuthCallback/', params)
}

2. PC端用户登录页

<template>
<div class="other-login">
  <el-divider>
    <span>第三方账号登录</span>
  </el-divider>
  <div class="other-logo">
    <span @click="otherLogin('QQ')" class="pointer"><MyIcon type="icon-qq-logo"/></span>
    <span @click="otherLogin('PAY')" class="pointer"><MyIcon type="icon-alipay-logo"/></span>
    <span @click="otherLogin('BAIDU')" class="pointer"><MyIcon type="icon-baidu-logo"/></span>
    <span @click="otherLogin('WEIBO')" class="pointer"><MyIcon type="icon-weibo-logo"/></span>
    <span @click="otherLogin('GITHUB')" class="pointer"><MyIcon type="icon-github-logo"/></span>
    <span @click="otherLogin('MICROSOFT')" class="pointer"><MyIcon type="icon-microsoft-logo"/></span>
  </div>
</template>

<script setup>
import icon from '@/utils/icon'
import {onBeforeMount, onMounted, reactive, ref} from "vue";
import {getBgiUrl} from "@/api/public";
import {useRouter} from "vue-router";
import VerifyImgBtn from "@/components/verify/VerifyImgBtn.vue";
import VerifyCodeBtn from "@/components/verify/VerifyCodeBtn.vue"
import {ElMessage} from 'element-plus'
import {getOAuthID, getRegister, postCode, postLogin, postRegister} from "@/api/account";
import store from "@/store";
import {getSiteConfig} from "@/api/management";

const router = useRouter();
let {MyIcon} = icon()
// 第三方登录
const otherLogin = (kind) => {
ElMessage('正在跳转至第三方平台,请稍候……')
console.log(kind)
let domain = window.location.protocol + "//" + window.location.host
if (kind === 'WEIBO') {
    getOAuthID(kind).then((response) => {
    console.log(response)
    let url = 'https://api.weibo.com/oauth2/authorize?client_id=' + response.clientId +
        '&response_type=code&redirect_uri=' + domain + '/OAuth/' + kind
    console.log(url)
    window.location.href = url;
    }).catch(response => {
    //发生错误时执行的代码
    console.log(response)
    ElMessage.error('获取第三方登录ID失败!')
    });
}
if (kind === 'QQ') {
    getOAuthID(kind).then((response) => {
    console.log(response)
    let url = 'https://graph.qq.com/oauth2.0/authorize?client_id=' + response.clientId +
        '&response_type=code&redirect_uri=' + domain + '/OAuth/' + kind + '&state=' +
        Math.random().toString(36).slice(-6)
    console.log(url)
    window.location.href = url;
    }).catch(response => {
    //发生错误时执行的代码
    console.log(response)
    ElMessage.error('获取第三方登录ID失败!')
    });
}
if (kind === 'PAY') {
    getOAuthID(kind).then((response) => {
    console.log(response)
    let url = 'https://openauth.alipay.com/oauth2/publicAppAuthorize.htm?app_id=' + response.clientId +
        '&scope=auth_user&redirect_uri=' + encodeURIComponent(domain + '/OAuth/' + kind) +
        '&state=' + Math.random().toString(36).slice(-6)
    console.log(url)
    window.location.href = url;
    }).catch(response => {
    //发生错误时执行的代码
    console.log(response)
    ElMessage.error('获取第三方登录ID失败!')
    });
}
if (kind === 'GITHUB') {
    getOAuthID(kind).then((response) => {
    console.log(response)
    let url = 'https://github.com/login/oauth/authorize?client_id=' + response.clientId +
        '&scope=user&redirect_uri=' + domain + '/OAuth/' + kind + '&state=' +
        Math.random().toString(36).slice(-6)
    console.log(url)
    window.location.href = url;
    }).catch(response => {
    //发生错误时执行的代码
    console.log(response)
    ElMessage.error('获取第三方登录ID失败!')
    });
}
if (kind === 'BAIDU') {
    getOAuthID(kind).then((response) => {
    console.log(response)
    let url = 'https://openapi.baidu.com/oauth/2.0/authorize?client_id=' + response.clientId +
        '&redirect_uri=' + domain + '/OAuth/' + kind + '&response_type=code&state=' +
        Math.random().toString(36).slice(-6)
    console.log(url)
    window.location.href = url;
    }).catch(response => {
    //发生错误时执行的代码
    console.log(response)
    ElMessage.error('获取第三方登录ID失败!')
    });
}
if (kind === 'MICROSOFT') {
    getOAuthID(kind).then((response) => {
    console.log(response)
    let url = 'https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?client_id=' + response.clientId +
        '&response_type=code&redirect_uri=' + domain + '/OAuth/' + kind +
        '&response_mode=query&scope=offline_access user.read&state=' + Math.random().toString(36).slice(-6)
    console.log(url)
    window.location.href = url;
    }).catch(response => {
    //发生错误时执行的代码
    console.log(response)
    ElMessage.error('获取第三方登录ID失败!')
    });
}
</script>

<style scoped lang="scss">

</style>

3. PC端授权回调页

<template>
  <Loading :type="'tips'" :text="'正在调用'+platform_name+'平台登录,请稍候……'"/>
</template>

<script setup>
import {useRouter} from "vue-router";
import {ElMessage} from 'element-plus'
import {onMounted, reactive, ref} from "vue";
import {postOAuthCallback} from "@/api/account";
import Loading from "@/components/common/Loading.vue"
import store from "@/store";

const router = useRouter()
// 平台名称
const platform_name = ref('')
// 回调登录表单
const OAuthForm = reactive({
  platform: '',
  kind: 'PC',
  code: '',
  redirect_uri: ''
})
// 向后端发送登录回调请求
const postCallback = () => {
  postOAuthCallback(OAuthForm).then((response) => {
    console.log(response)
    ElMessage({
      message: '登录成功!',
      type: 'success',
    })
    store.commit('setKeepLogin', false)
    store.commit('setUserSession', response)
    console.log(store.state.nextPath)
    router.push(store.state.nextPath)
  }).catch(response => {
    //发生错误时执行的代码
    console.log(response)
    ElMessage.error('自动登录异常,请更换其他登录方式!')
    router.push('/loginRegister')
  });
}
ref('')
onMounted(() => {
  OAuthForm.platform = router.currentRoute.value.params.platform
  OAuthForm.redirect_uri = window.location.protocol + "//" + window.location.host + router.currentRoute.value.path
  if (OAuthForm.platform === 'PAY') {
    OAuthForm.code = router.currentRoute.value.query.auth_code
  } else {
    OAuthForm.code = router.currentRoute.value.query.code
  }
  console.log(OAuthForm)
  switch (OAuthForm.platform) {
    case 'WEIBO':
      platform_name.value = '新浪微博'
      break;
    case 'QQ':
      platform_name.value = '腾讯QQ'
      break;
    case 'PAY':
      platform_name.value = '支付宝'
      break;
    case 'GITHUB':
      platform_name.value = 'GitHub'
      break;
    case 'BAIDU':
      platform_name.value = '百度'
      break;
    case 'MICROSOFT':
      platform_name.value = '微软'
      break;
    default:
      platform_name.value = '第三方'
  }
  postCallback()
})
</script>

<style scoped>

</style>

4. 手机端用户登录页

业务逻辑与手机端一致,只是授权页跳转时URL参数手机端和PC端略有差异

<template>
    <div class="other">
      <van-divider>第三方账号登录</van-divider>
      <div class="other-logo">
        <span @click="otherLogin('QQ')">
          <MyIcon class="logo-icon" type="icon-qq-logo"/>
          <p>QQ</p>
        </span>
        <span @click="otherLogin('PAY')">
          <MyIcon class="logo-icon" type="icon-alipay-logo"/>
          <p>支付宝</p>
        </span>
        <span @click="otherLogin('BAIDU')">
          <MyIcon class="logo-icon" type="icon-baidu-logo"/>
          <p>百度</p>
        </span>
        <span @click="otherLogin('WEIBO')">
          <MyIcon class="logo-icon" type="icon-weibo-logo"/>
          <p>微博</p>
        </span>
        <span @click="otherLogin('GITHUB')">
          <MyIcon class="logo-icon" type="icon-github-logo"/>
          <p>GitHub</p>
        </span>
        <span @click="otherLogin('MICROSOFT')">
          <MyIcon class="logo-icon" type="icon-microsoft-logo"/>
          <p>微软</p>
        </span>
      </div>
    </div>
</template>

<script setup>
import {Form, Button, Field, Divider, Icon, Checkbox, Toast} from 'vant';
import VerifyImgBtn from "@/components/verify/VerifyImgBtn.vue";
import {reactive, ref} from "vue";
import {getOAuthID, postLogin} from '@/api/account'
import store from "@/store/index";
import {useRouter} from "vue-router";
import icon from '@/utils/icon'

let {MyIcon} = icon()

const router = useRouter()
// 第三方登录
const otherLogin = (kind) => {
  Toast('正在跳转至第三方平台,请稍候……')
  console.log(kind)
  let domain = window.location.protocol + "//" + window.location.host
  if (kind === 'WEIBO') {
    getOAuthID(kind).then((response) => {
      console.log(response)
      let url = 'https://open.weibo.cn/oauth2/authorize?client_id=' + response.clientId +
          '&response_type=code&redirect_uri=' + domain + '/OAuth/' + kind + '&display=mobile'
      console.log(url)
      window.location.href = url;
    }).catch(response => {
      //发生错误时执行的代码
      console.log(response)
      Toast.fail('获取第三方登录ID失败!')
    });
  }
  if (kind === 'QQ') {
    getOAuthID(kind).then((response) => {
      console.log(response)
      let url = 'https://graph.qq.com/oauth2.0/authorize?client_id=' + response.clientId +
          '&response_type=code&redirect_uri=' + domain + '/OAuth/' + kind + '&display=mobile'
          + '&state=' + Math.random().toString(36).slice(-6)
      console.log(url)
      window.location.href = url;
    }).catch(response => {
      //发生错误时执行的代码
      console.log(response)
      Toast.fail('获取第三方登录ID失败!')
    });
  }
  if (kind === 'PAY') {
    getOAuthID(kind).then((response) => {
      console.log(response)
      let parameter = 'https://openauth.alipay.com/oauth2/publicAppAuthorize.htm?app_id=' +
          response.clientId + '&scope=auth_user&redirect_uri=' + domain + '/OAuth/' + kind + '&state=' + Math.random().toString(36).slice(-6)
      let url = 'alipays://platformapi/startapp?appId=20000067&url=' + encodeURIComponent(parameter)
      console.log(url)
      window.location.href = url;
    }).catch(response => {
      //发生错误时执行的代码
      console.log(response)
      Toast.fail('获取第三方登录ID失败!')
    });
  }
  if (kind === 'GITHUB') {
    getOAuthID(kind).then((response) => {
      console.log(response)
      let url = 'https://github.com/login/oauth/authorize?client_id=' + response.clientId +
          '&scope=user&redirect_uri=' + domain + '/OAuth/' + kind + '&state=' + Math.random().toString(36).slice(-6)
      console.log(url)
      window.location.href = url;
    }).catch(response => {
      //发生错误时执行的代码
      console.log(response)
      Toast.fail('获取第三方登录ID失败!')
    });
  }
  if (kind === 'BAIDU') {
    getOAuthID(kind).then((response) => {
      console.log(response)
      let url = 'https://openapi.baidu.com/oauth/2.0/authorize?client_id=' + response.clientId +
          '&redirect_uri=' + domain + '/OAuth/' + kind + '&response_type=code&display=mobile&state=' + Math.random().toString(36).slice(-6)
      console.log(url)
      window.location.href = url;
    }).catch(response => {
      //发生错误时执行的代码
      console.log(response)
      Toast.fail('获取第三方登录ID失败!')
    });
  }
  if (kind === 'MICROSOFT') {
    getOAuthID(kind).then((response) => {
      console.log(response)
      let url = 'https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?client_id=' + response.clientId +
          '&response_type=code&redirect_uri=' + domain + '/OAuth/' + kind +
          '&response_mode=query&scope=offline_access user.read&state=' + Math.random().toString(36).slice(-6)
      console.log(url)
      window.location.href = url;
    }).catch(response => {
      //发生错误时执行的代码
      console.log(response)
      Toast.fail('获取第三方登录ID失败!')
    });
  }
}
</script>

<style lang="scss" scoped>
@import "src/assets/style/index";

</style>

五、效果演示

1. 手机端登录

在这里插入图片描述

  • 授权回调页

在这里插入图片描述

  • 个人中心页

在这里插入图片描述

2. 电脑端登录

在这里插入图片描述

  • 授权回调页

在这里插入图片描述

  • 首页

在这里插入图片描述

3. admin后台

在这里插入图片描述

查看更多

微信公众号

微信公众号同步更新,欢迎关注微信公众号第一时间获取最近文章。在这里插入图片描述

博客网站

崔亮的博客-专注devops自动化运维,传播优秀it运维技术文章。更多原创运维开发相关文章,欢迎访问https://www.cuiliangblog.cn

  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值