day69——Forms组件源码、cookie/session、CBV添加装饰器

forms源码

is_valid()方法为切入点查看forms组件源码

def is_valid(self):
        """
        Returns True if the form has no errors. Otherwise, False. If errors are
        being ignored, returns False.
        """
   return self.is_bound and not self.errors

该方法返回一个与关系连接的条件,如果要返回的结果要为True那么self.is_bound必须为True并且self.errors必须为False。

先看self.is_bound在什么情况下才为True

def __init__(self, data=None,, files=None,...):
	...
	self.is_bound = data is not None or files is not None

通过看源码得知self.is_bound等于我们初始化传入的参数,也就是说只要我们实例化生成forms对象时只要传值了那么该属性的布尔值一定为True,接着我们在去看第二个条件self.errors什么时候为False。

@property  # 将方法伪装成属性
def errors(self):
        "Returns an ErrorDict for the data provided for the form"
        if self._errors is None:
            self.full_clean()
        return self._errors  # 私有属性
    
# self._errors
def __init__(self, ...)
        self._errors = None   # 默认等于None

errors方法不管在什么情况下都会返回一个self._errors属性,而self._errors

默认等于None,紧接着会触发self.full_clean方法的执行,而full_clean方法才是forms组件的核心所在,所有功能基本都出自与该方法。

def full_clean(self):
  	self._clean_fields()  # 校验字段 + 局部钩子
    self._clean_form()  # 全局钩子
    self._post_clean()  

self._clean_fields()校验字段+ 局部钩子

    def _clean_fields(self):
        	...
            try:
                ...
                self.cleaned_data[name] = value  # 通过一系列的判断后将合法的字段添加到cleaned_data字典中
                if hasattr(self, 'clean_%s' % name):  # 利用反射获取局部钩子函数
                    value = getattr(self, 'clean_%s' % name)()  # 局部钩子函数必须要有返回值
                    self.cleaned_data[name] = value
            except ValidationError as e:
                self.add_error(name, e)  # 添加提示报错信息
  • self.add_error(name, e):添加报错信息,当有不合法的字段的时候self._errors不再为None,也就是说self.errors不等于None,那么not self.errors的结果为False,最终is_valid()判断的结果为False,传值了有不合法的字段则is_valid()为False。

  • 如果传值了,所有字段都合法,不抛出异常,self.add_error(name, e)就不会执行,那么self._errors还等于None,也就意味着not self.errors的结果为True,最终is_valid()判断的结果为True。

全局钩子函数源码:

    def _clean_form(self):
        try:
            cleaned_data = self.clean()
           """获取全部钩子函数,self.clean内部又没有其他代码而是直接将self.cleaned_data返回,
           等于给开发者放开放一个扩展接口,后期写面向对象的时候在类中也可以运用方法"""
        except ValidationError as e:
            self.add_error(None, e)
        else:
            if cleaned_data is not None:
                self.cleaned_data = cleaned_data  # 全部钩子函数必须将所有的字段数据返回

cookie与session

网站发展史

  • 早期网址没有保存用户信息的需求,所有的访问后端都做相同的响应,返回的结果都是一样的

    eg:新闻、博客、文章……

  • 到后来出现了一些必须要报存用户信息的网址,

    eg:淘宝、支付宝、京东……

以登陆功能为例:

如果不保存用户登陆状态 也就意味着用户每次访问网站都需要重复的输入用户名和密码(你觉得这样的网站你还想用吗?),在用户第一次登陆成功之后,应该对用户的信息进行保存。

当用户第一次登陆成功之后 将用户的用户名密码返回给用户浏览器 让用户浏览器保存在本地,之后访问网站的时候浏览器自动将保存在浏览器上的用户名和密码发送给服务端,服务端获取之后自动验证,早期这种方式具有非常大的安全隐患

进一步优化:

当用户登陆成功之后,服务端产生一个随机字符串(在服务端保存数据,用kv键值对的形式),如:

