1-Django - 常用用户认证技术

before

Django2.2 + Python3.6.8

在用户认证中,常用的认证手段包括:

  • HTTP Basic Auth
  • OAuth
  • cookie
  • session
  • token
  • jwt

一起来看看吧

HTTP Basic Auth

HTTP Basic Auth简单来说就是每次请求的API都携带用户名和密码,简而言之,Basic Auth是配合Rest ful API使用最简单的认证方式,因为只需要提供用户名和密码,但也因此,有把用户名和密码暴露给第三方客户端的风险,慢慢的在生产环境中使用的越来越少。

OAuth

OAuth(开放授权)是一个开放的授权标准,允许用户让第三方应用访问该用户在某个web服务上存储的私密的资源(照片、视频、联系人列表等),而无需将用户明和密码提供给第三方应用。

本篇主要探讨cookie和session在Django中的应用。

cookie

首先要知道HTTP的请求是无状态的,无状态是指Web浏览器与Web服务器之间不需要建立持久的连接,这意味着当一个客户端向服务器端发出请求,然后Web服务器返回响应(Response),连接就被关闭了,在服务器端不保留连接的有关信息。也就是说,HTTP请求只能由客户端发起,而服务器不能主动向客户端发送数据。

为了临时或者永久的让服务器记住客户端,或者实现身份认证,就有了各种解决方案,而cookie就是其中一种。

Cookie就是由服务器发给客户端的特殊信息,而这些信息以文本文件的方式存放在客户端,然后客户端每次向服务器发送请求的时候都会带上这些特殊的信息。

下图演示了基于cookie机制,从浏览器登录某网站的用户认证的工作流程:

  • 首先由client端携带用户名密码向server端的登录接口发送一个登录http请求。
  • server端进行身份校验,校验通过则设置相关的键值对(其实是个字典)给client,这里称之为set cookie,一般设置在响应头中返回。
  • 然后浏览器会保存server端返回的键值对,即保存cookie到浏览器本地。
  • 当client端再向该网站时,会自动的(一般在请求头中)携带之前的cookie,而server会校验该cookie,从而完成身份认证。

当然,浏览器保存了很多网站的cookie,它是以域名为key,该域名的cookie值为值得方式保存到本地,避免混淆:

www.baidu.com:{"is_login": "200", "xx": "oo", "timeout":3600}
www.jd.com:{"is_login": "200", "xx": "oo"}

cookie在Django的应用

Django2.2

通过一个登录示例来查看cookies的用法:

示例详情

urls.py

from django.contrib import admin
from django.urls import path
from app01 import views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', views.login, name='login'),
    path('login/', views.login, name='login'),
    path('index/', views.index, name='index'),
    path('news/', views.news, name='news'),
    path('logout/', views.logout, name='logout'),
]

views.py

from django.shortcuts import render, redirect


def login(request):
    if request.method == "POST":
        user = request.POST.get("username")
        pwd = request.POST.get("password")
        if user == 'zhangkai' and pwd == "123":
            # set cookies
            ret = redirect('/index/')
            ret.set_cookie('is_login', "200", 3600)
            ret.set_cookie('xx', 'oo')  # 如果key已存在就更新对应的值
            return ret
        else:
            return redirect('/login/')
    else:
        return render(request, 'login.html')


def index(request):
    status = request.COOKIES.get('is_login')
    if status == "200":
        return render(request, 'index.html')
    else:
        return redirect('/login/')


def news(request):
    status = request.COOKIES.get('is_login')
    if status == "200":
        news_list = ['皇家葡萄城开业了', '皇家葡萄城被举报关闭了', '新皇家葡萄城开业了']
        return render(request, 'news.html', {"new_list": news_list})
	else:
        return redirect('/login/')

def logout(request):
    ret = redirect('/login/')
    ret.delete_cookie('is_login')
    return ret

