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

一、OAuth概述

1. OAuth功能

oauth2.0 单点登录集成(前后端分离项目OAuth登录总结)(1)

举个例子,当你想要上班摸鱼逛知乎时,但是你从来没有注册过知乎账号,而且你又嫌麻烦不愿意注册知乎账号,此时就可以使用第三方社交账号登录,例如使用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流程

oauth2.0 单点登录集成(前后端分离项目OAuth登录总结)(2)

上述例子中的知乎就是客户端,QQ就是认证服务器,OAuth2.0就是客户端和认证服务器之间为了解决相互不信任而产生的一个授权协议。(要是相互信任那豆瓣直接读取QQ的数据库登录不就好了,搞这么费劲作甚)

整个流程分为以下三个阶段

① 用户点击QQ登录进入授权页面同意授权,登录完成后获取到code;

② 知乎网站请求QQ服务器,通过code换取授权access_token;

③ QQ通过网页授权access_token向知乎返回用户的基本信息。

二、项目开发思路

1. 整体流程思路

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

完整的设计思路如下:

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

oauth2.0 单点登录集成(前后端分离项目OAuth登录总结)(3)

3. QQ

qq申请地址https://connect.qq.com/

qq文档:https://wiki.connect.qq.com/oauth2-0开发文档

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,但不一定适合自己的账户权限)

oauth2.0 单点登录集成(前后端分离项目OAuth登录总结)(4)

oauth2.0 单点登录集成(前后端分离项目OAuth登录总结)(5)

四、后端代码

1. 项目配置(settings.py)

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

2. 模型(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

oauth2.0 单点登录集成(前后端分离项目OAuth登录总结)(6)

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

oauth2.0 单点登录集成(前后端分离项目OAuth登录总结)(7)

oauth2.0 单点登录集成(前后端分离项目OAuth登录总结)(8)

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. 手机端登录

oauth2.0 单点登录集成(前后端分离项目OAuth登录总结)(9)

oauth2.0 单点登录集成(前后端分离项目OAuth登录总结)(10)

oauth2.0 单点登录集成(前后端分离项目OAuth登录总结)(11)

2. 电脑端登录

oauth2.0 单点登录集成(前后端分离项目OAuth登录总结)(12)

oauth2.0 单点登录集成(前后端分离项目OAuth登录总结)(13)

oauth2.0 单点登录集成(前后端分离项目OAuth登录总结)(14)

3. admin后台

oauth2.0 单点登录集成(前后端分离项目OAuth登录总结)(15)

,