上一篇發現了用戶A可以通過請求介面篡改用戶B的問題,現在進行了修復。

修改視圖函數:除了檢查登錄,執行刪除前還要檢查當前登錄用戶是否是他的owner;如果不一致,得在日誌里記上一筆。

116 @login_required(login_url=/account/login/)
117 @require_POST
118 def delete_article(request):
119 article_id = request.POST["article_id"]
120 try:
121 line = ArticlePost.objects.get(id=article_id, author=request.user)
122 line.delete()
123 return HttpResponse("1")
124 except ArticlePost.DoesNotExist as e:
125 logger.error("Attack? delete_article article_id={}, author={}".format(article_id, request.user))
126 return HttpResponse("0")
127 except Exception as e:
128 logger.error(traceback.print_exc())
129 return HttpResponse("0")

不過這隻能驗證操作許可權。

由於POST介面是公開的,別人可以任意發請求。

要防止意外出現,首先是驗證用戶身份,然後在此基礎上驗證操作許可權。

那麼萬一這個合法用戶是別人冒充呢?

驗證身份也很重要!

驗證身份實際上是Django幫忙做了。看看細節。cookie里的sessionid欄位就是。

為此回到用戶登錄和認證的地方……導航欄里正好有驗證:

<ul class="nav navbar-nav navbar-right">
{% if user.is_authenticated %}
<li><a href="#" >{{user.username}}</a></li>
<li><a href="#" >Logout</a></li>
{% else %}
<li><a href="{% url account:user_login %}">LOGIN</a></li>
{% endif %}
</ul>

這裡有2個問題:

  1. user每次都是直接拿來用,它是怎麼傳進來的?
  2. user.is_authenticated是怎麼實現是否認證過的?

現在回答。

1、user等為什麼可以在模板直接用?

The render() shortcut renders templates with a request context. Template context processors take the request object and return a dictionary which is added to the context.

A common template context processor is the auth context processor, which takes the request object, and adds the logged-in user to the context.

Why does Djangos render() function need the "request" argument??

stackoverflow.com圖標

模板渲染的過程:

Using the template system in Python is a three-step process:

  1. You configure an Engine.配置引擎
  2. You compile template code into a Template.生成模板
  3. You render the template with a Context. 參數替換

具體來說:

1、For each DjangoTemplates backend in the TEMPLATES setting, Django instantiates anEngine. DjangoTemplates wraps Engine and adapts it to the common template backend API.

在設置Templates中配置不同的BACKEND得到不同的引擎,我這裡只有一個:

django.template.backends.django.DjangoTemplates,將會實例化一個a Django template engine.前者包裝了後者。

DjangoTemplates backend in the TEMPLATES setting,就1個

DjangoTemplates backend in the TEMPLATES settingTemplates | Django documentation | Django

DjangoTemplates backend in the TEMPLATES setting,就1個
DjangoTemplates backend in the TEMPLATES setting

其中的參數:context_processors:

a list of dotted Python paths to callables that are used to populate the context when a template is rendered with a request. These callables take a request object as their argument and return adictof items to be merged into the context.

於是這個引擎就具有抽取request信息的能力了。

Templates | Django documentation | Django?

docs.djangoproject.com
圖標

Template實例化時,如果沒有傳入engine,會指定為默認的。engine = Engine.get_default()

[docs]class Template:
def __init__(self, template_string, origin=None, name=None, engine=None):
# If Template is instantiated directly rather than from an Engine and
# exactly one Django template engine is configured, use that engine.
# This is required to preserve backwards-compatibility for direct use
# e.g. Template(...).render(Context({...}))
if engine is None:
from .engine import Engine
engine = Engine.get_default()
if origin is None:
origin = Origin(UNKNOWN_SOURCE)
self.name = name
self.origin = origin
self.engine = engine
self.source = str(template_string) # May be lazy.
self.nodelist = self.compile_nodelist()

然後看看默認的引擎什麼樣……the underlyingEnginefrom the first configured DjangoTemplatesengine,就是第一個配置的引擎。

