Django框架(二十四:前后端分离之分页的设置和csrf认证的解决)

前后端分离开发和混合开发的区别还是很大的。前后端分离我们需要遵循restful规范,先介绍什么是restful api规范

a.同一种数据的操作,只设置一个url路由。也就是根据请求方法来区分具体的处理逻辑。而不再设置多个增删改查的路由。
        (1)可以基于FBV来通过请求方法的不同,处理不同的逻辑。
        url(r'^order/', views.order),
        def order(request):
            if request.method == 'GET':
                return HttpResponse('获取订单')
            elif request.method == 'POST':
                return HttpResponse('创建订单')
            elif request.method == 'PUT':
                return HttpResponse('更新订单')
            elif request.method == 'DELETE':
                return HttpResponse('删除订单')
        (2)可以基于CBV来实现处理不同的逻辑
        url(r'^order/', views.OrderView.as_view()),
        class OrderView(View):
            def get(self,request,*args,**kwargs):
                return HttpResponse(json.dumps(ret),status=201)
            def post(self,request,*args,**kwargs):
                return HttpResponse('创建订单')
            def put(self,request,*args,**kwargs):
                return HttpResponse('更新订单')
            def delete(self,request,*args,**kwargs):
                return HttpResponse('删除订单')
	 而两种方式中,最建议使用CBV的方式去写接口,更加简洁,不用判断了。
	
b. 域名建议
        为了对用户使用的url和网页中使用的接口api进行区别,设置如下规则
        (1)子域名的方式区分(需要解决跨域的问题):
            www.baidu.com (用户在浏览器中输入的地址,可以访问网站页面)
            但是网页需要到后台请求接口,获取数据,那么接口的api应该如何命名呢?
            api.baidu.com/v1/login.json
            用户一看到域名是以api开头的,就知道是接口,返回的是json数据。
        (2)URL的方式进行区分(不需要解决跨域问题):
            www.baidu.com (用户使用的URL)
            www.baidu.com/api/v1/login.json
        不管使用哪种方式,就是为了能够一眼区分出来这是一个api接口。
        
        两种方式哪一种更好呢?
        答案是第二种,因为第一种可能会出现跨域请求,也就是当域名不同或者端口不同的时候,都会出现跨域请求,而第二种保证了域名和端口的一致性,只是url不一样而已。
        跨域:因为浏览器的同源策略,当你通过浏览器向www.baidu.com前端页面发送请求的时候,网页需要向后台请求接口,但是如果接口的域名和当前的域名不一致,就会出现跨源请求的错误,无法访问到页面。而跨源是网页向api发送请求之后,服务器响应了这个请求,但是是浏览器端把这一次请求的响应给阻止了,并不是在请求不同域名的接口时,服务端不会响应这个请求。跨源是浏览器端的阻止行为,而不是服务器端的。


 c. 版本规则
        两个版本共存的时候,应该将API的版本号放入URL。
        api.example.com/api/v1/
        另一种做法是,将版本号放在HTTP头信息中,但不如放入URL方便和直观。Github采用这种做法。
    
    d. 面向资源编程
        将网络中的任何东西都看作是资源,对资源可以进行增删改查的操作,但是资源表示的是一个名称,如果一个url后面跟的是一个名词(单复数都可以),所用的名词往往与数据库的表格名对应,就表示要对这个资源进行增删改查的操作了。而get/post/delete/put是动词,所以url中不建议出现动词。
        www.baidu.com/api/v1/order/ (遵循规范)
        www.baidu.com/api/v1/orders/ (遵循规范)
        www.baidu.com/api/v1/get_order/ (没有遵循规范)
    
    e. HTTP方法规范
        GET:从服务器上获取一个或者多个资源
        POST:在服务器上新建一个资源
        PUT:在服务器跟新全部资源
        PATCH:在服务器更新部分资源
        比如用户表就有用户名,密码,性别,如果是PUT就全部更新。如果是PATHCH就只更新密码。
        
    f. 过滤规范
        www.baidu.com/api/v1/orders/?status=1&page=2&offset
    
    g. 状态码规范(状态码+code码)
        后台提供的状态码,供前端使用。
        200系列,300系列表示重定向,400系列表示客户端错误,500系列表示服务端错误(后台代码错误)。
        但是只有状态码还是不够的,请求的状态太多,所以除了使用状态码表示状态以外,还应该有code码来表示更加详细的请求情况。
        比如:支付宝的code码,20000,20001等
        
        def get(self,request,*args,**kwargs):
            ret  = {
                'code':1000,
                'msg':'没有携带cookie'
            }
            return HttpResponse(json.dumps(ret),status=201)
            
    h. 请求的返回值规范
        GET /order/:返回资源对象的列表(数组)
        GET /order/1/:返回单个资源对象
        POST /order/:返回新生成的资源对象
        PUT /order/1/:返回完整的资源对象
        PATCH /order/1/:返回完整的资源对象
        DELETE /order/1/:返回一个空文档
    
    i. Hypermedia API超链接规范
        希望在请求结果中包含这一个资源的详细信息的api。
        比如,我们请求商品列表信息得到如下的结果:
        [
            {
                "id": 1,
                "name": "袜子"
            },
            {
                "id": 2,
                "name": "裤子"
            },
            {
                "id": 3,
                "name": "鞋子"
            }
        ]
        如果你想查看id=1的商品的详细信息,你需要使用这个id拼接详情页的url地址,并发出请求,但是restful中希望这个详情页的url直接包含在json数据中,不用再单独进行拼接了。
        [
            {
                "id": 1,
                "name": "袜子",
                "url": "http://www.baidu.com/api/v1/1/"
            },
            {
                "id": 2,
                "name": "裤子"
                "url": "http://www.baidu.com/api/v1/2/"
            },
            {
                "id": 3,
                "name": "鞋子"
                "url": "http://www.baidu.com/api/v1/2/"
            }
        ]