​ 随机字符串1:用户1相关信息
​ 随机字符串2:用户2相关信息
​ 随机字符串3:用户3相关信息

将随机字符串交给客服端浏览器进行保存,之后客服用浏览器访问服务端的时候,都带着该随机字符串,服务端去数据库中比对是否有对应随机字符串,从而获取字符串对应的用户数据。但是如果有人截获到了该随机字符串,那么他就可以冒充当前用户,其实还是有安全隐患的。

在进一步优化用到了token:

​ 登陆成功之后 将一段用户信息进行加密处理(加密算法只有公司的开发知道), 将加密之后的结果拼接在信息后面 整体返回给浏览器保存 , 浏览器下次访问的时候带着该信息 服务端自动切去前面一段信息再次使用自己的加密算法,跟浏览器尾部的密文进行比对,在之后就用到jwt认证。

cookie:

cookie指的是一个技术点,服务端保存在浏览器上的信息都可以称之为cookie,它的表现形式一般都是k:v键值对(可以有多个)

session:

数据是保存在服务端的并且它的表现形式一般也是k:v键值对(可以有多个),session是基于cookie工作的(其实大部分的保存用户状态的操作都需要使用到cookie)

cookie操作

浏览器设置cookie

虽然cookie是服务端告诉客户端浏览器需要保存内容,但是客服端浏览器可以选择选择拒绝保存,如果禁止了,那么只要是需要记录用户状态的网址,登录功能就都用不了了

以谷歌chrome浏览器为例:

点击在浏览器右上方的三个点打开更多选项,找到设置,进入到设置界面,之后点击隐私设置和安全性,在右侧找到网站设置。

点击“网站设置”,进入到网站设置界面:

在点击“cookie和网站数据”进入到cookie设置界面,修改cookie设置

将“允许网站保存和读取cookie数据禁止掉

这样设置之后在去网站登录账号,就怎么都登不上了,而且很多功能也无法实现了,一般都不会这么设置。

操作cookie的三种方式

因为视图函数有三种返回值的方式:

  • return HttpResponse()
  • return render()
  • return redirect()

三种返回方式都是返回一个对象,而cookie必须在return之前操作,就必须要用到obj对象了,对应的也有三种操作cookie的方式。

  • 方式一:

    obj1 = HttpResponse()
    # 操作cookie
    return obj1
    
  • 方式二:

    obj2 = render()
    # 操作cookie
    return obj2
    
  • 方式三:

    obj3 = redirect()
    # 操作cookie
    return obj3
    

操作cookie分为设置和获取两部分:

# 1 设置cookie
obj.set_cookie(key,value)

# 2 获取cookie
res = request.COOKIE.get(key)
# 3 删除cookie
obj.delete_cookie('key')
# 例如:
obj.set_cookie('username''tom')
"""
在设置cookie的时候可以添加一个超时时间
	max_age
	expires
		两者都是设置超时时间的,都是以秒为单位,
		不同的是,针对IE浏览器需要用expires才行
		超时主动删除cookie,可用于超时注销登录"""
obj.set_cookie('username','tom',expires=3,max_age=3)
res = request.COOKIE.get('username')

需求:

写一个只有三个页面的简单小网站,登录页面、首页、阅读页面,用户可以直接访问到首页以及登录页面,但是阅读页面只有用户登录之后在能进入。

分析:

用户如果在没有登陆的情况下想访问一个需要登陆的页面,那么先跳转到登录页面,当用户输入正确的用户名和密码之后登录成功,应该跳转到用户之前想要访问的页面去 ,需要写一个登录认证装饰器。

后端:

class MyForms(forms.Form):
    username = forms.CharField(label='用户账号', initial='tom',
                               widget=forms.widgets.TextInput({'class': 'form-control'}))
    password = forms.CharField(label='用户密码',
                               error_messages={'required': '密码不能为空'},
                               widget=forms.widgets.PasswordInput({'class': 'form-control'}))

    