login.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>login</title>
</head>
<body>
<form action="" method="post">
    {% csrf_token %}
    <input type="text" name="username" placeholder="username">
    <input type="password" name="password" placeholder="password">
    <input type="submit">
</form>
</body>
</html>

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>index</title>
</head>
<body>
<h1>welcome index</h1>
<a href="{% url 'news' %}">观看新闻</a>
</body>
</html>

news.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>news</title>
</head>
<body>

{% for foo in new_list %}
    <li>{{ foo }}</li>
{% endfor %}
<a href="{% url 'logout' %}">退出登录</a>

</body>
</html>

设置cookie

设置cookie使用set_cookie方法,该方法各参数:

class HttpResponseBase:
    # set_cookie源码
    def set_cookie(self, key, value='', max_age=None, expires=None, path='/',
                   domain=None, secure=False, httponly=False, samesite=None): pass

常用参数:

params description
key 要设置的cookie的键
value='' 要设置的cookie的值
max_age=None 超时时间,单位是秒,默认时间是2周,如果设置为None,则cookie随着浏览器关闭而失效
expires=None datetime类型的日期对象,到这个日期就失效
path='/' cookie生效的路径,"/"是所有路径都能获得cookie。浏览器只会把cookie回传给带有该路径的页面,这样可以避免将cookie传给站点中的其他的应用。 "/" 表示根路径,特殊的:根路径的cookie可以被任何url的页面访问。
domain=None cookie生效的域名,你可用这个参数来构造一个跨站cookie,如domain=".example.com",所构造的cookie对下面这些站点都是可读的:www.example.comwww2.example.coman.other.sub.domain.example.com。如果该参数设置为 None ,cookie只能由设置它的站点读取。
secure=False 如果设置为 True ,浏览器将通过HTTPS来回传cookie
httponly=False 只能http协议传输,无法被JavaScript获取,当然,这不是绝对的,底层抓包可以获取到也可以被覆盖。

删除cookie

def logout(request):
    ret = redirect('/login/')
    ret.delete_cookie('is_login')  # 删除cookie中指定的key
    return ret

注意,设置cookie时,不允许有中文,非要设置中文怎么办?

# 方式1
def login(request):

    ret = HttpResponse('ok')
    
    ret.set_cookie('k1','你好'.encode('utf-8').decode('iso-8859-1'))
    
    #取值:request.COOKIES['k1'].encode('utf-8').decode('iso-8859-1').encode('iso-8859-1').decode('utf-8')

    return ret

方式2 json

def login(request):

    ret = HttpResponse('ok')
    
    import json

    ret.set_cookie('k1',json.dumps('你好'))
    #取值 json.loads(request.COOKIES['k1'])
    return ret

但尽量不要有中文。

cookie的特点

  • cookie是客户端技术,且cookie是存储在客户端的文本文件。
  • cookie的大小上限为4KB。
  • 一个服务器最多在客户端浏览器上保存20组键值对,一个浏览器最多保存300个cookie。
  • 不同浏览器之间不能共享cookie。
  • cookie是明文的,不安全。

加盐的cookie

上述示例中用到的cookie是不加盐的,这里再补充一个加盐的用法:

# 设置cookie
# 比如在登录校验成功后,就可以添加cookie之后跳转到主要
response = redirect('/index/')
# 其他参数跟不加盐的一致,其实加盐的cookie背后也是调用的不加盐的set_cookie
response.set_signed_cookie(key1, value1, salt="你加的盐", max_age=3600)  
response.set_signed_cookie(key2, value2, salt="你加的盐")
return response

# 主页就可以通过request对象获取cookie
# 这里一定要注意,default=None参数,加盐的cookie在获取key时,如果不存在则报keyerror的错,所以要加上default
request.get_signed_cookie(key, salt="你加的盐", default=None)

session

about session

由于cookie本身的特点(明文存储、存储大小限制),后来在cookie技术的基础行,有了session技术,即将cookie数据不仅仅存储在客户端,而是要在服务器中也存储一份,且将数据进行加密。