根据上面的规范来进行实例的操作:
url路由的定义应该尽量使用通用视图:

from django.contrib import admin
from django.urls import path
from stuapp.views import *

urlpatterns = [
    path('admin/', admin.site.urls),
    path('token/', get_csrftoken),
    path('students/', StudentView.as_view()),
    path('inspect/', StudentInspectView.as_view()),
]

views.py文件中实现通用视图类函数,并将相关数据传给前端

# CBV和FBV
# CBV:基于类的视图 class StudentView(View):
# FBV:基于函数的视图 def student(request):
class StudentView(View):
    @method_decorator(allow_origin)
    def get(self, request):
        """
        学生数据查询接口
        功能:分页返回学生数据,前端传递page=1就返回第一页的数据;
        ?page: 页码
        ?size: 每页的数据个数
        :param request:
        :return:
        """
        error = ''
        # 1. 查询所有的学生数据
        stus = StuModel.objects.all()
        # 2. 根据前端ajax传递的page&size参数开始做分页数据
        size = int(request.GET.get('size', '2'))
        page_number = int(request.GET.get('page', '1'))
        paginator = Paginator(stus, size)
        try:
            page = paginator.page(page_number)
        except (EmptyPage, PageNotAnInteger, InvalidPage):
            error = '已经是最后一页了'
            page = paginator.page(paginator.num_pages)
            page_number = paginator.num_pages

        # 3. 开始做分页
        # 假设分页器上只显示5个页码,分页器出现滚动之后,当前页始终在中间,当前页前后各两个页码;
        if paginator.num_pages <= 5:
            # 全部展示,将当前所有页码的值返回给前端
            page_nums = [x for x in range(1, paginator.num_pages + 1)]
        elif page_number < 4:
            # 如果总页数超过5页了,但是当前页的页码小于4的时候,分页器是同样不会滚动的。
            # 1 2 3 4 5
            # 2 3 4 5 6
            # 3 4 5 6 7
            page_nums = [x for x in range(1, 6)]
        elif page_number - 4 >= 0 and page_number <= paginator.num_pages - 2:
            # 如果总页数超过5页了,分页器需要滚动
            page_nums = [x for x in range(page_number - 2, page_number + 3)]
        else:
            # 超过5页,但是已经到最后一页了,页面不再滚动
            page_nums = [x for x in range(paginator.num_pages - 4, paginator.num_pages+1)]

        #4. 向前端返回json数据
        previous = page.has_previous()
        next = page.has_next()
        data = {
            'code': 100,
            'status': 'ok',
            'error': error,
            # 总的数据个数
            'total_pages': len(stus),
            # 是否有上一页
            'has_previous': previous,
            'previous_url': page_number-1 if previous else None,
            # 是否有下一页
            'has_next': next,
            'next_url': page_number+1 if next else None,
            'page_nums': page_nums,
            # 当前页的数据列表
            'results': object_to_json(page.object_list),
            'current_page': page_number
        }
        # response = JsonResponse(data)
        # 允许所有的源,向这个接口发送请求并得到响应。(改变浏览器默认的禁止跨域,此时就是允许跨域。)
        # response['Access-Control-Allow-Origin'] = '*'
        return data