# 登录认证装饰器
def auth(func):
    def wrapper(request, *args, **kwargs):
        target_url = request.get_full_path() # 获取到用户上一次想要访问的url
        if request.COOKIES.get('username'): # 如果cookie有值说明用户已登录
            return func(request, *args, **kwargs) # 直接返回对应视图函数的执行结果。
        else:  # 用户没有登录跳转到登录页面
            return redirect(f'/login/?next={target_url}') 
            # 将用户的目址url拼接在login后面,跳转时一并传给登录界面
    return wrapper


# 首页视图函数
def home(request):
    return render(request, 'home.html', locals())


# 登录功能
def login(request):
    form_obj = MyForms()
    if request.method == 'POST':
        form_obj = MyForms(request.POST)
        if form_obj.is_valid():
            username = form_obj.cleaned_data.get('username')
            password = form_obj.cleaned_data.get('password')
            if username == 'tom' and password == '666666':
                # 获取用户的目标url,结果可能是None
                target_url = request.GET.get('next')
                if target_url:
                    # 目标url有值则在设置cookie后跳转到目标页面
                    obj = redirect(target_url)
                else:
                    # 否则设置cookie后跳转到首页
                    obj = redirect('/home/')
                # 让浏览器记录cookie数据    
                obj.set_cookie('username', '666666',max_age=3)
                """
                浏览器不单单会帮你存
                而且后面每次访问你的时候还会带着它过来
                """
                return obj
    return render(request, 'login.html', locals())


# 阅读页面视图函数
# 用装饰器修饰read视图函数,让其在登录后才能被访问
@auth
def read(request):
    return render(request, 'read.html', locals())

前端:

