一步步玩转测试平台开发(五):如何使用Keycloak进行Django身份验证(中)

在上文《如何使用Keycloak进行Django身份验证(上)》中讲述了Keycloak是干什么,如何搭建本地的Keycloak的环境,简单描述了系统对接Keycloak的步骤以及一个demo app的动图演示。本文将会手把手一步步讲解如何实现这个demo app。

在上干货前还是先把为了实现使用Keycloak进行Django身份验证这个方案所经历的波折说一下,实在是太坎坷了。

本来我觉得Django是一个很常用的Web框架, Keycloak也不是一个新生的事务,那自然而然网上应该有一大把实现使用Keycloak进行Django身份验证的方案供参考。结果百度了一大堆,发现国内居然没有一篇涉及这个方案的,大部分都是spring系对接的方案。当时就觉得头大了,于是我将目光放到了Google国外的文章上。虽然能查到的结果很少很少,但还是有参考的,于是接下来用了一周的时间试了django-keycloak、django-rest-framework-keycloak、python-keycloak和Keycloak-client这四个第三方库。但照着提供的说明文档进行尝试,总是碰到各种莫名其妙的异常,也尝试着在github上联系作者提issue,但最终都没能解决我碰到的问题。原因在两个地方:第一、我用的Django版本是3.0.4,上面提到的这些包基本都是适配1.x版本;第二,由于自己在Django这个框架上还属于小学生,很多底层的东西确实没有深入的研究过,因而也可能存在哪些配置有出入的地方。但总之所有的尝试都失败了,头真的彻底大了。

谁都知道选择放弃就等于跟成功拜拜,我必须要实现使用Keycloak进行Django身份验证,否则平台就不会符合部门的安全要求。目标很明确,那就继续查继续试吧。大家还记得在上文中介绍Keycloak的时候,首先就说了Keycloak 是基于 OAuth 2.0JSON Web Token(JWT) 规范,还特意给这两个规范做了加粗处理,这就是给本文做的伏笔。既然上面这条路走不通,那么就从这两个规范找突破口吧!于是我先尝试网上找Django Oauth 2.0的实现方案,所有查到结果都指向了social-auth-app-django这个库,通过这个库可以实现Django基于Oauth 2.0通过Facebook, LiknkedIn的账户登录。既然如此,那就试试这个库吧。等安装完这个库后,我浏览了它的源码文件结构,欣喜的发现它的核心包social_core/backends下包含了keycloak.py这个文件,再经过浏览这个文件的源码后,比对了基于Facebook做身份登录的配置说明,突然心情大好,正应了陆游他老人家的那句话:“山重水复疑无路,柳暗花明又一村。”

好,苦水吐完了,开始上干货(应该是国内首篇图文并茂详细讲解如何实现使用Keycloak进行Django身份验证的文章)。

本文我们将构建一个Django的demo应用程序,该应用程序允许用户通过Keycloak帐户登录。只有python 3.x才能跟随后续的教程完成最终demo应用,并且假定您已具有了Django的基本工作知识,已经在本地配置好了pip环境。

注:在本系列技术连载的后续文章中我将会占用一定篇幅数来对Django的基本工作知识做一下普及

让我正式开始吧!

第一步: 创建并注册Django应用

我们将创建一个新的Django项目并安装依赖项。这里我又要吐槽一下,很多文章在说明Django的时候都是命令行的方式来说明如何配置Django,但实际上我们平时工作在进行python开发的时候都是使用Pycharm,有多少人是通过命令行的方式来配置Django呢?所以接下来如果可以通过Pytharm就能搞定的事,我就上Pytharm的操作截图来进行说明。此处如有不惯,请见谅,你可以退票,但票务都下班了。

接下来要安装Django和相关的依赖包,这个时候你可以在虚拟环境中,也可以在本地全局环境中安装,这里就不做特别要求了,客官随意:

在安装的过程中如果后面没有跟版本号,pip会自动安装最新的包,目前django最新的版本是3.0.8

pip install django
pip install social-auth-app-django

注: pip命令换成pip3,根据你本地的pip环境配置决定

Pycharm中创建一个Django项目,我们起名叫“django_keycloak_demo”, 具体操作看下图: Pycharm新建Django项目.jpg

操作完,在Pycharm项目栏你就能看到你新建的项目了,这个时候你就会在这个新建的项目下看到跟项目同名的主应用也创建好了: 创建好后的项目.jpg

接下来我们将创建一个名为“keycloak_demo”的Django app 创建django应用.jpg

成功创建后,你就会在django_keycloak_demo这个Django主应用下看到你刚才新创建的应用了,并且该应用下相关文件也一并都创建好了:

自定义的Django应用创建后.jpg

