zoukankan      html  css  js  c++  java
  • 富文本编辑框和防止xss攻击

    一、后台管理页面构建

     1、创建后台管理url

    urlpatterns = [
        ...
        # 后台管理url
        re_path("cn_backend/$", views.cn_backend),
        re_path("cn_backend/add_article/$", views.add_article),
        ...
    ]
    

    2、构造视图函数

    from django.contrib.auth.decorators import login_required  # 装饰器:login_required()
    
    @login_required
    def cn_backend(request):
        """
        后台管理页面
        :param request:
        :return:
        """
        # 当前登录人发布过得文章列表
        article_list = models.Article.objects.filter(user=request.user)
    
        return render(request, "backend/backend.html", locals())
    
    @login_required
    def add_article(request):
        """
        后台管理添加文章视图函数
        :param request:
        :return:
        """
        if request.method == "POST":
            title = request.POST.get("title")
            content = request.POST.get("content")
            # 生成article对象
            models.Article.objects.create(title=title, content=content, user=request.user)
            return redirect("/cn_backend/")
    
        return render(request, "backend/add_article.html")
    

    3、在templates目录下创建backend子目录,构建模板

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>博客后台管理 - 博客园</title>
        <!-- 引入 Bootstrap 核心 CSS 文件 -->
        <link rel="stylesheet" href="/static/blog/bootstrap-3.3.7/css/bootstrap.css">
        <!-- jQuery (Bootstrap 的所有 JavaScript 插件都依赖 jQuery,所以必须放在前边) -->
        <script src="/static/js/jquery-3.3.1.js"></script>
        <!-- 引入 Bootstrap 核心 JavaScript 文件 -->
        <script src="/static/blog/bootstrap-3.3.7/js/bootstrap.js"></script> <!--依赖jquery-->
        <link rel="stylesheet" href="/static/blog/css/backend.css">
    </head>
    <body>
    
    <div class="header">
        <p class="title">
            后台管理
    
            <a class="info" href="/logout/">注销</a>
            <span class="info"><span class="glyphicon glyphicon-user"></span>&nbsp;&nbsp;{{ request.user.username }}</span>
        </p>
    </div>
    
    
    <div class="container">
        <div class="col-md-3">
            <div class="panel-group" id="accordion" role="tablist" aria-multiselectable="true">
                <div class="panel panel-default">
                    <div class="panel-heading" role="tab" id="headingOne">
                        <h4 class="panel-title">
                            <a role="button" data-toggle="collapse" data-parent="#accordion" href="#collapseOne"
                               aria-expanded="true" aria-controls="collapseOne">
                                操作
                            </a>
                        </h4>
                    </div>
                    <div id="collapseOne" class="panel-collapse collapse in" role="tabpanel" aria-labelledby="headingOne">
                        <div class="panel-body">
                            <!--添加文章url跳转-->
                            <p><a href="/cn_backend/add_article/">添加文章</a></p>
                        </div>
                    </div>
                </div>
    
            </div>
        </div>
        <div class="col-md-9">
    
            <div>
                <!-- Nav tabs -->
                <ul class="nav nav-tabs" role="tablist">
                    <li role="presentation" class="active"><a href="#home" aria-controls="home" role="tab"
                                                              data-toggle="tab">文章</a></li>
                    <li role="presentation"><a href="#profile" aria-controls="profile" role="tab"
                                               data-toggle="tab">日记</a></li>
                    <li role="presentation"><a href="#messages" aria-controls="messages" role="tab" data-toggle="tab">眼镜</a>
                    </li>
                    <li role="presentation"><a href="#settings" aria-controls="settings" role="tab" data-toggle="tab">相册</a>
                    </li>
                </ul>
    
                <!-- Tab panes -->
                <div class="tab-content">
                    <div role="tabpanel" class="tab-pane active" id="home">
                        {% block content %}
    
                        {% endblock %}
                    </div>
                    <div role="tabpanel" class="tab-pane" id="profile">
    
                        <img src="/static/blog/img/meinv2.jpg" alt="">
                        <img src="/static/blog/img/meinv3.jpg" alt="">
                        <img class="pull-right" src="/static/blog/img/meinv.jpg" alt="">
                    </div>
                    <div role="tabpanel" class="tab-pane" id="messages">
    
                        <img width="180" height="180" src="/static/blog/img/hashiqi2.jpg" alt="">
    
                        <img width="180" height="180" src="/static/blog/img/dogg4.jpg" alt="">
                        <img width="180" height="180" src="/static/blog/img/linhaifeng.jpg" alt=""><br>
                        <img width="180" height="180" src="/static/blog/img/dogg3.jpeg" alt="">
                        <img width="180" height="180" src="/static/blog/img/dogge2.jpg" alt="">
    
                        <img width="180" height="180" src="/static/blog/img/dogg5.jpg" alt="">
    
                    </div>
                    <div role="tabpanel" class="tab-pane" id="settings">
    
                    </div>
                </div>
    
            </div>
    
        </div>
    </div>
    
    </body>
    </html>
    backend/base.html

      扩展模板:

    {% extends 'backend/base.html' %}
    
    {% block content %}
        <div class="article_list small">
    
            <table class="table table-hover table-striped">
                <thead>
                <th>标题</th>
                <th>评论数</th>
                <th>点赞数</th>
                <th>操作</th>
                <th>操作</th>
                </thead>
                <tbody>
                {% for article in article_list %}
                    <tr>
                        <td>{{ article.title }}</td>
                        <td>{{ article.comment_count }}</td>
                        <td>{{ article.up_count }}</td>
                        <td><a href="">编辑</a></td>
                        <td><a href="">删除</a></td>
                    </tr>
                {% endfor %}
    
                </tbody>
            </table>
        </div>
    {% endblock %}
    backend/backend.html
    {% extends 'backend/base.html' %}
    
    {% block content %}
        {# 提交form表单 #}
        <form action="" method="post">
            {% csrf_token %}
            <div class="add_article">
                <div class="alert-success text-center">添加文章</div>
    
                <div class="add_article_region">
                    <div class="title form-group">
                        <label for="">标题</label>
                        <div>
                            <input type="text" name="title">
                        </div>
                    </div>
    
                    <div class="content form-group">
                        <label for="">内容(Kindeditor编辑器,不支持拖放/粘贴上传图片) </label>
                        <div>
                            <textarea name="content" id="article_content" cols="30" rows="10"></textarea>
                        </div>
                    </div>
    
                    <input type="submit" class="btn btn-default">
    
                </div>
            </div>
        </form>
        <script charset="utf-8" src="/static/blog/kindeditor/kindeditor-all.js"></script>
    
        <script>
            KindEditor.ready(function (K) {
                window.editor = K.create('#article_content', {   // 找到这个标签就会引入文本编辑器
                     "800",
                    height: "600",
                    resizeType: 0,
                    uploadJson: "/upload/",
                    extraFileUploadParams: {
                        csrfmiddlewaretoken: $("[name='csrfmiddlewaretoken']").val()
                    },
                    filePostName: "upload_img"
                });
            });
        </script>
    {% endblock %}
    backend/add_article.html

      在/static/blog/css/backend.css构建模板样式:

    * {
        margin: 0;
        padding: 0;
    }
    
    .header {
        width: 100%;
        height: 60px;
        background-color: #333333;
        line-height: 60px;
    }
    
    .header .title {
        color: white;
        font-size: 20px;
        margin-left: 20px;
        font-weight: 100;
    }
    
    .header .title .info {
        float: right;
        margin-right: 20px;
        font-size: 14px;
        text-decoration: none;
        color: white;
    
    }
    
    #accordion {
        margin-top: 20px;
    }
    
    .article_list {
        padding: 30px;
    }
    
    .alert-success {
        margin: 8px 0;
    }
    
    [name='title'] {
        width: 100%;
    }
    
    .tab-pane {
        margin-top: 40px;
        margin-left: 40px;
    }
    
    .tab-pane img {
    
        border-radius: 80%;
        margin: 20px 20px;
    }
    backend.css

    4、展示效果

      

    二、编辑器引入和使用

    1、kindeditor编辑器

      kindeditor编辑器官网和文档:http://kindeditor.net/doc.php

      kindeditor编辑器下载页面: http://www.kindsoft.net/down.php

    (1)引入编辑器

      在页面中添加以下脚本

    <script charset="utf-8" src="/editor/kindeditor.js"></script>
    <script charset="utf-8" src="/editor/lang/zh-CN.js"></script>
    <script>
            KindEditor.ready(function(K) {
                    window.editor = K.create('#editor_id');
            });
    </script>
    

      注意将下载好的文件中的kindeditor.js文件放入项目目录中引入。在K.create()查找的标签引入文本编辑器。

    (2)配置编辑器参数

      通过K.create函数的第二个参数,可以对编辑器进行配置,具体参数请参考 编辑器初始化参数 。

      width:编辑器的宽度,可以设置px或%,比textarea输入框样式表宽度优先度高。

      height:编辑器的高度,只能设置px,比textarea输入框样式表高度优先度高。

      resizeType:2或1或0,2时可以拖动改变宽度和高度,1时只能改变高度,0时不能拖动。

      uploadJson:指定上传文件的服务器端程序。

      extraFileUploadParams:上传图片、Flash、视音频、文件时,支持添加别的参数一并传到服务器。

      filePostName:指定上传文件form名称。

    2、在博客项目中引入编辑器

      在backend/add_article.html中添加编辑器:

    {% extends 'backend/base.html' %}
    
    {% block content %}
        {# 提交form表单 #}
        <form action="" method="post">
            ...
        </form>
    <script charset="utf-8" src="/static/blog/kindeditor/kindeditor-all.js"></script> <script> KindEditor.ready(function (K) { window.editor = K.create('#article_content', { // 找到这个标签就会引入文本编辑器 "800", height: "600", resizeType: 0, uploadJson: "/upload/", extraFileUploadParams: { csrfmiddlewaretoken: $("[name='csrfmiddlewaretoken']").val() }, filePostName: "upload_img" }); }); </script> {% endblock %}

      编辑器显示效果:

      

    3、编辑器上传功能

    (1)添加upload路由

    urlpatterns = [
        ...
        path('upload/', views.upload),
        ...
    ]
    

    (2)直接使用编辑器的文件上传功能,发现forbidden报错。

      这是由于文本编辑器发送文件时使用的POST请求,被django拦截,因此需要在数据中携带一个csrf_token。

      这里使用extraFileUploadParams(上传图片、Flash、视音频、文件时,支持添加别的参数一并传到服务器)来实现携带csrf_token参数。

    <script>
        KindEditor.ready(function (K) {
            window.editor = K.create('#article_content', {   // 找到这个标签就会引入文本编辑器
                 "800",
                height: "600",
                resizeType: 0,
                uploadJson: "/upload/",
                extraFileUploadParams: {
                    csrfmiddlewaretoken: $("[name='csrfmiddlewaretoken']").val()
                }
            });
        });
    </script>

      在这里它添加的是一对键值,键是csrfmiddlewaretoken,值是取的页面中属性name为csrfmiddlewaretoken对应的值。

    (3)添加upload视图函数

    def upload(request):
        print(request.FILES)  
        return HttpResponse("ok")
    

      request.FILES中的键来自于表单中的<input type="file" name="" />的name值,request.FILES中的值均为UploadedFile类文件对象。

      上传文件后输出如下:

    <MultiValueDict: {'imgFile': [<InMemoryUploadedFile: dogg4.jpg (image/jpeg)>]}>
    

    (4)指定修改上传文件的键

      filePostName:指定上传文件form名称

    <script>
        KindEditor.ready(function (K) {
            window.editor = K.create('#article_content', {   // 找到这个标签就会引入文本编辑器
                 "800",
                height: "600",
                resizeType: 0,
                uploadJson: "/upload/",
                extraFileUploadParams: {
                    csrfmiddlewaretoken: $("[name='csrfmiddlewaretoken']").val()
                },
                filePostName: "upload_img"
            });
        });
    </script>
    

      修改后,上传文件输出如下:

    <MultiValueDict: {'upload_img': [<InMemoryUploadedFile: dogg4.jpg (image/jpeg)>]}>
    

    4、用户上传文件处理

    (1)之前将用户上传的头像是存放在/media/avatars目录下,现在再创建一个/media/add_article_img目录来存放上传的图片文件。

    (2)用户上传的文件指定到了upload视图函数,构建upload来下载图片到本地服务器

    def upload(request):
        print(request.FILES)  # <MultiValueDict: {'upload_img': [<InMemoryUploadedFile: dogg4.jpg (image/jpeg)>]}>
        img = request.FILES.get("upload_img")  # 文件对象
        print(img.name)  # 文件名
    
        path = os.path.join(settings.MEDIA_ROOT, "add_article_img", img.name)
        with open(path, "wb") as f:
            for line in img:
                f.write(line)
    
        return HttpResponse("ok")
    

      上传完成后,在服务器查看下载好的图片文件:

      

    5、处理用户上传完成后信息显示

      完成图片上传后,浏览器显示的信息如下:

      

      需要在文本编辑器中显示上传的图片,需要继续构建upload函数,下载完文件后把对应的路径交给文本编辑器:

    def upload(request):
        print(request.FILES)  # <MultiValueDict: {'upload_img': [<InMemoryUploadedFile: dogg4.jpg (image/jpeg)>]}>
        img = request.FILES.get("upload_img")  # 文件对象
        print(img.name)  # 文件名
    
        path = os.path.join(settings.MEDIA_ROOT, "add_article_img", img.name)
        with open(path, "wb") as f:
            for line in img:
                f.write(line)
    
        # 下载完文件后,把对应的路径交给文本编辑器,文本编辑器构建img标签,我们就能够看到这个图片了。
        response = {
            "error": 0,
            "url": "/media/add_article_img/%s" % img.name,   # 能够预览这张图片的路径
        }
    
        # 将这个字典交还给文本编辑器,做json序列化
        import json
        return HttpResponse(json.dumps(response))
    

      注意:

    (1)显示图片的思路是:下载完文件后,把对应的路径交给文本编辑器,文本编辑器构建img标签,就能够看到这个图片了。

      构建的字典有两对键值:error=0代表没有发生任何错误;

      url对应的路径不是图片存储的绝对路径而是能够访问查看的路径:127.0.0.1:8000/media/add_article_img/dog3.jpg

    (2)编辑器要求返回的数据必须是json数据,因此需要将构建好的字典做json序列化后回传

    # 将这个字典交还给文本编辑器,做json序列化
    import json
    return HttpResponse(json.dumps(response))
    

    (3)检查验证:

      

      检查html:

      

    三、文章摘要的保存

    1、修改add_article视图,截取文章内容作为文章摘要

    @login_required
    def add_article(request):
        """
        后台管理添加文章视图函数
        :param request:
        :return:
        """
        if request.method == "POST":
            title = request.POST.get("title")
            content = request.POST.get("content")
    
            desc = content[0:150]  # 截取文章内容作为摘要
    
            # 生成article对象
            models.Article.objects.create(title=title, desc=desc, content=content, user=request.user)
            return redirect("/cn_backend/")
    
        return render(request, "backend/add_article.html")
    

      这种情况下,如果文章内容是纯文本,摘要内容是会正常显示的。但如果文章内容是标签字符串,包含有各种样式,那就会出现问题。

    2、截取摘要问题分析

    (1)index.html中文章摘要添加safe过滤器

      如果不添加safe过滤器,那首页显示的文章摘要将以标签字符串的形式展示:

      

      添加过滤器后不再对字符串进行转义:

    <span class="media-right">
        {# 文章摘要 #}
        {{ article.desc|safe }}
    </span>

    (2)添加|safe过滤器后,摘要样式产生了错乱

       

      这是由于截取的150的标签字符串没有闭合导致。因此不应该是截取content,而是应该截取content中的文本前150个。

    3、Beautiful Soup库(bs4)介绍

      HTML文件其实就是由一组尖括号构成的标签组织起来的,每一对尖括号形式一个标签,标签之间存在上下关系,形成标签树;因此可以说Beautiful Soup库是解析、遍历、维护“标签树”的功能库

      安装Beautiful Soup:

    pip3 install bs4
    

      Beautiful Soup支持Python标准库中的HTML解析器,还支持一些第三方的解析器,这里使用Python默认支持的解析器:html.parser。

      使用方法:

    from bs4 import BeautifulSoup
    
    s = "<h1>hello</h1><span>123</span>"
    
    soup = BeautifulSoup(s, "html.parser")
    
    print(soup.text)   # 输出:hello123
    

      可以看到输出的是标签内的文本。

    4、利用bs4来解决摘要截取问题

    from bs4 import BeautifulSoup
    
    @login_required
    def add_article(request):
        """
        后台管理添加文章视图函数
        :param request:
        :return:
        """
        if request.method == "POST":
            title = request.POST.get("title")
            content = request.POST.get("content")
    
            soup = BeautifulSoup(content, "html.parser")   # 标签字符串是content
    
            # 构建摘要数据,获取标签字符串的文本前150个符号
            desc = soup.text[0:150]+"..."
    
            # 生成article对象
            models.Article.objects.create(title=title, desc=desc, content=content, user=request.user)
            return redirect("/cn_backend/")
    
        return render(request, "backend/add_article.html")
    

      再次添加文章,注意要将编辑器切换到html标签预览模式,再粘贴标签字符串。

      首页摘要显示效果:

      

    5、利用bs4模块防御xss攻击

    (1)利用bs4过滤非法标签:

    from bs4 import BeautifulSoup
    
    s = "<h1>hello</h1><span>123</span><script>alert(123)</script>"
    # Python默认支持的解析器:html.parser
    soup = BeautifulSoup(s, "html.parser")
    # print(soup.text)
    
    print(soup.find_all())   # 获取所有的标签对象:[<h1>hello</h1>, <span>123</span>, <script>alert(123)</script>]
    
    for tag in soup.find_all():
        print(tag.name)
        """
        h1
        span
        script
        """
        if tag.name == "script":
            # 非法标签
            tag.decompose()   # 从soup中删除标签
    
    print(str(soup))   # <h1>hello</h1><span>123</span>
    

      注意:

     1)每个tag都有自己的名字,通过 .name 来获取;

     2)Tag.decompose() 方法将当前节点移除文档树并完全销毁。

     3)find_all( name , attrs , recursive , string , **kwargs ),find_all() 方法搜索当前tag的所有tag子节点,并判断是否符合过滤器的条件.

    (2)应用在add_article视图函数中

    from bs4 import BeautifulSoup
    
    @login_required
    def add_article(request):
        """
        后台管理添加文章视图函数
        :param request:
        :return:
        """
        if request.method == "POST":
            title = request.POST.get("title")
            content = request.POST.get("content")
    
            soup = BeautifulSoup(content, "html.parser")   # 标签字符串是content
    
            # 防止xss攻击,过滤script标签
            for tag in soup.find_all():
                print(tag.name)
                if tag.name == "script":
                    tag.decompose()
    
            # 构建摘要数据,获取标签字符串的文本前150个符号
            desc = soup.text[0:150]+"..."
    
            # 生成article对象
            models.Article.objects.create(title=title, desc=desc, content=str(soup), user=request.user)
            return redirect("/cn_backend/")
    
        return render(request, "backend/add_article.html")
    

      注意保存的content不再是原始的content而是处理之后的标签字符串。

    (3)检查验证

      创建新文章:

      

      检查script标签是否清除:

      

      数据库查看content内容:

      

  • 相关阅读:
    LeetCode 23. 合并K个排序链表
    LeetCode 199. 二叉树的右视图
    LeetCode 560. 和为K的子数组
    LeetCode 1248. 统计「优美子数组」
    LeetCode 200. 岛屿数量
    LeetCode 466. 统计重复个数
    LeetCode 11. 盛最多水的容器
    LeetCode 55. 跳跃游戏
    LeetCode 56. 合并区间
    Java生鲜电商平台-订单架构实战
  • 原文地址:https://www.cnblogs.com/xiugeng/p/9439473.html
Copyright © 2011-2022 走看看