当然,session技术也有多种实现手段,但这里只对其中一种进行介绍,那就是基于cookie技术实现的session技术。

session的特点

  • 数据存储在server端,也因此存储手段更为灵活,比如存储到文件中、数据库中、缓存中、内存中等等。
  • 存储大小无限制。
  • 由于是密文存储,比cookie安全,但也是相对安全。

下图展示了基于session的登录工作流程:

不同的框架有不同的session实现机制,这里只从Django实现session展开学习。

session在Django的应用

Django2.2

还是通过一个示例来简单了解session的用法。

示例详情

urls.py

from django.contrib import admin
from django.urls import path
from app01 import views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', views.login, name='login'),
    path('login/', views.login, name='login'),
    path('index/', views.index, name='index'),
    path('news/', views.news, name='news'),
    path('logout/', views.logout, name='logout'),
]

views.py

from django.shortcuts import render, redirect

def login(request):
    if request.method == "POST":
        user = request.POST.get("username")
        pwd = request.POST.get("password")
        if user == 'zhangkai' and pwd == "123":
            # set session
            request.session['is_login'] = True  # key存在则是更新,否则是添加
            request.session['xx'] = 'oo'
            """
            1. 生成随机字符串,该随机字符串为key,设置的键值对为value
            2. 将生成的随机字符串,设置到cookie中:{session_id: 随机字符串}   session_id可以自定制
                前端的cookie长这样,比如response cookie:
                    name        value
                    sessionid   随机字符串
            3. 将设置的键值对,序列化后加密,然后将加密数据保存到数据库中
                id     session_key    session_data     过期时间
                1       随机字符串       加密键值对       具体的过期时间
            """
            return redirect('/index/')
        else:
            return redirect('/login/')
    else:
        return render(request, 'login.html')


def index(request):
    status = request.session.get('is_login', False)
    """
    1. 取出cookie中的sessionid对应的随机字符串
    2. 通过随机字符串去Django-seesion表过滤出对应的记录,并返回
    3. 将记录中的session_data字段的数据进行解密并反序列化,反序列后数据可以进行字典操作
    """
    if status:  # is_login 的值反序列化后还是True,能直接判断
        return render(request, 'index.html')
    else:
        return redirect('/login/')


def news(request):
    status = request.session.get('is_login', False)
    if status:  # is_login 的值反序列化后还是True,能直接判断
        news_list = ['皇家葡萄城开业了', '皇家葡萄城被举报关闭了', '新皇家葡萄城开业了']
        return render(request, 'news.html', {"new_list": news_list})
    else:
        return redirect('/login/')


def logout(request):
    # 删除指定的key
    # del request.session['is_login']
    # 清空session
    request.session.flush()
    """
    1. 删除cookie中的sessionid
    2. 删除数据库中对应的记录
    """
    return redirect('/login/')

login.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>login</title>
</head>
<body>
<form action="" method="post">
    {% csrf_token %}
    <input type="text" name="username" placeholder="username">
    <input type="password" name="password" placeholder="password">
    <input type="submit">
</form>
</body>
</html>

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>index</title>
</head>
<body>
<h1>welcome index</h1>
<a href="{% url 'news' %}">观看新闻</a>
</body>
</html>

news.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>news</title>
</head>
<body>

{% for foo in new_list %}
    <li>{{ foo }}</li>
{% endfor %}
<a href="{% url 'logout' %}">退出登录</a>
</body>
</html>

Django为request对象封装session来管理session,且内置了django-session表用于在server端存储session记录。所以,想要使用session请先执行数据库迁移指令,另外由于是django-session表是内置的表,我们并不能直观的看到模型类。

上面示例中,创建的session在django-session表中长这样:

设置session

# 存在则更新,否则新增
request.session["is_login"] = True
request.session.setdefault('xx','oo')

取值