在django_keycloak_demo这个Django主应用下找到settings.py文件,并同时将django_keycloak_demo和keycloak_demo添加到INSTALLED_APPS(即:在Web应用进行注册):

# django_keycloak_demo/settings.py

INSTALLED_APPS = [
 'django.contrib.admin',
 'django.contrib.auth',
 'django.contrib.contenttypes',
 'django.contrib.sessions',
 'django.contrib.messages',
 'django.contrib.staticfiles',
 'social_django',
 'keycloak_demo'
]

Django注册应用.jpg

第二步: Django配置MySQL数据库

因为绝大部分的使用场景就是MySQL + Django,因此本次的demo项目我们也使用MySQL + Django的组合

首先,我们先要在MySQL数据库创建一个字符集为:utf8 -- UTF-8 Unicode的数据库: 创建数据库.jpg

在django_keycloak_demo这个Django主应用下找到settings.py文件配置该数据库:

# django_keycloak_demo/settings.py

DATABASES = {
           'default': {
                        'ENGINE': 'django.db.backends.mysql',
                        'NAME': 'keycloak_demo',
                        'HOST': '127.0.0.1',
                        'PORT': 3306,
                        'USER': 'root',
                        'PASSWORD': '123456'
                 }
}

Django设置数据库.jpg

安装pymysql第三方库

pip install pymysql

在django_keycloak_demo这个Django主应用下找到init.py文件添加读写MySQL的初始化代码:

# django_keycloak_demo/_init_.py

import pymysql
pymysql.install_as_MySQLdb()

初始化读写mysql.jpg

通过migrate命令初始化django数据库 migrate数据库.jpg.png

操作成功后,你就会在MySQL数据库中看到Django的系统表都创建好了,如果你看到social_auth_xxxxxx 这些表存在了,那么OK恭喜你配置成功了 初始化Django-MySQL数据表.jpg

第三步: 配置身份验证类

在用户身份验证期间,Django系统后台维护了一个检查“身份验证后端”列表。如果第一种身份验证方法失败,则Django尝试第二种身份验证,依此类推,直到尝试了所有后端。

该AUTHENTICATION_BACKENDS 数组包含身份验证后端类的列表,并且默认情况下设置为:

['django.contrib.auth.backends.ModelBackend']

我们可以对其进行更新并添加新的身份验证类,以允许使用Keycloak进行身份验证。要对此进行更新,请在settings.py文件中添加以下代码:

# django_keycloak_demo/settings.py

# 重写django的认证后端,需要配置
AUTHENTICATION_BACKENDS =(
                     'social_core.backends.keycloak.KeycloakOAuth2', #使用keycloak登录
                     'django.contrib.auth.backends.ModelBackend', # 指定django的ModelBackend类
)

第四步: 添加模板和静态文件

上面几步都是设置和配置应用程序,现在继续进行可视化操作,我们将建立显示应用程序的模板基础。

在项目下创建templates文件夹,并在该文件夹下创建三个html文件:base.html、login.html、 home.html。

创建三个网页.jpg

现在,打开base.html文件并粘贴以下代码段:

<!-- templates/base.html -->