def get_default():
"""
Return the first DjangoTemplates backend thats configured, or raise
ImproperlyConfigured if none are configured.

This is required for preserving historical APIs that rely on a
globally available, implicitly configured engine such as:

>>> from django.template import Context, Template
>>> template = Template("Hello {{ name }}!")
>>> context = Context({name: "world"})
>>> template.render(context)
Hello world!
"""
# Since Engine is imported in django.template and since
# DjangoTemplates is a wrapper around this Engine class,
# local imports are required to avoid import loops.
from django.template import engines
from django.template.backends.django import DjangoTemplates
for engine in engines.all():
if isinstance(engine, DjangoTemplates):
return engine.engine
raise ImproperlyConfigured(No DjangoTemplates backend is configured.)

The Django template language: for Python programmers?

docs.djangoproject.com

2、The django.template.loader module provides functions such as get_template()for loading templates. They return a django.template.backends.django.Templatewhich wraps the actual django.template.Template.

render調用了render_to_string,render_to_string就是在loading template產生template——這是一個template.backends.django.Template類型,包裝了真正的django.template.Template

template.backends.django.Template具有render方法,到了第3步就可以直接調用。

[docs]def render_to_string(template_name, context=None, request=None, using=None):
"""
Load a template and render it with a context. Return a string.

template_name may be a string or a list of strings.
"""
if isinstance(template_name, (list, tuple)):
template = select_template(template_name, using=using)
else:
template = get_template(template_name, using=using)
return template.render(context, request)

其中get_template就是先找到引擎,然後調用引擎的engine.get_template(template_name)。得到的template就帶有引擎的信息。

[docs]def get_template(template_name, using=None):
"""
Load and return a template for the given name.

Raise TemplateDoesNotExist if no such template exists.
"""
chain = []
engines = _engine_list(using)
for engine in engines:
try:
return engine.get_template(template_name)
except TemplateDoesNotExist as e:
chain.append(e)

raise TemplateDoesNotExist(template_name, chain=chain)

其中,_engine_list返回

def _engine_list(using=None):
return engines.all() if using is None else [engines[using]]

這裡的engine其實是:

engines = EngineHandler()

engines.all()是classEngineHandler:的一個方法:

def all(self):
return [self[alias] for alias in self]

這裡for alias in self拆開,迭代對象就是self.templates

def __iter__(self):
return iter(self.templates)

self[alias]拆開就是以self.templates的每一項為key,依次self._getitem__(key)……這裡有有點沒理順。。

def __getitem__(self, alias):
try:
return self._engines[alias]
except KeyError:
try:
params = self.templates[alias]
except KeyError:
raise InvalidTemplateEngineError(
"Could not find config for {} "
"in settings.TEMPLATES".format(alias))

# If importing or initializing the backend raises an exception,
# self._engines[alias] isnt set and this code may get executed
# again, so we must preserve the original params. See #24265.
params = params.copy()
backend = params.pop(BACKEND)
engine_cls = import_string(backend)
engine = engine_cls(params)

self._engines[alias] = engine
return engine

總之,engines = _engine_list(using)獲取了所有引擎(代碼還有點沒看懂),然後engine.get_template(template_name)載入產生了template。

抽取request的信息就是在engine.get_template里完成的

context = make_context(context, request, autoescape=self.backend.engine.autoescape)

具體過程:

https://github.com/django/django/blob/master/django/template/context.py#L265?

github.com

def make_context(context, request=None, **kwargs):
"""
Create a suitable Context from a plain dict and optionally an HttpRequest.
"""
if context is not None and not isinstance(context, dict):
raise TypeError(context must be a dict rather than %s. % context.__class__.__name__)
if request is None:
context = Context(context, **kwargs)
else:
# The following pattern is required to ensure values from
# context override those from template context processors.
original_context = context
context = RequestContext(request, **kwargs)
if original_context:
context.push(original_context)
return context

3)最終return template.render(context, request)

這裡的context已經是包含request信息更新的了……

django.template.base | Django documentation | Django?

docs.djangoproject.com

2、authenticate怎麼做?依據是什麼?

1)is_authenticated是User的一個屬性,只能判斷這個User是不是登錄用戶。

幾乎等效的視圖裝飾器:@login_required

is_authenticated?

Read-only attribute which is always True (as opposed toAnonymousUser.is_authenticated which is always False). This is a way to tell if the user has been authenticated. This does not imply any permissions and doesn』t check if the user is active or has a valid session. Even though normally you will check this attribute onrequest.user to find out whether it has been populated by theAuthenticationMiddleware (representing the currently logged-in user), you should know this attribute is True for any User instance.