在这我们使用了一个装饰器@method_decorator(allow_origin),它的作用就是解决跨域的问题,//前后端分离下,经常出现跨域(CORS)问题,这个问题是浏览器的原因造成的,跟后台没有关系。
//一般在当前页面中,请求了和当前域名及端口不一致的url,就会出现跨域;
// 页面:http://localhost:63342 接口:http://localhost:8000
// 浏览器默认情况下阻止跨域:主要是为了保护网站的安全性的一种同源策略,在不确定安全性的前提下,不允许访问和网站本身端口域名不相同的地址的。
// 跨域是浏览器向后台接口发送请求,并且后台也响应了这个请求,但是浏览器的同源策略将这个跨域的响应拦截了。具体内容如下

from django.http import JsonResponse
def allow_origin(func):
    def _func(*args, **kwargs):
        data = func(*args, **kwargs)
        response = JsonResponse(data)
        response['Access-Control-Allow-Origin'] = '*'
        return response
    return _func

另外还使用了object_to_json()函数,它的作用是将QuerySet对象转为一个字典,因为前端不能识别QuerySet对象,实现代码如下:

from django.db.models.query import QuerySet
def object_to_json(model, ignore=None):
    """
    函数的作用就是将ORM中的Model对象,转化成json对象,再返回给前端
    :param model:
    :param ignore:
    :return:
    """
    if ignore is None:
        ignore = []
    if type(model) in [QuerySet, list]:
        json = []
        for element in model:
            json.append(_django_single_object_to_json(element, ignore))
        return json
    else:
        return _django_single_object_to_json(model, ignore)

def _django_single_object_to_json(element, ignore=None):
    return dict([(attr, getattr(element, attr)) for attr in [f.name for f in element._meta.fields if f not in ignore]])

前端的js代码:

