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
about 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.com 、www2.example.com 、an.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: