TOP

django 接入 oauth2

原理

Auth2 的协议说明  

https://oauth.net/2/

相关python包推荐

https://oauth.net/code/

概述

场景适用

有一个 "快浪" 的论坛网站,访问者必须要注册用户后才可访问, 且允许直接基于 QQ 关联注册, 登录访问

QQ 不是很随便的怕平台, 基于用户授权后, 才会同意 快浪 读取 QQ 所保存的信息进行访问或者注册

传统方法是,用户将自己的 QQ 用户名和密码存储在 快浪 ,快浪 就可以登录qq后读取用户的照片了, 随之而来也存在一系列问题

(1)"快浪" 为了保障后续服务,需要保存用户登录名以及密码,这样很不安全。

(2) QQ 不得不部署密码登录,而我们知道,单纯的密码登录并不安全。

(3)"快浪" 拥有了获取用户储存在 QQ 所有资料的权力,用户没法限制 "快浪" 获得授权的范围和有效期。

(4)用户只有修改密码,才能收回赋予 "快浪" 的权力。但是这样做,会使得其他所有获得用户授权的第三方应用程序全部失效。

(5)只要有一个第三方应用程序被破解,就会导致用户密码泄漏,以及所有被密码保护的数据泄漏。

OAuth就是为了解决上面这些问题而诞生的。

角色定义

(1) Third-party application:第三方应用程序,又称"客户端"(client),即场景中的 "快浪"

(2)HTTP service:HTTP服务提供商,简称"服务提供商",即场景中的 QQ 

(3)Resource Owner:资源所有者,又称"用户"(user

(4)User Agent:用户代理,即 浏览器

(5)Authorization server:认证服务器,即服务提供商专门用来处理认证的服务器

(6)Resource server:资源服务器,即服务提供商存放用户生成的资源的服务器, 可与认证服务器共用

流程概述

(A)用户打开客户端以后,客户端要求用户给予授权 --- (用户访问快浪, 去请求QQ的信息)

(B)用户同意给予客户端授权 --- (在QQ的认证页面同意了对快狼的授权)

(C)客户端使用上一步获得的授权,向认证服务器申请令牌 --- (快浪拿到了授权信息, 对QQ的认证服务器换取token)

(D)认证服务器对客户端进行认证以后,确认无误,同意发放令牌 --- (QQ认证通过后发放token)

(E)客户端使用令牌,向资源服务器申请获取资源 --- (快浪携带token向QQ申请访问资源)

(F)资源服务器确认令牌无误,同意向客户端开放资源 --- (QQ检查token无误, 给予资源相应)

授权模式

客户端必须得到用户的授权(authorization grant),才能获得令牌(access token)。OAuth 2.0定义了四种授权方式。

  • 授权码模式(authorization code)
  • 简化模式(implicit)
  • 密码模式(resource owner password credentials)
  • 客户端模式(client credentials)

授权码模式

工作流程

授权码模式(authorization code)是功能最完整、流程最严密的授权模式。它的特点就是通过客户端的后台服务器,与"服务提供商"的认证服务器进行互动。

(A)用户访问客户端,客户端导向认证服务器

(B)用户选择是否给予客户端授权

(C)假设用户给予授权,认证服务器将用户导向客户端事先指定的"重定向URI"(redirection URI),同时附上一个授权码

(D)客户端收到授权码,附上早先的"重定向URI",向认证服务器申请令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见

(E)认证服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)

结合场景示例进行简述

(1)用户通过浏览器访问快浪

(2)快浪将用户导向QQ认证页面

(3)用户在QQ授权页面处理是否授权

(4)若用户同意授权,获得授权码

(5)QQ认证页面将用户重定向到快浪事先指定的 "重定向URI" ,同时携带授权码

(6)快浪的后台收到授权码后带上指定好的"重定向URI",向QQ的认证服务器申请访问相关凭证

(7)QQ认证服务器核对授权码以及重定向URI无误后,向快浪发送访问相关凭证

(8)快浪携带访问相关凭证, 对QQ的相关资源进行访问

安装

pip install django-oauth-toolkit
django-oauth-toolkit==2.3.0

配置

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'users',
    'oauth2_provider',
]

配置后执行迁移命令

python manage.py migrate

在路由中配置 iam 分发子路由

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

urlpatterns = [
    path('admin/', admin.site.urls),
    path('o/', include('oauth2_provider.urls', namespace='oauth2_provider')),
]

配置文件中设置登录url, 可以设置相对路径域名也可以设置绝对域名

LOGIN_URL='/admin/login/'
# LOGIN_URL = 'https://xxx.cn/login'

授权码模式接入流程

app 注册

这里设置系统的端口为 10081. 首先注册一个app, 访问以下链接进行注册

http://127.0.0.1:10081/o/applications/register/

进入如下页面然后基于授权码模式进行注册

这里记得必须记录下来  Client secret , 因为离开这个页面后就再也没法看到了 (后续再进来就是加密后的信息, 且无法反向解密)

这里我们先记录下来

Client id:  fB6AqkQB5vPaiPUz9UorbNG8mvEUHIzdCf9GSSSM
Client secret: zK9zVvdZYWTeIiPGaQVlHe15X8DM7AZUkItAtHrI4xMya7sOQxMNa2QRc5mQySO99AsNwjMUZ3kAjeufo1riLwd6ugkwbe3ums1muajKKfuAZZgYfvW3W0svQKME52WI
http://127.0.0.1:10081/api/v1/job/

生成授权码

import random
import string
import base64
import hashlib

code_verifier = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(random.randint(43, 128)))