function loadData(page) {
    // 该函数是根据page的值,加载当前页数据的函数;
    // page: 表示当前页的页码
    list_url = 'http://127.0.0.1:8000/students/?page=' + page + '&size=3';
    $.get(list_url, function(data){
        // 解析后台接口返回的json数据
        // 因为loadData(page)是循环调用的,所以在每次append()之前,先将上一次的数据清空,然后再append()新的数据。
        $('tbody').empty();
        for (var index in data.results){
            var student = data.results[index];
            tr = $('');
            // 向标签中添加三个,分别是姓名、ID、年龄
            tr.append($('').text(student.name), $('').text(student.id), $('').text(student.age));
            // 继续向标签中添加编辑图标和删除图标
            tr.append($('').append($('').attr({
                'class': 'glyphicon glyphicon-edit',
                'data-toggle': 'modal',
                'data-target': '#editModal'
            })));
            tr.append($('').append($('').attr({'class': 'glyphicon glyphicon-remove'})));
            $('tbody').append(tr);
        }
        // 开始设置分页
        $('.pagination').empty();
        if (data.has_previous){
            // 有上一页
            $('.pagination').append($('
  • ').append($('').attr({'aria-label': 'Previous', 'href': 'javascript:loadData(' + data.previous_url + ');'}).text('<<'))); } // [2 3 4 5 6] for (var index in data.page_nums){ var page_number = data.page_nums[index]; var li = $('
  • '); if (data.current_page == page_number){ //如果当前请求的页码的值current_page和遍历出来的page_number的值相等,将这个页码标记为选中状态。 li.attr('class', 'active'); } // 向这个li内部添加一个a标签 li.append($('').attr({'href': 'javascript:loadData(' + page_number + ');'}).text(page_number)); // 再将这个li添加到ul标签中; $('.pagination').append(li); } // 下一页 if (data.has_next){ $('.pagination').append($('
  • ').append($('').attr({'aria-label': 'Next', 'href': 'javascript:loadData(' + data.next_url + ');'}).text('>>'))); } }); } loadData(1);
  • 前后端分离中CSRF的问题:

    1. 在前后端分离中,接口的调用本身就是一种跨站请求,因为这个接口既要被安卓端的站点访问,又要被苹果端的站点访问,所以在这种模式下,CSRF的认证就失去作用了。解决方案:取消CsrfMilldeware中间件对于CSRF的认证;
    2. 如果在前后端分离中,必须要进行csrf认证,也可以实现;

    CSRF工作原理:

    FORM表单提交POST请求时:
    每次渲染页面,在Html模版中,{{ csrf_token }}都会加载一个随机字符串,每次的值都是不一样的;这个值会放在请求体Form Data中,提交至后台;

    AJAX提交POST请求时:
    不一定非得通过{{ csrf_token }}来认证csrf,也可以通过在请求头中,添加X-CSRFToken: csrftoken字段,同样也能通过csrf认证;
    由于前后端分离:前端页面无法识别{{ csrf_token }},那么前后端分离如何通过csrf的认证呢?通过在请求头中,添加X-CSRFToken: csrftoken字段

    不需要csrf认证时的配置:
    csrf_exempt: 对某一个FBV的视图取消csrf认证;不能用户CBV
    csrf_protect: 对某一个FBV的视图添加csrf认证;不能用户CBV
    如果想让整个项目都取消csrf认证,去settings.py中关闭中间件。

    在CBV中怎样设置呢?
    很简单,如下,在urls里配置就可

    from django.views.decorators.csrf import csrf_exempt
    path('inspect/', csrf_exempt(StudentInspectView.as_view()) ),
    

    在前后端分离情况下需要csrf认证又怎么设置呢

    安装第三方跨域包:django-cors-headers
    解决:
    1. 基本的跨域请求能够实现;
    2. 能够获取跨域请求返回的响应头中的所有字段(默认只返回Content-Type);
    3. 能够发起跨域请求的时候携带Cookie(默认不允许带Cookie);
    
    配置settings.py文件:
    INSTALLED_APPS = [
        'corsheaders',
    ]
    
    MIDDLEWARE = [
        'corsheaders.middleware.CorsMiddleware',
        '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',
    ]
    # 设置为False,如果设置为True,则请求无法携带Cookie了。
    CORS_ORIGIN_ALLOW_ALL = False
    # 设置请求是否允许携带Cookie,必须和xhrFields: {withCredentials: true,}同时使用。
    CORS_ALLOW_CREDENTIALS = True
    # 跨域源的白名单,需要跨域的源设置在这里
    CORS_ORIGIN_WHITELIST = [
        'localhost:63342',
    ]
    # 指定哪些方法可以发送跨域请求
    CORS_ALLOW_METHODS = (
        'DELETE',
        'GET',
        'OPTIONS',
        'PATCH',
        'POST',
        'PUT',
    )
    # 指定请求头中允许携带的字段
    CORS_ALLOW_HEADERS = (
        'accept',
        'accept-encoding',
        'authorization',
        'content-type',
        'dnt',
        'origin',
        'user-agent',
        'x-csrftoken',
        'x-requested-with',
        'cookie',
    )
    
    后台views.py配置:
    class StudentInspectView(View):
        """
        检查学员学号是否唯一的CBV视图类接口。
        """
        def post(self, request):
            stu_id = request.POST.get('sid')
            data = {}
            if StuModel.objects.filter(id=stu_id):
                # 学号已经存在
                data['is_exist'] = 1
                data['message'] = '该学号已经存在'
            else:
                data['is_exist'] = 0
                data['message'] = '该学号可以使用'
    		# 需要返回Response对象
            return JsonResponse(data)
    
    前端js配置:
    // 检查学号是否已经存在
    $('#sid').blur(function () {
        //在数据没有合法之前,"提交按钮" 不能点击;
        $('#add').attr('disabled', true);
        $.ajax({
            url: 'http://localhost:8000/inspect/',
            type: 'POST',
            data: {
                'sid': $('#sid').val()
            },
            xhrFields: {
                withCredentials: true
            },
            headers: {
                'X-CSRFToken': $.cookie('csrftoken'),
            },
            success: function (data, status) {
                $('#sid').next().text(data.message);
            }
        });
    });
    
    
    // 发送ajax请求,在页面刷新的时候,就获取csrftoken的值
    $(function () {
        $.ajax({
            url: 'http://localhost:8000/token/',
            // 默认情况下,跨域请求的请求头是不允许携带Cookie的,另外一方面,跨域请求的响应头中默认只包含content-type,后台的响应中只返回这一个字段。
            xhrFields: {
                withCredentials: true,
            }
        });
    });
    
    

    你可能感兴趣的:(Django,前后端分离)