request.session['k1']
request.session.get('k1',None)  
"""
request.session这句是帮你从cookie里面将sessionid的值取出来
将django-session表里面的对应sessionid的值的那条记录中的session-data字段的数据给你拿出来(并解密),
get方法就取出k1这个键对应的值
"""
# 还能这么取值
request.session.keys()
request.session.values()
request.session.items()

# 获取session_key
request.session.session_key

# 检查session_key是否存在
request.session.exists('session_key')

删除session

# 删除当前会话的所有Session数据
request.session.delete()

# 删除当前的会话数据并删除会话的Cookie。
request.session.flush()  #常用,清空所有cookie---删除session表里的这个会话的记录,
# 这用于确保前面的会话数据不可以再次被用户的浏览器访问
# 例如,django.contrib.auth.logout() 函数中就会调用它。

设置session超时时间

# 设置会话Session和Cookie的超时时间
request.session.set_expiry(value)
"""
如果value是个整数,session会在些秒数后失效。
如果value是个datatime或timedelta,session就会在这个时间后失效。
如果value是0,用户关闭浏览器session就会失效。
如果value是None,session会依赖全局session失效策略。
"""

你也可以在settings.py中对session进行其他的配置

# 数据库Session
SESSION_ENGINE = 'django.contrib.sessions.backends.db'   # 引擎(默认)

# 缓存Session
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'  # 引擎,
SESSION_CACHE_ALIAS = 'default'                            # 使用的缓存别名(默认内存缓存,也可以是memcache),此处别名依赖缓存的设置
# 注意,如果不是结合Redis作为缓存,那么这里指定cache则用的是内存缓存,本地开发没问题,因为本地开发单进程,独享内存资源,也能访问到内存中的session缓存。但通过uwsgi部署就不行了,多进程中,就找不到这个session缓存了,亲测发现的问题。
# 所以,如果想要使用cache作为session的内存缓存,则搭配Redis使用,否则,就老老实实的用那个db缓存就好了,uwsgi中使用也正常。




# 文件Session
SESSION_ENGINE = 'django.contrib.sessions.backends.file'    # 引擎
SESSION_FILE_PATH = None                                    # 缓存目录路径,如果为None,则使用tempfile模块获取一个临时地址tempfile.gettempdir() 
# 如 SESSION_FILE_PATH = os.path.join(BASE_DIR, 'xx')  # session文件会生成在项目目录下的xx目录内

# 缓存+数据库
SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db'        # 引擎

# 加密Cookie Session
SESSION_ENGINE = 'django.contrib.sessions.backends.signed_cookies'   # 引擎

# 其他公用设置项,优先级小于request.seesion设置
SESSION_COOKIE_NAME = "sessionid"                       # Session的cookie保存在浏览器上时的key,即:sessionid=随机字符串(默认)
SESSION_COOKIE_PATH = "/"                               # Session的cookie保存的路径(默认)
SESSION_COOKIE_DOMAIN = None                             # Session的cookie保存的域名(默认)
SESSION_COOKIE_SECURE = False                            # 是否Https传输cookie(默认)
SESSION_COOKIE_HTTPONLY = True                           # 是否Session的cookie只支持http传输(默认)
SESSION_COOKIE_AGE = 1209600                             # Session的cookie失效日期(2周)(默认),单位是秒
SESSION_EXPIRE_AT_BROWSER_CLOSE = False                  # 是否关闭浏览器使得Session过期(默认)
SESSION_SAVE_EVERY_REQUEST = False                       # 是否每请求都保存Session,默认修改之后才保存(默认)

注意:

  • 默认的,session的有效期是2周。
  • 在session有效期内,客户端(浏览器)单方面清理了cookie,那么server端的django-session表中对应的记录不会清除,但不要怕,Django会自动检查django-session表中的expir_date字段指定的时间,进行自动删除。当然,如果是手动删除session,如request.session.flush(),django-session表中的对应记录会自动删除。
  • 在session有效期内,如果同一个客户端(浏览器)重复登录,并不会每登录一次,就产生一条session记录,而是会更新对应的session记录的expir_date字段的值——最后一次登录的时间。