code_challenge = hashlib.sha256(code_verifier.encode('utf-8')).digest()
code_challenge = base64.urlsafe_b64encode(code_challenge).decode('utf-8').replace('=', '')

同样记录下来

code_verifier:    F5C4PGIU5KZRKVE3AS79YC06IRX0YPURI8FNZ6MN4SZSUHUFZSCEKISOV98N3J236YJHA02H
code_challenge:  Y5O6SKj_f4vAyWrFPEK9oIMHZJmZfNeueqs0jh-7kw0

构造认证链接

认证链接由以下部分构成

http://127.0.0.1:10081/o/authorize/?response_type=code&code_challenge={}&code_challenge_method=S256&client_id={}&redirect_uri={}

 

  • response_type              固定 code
  • code_challenge             授权码
  • code_challenge_method   固定 S256
  • client_id                         注册的 app 客户端id
  • redirect_uri                   关联注册 app 回调地址
 结合上面的的信息, 拼接后的结果如下
http://127.0.0.1:10081/o/authorize/?response_type=code&code_challenge=Y5O6SKj_f4vAyWrFPEK9oIMHZJmZfNeueqs0jh-7kw0&code_challenge_method=S256&client_id=fB6AqkQB5vPaiPUz9UorbNG8mvEUHIzdCf9GSSSM&redirect_uri=http://127.0.0.1:10081/api/v1/job/

进入授权页面, 点击授权后则进入回调地址 (redirect_url)

把 authorize.html 复制出来,. 放在 templates/oauth2_provider下 调整可以自定义一些展示信息

{% extends "oauth2_provider/base.html" %}

{% load i18n %}
{% block content %}
    <div class="block-center">
        {% if not error %}
            <form id="authorizationForm" method="post">
                <h3 class="block-center-heading">{% trans "是否授权" %} {{ application.name }}?</h3>
                {% csrf_token %}

                {% for field in form %}
                    {% if field.is_hidden %}
                        {{ field }}
                    {% endif %}
                {% endfor %}

                <p>{% trans "应用需要以下权限" %}</p>
                <ul>
                    {% for scope in scopes_descriptions %}
                        <li>{{ scope }}</li>
                    {% endfor %}
                </ul>

                {{ form.errors }}
                {{ form.non_field_errors }}

                <div class="control-group">
                    <div class="controls">
                        <input type="submit" class="btn btn-large" value="{% trans '拒绝' %}"/>
                        <input type="submit" class="btn btn-large btn-primary" name="allow" value="{% trans '同意' %}"/>
                    </div>
                </div>
            </form>

        {% else %}
            <h2>Error: {{ error.error }}</h2>
            <p>{{ error.description }}</p>
        {% endif %}
    </div>
{% endblock %}
authorize.html

调整后如下

 具体的可授权信息在配置文件中也可以配置

OAUTH2_PROVIDER = {
    # this is the dictionary of available scopes that will be visible to the user
    'SCOPES': {
        '用户名': '查看用户名信息',
        '用户邮箱': '查看用户邮箱信息'
    }
}

获取TOKEN

通过上面的授权后. 则重定向到回调地址  redirect_uri, 同时会附带授权码

这里记录下来 这个 code 

K3IuLkKYWAHVBheBa1Y6wcEPGHFp0t

结合之前的信息, 发起token的申请

http://127.0.0.1:10081/o/token/

client_id:fB6AqkQB5vPaiPUz9UorbNG8mvEUHIzdCf9GSSSM client_secret:zK9zVvdZYWTeIiPGaQVlHe15X8DM7AZUkItAtHrI4xMya7sOQxMNa2QRc5mQySO99AsNwjMUZ3kAjeufo1riLwd6ugkwbe3ums1muajKKfuAZZgYfvW3W0svQKME52WI code:K3IuLkKYWAHVBheBa1Y6wcEPGHFp0t code_verifier:F5C4PGIU5KZRKVE3AS79YC06IRX0YPURI8FNZ6MN4SZSUHUFZSCEKISOV98N3J236YJHA02H redirect_uri:http:
//127.0.0.1:10081/api/v1/job/ grant_type:authorization_code

验证TOKEN

函数校验视图

这里直接在 urls.py 里面写个函数视图作为演示, 这里基于当前用户返回信息

from django.http import JsonResponse
from django.urls import path, include
from oauth2_provider.decorators import protected_resource

@protected_resource()
def my_view(request):
    user = request.user
    return JsonResponse({'username': user.username, 'email': user.email})

urlpatterns = [
    # 前缀:/api/v1/user/
    path('auth2/myo/', my_view),
]

类校验视图

# -*- coding:utf-8 -*-
"""
urls
"""
from django.urls import path
from oauth2_provider.contrib.rest_framework import TokenHasReadWriteScope, OAuth2Authenticationfrom rest_framework import serializers, generics, permissions

from user.models import User class Auth2UserSerializer(serializers.ModelSerializer): class Meta: model = User fields = ('username',) class UserList(generics.ListCreateAPIView): authentication_classes = (OAuth2Authentication,) permission_classes = [permissions.IsAuthenticated, TokenHasReadWriteScope] queryset = User.objects.all() serializer_class = Auth2UserSerializer urlpatterns = [ # 前缀:/auth2/ path('class_myo/', UserList.as_view()), ]

校验结果

成功

携带token 后可以访问并获取当前的登录用户信息

异常

若不携带则会返回 403 

提供错误的 token 一样 403

 

posted @ 2024-02-18 16:27  羊驼之歌  阅读(388)  评论(0编辑  收藏  举报