{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
     <meta charset="UTF-8" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
     <meta http-equiv="X-UA-Compatible" content="ie=edge" />
     <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO"
 crossorigin="anonymous" />
     <link rel="stylesheet" href="{% static 'css/index.css' %}" />
     <title>Social Auth with Django</title>
</head>
<body>
     <div class="container-fluid">
         <div>
             <h1 class="text-white text-center">{% block title %}{% endblock %}</h1>
             <div class="card p-5">
                  {% block content %}
                  {% endblock %}
             </div>
         </div>
     </div>
</body>
</html>

将以下代码段粘贴到login.html文件中:

<!-- templates/login.html -->
 {% extends 'base.html' %}
 {% block title %} Sign in {% endblock %}
 {% block content %}
 <div class="row">
      <div class="col-md-8 mx-auto social-container my-2 order-md-1">
           <button class="btn btn-danger mb-2">
                 <a href="{% url 'social:begin' 'keycloak' %}">Login with Keycloak</a>
           </button>
     </div>
 </div>
 {% endblock %}

最后,home.html使用以下代码更新文件:

<!-- templates/home.html -->
{% extends 'base.html' %}
{% block title %} Home {% endblock %}
{% block content %}
<div class="row">
     <div class="col-sm-12 mb-3">
          <h4 class="text-center"> Welcome {{ user.username }} </h4>
     </div>
</div>
{% endblock %}

我们需要一些样式来帮助我们的代码在呈现时看起来不错,因此让我们在文件夹static的根目录中创建一个名为的core文件夹,然后将样式存储在该文件夹中。

在settings.py文件中设置系统访问static文件夹的配置:

# django_keycloak_demo/settings.py

# Static files (CSS, JavaScript, Images)
# [https://docs.djangoproject.com/en/3.0/howto/static-files/](https://docs.djangoproject.com/en/3.0/howto/static-files/)
STATIC_URL = '/static/'
STATICFILES_DIRS = (os.path.join(BASE_DIR, 'static'),)

css在static目录中创建一个名为folder 的文件夹,最后,index.css在该css文件夹中创建一个文件。 创建static文件.jpg

现在打开index.css文件并使用以下代码更新它:

/*/_ index.css _/*/

img {
        border: 3px solid #282c34;
}
.container-fluid {
        height: 100vh;
        background-color: #282c34;
        display: flex;
        justify-content: center;
        align-items: center;
}

.container-fluid > div {
        width: 85%;
        min-width: 300px;
        max-width: 500px;
}

.card {
       width: 100%;
}

.social-container {
       display: flex;
       flex-direction: column;
       justify-content: center;
}

.btn a, .btn a:hover {
       color: white;
       text-decoration: none ;
}

第五步: 设置视图和URL

我们将定义视图并注册应用程序需要工作的URL,因此打开keycloak_demo/views.py文件并用下面的代码段替换其内容:

# keycloak_demo/views.py

from django.shortcuts import render
from django.contrib.auth.decorators import login_required

# Create your views here.

def login(request):
      return render(request, 'login.html')

@login_required
def home(request):
      return render(request, 'home.html')

接下来,我们将为应用程序注册路由并附加其匹配的视图功能。用django_keycloak_demo/urls.py下面的代码替换文件的内容:

#django_keycloak_demo/urls.py

from django.contrib import admin
from django.urls import path,include
from django.contrib.auth import views as auth_views
from keycloak_demo import views

urlpatterns = [
              path('admin/', admin.site.urls),
              path("login/", views.login, name="login"),
              path("logout/", auth_views.LogoutView.as_view(), name="logout"),
              path('', include('social_django.urls', namespace="social")),
              path("", views.home, name="home"),
]

在settings.py文件中,我们需要设置四个新的配置项:LOGIN_URLLOGOUT_URLLOGIN_REDIRECT_URLLOGOUT_REDIRECT_URL。 它们将在用户在认证完成后重定向使用

# django_keycloak_demo/settings.py

LOGIN_URL = 'login'  
LOGIN_REDIRECT_URL = 'home'  
LOGOUT_URL = 'logout'  
LOGOUT_REDIRECT_URL = 'login'

在settings.py文件中,设置允许访问host

# django_keycloak_demo/settings.py
ALLOWED_HOSTS = ["*"]

OK,现在我们可以运行该应用程序以查看到目前为止已构建的内容。让我们使用以下命令启动服务器: 运行Django.jpg

我们可以在 http://127.0.0.1:8000/login/ 上访问该登录网页了 login界面.jpg

能看到这个界面证明之前的几步都操作正确,在下一部分中,我们将在Keycloak系统中注册我们的应用程序,以便可以通过Keycloak对用户进行身份验证。

第六步:在Keycloak系统中注册应用程序

我相信您已经按照上文《如何使用Keycloak进行Django身份验证(上)》中的描述进行对Keycloak系统添加Realm的操作和在该Realm下创建用户的操作了,那么接下来,我们就要开始在Keycloak系统上注册我们的应用程序。登录Keycloak后台系统,点击添加Client按钮进行注册Client(我们的应用)操作: Keycloak创建client按钮.jpg

设置该Client ID为“demo-project” Keycloak新建client界面.jpg Keycloak的Client清单界面.jpg

依据下图配置该Client 配置client.jpg

给client配置Realm clent配置Realm.jpg

截止目前所有在Keycloak注册我们的应用的工作就结束了,下一步就又要回到Django中来配置相应keycloak的访问鉴权设置

第七步:在Django后端配置相应Keycloak的访问鉴权设置

首先在Django后端配置相应Keycloak的访问鉴权设置前,我们要先拿到Keycloak要是用的Realm的公钥,和你的应用在Keycloak注册时生成的私钥。

按照下图来获取Realm的公钥: 获取Keycloak-Realm公钥.jpg

按照下图来获取Client的私钥: 获取Client公钥.jpg

在django_keycloak_demo的settings.py中配置相应Keycloak的访问鉴权设置:

# django_keycloak_demo/settings.py

SOCIAL_AUTH_KEYCLOAK_KEY = 'demo-project' # Client ID
SOCIAL_AUTH_KEYCLOAK_SECRET = '9d8a6ecf-cd85-4bce-813d-eb5c750274a4'# Client 秘钥
SOCIAL_AUTH_KEYCLOAK_PUBLIC_KEY = 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm/MhrpftW+jWxTD1PqdLYiM85ZfnCoGtYFixONfEjrgmd36lMIeqmrbhIjyLEQ+ETHSrDMOTl9ud1Gs+bRsYfabwV3cFJKEVhqCyn+kk5fbd0PU3dNN71PnE6AU02elpL/trrCDwHtif3zQttF8JadfmwOzLEmkDXzMmhk+lyGFnq0Wa04u4OMLi8FWSMY5Gn0rQVocSTc4fhaveq6CUBz/HQAHTFu7jgwi2Rsd+zeEpTInop235Gj1LUKAV9Gyh12bVkZOTM9H2IQ3TSnPY1txB3ACYxJ2ZWNQsr/ZU7onCw7OdJcyLSJqbCmA5RhaV9HGW02lDV/uzrQilU4D9zwIDAQAB'# Realm 公钥
SOCIAL_AUTH_KEYCLOAK_AUTHORIZATION_URL = 'http://127.0.0.1:8080/auth/realms/employee_demo/protocol/openid-connect/auth' # keyclock鉴权链接,注意realms/后面的名称是你的用的realm的名称
SOCIAL_AUTH_KEYCLOAK_ACCESS_TOKEN_URL = 'http://127.0.0.1:8080/auth/realms/employee_demo/protocol/openid-connect/token'# 获取keyclock 访问token的链接, 注意realms/后面的名称是你的用的realm的名称

检查一下templates下的login.html中登录按钮的链接是否正确: login文件链接检查.jpg

到此为止,所有在Django和Keycloak上的配置工作都做完了,下面我们就要开始测试这个demo程序工作是否正常了

第八步:在Django后端配置相应Keycloak的访问鉴权设置

经过前面繁琐的操作后,我们终于可以测试这个demo应用是否工作正常,首先按照上面的第五步最后的操作通过runserver启动demo应用,并且访问http://127.0.0.1:8000/login/ 登录界面,然后点击登录按钮,如果你是按照我上面的说明一步步操作的,那么这个时候就会很顺利的跳转到keycloak的登录界面,如下图: keycloak用户登录界面.jpg

然后在这个界面输入你之前在realm下创建的用户的用户名和密码后,点击login。如果一切正常的化,就应该进入我们应用的Home页面。

呵呵,理想很丰满,现实却很骨感。如果你确实按照我上面说的操作了,那么99.99999999%的可能你会看到下面这个错误提示页,而不是home页。通过错误页的提示,你得知这个鉴权过程被取消了。懵逼了吧,为什么啊!我的天啊,一下子就回到了解放前?问题出在哪了?这些确实是我当时看到这个错误后的真实心理活动。 鉴权被取消错误提示界面.jpg

于是为了解决这个你无论如何都百度或者Google不到答案的恶心问题,我开始在一层层的逻辑中打印log,同时疯狂的通过抓包和用Postman模拟接口请求。大概折腾了我将近2个小时,终于找到原因了,居然问题出在了social-auth-app-django这个第三方库的social_core\backends\oauth.py源码内。这个源码的第348行是要给keycloak系统获取token的接口请求body体设置“grant_type”的值不对,正确的应该是:"client_credentials",可源码却设置为"authorization_code",这样就导致你永远拿不到keycloak给你返回的token,我真是疯了,为什么啊!于是乎你要将源码修改成如下: 修改源码.jpg

修改完源码后,你还要在keycloak上把该client的session table设置页中所有活着的session都logout后,再重新点击demo应用的login按钮,然后输入正确的用户名和密码,之后点击login。Home页就这样华丽的出现在你的眼前了,大功告成! Home页.jpg

最后:留了一个巨大的坑

其实你如果多创建几个用户后再试着都登陆一遍,这个时候你就会从刚才的巨大喜悦中慢慢冷静下来,怎么所有的用户登录后,demo应用都把用户认为是 service-account-demo-project这个用户呢?对,没错,这就是这个中篇的实现方案留下的一个巨大的坑。目前这个方案只是解决了我们现在可以用Keycloak进行Django身份认证有无问题,但是它还不是一个终极完美的方案。但是由于本人一直在忙于测试平台后续功能的开发,而刚才提到的问题暂时还不是一个致命问题。因而直到现在还没腾出手来深究该如何解决它,所以《如何使用Keycloak进行Django身份验证》该短系列的下篇,我将保留到彻底把这个大坑填好后再来编写了,还请各位读者见谅,当然如果你已经解决了该问题,也请给我留言,小弟将不胜感激!

谢谢!基于上面说的原因,《一步步玩转测试平台开发》系列连载的下篇文章我将会新启一个小的系列,敬请期待后面的精彩!