user要麼是User實例要麼是AnonymousUser,就算is_authenticated==True

,只能證明不是AnonymousUser……並不驗證user is active or has a valid session。所以需要進一步檢查request.user的user這個對象的屬性

Customizing authentication in Django?

docs.djangoproject.com

Using the Django authentication system?

docs.djangoproject.com

2)authenticate(request=None,**credentials)[source]?

If the given credentials are valid, return a User object.

驗證通過就返回User對象, 否則是None

credentials一般就是username and password,拿著credentials作為關鍵參數一次找每一個backend驗證。只要有1個是valid,就返回User對象;如果全部都不是valid或者只要有1個PermissionDenied, it returns None。

django.contrib.auth | Django documentation | Django

在用戶登錄的時候用到過:

首先驗證表單有效,接著驗證用戶名+密碼有效。條件都滿足,才會調用真正的login函數。

這裡找的backend是默認的django.contrib.auth.backends.ModelBackend(github.com/django/djang

順便,settings默認配置在django/conf/global_settings.py

11 def user_login(request):
12 if request.method == "POST":
13 print(request.POST)
14 login_form = LoginForm(request.POST) # requests.POST(dict)
15 if login_form.is_valid():
16 cd = login_form.cleaned_data # cd(dict)
17 user = authenticate(username=cd["username"],password=cd["password"]) #return a User instance if ma tch else None
18 if user:
19 login(request, user) # execute login
20 return HttpResponse("Login~Welcome~")
21 else:
22 return HttpResponse("Fail!Check your username or password")
23 else:
24 return HttpResponse("Invalid data")

3)許可權Permission--暫時沒有用到這個模塊

four default permissions – add, change, delete, and view

Whendjango.contrib.authis listed in yourINSTALLED_APPSsetting, 所有註冊app的medels都會支持這個許可權。

Assuming you have an application with an app_label foo and a model named Bar, to test for basic permissions you should use:

  • add: user.has_perm(foo.add_bar)
  • change: user.has_perm(foo.change_bar)
  • delete: user.has_perm(foo.delete_bar)
  • view: user.has_perm(foo.view_bar)

4)session

session根據保存位置不同有好幾種,對應不同的SESSION_ENGINE:

  1. database-backed sessions
  2. cached sessions
  3. file-based sessions
  4. cookie-based sessions(cookie 會被加密,只有 Server 才知道如何解開)

數據模型在:using the modeldjango.contrib.sessions.models.Session

默認的SESSION_ENGINE=django.contrib.sessions.backends.db,session內容保存在資料庫。

默認配置:

# SESSIONS #
############

# Cache to store session data if using the cache session backend.
SESSION_CACHE_ALIAS = default
# Cookie name. This can be whatever you want.
SESSION_COOKIE_NAME = sessionid
# Age of cookie, in seconds (default: 2 weeks).
SESSION_COOKIE_AGE = 60 * 60 * 24 * 7 * 2
# A string like "example.com", or None for standard domain cookie.
SESSION_COOKIE_DOMAIN = None
# Whether the session cookie should be secure (https:// only).
SESSION_COOKIE_SECURE = False
# The path of the session cookie.
SESSION_COOKIE_PATH = /
# Whether to use the non-RFC standard httpOnly flag (IE, FF3+, others)
SESSION_COOKIE_HTTPONLY = True
# Whether to set the flag restricting cookie leaks on cross-site requests.
# This can be Lax, Strict, or None to disable the flag.
SESSION_COOKIE_SAMESITE = Lax
# Whether to save the session data on every request.
SESSION_SAVE_EVERY_REQUEST = False
# Whether a users session cookie expires when the Web browser is closed.
SESSION_EXPIRE_AT_BROWSER_CLOSE = False
# The module to store session data
SESSION_ENGINE = django.contrib.sessions.backends.db
# Directory to store session files if using the file session module. If None,
# the backend will use a sensible default.
SESSION_FILE_PATH = None
# class to serialize session data
SESSION_SERIALIZER = django.contrib.sessions.serializers.JSONSerializer

How to use sessions


推薦閱讀:
相关文章