Django+DRF+social-auth-app-django网站第三方登录

发布于 2020-11-10  1,442 次阅读


social-auth-app-django模块是专门用于Django的第三方登录OAuth2协议模块

目前流行的第三方登录都采用了OAuth2协议

安装

pip install social-auth-app-django

social-auth-app-django最新版本目前不支持飞书登录,本文将以飞书登录的方式介绍social-auth-app-django模块的使用

首先你需要去飞书开发平台创建一个你的应用,获取到App ID和App Secret

使用配置

1.将social_django添加到app配置,settings.py

2.输入命令migrate来生成第三方登录需要的表

python manage.py migrate social_django

3.配置需要的第三方登录模块,settings.py

第三方登录模块放在social_core插件下的backends目录,里面有很多第三方登录模块,比如,微信,微博,QQ,Facebook,GitHub,亚马逊等,没有飞书

# 第三方登录设置邮箱和用户名和手机号均可登录
AUTHENTICATION_BACKENDS = (
    #第三方登录配置之微博登录
    'social_core.backends.weibo.WeiboOAuth2',
    #第三方登录配置之QQ登录
    'social_core.backends.qq.QQOAuth2',
    #第三方登录配置之微信登录,微信有两种:oauth2 和 oauth2app,我们使用auth2
    'social_core.backends.weixin.WeixinOAuth2',

    'common.feishu.feishu.FeiShuOAuth2', # 使用自定义飞书登录
    'django.contrib.auth.backends.ModelBackend',
)

# 第三方登录相关

#微博应用的key与secret
SOCIAL_AUTH_WEIBO_KEY = ''
SOCIAL_AUTH_WEIBO_SECRET = ''

#QQ应用的key与secret
SOCIAL_AUTH_QQ_KEY = ''
SOCIAL_AUTH_QQ_SECRET = ''

#微信应用的key与secret
SOCIAL_AUTH_WEIXIN_KEY = ''
SOCIAL_AUTH_WEIXIN_SECRET = ''

#飞书应用的key与secret
SOCIAL_AUTH_FEISHU_KEY = ''
SOCIAL_AUTH_FEISHU_SECRET = ''

# 第三方登录成功后跳转到首页
SOCIAL_AUTH_LOGIN_REDIRECT_URL = 'http://xx.xxxx.cn/#/home/project/api/'

4.编写飞书OAuth2类

# -*- coding: utf-8 -*-
# @Time    : 2020-11-06 22:10
# @Author  : chenshiyang
# @Email   : chenshiyang@blued.com
# @File    : feishu.py
# @Software: PyCharm
import json
from urllib import parse
from requests import HTTPError
from social_core.backends.oauth import BaseOAuth2
from social_core.exceptions import AuthCanceled, AuthUnknownError


class FeiShuOAuth2(BaseOAuth2):
    """feishu OAuth authentication backend"""

    name = 'feishu'
    AUTHORIZATION_URL = 'https://open.feishu.cn/open-apis/authen/v1/index'
    ACCESS_TOKEN_URL = 'https://open.feishu.cn/open-apis/authen/v1/access_token'
    APP_ACCESS_TOKEN = 'https://open.feishu.cn/open-apis/auth/v3/app_access_token/internal/'
    REFRESH_TOKEN_URL = 'https://open.feishu.cn/open-apis/authen/v1/refresh_access_token'
    ACCESS_TOKEN_METHOD = 'POST'
    REDIRECT_STATE = False
    CACHE = {}
    EXTRA_DATA = [
        ('username', 'name'),
        ('avatar', 'avatar_big'),
    ]

    def get_user_details(self, response):
        """Return user details from feishu. API URL is:
        """

        if self.setting('DOMAIN_AS_USERNAME'):
            username = response.get('domain', '')
        else:
            username = response.get('name', '')

        return {
            'username': username,
            'email': response.get('email', ''),
            # 'avatar': response.get('avatar_big', ''),
            'feishu_userid': response.get('user_id', ''),
        }

    def user_data(self, access_token, *args, **kwargs):
        """获取用户信息

        Args:
            access_token:
            *args:
            **kwargs:

        Returns:

        """
        data = self.get_json('https://open.feishu.cn/open-apis/authen/v1/user_info', headers={
            'Content-Type': 'application/json',
            'Authorization': f'Bearer {access_token}',
        })
        data = data['data']
        nickname = data.get('name')
        if nickname:
            data['name'] = nickname.encode(
                'raw_unicode_escape'
            ).decode('utf-8')
        return data

    def auth_params(self, state=None):
        """获取回调地址

        Args:
            state:

        Returns:

        """
        appid, secret = self.get_key_and_secret()
        redirect_uri = self.get_redirect_uri(state)

        params = {
            'app_id': appid,
            'redirect_uri': parse.quote(redirect_uri)
        }


        return params

    def app_access_token(self):
        """获取 app_access_token(企业自建应用)

        Returns:
            {
                "code":0,
                "msg":"ok",
                "app_access_token":"xxxxx",
                "expire":7200,  // 过期时间,单位为秒(两小时失效)
                "tenant_access_token":"xxxxx"
            }
        """
        appid, secret = self.get_key_and_secret()

        try:
            response = self.request_access_token(
                self.APP_ACCESS_TOKEN,
                data={'app_id': appid, 'app_secret': secret},
                headers=self.auth_headers(),
                method=self.ACCESS_TOKEN_METHOD
            )
        except HTTPError as err:
            if err.response.status_code == 400:
                raise AuthCanceled(self, response=err.response)
            else:
                raise
        except KeyError:
            raise AuthUnknownError(self)
        if 'errcode' in response:
            raise AuthCanceled(self)

        return response['app_access_token']

    def auth_complete_params(self, state=None):
        """飞书code效验

        Args:
            state:

        Returns:

        """
        appid, secret = self.get_key_and_secret()
        return {
            'grant_type': 'authorization_code',  # request auth code
            'code': self.data.get('code', ''),  # server response code
            'app_id': appid,
            'secret': secret,
            'app_access_token': self.app_access_token()
        }

    def refresh_token_params(self, token, *args, **kwargs):
        """刷新token

        Args:
            token:
            *args:
            **kwargs:

        Returns:

        """
        return {
            'app_access_token': token,
            'refresh_token': kwargs['refresh_token'],
            'grant_type': 'refresh_token',
        }

    def auth_complete(self, *args, **kwargs):
        """Completes login process, must return user instance
        获取用户身份信息
        """

        self.process_error(self.data)

        try:

            data = self.auth_complete_params()
            response = self.request_access_token(
                self.ACCESS_TOKEN_URL,
                data=json.dumps(data),
                headers={'Content-Type': 'application/json'},
                method=self.ACCESS_TOKEN_METHOD
            )

            if response['code'] == 0:
                self.CACHE['response'] = response
            else:
                response = self.CACHE['response']
        except HTTPError as err:
                if err.response.status_code == 400:
                    raise AuthCanceled(self, response=err.response)
                else:
                    raise
        except KeyError:
            raise AuthUnknownError(self)

        if 'errcode' in response:
            raise AuthCanceled(self)

        self.process_error(response)

        return self.do_auth(response['data']['access_token'], response=response['data'],
                                *args, **kwargs)

5.配置第三方登录路由url

6.配置 TEMPLATES,settings.py

配置这里,当用户登录的时候,如果用户不存在,会自动在用户表创建用户,并且关联用户信息

7.配置后,启动程序,根据url来构造请求地址和,回调地址

文件路径在social_django下

urlpatterns = [
    # authentication / association
    # <backend>是一个变量接收要使用登录模块的名称小写
    # 请求URL
    url(r'^login/(?P<backend>[^/]+){0}$'.format(extra), views.auth,
        name='begin'),
    # 回调URL
    url(r'^complete/(?P<backend>[^/]+){0}$'.format(extra), views.complete,
        name='complete'),
    # disconnection
    url(r'^disconnect/(?P<backend>[^/]+){0}$'.format(extra), views.disconnect,
        name='disconnect'),
    url(r'^disconnect/(?P<backend>[^/]+)/(?P<association_id>\d+){0}$'
        .format(extra), views.disconnect, name='disconnect_individual'),
]

请求URL构造为:http://域名或者ip/login/使用模块名称小写/

如:http://127.0.0.1:8000/danlan/login/feishu/

回调URL构造为:http://域名或者ip/complete/使用模块名称小写/

如:http://127.0.0.1:8000/danlan/complete/feishu/

8.飞书应用后台添加安全重定向url

添加重定向 URL 作为免登授权码跳转地址。其他重定向 URL 将无法获取免登授权码

9.配置成功后前端调用

10.DRF配置

前后端分离的项目中将token放进响应cookie中实现登录跳转之后自动登录,此处我们需要修改social_core/actions.py 文件来适配DRF

需要在文件头部导入

from rest_framework_jwt.serializers import jwt_payload_handler, jwt_encode_handler

具体代码如下

# return backend.strategy.redirect(url)
response = backend.strategy.redirect(url)
payload = jwt_payload_handler(user)
response.set_cookie("username", user.username, max_age=24 * 3600)
response.set_cookie("avatar", user.avatar, max_age=24 * 3600)
response.set_cookie("id", user.id, max_age=24 * 3600)
response.set_cookie("token", jwt_encode_handler(payload), max_age=24 *3600)
return response

11.常见报错

  • 飞书重定向地址需要转义
  • 飞书登录时user表里有相同的相同邮箱(用户名密码注册过的)会登录失败

一名测试工作者,专注接口测试、自动化测试、性能测试、Python技术。