精华内容
下载资源
问答
  • 基于flask,前后端分离的运维平台 前端,后端都根据功能可拆分部署,一个目录是一单独项目 python3.5 登陆 auth 用户首次登陆获取到token,通过token与后端通信 动态菜单 后端根据用户权限生成菜单 CMDB 服务器资源池 ...
  • 今天拿这个为例聊下前后端分离项目的结构。 下面是Github上搜到的社工库项目https://github.com/Leezj9671/socialdb_vue_flask,socialdb_vue_flask,同样只是技术说明,不提供数据,并做了下小修改:添加了级联...

    所谓的社工库可能是最简单的web项目了,没有登录、注册、权限等逻辑,当然这里只是最简单的那种。今天拿这个为例聊下前后端分离项目的结构。

    下面是Github上搜到的别人开源社工库项目https://github.com/Leezj9671/socialdb_vue_flask,socialdb_vue_flask,同样只是技术说明,不提供数据,并做了下小修改:添加了级联搜索多进程并发写入数据库

    环境:前端 Nodejs Vue

    ​ 后端 Python Flask

    ​ 数据库 MongoDB

    常规小的web项目,比如Flask + Bootstrap,不同的是可以前后端约定后接口后分别独立开发调试,然后部署整合成一个项目

    原作者的前端页面

    image

    后端接口

    api_main.py

    '''
    api
    存在问题:
    - 并发请求时,且前一请求正在查询会导致卡死
    '''
    
    import time
    from pymongo import MongoClient
    from flask import Flask, request, jsonify, redirect, url_for
    from flask_restful import Api, Resource, reqparse
    from conf.config import MongoDBConfig
    
    app = Flask(__name__)
    client = MongoClient(MongoDBConfig.g_server_ip, MongoDBConfig.g_server_port)
    db = client[MongoDBConfig.g_db_name]
    
    
    def response_cors(data=None, datacnts=None, status=None):
        '''为返回的json格式进行跨域请求'''
        if data:
            resp = jsonify({"status": status, "data": data, "datacounts": datacnts})
        else:
            resp = jsonify({"status": status})
        resp.headers['Access-Control-Allow-Origin'] = '*'
        return resp
    
    
    class Person(Resource):
        '''人员类'''
    
        def get(self, user=None, email=None, password=None, passwordHash=None, source=None, xtime=None):
            # 该处可能存在安全问题,做出限制会更好
            # print(user)
            parser = reqparse.RequestParser()
            parser.add_argument('limit', type=int, help='Show [limitn] datas in one page')
            parser.add_argument('skip', type=int, help='Skip [skipn] datas')
            args = parser.parse_args()
            limitn = 10 if args['limit'] is None else args['limit']
            skipn = 0 if args['skip'] is None else args['skip']
    
            # data用于存储获取到的信息
            data = []
            datacnts = 0
    
            # 待改进
            if user:
                persons_info = db.person.find({"user": {"$regex": user, "$options": "$i"}}, {"_id": 0}).limit(limitn).skip(
                    skipn)
                datacnts = db.person.find({"user": {"$regex": user, "$options": "$i"}}, {"_id": 0}).count()
    
            elif email:
                persons_info = db.person.find({"email": {"$regex": email, "$options": "$i"}}, {"_id": 0}).limit(
                    limitn).skip(skipn)
                datacnts = db.person.find({"email": {"$regex": email, "$options": "$i"}}, {"_id": 0}).count()
    
            elif password:
                persons_info = db.person.find({"password": {"$regex": password, "$options": "$i"}}, {"_id": 0}).limit(
                    limitn).skip(skipn)
                datacnts = db.person.find({"password": {"$regex": password, "$options": "$i"}}, {"_id": 0}).count()
    
            elif passwordHash:
                persons_info = db.person.find({"passwordHash": {"$regex": passwordHash, "$options": "$i"}},
                                              {"_id": 0}).limit(limitn).skip(skipn)
                datacnts = db.person.find({"passwordHash": {"$regex": passwordHash, "$options": "$i"}}, {"_id": 0}).count()
    
            # elif source:
            #     persons_info = db.person.find({"source": {"$regex": source, "$options":"$i"}}, {"_id": 0}).limit(limitn).skip(skipn)
    
            # elif xtime:
            #     persons_info = db.person.find({"xtime": {"$regex": xtime, "$options":"$i"}}, {"_id": 0}).limit(limitn).skip(skipn)
    
            else:
                # 限制只能查询10个
                persons_info = db.person.find({}, {"_id": 0, "update_time": 0}).limit(10)
    
            for person in persons_info:
                data.append(person)
    
            # 判断有无数据返回
            if data:
                return response_cors(data, datacnts, "ok")
            else:
                return response_cors(data, datacnts, "not found")
    
        def post(self):
            '''
            以json格式进行提交文档
            '''
            data = request.get_json()
            if not data:
                return {"response": "ERROR DATA"}
            else:
                user = data.get('user')
                email = data.get('email')
    
                if user and email:
                    if db.person.find_one({"user": user, "email": email}, {"_id": 0}):
                        return {"response": "{{} {} already exists.".format(user, email)}
                    else:
                        data.create_time = time.strftime('%Y%m%d', time.localtime(time.time()))
                        db.person.insert(data)
                else:
                    return redirect(url_for("person"))
    
        # 暂时关闭高危操作
        # def put(self, user, email):
        #     '''
        #     根据user和email进行定位更新数据
        #     '''
        #     data = request.get_json()
        #     db.person.update({'user': user, 'email': email},{'$set': data},)
        #     return redirect(url_for("person"))
    
        # def delete(self, email):
        #     '''
        #     email作为唯一值, 对其进行删除
        #     '''
        #     db.person.remove({'email': email})
        #     return redirect(url_for("person"))
    
    
    class Info(Resource):
        '''个人信息类'''
    
        def get(self, id=None, name=None, sex=None, qq=None, phonenumber=None):
            # 该处可能存在安全问题,做出限制会更好
            parser = reqparse.RequestParser()
            parser.add_argument('limit', type=int, help='Show [limitn] datas in one page')
            parser.add_argument('skip', type=int, help='Skip [skipn] datas')
            args = parser.parse_args()
            limitn = 10 if args['limit'] is None else args['limit']
            skipn = 0 if args['skip'] is None else args['skip']
    
            # data用于存储获取到的信息
            data = []
            datacnts = 0
    
            # 待改进
            if id:
                my_info = db.info.find({"id": id}, {"_id": 0}).limit(limitn).skip(
                    skipn)
                datacnts = db.info.find({"id": id}, {"_id": 0}).count()
    
            elif name:
                my_info = db.info.find({"name": {"$regex": name, "$options": "$i"}}, {"_id": 0}).limit(
                    limitn).skip(skipn)
                datacnts = db.info.find({"name": {"$regex": name, "$options": "$i"}}, {"_id": 0}).count()
    
            elif sex:
                my_info = db.info.find({"sex": {"$regex": sex, "$options": "$i"}}, {"_id": 0}).limit(
                    limitn).skip(skipn)
                datacnts = db.info.find({"sex": {"$regex": sex, "$options": "$i"}}, {"_id": 0}).count()
    
            elif qq:
                my_info = db.info.find({"qq": qq},
                                       {"_id": 0}).limit(limitn).skip(skipn)
                datacnts = db.info.find({"qq": qq}, {"_id": 0}).count()
    
            elif phonenumber:
                my_info = db.info.find({"phonenumber": phonenumber},
                                       {"_id": 0}).limit(limitn).skip(skipn)
                datacnts = db.info.find({"phonenumber": phonenumber}, {"_id": 0}).count()
    
            else:
                # 限制只能查询10个
                my_info = db.info.find({}, {"_id": 0, "update_time": 0}).limit(10)
    
            for person in my_info:
                data.append(person)
    
            # 判断有无数据返回
            if data:
                return response_cors(data, datacnts, "ok")
            else:
                return response_cors(data, datacnts, "not found")
    
        def post(self):
            '''
            以json格式进行提交文档
            '''
            data = request.get_json()
            if not data:
                return {"response": "ERROR DATA"}
            else:
                user = data.get('user')
                email = data.get('email')
    
                if user and email:
                    if db.person.find_one({"user": user, "email": email}, {"_id": 0}):
                        return {"response": "{{} {} already exists.".format(user, email)}
                    else:
                        data.create_time = time.strftime('%Y%m%d', time.localtime(time.time()))
                        db.person.insert(data)
                else:
                    return redirect(url_for("person"))
    
        # 暂时关闭高危操作
        # def put(self, user, email):
        #     '''
        #     根据user和email进行定位更新数据
        #     '''
        #     data = request.get_json()
        #     db.person.update({'user': user, 'email': email},{'$set': data},)
        #     return redirect(url_for("person"))
    
        # def delete(self, email):
        #     '''
        #     email作为唯一值, 对其进行删除
        #     '''
        #     db.person.remove({'email': email})
        #     return redirect(url_for("person"))
    
    
    class Analysis(Resource):
        '''
        分析功能
        '''
    
        def get(self, type_analyze):
            '''
            type为分析类型,包括邮箱后缀、泄漏来源、泄漏时间
            type: [suffix_email, source, xtime, create_time]
            '''
            if type_analyze in ["source", "xtime", "suffix_email", "create_time"]:
                pipeline = [{"$group": {"_id": '$' + type_analyze, "sum": {"$sum": 1}}}]
                return response_cors(list(db.person.aggregate(pipeline)), None, "ok")
    
            else:
                return response_cors("use /api/analysis/[source, xtime, suffix_email] to get analysis data.", None, "error")
    
    
    class Getselector(Resource):
        '''
        获取级联数据功能
        '''
    
        def get(self):
            '''
            type为分析类型,包括邮箱后缀、泄漏来源、泄漏时间
            type: [suffix_email, source, xtime, create_time]
            '''
            subject = [
                {
                    "id": 1,
                    "name": "账密",
                    "select": "find",
                    "obj": [
                        {
                            "id": 3,
                            "name": "用户名",
                            "select": "user"
                        },
                        {
                            "id": 4,
                            "name": "密码",
                            "select": "password"
                        },
                        {
                            "id": 5,
                            "name": "邮箱",
                            "select": "email"
                        },
                        {
                            "id": 6,
                            "name": "哈希密码",
                            "select": "passwordHash"
                        }
                    ]
                },
                {
                    "id": 2,
                    "name": "身份信息",
                    "select": "info",
                    "obj": [
                        {
                            "id": 7,
                            "name": "手机号",
                            "select": "phonenumber"
                        },
                        {
                            "id": 8,
                            "name": "QQ",
                            "select": "qq"
                        },
                        {
                            "id": 9,
                            "name": "身份证",
                            "select": "id"
                        },
                        {
                            "id": 10,
                            "name": "姓名",
                            "select": "name"
                        }
                    ]
                }
            ]
            return response_cors(subject, None, "ok")
    
    
    # 添加api资源
    api = Api(app)
    api.add_resource(Person, "/api/find")
    api.add_resource(Person, "/api/find/user/<string:user>", endpoint="user")
    api.add_resource(Person, "/api/find/email/<string:email>", endpoint="email")
    api.add_resource(Person, "/api/find/password/<string:password>", endpoint="password")
    api.add_resource(Person, "/api/find/passwordHash/<string:passwordHash>", endpoint="passwordHash")
    api.add_resource(Person, "/api/find/source/<string:source>", endpoint="source")
    api.add_resource(Person, "/api/find/time/<string:xtime>", endpoint="xtime")
    api.add_resource(Info, "/api/info")
    api.add_resource(Info, "/api/info/id/<int:id>", endpoint="id")
    api.add_resource(Info, "/api/info/name/<string:name>", endpoint="name")
    api.add_resource(Info, "/api/info/sex/<string:sex>", endpoint="sex")
    api.add_resource(Info, "/api/info/qq/<int:qq>", endpoint="qq")
    api.add_resource(Info, "/api/info/phonenumber/<int:phonenumber>", endpoint="phonenumber")
    api.add_resource(Analysis, "/api/analysis/<string:type_analyze>", endpoint="type_analyze")
    api.add_resource(Getselector, "/api/get_selector")
    
    if __name__ == '__main__':
        app.run(host='0.0.0.0', debug=True)
    
    

    后端起了Flask服务器,默认监听5000端口,主要使用Flask Restful API编写,负责处理接口逻辑+数据库操作,其他还有写入数据库操作。

    在 Person类Get方法中编写get请求业务逻辑

    前端也相当精简,甚至简陋,只有两个component,search.vue(搜索)和 Analysis.vue(分析)

    其中search.vue加了级联前端和请求级联数据方法(created /get_selector方法)

    <template>
      <div class="Search">
    
        <select name="province" id="province" class="Select" v-on:change="indexSelect01" v-model="indexId">
          <option :value="item.select" v-for="(item,index) in select01" class="options">{{item.name}}</option>  <!---->
        </select>
        <!--二级菜单-->
        <select name="city" id="city" class="Select" v-model="indexId2" >
          <option :value="k.select" v-for="k in select02" class="options">{{k.name}}</option>
        </select>
    
        <input
            placeholder="请输入并按下回车进行搜索(忽略大小写)"
            type="text"
            class="searchInput"
            v-model="searchStr"
            v-on:keyup.enter="search"
        />
        <h2 v-show="errorinfo">暂无数据,请重新输入</h2>
        <div v-if="indexId=== 'find'" class="container" v-show="retItems.length">
            <table>
                <thead>
                <tr>
                    <th width="10%">用户名</th>
                    <th width="15%">邮箱</th>
                    <th width="10%">来源</th>
                    <th width="10%">泄漏时间</th>
                    <th width="20%">密码</th>
                    <th width="35%">hash密码</th>
                </tr>
                </thead>
                <tbody>
                    <tr v-for="item in retItems">
                        <td width="10%">{{ item.user }}</td>
                        <td width="15%">{{ item.email }}</td>
                        <td width="10%">{{ item.source }}</td>
                        <td width="10%">{{ item.xtime }}</td>
                        <td width="20%">{{ item.password }}</td>
                        <td width="35%">{{ item.passwordHash    }}</td>
                    </tr>
                </tbody>
            </table>
    
            <div v-show="datacnts>10" class="pageselect">
                <select class="showpages" @change="changepages" v-model="selectedP">
                    <option v-for="opt in pageoptions" v-bind:value="opt.value" class="options">
                        {{ opt.text }}
                    </option>
                </select>
                <p>每页显示数据条数:
                    <input
                        type="int"
                        class="limitInput"
                        v-model="limit"
                        v-on:keyup.enter="changepages"
                    />
                </p>
            </div>
            <p v-model="datacnts">查询结果有 {{ datacnts }} 条数据</p>
        </div>
        <div v-else-if="indexId=== 'info'" class="container" v-show="retItems.length">
          <table>
            <thead>
            <tr>
              <th width="10%">身份证</th>
              <th width="15%">姓名</th>
              <th width="10%">性别</th>
              <th width="10%">地址</th>
              <th width="20%">QQ</th>
              <th width="35%">手机号</th>
            </tr>
            </thead>
            <tbody>
            <tr v-for="item in retItems">
              <td width="10%">{{ item.id }}</td>
              <td width="15%">{{ item.name }}</td>
              <td width="10%">{{ item.sex }}</td>
              <td width="10%">{{ item.address }}</td>
              <td width="20%">{{ item.qq }}</td>
              <td width="35%">{{ item.phonenumber }}</td>
            </tr>
            </tbody>
          </table>
    
          <div v-show="datacnts>10" class="pageselect">
            <select class="showpages" @change="changepages" v-model="selectedP">
              <option v-for="opt in pageoptions" v-bind:value="opt.value" class="options">
                {{ opt.text }}
              </option>
            </select>
            <p>每页显示数据条数:
              <input
                type="int"
                class="limitInput"
                v-model="limit"
                v-on:keyup.enter="changepages"
              />
            </p>
          </div>
          <p v-model="datacnts">查询结果有 {{ datacnts }} 条数据</p>
        </div>
        <div v-else>
          查询错误
        </div>
      </div>
    </template>
    
    <script>
    // 改为CDN引入
    // import axios from 'axios'
    export default {
      name: 'Search',
      data () {
        return {
          limit : 10,
          selectedP: 1,
          searchStr: '',
          pageStr: '',
          errorinfo: '',
          datacnts: 0,
          pageoptions: [],
          options: [
            { text: '用户名', value: 'user' },
            { text: '密码', value: 'password' },
            { text: '邮箱', value: 'email' },
            { text: '哈希密码', value: 'passwordHash' }
          ],
          retItems: [],
          analysisInfos: [],
    
          select01: [],//获取的一级数组数据
          select02: [],//获取的二级数组数据
          indexId:'账密',//定义分类一的默认值
          indexId2:'用户名',
          indexNum:0,//定义一级菜单的下标
    
    
        }
      },
      created() {
        axios.get('/get_selector')
          .then(response => {
            if(response.data.status === 'ok'){
              let mes = response.data;
              this.select01 = mes.data;
              console.log("省级")
              console.log(this.select01)
              this.indexSelect01();
            }
            else{
              this.errorinfo = '初始化级联数据错误';
            }
          })
          .catch(error => {
            console.log(error);
          });
      },
    
      methods:{
    
            search: function () {
                this.pageoptions = [];
                this.limit = 10;
                this.selectedP = 1;
                this.errorinfo = '';
                console.log(this.indexId)
                console.log(this.indexId2)
    
                // axios.get('/find/user/' + this.searchStr)
              // axios.get('/find/'+ this.indexId2 + '/' + this.searchStr)
              axios.get('/'+ this.indexId +  '/'+ this.indexId2 + '/' + this.searchStr)
                    .then(response => {
                        if(response.data.status === 'ok'){
                            this.retItems = response.data.data.concat();
                            this.pageStr = this.searchStr;
                            this.searchStr = '';
                            this.datacnts = response.data.datacounts;
                            var n = 0;
                            while ( n < Math.ceil(this.datacnts/this.limit)) {
                                n = n + 1;
                                this.pageoptions.push({
                                    text: '第 ' +  n + ' 页',
                                    value: n
                                });
                            }
                        }
                        else{
                            this.retItems = [];
                            this.searchStr = [];
                            this.datacnts = 0;
                            this.errorinfo = '输入错误';
                        }
                    })
                    .catch(error => {
                        console.log(error);
                    });
            },
            changepages: function() {
                axios.get('/'  + this.indexId + '/'+ this.indexId2 + '/' + this.pageStr + '?limit=' + this.limit + '&skip=' + this.limit * (this.selectedP-1))
                    .then(response => {
                        if(response.data.status === 'ok'){
                            this.pageoptions = [];
                            var n = 0;
                            while ( n <  Math.ceil(this.datacnts/this.limit)) {
                                n = n + 1;
                                this.pageoptions.push({
                                    text: '第 ' +  n + ' 页',
                                    value: n
                                });
                            }
                            this.retItems = response.data.data.concat();
                            this.searchStr = '';
                            this.datacnts = response.data.datacounts;
                        }
                        else{
                            this.retItems = [];
                            this.searchStr = [];
                            this.datacnts = 0;
                            this.errorinfo = '输入错误';
                        }
                    })
                    .catch(error => {
                        console.log(error);
                    });
            }
            ,
            indexSelect01(){
              let i = 0;
              for (i = 0;i<this.select01.length;i++) {
                if (this.select01[i].select == this.indexId){
                  // if (this.select01[i].id == this.indexId){
                  this.indexNum = i;
                  break
                }
              }
    
              this.select02 = this.select01[this.indexNum].obj;
              console.log("市级数据")
              console.log(this.select02)
            }
    
        }
    }
    </script>
    
    <style>
    
    h1, h2 {
      font-weight: normal;
    }
    
    h1 {
      color: #fff;
    }
    
    ul {
      list-style-type: none;
      padding: 0;
    }
    
    li {
      display: inline-block;
      margin: 0 10px;
    }
    
    table{
        margin-top: 2em;
        border:1px solid ;
        padding: 20px;
        border-collapse: collapse;
        font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
        font-size: 0.8em;
    }
    
    .container {
      text-align: center;
      overflow: hidden;
      width: 60em;
      margin: 0 auto;
    }
    
    .container table {
      width: 100%;
    }
    
    .container td {
        font-size: 10px;
    }
    .container td, .container th {
      font-size: 1.2em;
      overflow: auto;
      padding: 10px;
    }
    
    .container th {
      border-bottom: 1px solid #ddd;
      position: relative;
      width: 30px;
    }
    .searchInput {
        outline: none;
        height: 30px;
        width: 680px;
        border : 1px solid  #FFFFFF;
        padding : 15px 30px 15px 30px;
        font-size: 1em;
        font-family: BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
    }
    .searchInput:focus {
        box-shadow: 2px 2px 2px #336633;
    }
    .limitInput {
        outline: none;
        height: 15px;
        width: 20px;
        border : 1px solid  #FFFFFF;
        padding : 5px 5px 5px 5px;
        font-size: 1em;
        font-family: BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
    }
    .limitInput:focus {
        box-shadow: 2px 2px 2px #336633;
    }
    .Select {
        border : 1px solid  #FFFFFF;
        font-size: 1em;
        font-family: BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
        outline: none;
        border: none;
        padding: 0 0 0 10px;
    }
    .Select .options {
        outline: none;
        border: none;
    }
    .Select {
        height: 62px;
        width: 100px;
    }
    .showpages {
        border : 1px solid  #FFFFFF;
        font-size: 1em;
        font-family: BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
        outline: none;
        border: none;
        padding: 0 0 0 10px;
        position: relative;
        margin: 0 auto;
        margin-top: 1em;
    }
    .pageselect input{
    }
    </style>
    
    

    安装配置方法参考项目中README.md就好,原作者使用网易52G数据测试,我这边拿Q绑数据测试,对应的身份信息-手机号,身份信息collection名是info,具体字段名在前端页面里面,id 身份证 姓名 性别 地址 qq 手机号

              <td width="10%">{{ item.id }}</td>
              <td width="15%">{{ item.name }}</td>
              <td width="10%">{{ item.sex }}</td>
              <td width="10%">{{ item.address }}</td>
              <td width="20%">{{ item.qq }}</td>
              <td width="35%">{{ item.phonenumber }}</td>
    

    image-20210511202457846

    Github链接:https://github.com/wdsjxh/socialdb_vue_flask_new

    展开全文
  • python_web使用django框架个人博客管理系统,前端+后台...使用分页插件,jquery实现万年历,实现时间自动同步更新,调用QQAPI会话面板,前后端分离,后台对数据的增删查改,对用户增删查改,对用户权限赋予.....等等。
  • 前后端分离通过Restful API进行数据交互时,如何验证用户的登录信息及权限。在原来的项目中,使用的是最传统也是最简单的方式,前端登录,后端根据用户信息生成一个token,并保存这个token 和对应的用户id到数据库或...
  • 前后端分离开发时为...前后端分离通过Restful API进行数据交互时,如何验证用户的登录信息及权限。在原来的项目中,使用的是最传统也是最简单的方式,前端登录,后端根据用户信息生成一个token,并保存这个token 和

    在前后端分离开发时为什么需要用户认证呢?原因是由于HTTP协定是不储存状态的(stateless),这意味着当我们透过帐号密码验证一个使用者时,当下一个request请求时它就把刚刚的资料忘了。于是我们的程序就不知道谁是谁,就要再验证一次。所以为了保证系统安全,我们就需要验证用户否处于登录状态。

    一、传统方式

    前后端分离通过Restful API进行数据交互时,如何验证用户的登录信息及权限。在原来的项目中,使用的是最传统也是最简单的方式,前端登录,后端根据用户信息生成一个token,并保存这个token 和对应的用户id到数据库或Session中,接着把token 传给用户,存入浏览器 cookie,之后浏览器请求带上这个cookie,后端根据这个cookie值来查询用户,验证是否过期。

    但这样做问题就很多,如果我们的页面出现了 XSS 漏洞,由于 cookie 可以被 JavaScript 读取,XSS 漏洞会导致用户 token 泄露,而作为后端识别用户的标识,cookie 的泄露意味着用户信息不再安全。尽管我们通过转义输出内容,使用 CDN 等可以尽量避免 XSS 注入,但谁也不能保证在大型的项目中不会出现这个问题。

    在设置 cookie 的时候,其实你还可以设置 httpOnly 以及 secure项。设置 httpOnly后 cookie 将不能被 JS 读取,浏览器会自动的把它加在请求的 header 当中,设置 secure的话,cookie 就只允许通过 HTTPS 传输。secure 选项可以过滤掉一些使用 HTTP 协议的 XSS 注入,但并不能完全阻止。

    httpOnly 选项使得 JS 不能读取到 cookie,那么 XSS 注入的问题也基本不用担心了。但设置 httpOnly就带来了另一个问题,就是很容易的被 XSRF,即跨站请求伪造。当你浏览器开着这个页面的时候,另一个页面可以很容易的跨站请求这个页面的内容。因为 cookie 默认被发了出去。

    另外,如果将验证信息保存在数据库中,后端每次都需要根据token查出用户id,这就增加了数据库的查询和存储开销。若把验证信息保存在session中,有加大了服务器端的存储压力。那我们可不可以不要服务器去查询呢?如果我们生成token遵循一定的规律,比如我们使用对称加密算法来加密用户id形成token,那么服务端以后其实只要解密该token就可以知道用户的id是什么了。不过呢,我只是举个例子而已,要是真这么做,只要你的对称加密算法泄露了,其他人可以通过这种加密方式进行伪造token,那么所有用户信息都不再安全了。恩,那用非对称加密算法来做呢,其实现在有个规范就是这样做的,就是我们接下来要介绍的 JWT。

    二、Json Web Token(JWT)

    WT 是一个开放标准(RFC 7519),它定义了一种用于简洁,自包含的用于通信双方之间以 JSON 对象的形式安全传递信息的方法。JWT 可以使用 HMAC 算法或者是 RSA 的公钥密钥对进行签名。它具备两个特点:

    • 简洁(Compact)
      可以通过URL, POST 参数或者在 HTTP header 发送,因为数据量小,传输速度快
    • 自包含(Self-contained)
      负载中包含了所有用户所需要的信息,避免了多次查询数据库

    JWT 组成

    • Header 头部
      头部包含了两部分,token 类型和采用的加密算法
    {
      "alg": "HS256",
      "typ": "JWT"
    }
    

    它会使用 Base64 编码组成 JWT 结构的第一部分,如果你使用Node.js,可以用Node.js的包base64url来得到这个字符串。
    ------------ Base64是一种编码,也就是说,它是可以被翻译回原来的样子来的。它并不是一种加密过程。

    • Payload 负载

    负载就是存放有效信息的地方。这些有效信息包含三个部分:
    ----标准中注册声明
    ----公共的声明
    ----私有的声明

    公共的声明:
    公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密。

    私有的声明:
    私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息

    {
        "iss": "lion1ou JWT",
        "iat": 1441593502,
        "exp": 1441594722,
        "aud": "www.example.com",
        "sub": "lion1ou@163.com"
    }
    
    // 包括需要传递的用户信息;
    { 
      "iss": "Online JWT Builder", 
      "iat": 1416797419, 
      "exp": 1448333419, 
      "aud": "www.gusibi.com", 
      "sub": "uid", 
      "nickname": "goodspeed", 
      "username": "goodspeed", 
      "scopes": [ "admin", "user" ] 
    }
    
    • iss: 该JWT的签发者,是否使用是可选的;
    • sub: 该JWT所面向的用户,是否使用是可选的;
    • aud: 接收该JWT的一方,是否使用是可选的;
    • exp(expires): 什么时候过期,这里是一个Unix时间戳,是否使用是可选的;
    • iat(issued at): 在什么时候签发的(UNIX时间),是否使用是可选的;

    其他还有:

    • nbf (Not Before):如果当前时间在nbf里的时间之前,则Token不被接受;一般都会留一些余地,比如几分钟;,是否使用是可选的;

    • jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
      同样的,它会使用 Base64 编码组成 JWT 结构的第二部分。

    • Signature 签名
      前面两部分都是使用 Base64 进行编码的,即前端可以解开知道里面的信息。Signature 需要使用编码后的 header 和 payload 以及我们提供的一个密钥,然后使用 header 中指定的签名算法(HS256)进行签名。签名的作用是保证 JWT 没有被篡改过。

    // 根据alg算法与私有秘钥进行加密得到的签名字串;
    // 这一段是最重要的敏感信息,只能在服务端解密;
    HMACSHA256(  
        base64UrlEncode(header) + "." +
        base64UrlEncode(payload),
        SECREATE_KEY
    )
    

    三个部分通过.连接在一起就是我们的 JWT 了,它可能长这个样子,长度貌似和你的加密算法和私钥有关系。

    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjU3ZmVmMTY0ZTU0YWY2NGZmYzUzZGJkNSIsInhzcmYiOiI0ZWE1YzUwOGE2NTY2ZTc2MjQwNTQzZjhmZWIwNmZkNDU3Nzc3YmUzOTU0OWM0MDE2NDM2YWZkYTY1ZDIzMzBlIiwiaWF0IjoxNDc2NDI3OTMzfQ.PA3QjeyZSUh7H0GfE0vJaKW4LjKJuC3dVLQiY4hii8s
    

    其实到这一步可能就有人会想了,HTTP 请求总会带上 token,这样这个 token 传来传去占用不必要的带宽啊。如果你这么想了,那你可以去了解下 HTTP2,HTTP2 对头部进行了压缩,相信也解决了这个问题。

    • 签名的目的

    最后一步签名的过程,实际上是对头部以及负载内容进行签名,防止内容被窜改。如果有人对头部以及负载的内容解码之后进行修改,再进行编码,最后加上之前的签名组合形成新的JWT的话,那么服务器端会判断出新的头部和负载形成的签名和JWT附带上的签名是不一样的。如果要对新的头部和负载进行签名,在不知道服务器加密时用的密钥的话,得出来的签名也是不一样的。

    • 信息暴露

    在这里大家一定会问一个问题:Base64是一种编码,是可逆的,那么我的信息不就被暴露了吗?

    是的。所以,在JWT中,不应该在负载里面加入任何敏感的数据。在上面的例子中,我们传输的是用户的User ID。这个值实际上不是什么敏感内容,一般情况下被知道也是安全的。但是像密码这样的内容就不能被放在JWT中了。如果将用户的密码放在了JWT中,那么怀有恶意的第三方通过Base64解码就能很快地知道你的密码了。

    因此JWT适合用于向Web应用传递一些非敏感信息。JWT还经常用于设计用户认证和授权系统,甚至实现Web应用的单点登录。

    token 生成好之后,接下来就可以用token来和服务器进行通讯了。

    三、JWT 使用

    下图是client 使用 JWT 与server 交互过程:

    在这里插入图片描述

    1.这里在第三步我们得到 JWT 之后,需要将JWT存放在 client,之后的每次需要认证的请求都要把JWT发送过来。(请求时可以放到 header 的 Authorization )
    首先,前端通过Web表单将自己的用户名和密码发送到后端的接口。这一过程一般是一个HTTP POST请求。建议的方式是通过SSL加密的传输(https协议),从而避免敏感信息被嗅探。

    2.后端核对用户名和密码成功后,将用户的id等其他信息作为JWT Payload(负载),将其与头部分别进行Base64编码拼接后签名,形成一个JWT。形成的JWT就是一个形同lll.zzz.xxx的字符串。

    3.后端将JWT字符串作为登录成功的返回结果返回给前端。前端可以将返回的结果保存在localStorage或sessionStorage上,退出登录时前端删除保存的JWT即可。

    4.前端在每次请求时将JWT放入HTTP Header中的Authorization位。(解决XSS和XSRF问题)

    5.后端检查是否存在,如存在验证JWT的有效性。例如,检查签名是否正确;检查Token是否过期;检查Token的接收方是否是自己(可选)。

    6.验证通过后后端使用JWT中包含的用户信息进行其他逻辑操作,返回相应结果。

    四、JWT 使用场景

    WT的主要优势在于使用无状态、可扩展的方式处理应用中的用户会话。服务端可以通过内嵌的声明信息,很容易地获取用户的会话信息,而不需要去访问用户或会话的数据库。在一个分布式的面向服务的框架中,这一点非常有用。

    但是,如果系统中需要使用黑名单实现长期有效的token刷新机制,这种无状态的优势就不明显了。

    • 优点
      快速开发
      不需要cookie
      JSON在移动端的广泛应用
      不依赖于社交登录
      相对简单的概念理解

    • 缺点
      Token有长度限制
      Token不能撤销
      需要token有失效时间限制(exp)

    五、和Session方式存储id的差异

    Session方式存储用户id的最大弊病在于Session是存储在服务器端的,所以需要占用大量服务器内存,对于较大型应用而言可能还要保存许多的状态。一般而言,大型应用还需要借助一些KV数据库和一系列缓存机制来实现Session的存储。

    而JWT方式将用户状态分散到了客户端中,可以明显减轻服务端的内存压力。除了用户id之外,还可以存储其他的和用户相关的信息,例如该用户是否是管理员、用户所在的分组等。虽说JWT方式让服务器有一些计算压力(例如加密、编码和解码),但是这些压力相比磁盘存储而言可能就不算什么了。具体是否采用,需要在不同场景下用数据说话。

    • 单点登录

    Session方式来存储用户id,一开始用户的Session只会存储在一台服务器上。对于有多个子域名的站点,每个子域名至少会对应一台不同的服务器,例如:www.taobao.com,nv.taobao.com,nz.taobao.com,login.taobao.com。所以如果要实现在login.taobao.com登录后,在其他的子域名下依然可以取到Session,这要求我们在多台服务器上同步Session。使用JWT的方式则没有这个问题的存在,因为用户的状态已经被传送到了客户端。

    六、总结

    JWT的主要作用在于:
    (一)可附带用户信息,后端直接通过JWT获取相关信息。
    (二)使用本地保存,通过HTTP Header中的Authorization位提交验证。

    七、附加,python使用JWT

    python 中djagno rest framework要使用jwt,可以使用以下这个模块:githubs文档有使用说明
    https://github.com/GetBlimp/django-rest-framework-jwt

    pip install djangorestframework-jwt
    

    不是使用django的话,我们可以使用 pyjwt:
    https://github.com/jpadilla/pyjwt/
    使用比较方便,下边是我在应用中使用的例子:

    import jwt
    import time
    
    # 使用 sanic 作为restful api 框架 
    def create_token(request):
        grant_type = request.json.get('grant_type')
        username = request.json['username']
        password = request.json['password']
        if grant_type == 'password':
            account = verify_password(username, password)
        elif grant_type == 'wxapp':
            account = verify_wxapp(username, password)
        if not account:
            return {}
        payload = {
            "iss": "gusibi.com",
             "iat": int(time.time()),
             "exp": int(time.time()) + 86400 * 7,
             "aud": "www.gusibi.com",
             "sub": account['_id'],
             "username": account['username'],
             "scopes": ['open']
        }
        token = jwt.encode(payload, 'secret', algorithm='HS256')
        return True, {'access_token': token, 'account_id': account['_id']}
        
    
    def verify_bearer_token(token):
        #  如果在生成token的时候使用了aud参数,那么校验的时候也需要添加此参数
        payload = jwt.decode(token, 'secret', audience='www.gusibi.com', algorithms=['HS256'])
        if payload:
            return True, token
        return False, token
    
    展开全文
  • python3环境,前后端分离,前端使用bootstrap2框架,后端使用django2.0框架,只是个人日常记录,仅供参考 目的 RBAC权限访问限制,有权限访问,无权限禁止访问 实现思路 请求接口前从session中获取用户的访问权限,判断...

    django2.0记录 基于RBAC的权限访问限制中间件

    背景介绍

    python3环境,前后端不分离,前端使用bootstrap2框架,后端使用django2.0框架,只是个人日常记录,仅供参考

    目的

    RBAC权限访问限制,有权限访问,无权限禁止访问

    实现思路

    请求接口前从session中获取用户的访问权限,判断请求方式及请求地址是否在权限范围中,扫描权限白名单,判断是否可以请求

    后端实现代码

    from django.conf import settings
    from django.shortcuts import HttpResponse, redirect
    import re
    
    
    class MiddlewareMixin(object):
        def __init__(self, get_response=None):
            self.get_response = get_response
            super(MiddlewareMixin, self).__init__()
    
        def __call__(self, request):
            response = None
            if hasattr(self, 'process_request'):
                response = self.process_request(request)
            if not response:
                response = self.get_response(request)
            if hasattr(self, 'process_response'):
                response = self.process_response(request, response)
            return response
    
    
    class RbacMiddleware(MiddlewareMixin):
        """
        检查用户的url请求是否是其权限范围内
        """
    
        def process_request(self, request):
            request_url = request.path_info
            method = request.method
            permission_url = request.session.get(settings.SESSION_PERMISSION_REQUEST_KEY)
            print('访问url', method, request_url)
            print('权限--', permission_url)
            # 如果请求url在白名单,放行
            for url in settings.SAFE_URL:
                if re.match(url, request_url):
                    print('白名单通过')
                    return None
    
            # 如果未取到permission_url, 重定向至登录
            # Login必须设置白名单
            if not permission_url:
                return redirect(settings.LOGIN_URL)
    
            # 循环permission_url,作为正则,匹配用户request_url
            # 正则应该进行一些限定,以处理:/user/ -- /user/add/匹配成功的情况
            flag = False
            url_list = []
            for request in permission_url:
                url = request.get('request_url')
                url_list.append(url)
                request_method = request.get('request_method')
                url_pattern = settings.REGEX_URL.format(url=request_url)
                if re.match(url_pattern, url) and method == request_method:
                    flag = True
                    break
            if flag:
                print('可以访问')
                return None
            else:
                print('不可访问')
                # 如果是调试模式,显示可访问url
                if settings.DEBUG:
                    info = '<br/>' + ('<br/>'.join(url_list))
                    return HttpResponse('无权限,请尝试访问以下地址:%s' % info)
                else:
                    return HttpResponse('无权限访问')
    
    展开全文
  • 使用Python3.6与Django2.0.2(Django-rest-framework)以及前端vue开发的前后端分离的商城网站 项目支持支付宝支付(暂不支持微信支付),支持手机短信验证码注册, 支持第三方登录。集成了sentry错误监控系统。 ...

    Vue+Django REST framework实战

    使用Python3.6与Django2.0.2(Django-rest-framework)以及前端vue开发的前后端分离的商城网站

    项目支持支付宝支付(暂不支持微信支付),支持手机短信验证码注册, 支持第三方登录。集成了sentry错误监控系统。

    线上演示地址: http://vueshop.mtianyan.cn/
    github源代码地址: https://github.com/mtianyan/VueDjangoFrameWorkShop

    本小节内容: drf的权限验证

    drf的权限验证

    看起来已经完成了用户添加收藏,删除收藏的功能。但是正常的业务逻辑应该是用户只能删除自己的收藏。

    http://www.django-rest-framework.org/api-guide/permissions/

    auth 和 permission是两种东西。auth是用来做用户验证的,permission是用来做权限判断的。

    AllowAny:不管有没有权限都可以访问。

    IsAuthenticated: 判断是否已经登录

    IsAdminUser:判断用户是否是一个管理员。user.is_staff

    第一步判断用户是否登录了。

    实现代码:

    from rest_framework.permissions import IsAuthenticated
    permission_classes = (IsAuthenticated,)
    

    用户未登录访问userfav的list会给我们抛出401的错误。

    mark
    mark

    官方例子,ip是否在白名单中

    实现代码:

    from rest_framework import permissions
    
    class BlacklistPermission(permissions.BasePermission):
        """
        Global permission check for blacklisted IPs.
        """
    
        def has_permission(self, request, view):
            ip_addr = request.META['REMOTE_ADDR']
            blacklisted = Blacklist.objects.filter(ip_addr=ip_addr).exists()
            return not blacklisted
    

    拿着model做一下过滤实现

    • 官方例子:是否是所有者,否则仅仅可读

    实现代码:

    class IsOwnerOrReadOnly(permissions.BasePermission):
        """
        Object-level permission to only allow owners of an object to edit it.
        Assumes the model instance has an `owner` attribute.
        """
    
        def has_object_permission(self, request, view, obj):
            # Read permissions are allowed to any request,
            # so we'll always allow GET, HEAD or OPTIONS requests.
            if request.method in permissions.SAFE_METHODS:
                return True
    
            # Instance must have an attribute named `owner`.
            return obj.owner == request.user
    

    在utils中新建permissions,这是我们自定义的permissions,然后粘贴上面的IsOwnerOrReadOnly

    这个自定义的permission类继承了我们的BasePermission。它有一个方法叫做has_object_permission,是否有对象权限。

    会检测我们从数据库中拿出来的obj的owner是否等于request.user

    这个obj是我们数据库中的表,所以这里的owner应该改为我们数据库中的外键user

    安全的方法也就是不会对数据库进行变动的方法,总是可以让大家都有权限访问到。

    views中添加该自定义的权限认证类

    实现代码:

    from utils.permissions import IsOwnerOrReadOnly
    
    permission_classes = (IsAuthenticated, IsOwnerOrReadOnly)
    

    这样在做删除的时候就会验证权限。

    不能让所有的收藏关系数据都被获取到。因此我们要重载get_queryset方法

    实现代码:

        def get_queryset(self):
            return UserFav.objects.filter(user=self.request.user)
    

    重载之后queryset的参数配置就可以注释掉了

    在model设计中。我们的str方法返回值为name。这个name是有可能为null的。

    所以报出错误

    __str__ return non-string(type NoneType)
    

    我们在return 的时候。将name改为username

    此时我们前往数据库将收藏关系的userid 改为一个非当前登录用户,会发现确实只能查看自己的收藏。

    mark
    mark

    删除一条不属于它自己的收藏记录时会提示不存在。

    在使用其他工具时输入用户名密码也可以进行登录,是因为我们配置了多种auth类。

    token的认证最好是配置到view里面去,而不是配置到全局中。

    前端的每一个request请求都加入我们的token的话,token过期了,当用户访问goods列表页等不需要token认证的页面也会拿不到数据。

    将setting中的'rest_framework.authentication.SessionAuthentication',删除掉。

    然后在具体的view中来import以及进行配置

    user_operation/views.py中实现代码:

    from rest_framework_jwt.authentication import JSONWebTokenAuthentication
    
    authentication_classes = (JSONWebTokenAuthentication, )
    

    此时在我们的api控制台以及无法使用登录进入userfav了,是因为我们的类内auth并不包含session auth

    实现代码:

    from rest_framework.authentication import SessionAuthentication
    
    authentication_classes = (JSONWebTokenAuthentication, SessionAuthentication)
    

    用户收藏功能和vue联调

    vue和后端联调的一些问题:

    未登录状态,商品详情页商品的收藏状态应该是未收藏,而登录状态我们就需要通过设置好的接口,动态的显示。

    /userfavs/5/获取某一个收藏的详情,这里的id是数据表里的这条收藏关系的id

    实际上我们并不知道这件商品收藏到数据库里面的id是什么,既想使用现有接口,又不想传这个id。前端提出需求,可不可以不传这个id而是传当前商品的goods_id

    后端通过传过来的goods_id和当前的user找到是否被收藏过。

    当我们添加RetrieveModelMixin之后,我们可以通过

    http://127.0.0.1:8000/userfavs/5/ 获取到某一条关系的详情。

    了解retrieve的原理

    实现代码:

        def retrieve(self, request, *args, **kwargs):
            instance = self.get_object()
            serializer = self.get_serializer(instance)
            return Response(serializer.data)
    

    调用get_object函数,这个函数在rest_framework/generics.py中的GenericAPIView中

    实现代码:

        def get_object(self):
            """
            Returns the object the view is displaying.
    
            You may want to override this if you need to provide non-standard
            queryset lookups.  Eg if objects are referenced using multiple
            keyword arguments in the url conf.
            """
            queryset = self.filter_queryset(self.get_queryset())
    
            # Perform the lookup filtering.
            lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
    
            assert lookup_url_kwarg in self.kwargs, (
                'Expected view %s to be called with a URL keyword argument '
                'named "%s". Fix your URL conf, or set the `.lookup_field` '
                'attribute on the view correctly.' %
                (self.__class__.__name__, lookup_url_kwarg)
            )
    
            filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]}
            obj = get_object_or_404(queryset, **filter_kwargs)
    
            # May raise a permission denied
            self.check_object_permissions(self.request, obj)
    
            return obj
    

    会根据我们传入的id默认搜索主键也就是id这个字段。我们希望它不要搜索id这个字段,而是搜索goods这个字段。lookup_field接收哪些字段,这个实际上是可以配置的。

    http://www.django-rest-framework.org/api-guide/generic-views/#genericapiview
    中有这个参数。

        lookup_field = 'pk'
    

    在apiview中这个字段默认值是pk,我们可以将它改为goods_id。

    mark
    mark

    因为在数据库表中goods这个外键实际存储的字段名是goods_id

    created () {
            this.productId = this.$route.params.productId;
            var productId = this.productId
            if(cookie.getCookie('token')){
              getFav(productId).then((response)=> {
                this.hasFav = true
              }).catch(function (error) {
                console.log(error);
              });
            }
            this.getDetails();
        },
    

    在src/views/productDetail/productDetail.vue中create时会首先获取到产品的id
    会在cookie中找token,如果有token,那么将会通过产品id执行getFav

    //判断是否收藏
    export const getFav = goodsId => { return axios.get(`${host}/userfavs/`+goodsId+'/') }
    

    如果response的状态是200,那么我们就可以将hasfav赋值true

    如果hasfav为true的情况下。

      <a v-if="hasFav" id="fav-btn" class="graybtn" @click="deleteCollect">
                                            <i class="iconfont">&#xe613;</i>已收藏</a>
                                      <a v-else class="graybtn" @click="addCollect">
                                        <i class="iconfont">&#xe613;</i>收藏</a>
    

    如果是true,v-if显示收藏,收藏状态下点击会执行删除收藏。
    未收藏时点击就会执行收藏。

    deleteCollect () {
                //删除收藏
              delFav(this.productId).then((response)=> {
                console.log(response.data);
                this.hasFav = false
              }).catch(function (error) {
                console.log(error);
              });
            },
    

    删除收藏调用的是delfav接口。

    //取消收藏
    export const delFav = goodsId => { return axios.delete(`${host}/userfavs/`+goodsId+'/') }
    
    //收藏
    export const addFav = params => { return axios.post(`${host}/userfavs/`, params) }
    

    添加收藏调用的是addFav接口。

    将添加收藏,取消收藏和获取某一条收藏关系的host改为localhost。而getalllfav会在个人中心才会进行实现

    login的url需要改为localhost(我前面已经改过了),需要修改的原因是本地登录加密
    解密的密钥是setting中的SECRET_KEY线上加密线下解密会导致失败

    每次生成项目时这个密钥都是不一样的。

    lookup_fields为goods会不会把其他用户收藏的该商品的收藏关系查询出来。

    实际上lookupfield的操作时在get_queryset的结果中来做的。已经做过过滤了。

    个人中心会大量的用到auth 和 permission

    展开全文
  • │ 01 前后端分离.mp4 │ 02 序列化.mp4 │ 03 使用序列化.mp4 │ 04 使用序列化.mp4 │ 05 api_view.mp4 │ 06 root api.mp4 │ 07 apiview.mp4 │ 08 使用混合.mp4 │ 09 使用混合高级版.mp4 │ 10 viewset.mp4 │...
  • 适合中小型前后端分离的项目,尤其是 BaaS、Serverless、互联网创业项目和企业自用项目。 通过万能的 API,前端可以定制任何数据、任何结构。 大部分 HTTP 请求后端再也不用写接口了,更不用写文档了。 前端再也不用...
  • 适合中小型前后端分离的项目,尤其是 BaaS、Serverless、互联网创业项目和企业自用项目。 通过自动化API,前端可以定制任何数据、任何结构! 大部分HTTP请求后端再也不用写接口了,更不用写文档了! 前端再也不用和...
  • <ol><li>基于模板渲染的动态页面</li><li>基于 AJAX 的前后端分离</li><li>基于 Node.js 的前端工程化</li><li>基于 Node.js 的全栈开发</li></ol> <p><a name="315dd60e"></a></p> 基于模板渲染的动态页面 在早起的...
  • solution_programme.zip

    2020-05-07 21:20:13
    解决方案管理平台,python语言,Django框架,在线文档预览、权限管理平台,包含vue前端框架,前后端分离
  • 该项目为前后端分离项目的前端部分,后端项目mall地址:传送门。 项目介绍 mall-admin-web是一个电商后台管理系统的前端项目,基于Vue+Element实现。主要包括商品管理、订单管理、会员管理、促销管理、运营管理、...
  • 最近学到的前后端分离知识 最近我学到的ABTest知识 最近学到的「短链接」知识 手把手教你怎么使用云服务器 带你了解什么是Push消息推送 人在家中坐,班从天上来「小程序推送」 Java发送邮件时,必须要的一个配置! ...
  • 微人事是一个前后端分离的人力资源管理系统,项目采用 SpringBoot+Vue 开发,项目加入常见的企业级应用所涉及到的技术点,例如 Redis、RabbitMQ 等。 项目地址:https://github.com/lenve/vhr 项目部署视频教程...
  • 前后端分离的接口规范,我们是这样做的! 前后端分离式开发:高效协作10板斧 前后端都分离了,该搞个好用的API管理系统了! 微服务 RPC框架实践之:Apache Thrift RPC框架实践之:Google gRPC 微服务调用链追踪...
  • 前后端分离,提高服务器性能 支持多机器多进程判题,判题更高效 支持 C/C++/Java/Python2/Python3和Swift5.1语言 支持 Special Judge和选择题判题 丰富的API,开放的源代码 一键保存和导出代码模板 支持类似LeetCode...
  • 使用vue-element-admin的后台前端解决方案,基于Vue和element-ui快速搭建前后端分离的商城管理平台 通过uni-app使用Vue开发实现跨所有前端的应用,包含微信小程序、APP应用 使用Docker快速构建项目环境和一键...
  • 使用vue-element-admin的后台前端解决方案,基于Vue和element-ui快速搭建前后端分离的商城管理平台 通过uni-app使用Vue开发实现跨所有前端的应用,包含微信小程序、APP应用 使用Docker快速构建项目环境和一键...
  • Gin-vue-admin是一个基于vue和gin开发的全栈前后端分离的后台管理系统,集成jwt鉴权,动态路由,动态菜单,casbin鉴权,表单生成器,代码生成器等功能,提供多种示例文件,让您把更多时间专注在业务开发上。...
  • eladmin : 项目基于 Spring Boot 2.1.0 、 Jpa、 Spring Security、redis、Vue 的前后端分离的后台管理系统,项目采用分模块开发方式, 权限控制采用 RBAC,支持数据字典与数据权限管理,支持一键生成前后端代码,...
  • 本项目支持前后端分离前端部分(www 目录下的文件)可部署在第三方 Web 服务器上。 例如演示站点的前端部署于 GitHub Pages 服务,从而可使用个性域名(*.github.io),还能减少一定的流量开销。 Fork 本项目,...
  • Gin-vue-admin是一个基于vue和gin开发的全栈前后端分离的后台管理系统,集成jwt鉴权,动态路由,动态菜单,casbin鉴权,表单生成器,代码生成器等功能,提供多种示例文件,让您把更多时间专注在业务开发上。...

空空如也

空空如也

1 2
收藏数 23
精华内容 9
关键字:

python前后端分离前端权限

python 订阅