参考:https://docs.djangoproject.com/zh-hans/3.2/ref/settings/#id15

将session缓存到redis中

默认的,session组件会将session保存到默认的session表中,那么如果不想用这个默认的session表,或者说为了更好的性能,我们选择将session缓存到redis中,你就需要做这些个配置了。
首先来看项目的配置文件settings.py文件:


# 经过下面这么注释,算是个纯净版的Django,像admin和auth这些都没有用,当然session这个内置app也没启用
# 这里注释掉,就是说执行数据库迁移的时候,不生成这些相关表,也就是说session表也不创建了。
INSTALLED_APPS = [
	# 'django.contrib.admin',
	# 'django.contrib.auth',
	# 'django.contrib.contenttypes',
	# 'django.contrib.sessions',
	# 'django.contrib.messages',
	'django.contrib.staticfiles',
	'api.apps.ApiConfig',
]

MIDDLEWARE = [
	'django.middleware.security.SecurityMiddleware',
	'django.contrib.sessions.middleware.SessionMiddleware',
	'django.middleware.common.CommonMiddleware',
	# 'django.middleware.csrf.CsrfViewMiddleware',
	# 'django.contrib.auth.middleware.AuthenticationMiddleware',
	'django.contrib.messages.middleware.MessageMiddleware',
	'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'demo.urls'

TEMPLATES = [
	{
		'BACKEND': 'django.template.backends.django.DjangoTemplates',
		'DIRS': [os.path.join(BASE_DIR, 'templates')]
		,
		'APP_DIRS': True,
		'OPTIONS': {
			'context_processors': [
				'django.template.context_processors.debug',
				'django.template.context_processors.request',
				# 'django.contrib.auth.context_processors.auth',
				'django.contrib.messages.context_processors.messages',
			],
		},
	},
]

# 重点在下面,首先配置缓存
# cache缓存
CACHES = {
	"default": {
		"BACKEND": "django_redis.cache.RedisCache",
		"LOCATION": "redis://127.0.0.1:6379",
		"OPTIONS": {
			"CLIENT_CLASS": "django_redis.client.DefaultClient",
			"CONNECTION_POOL_KWARGS": {
				"max_connections": 100,
				"encoding": "utf-8",
				# "decode_responses": True  # session缓存到redis中,配置项不能加这个,否则报KeyError的错误
			},
			"PASSWORD": "",
		}
	}
}

# 将session配置到Redis中的配置参数
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
SESSION_CACHE_ALIAS = 'default'
SESSION_COOKIE_AGE = 60 * 60 * 24 * 7
SESSION_KEY = 'nb_user_info'  # 这个是session在redis中的key,随便写个,Django内部会识别的

视图函数就比较简单了:

from django.shortcuts import render, HttpResponse
from django.conf import settings
from api.models import UserInfo

def login(request):
	if request.method == "GET":
		# 为了简单用户以get形式提交用户名和密码模拟登录
		# http://127.0.0.1:8000/login/?name=likai&pwd=123
		instance = UserInfo.objects.filter(
			name=request.GET.get('name'),
			pwd=request.GET.get('pwd'),
		).first()
		# 匹配到了之后,生成session
		request.session[settings.SECRET_KEY] = {'id': instance.id, 'name': instance.name}
		
		return HttpResponse("GET 200 OK")
	# 用户以post形式,不用携带参数,访问该视图函数,我们后台就能打印出来改用户写入的session信息
	print(request.session[settings.SECRET_KEY])  # {'id': 2, 'name': 'likai'}
	return HttpResponse("POST 200 OK")

token

使用基于token的身份验证方法,


that's all, see also:

Cookie | Django基础八之cookie和session | 深入理解Cookie |

posted @ 2018-09-05 10:37  听雨危楼  阅读(374)  评论(0编辑  收藏  举报