{#首页:#}
<body>
<h1 class="text-center" style="font-size: 100px;font-weight: bold">首页</h1>
<p><a href="/login" class="btn btn-success btn-lg" style="font-size:50px;font-weight: bold;">登录</a><a href=""></a><a href=""></a></p>
<p><a href="/read" class="btn btn-info btn-lg" style="font-size:50px;font-weight: bold;">阅读</a><a href=""></a><a href=""></a></p>
</body>


{#登录页面:#}
<body>
<h1 class="text-center" style="font-size: 100px;font-weight: bold;color: #5cb85c">登录</h1>
<form action="" method="post" novalidate>
    {% for form in form_obj %}
    <p>{{ form.label }}:{{ form }}<span style="color:red">{{ form.errors.0}}</span></p>
    {% endfor %}
    <input type="submit" class="btn-success btn-lg">
</form>
</body>


{#阅读页面:#}
<body>
<h1 class="text-center" style="font-size: 100px;font-weight: bold;color: #5bc7de">阅读</h1>
<p style="font-size:30px;font-weight: bold;">吾辈当自强</p>
<p><a href="/home" class="btn btn-success btn-lg" style="font-size:10px;font-weight: bold;">返回首页</a><a href=""></a><a href=""></a></p>
</body>

sesstion操作

session数据是保存在服务端的,给客服端返回的是一个随机字符串,服务端可以选择sesstion的保存位置,如:

  • MySQL
  • 文件
  • redis
  • memcache

在默认情况下操作session的时候首先需要有django默认的一张django_session表,之前我们在执行数据库迁移命名的时候django会帮我们创建很多表,而django_session就是其中的一张,用于存放session数据。

session_key字段对应的就是django返回的服务端的随机字符串,session_data字段就是用户信息,expire_data 则是过期时间,注意django默认的session过期时间是14天,当然也可以修改。

django_session数据的条数:

同一个计算机上(IP地址)同一个浏览器只会有一条数据生效,当session过期的时候可能会出现多条数据对应一个浏览器,但是该现象不会持续多久,内部会自动识别过期的数据,进行删除,也可以写代码进行清除,主要是为了节省服务端数据库资源。

操作session的方式很简单

设置session:

request.session['key'] =value

设置过期时间,过期时间必须分开来设置

request.session.set_expiry()

括号内可以放四种类型的参数:

  1. 整数————多少秒失效
  2. 日期对象————到指定日期自动失效
  3. 0————一旦当前浏览器窗口关闭立即失效
  4. 不写————失效时间取决于django内部全局的session默认的失效时间

设置session django内部做了哪些事

request.session['hobby'] = 'read'
  1. django内部会自动产生一个随机的字符串
  2. django内部自动将随机字符串和对应的数据存储到django_session表中
    • 先在内存中产生操作数据的缓存
    • 在响应结果django中间件的时候才真正的操作数据库
  3. 将产生的随机字符串返回给浏览器保存

获取session:

request.session.get('hobby')

从上面的设置和获取session可以看成,操作session相对于在操作一个字典。

获取session django内部做了哪些事

  1. 自动从浏览器请求中获取sessionid对应的随机字符串
  2. 拿着该数据字符串去django_session表中查找对应的数据
  3. 获取字符串对应的数据
    • 如果对比上了则将对应的数据取出并以字典的形式封装到request.sessoin中,通过key就能取值
    • 如果对比不上,则request.session.get()返回的是None。

清除session

  • request.session.delete()————只删服务端的,客户端的不删
  • request.session.flush()————浏览器和服务端都清空(推荐使用)

Session版登陆验证

from functools import wraps


def check_login(func):
    @wraps(func)
    def inner(request, *args, **kwargs):
        next_url = request.get_full_path()
        if request.session.get("user"):
            return func(request, *args, **kwargs)
        else:
            return redirect("/login/?next={}".format(next_url))
    return inner


def login(request):
    if request.method == "POST":
        user = request.POST.get("user")
        pwd = request.POST.get("pwd")

        if user == "alex" and pwd == "alex1234":
            # 设置session
            request.session["user"] = user
            # 获取跳到登陆页面之前的URL
            next_url = request.GET.get("next")
            # 如果有,就跳转回登陆之前的URL
            if next_url:
                return redirect(next_url)
            # 否则默认跳转到index页面
            else:
                return redirect("/index/")
    return render(request, "login.html")


@check_login
def logout(request):
    # 删除所有当前请求相关的session
    request.session.flush()
    return redirect("/login/")


@check_login
def index(request):
    current_user = request.session.get("user", None)
    return render(request, "index.html", {"user": current_user})

CBV如何实现添加装饰器

有三种添加方式,在方法上方添加、在类上方添加、在类中自定义dispatch方法,在该方法上方定义装饰器(装饰器在类里的所有方法都会生效)。

  • 方式一:

    from django.views import View
    form django.utils.decorators import method_docorator
    
    
    class MyLogin(View):
        @method_decorator(login_auth)
        def get(self,request):
            return HttpResponse("get请求")
        
        @method_decorator(login_auth)
        def post(self,request):
            return HttpResponse('post请求')
    

    CBV中django不建议你直给类的方法加装饰器,无论该装饰器都能正常的给到方法,但是还是不建议直接加, 所以这样添加装饰器的方式不推荐使用。

  • 方式二:

    @method_decorator(login_auth,name = 'get')
    @method_decorator(login_auth,name = 'post')
    class MyLogin(View):
        ...
    

    第一个参数是装饰器的名字,name指定给哪个方法添加该装饰器,这种方式能够实现针对不同的方法加不同的装饰器,推荐使用。

  • 方式三:

    class MyLogin(View):
        @method_decorator(login_auth)
        def dispatch(self,*args,**kwargs):
            return super().dispatch(request,*args,**kwargs)
    

    这种方式添加的装饰器,它会直接作用于当前类里面的所有的方法。

原文地址:https://www.cnblogs.com/zhangtieshan/p/13063248.html