いわゆるソーシャル ワーク ライブラリは、ログイン、登録、権限などのロジックを持たない最も単純な Web プロジェクトである可能性があります。もちろん、これは最も単純なプロジェクトに過ぎません。今日はこれを例として、フロントエンドとバックエンドの分離プロジェクトの構造について説明します。
以下はGithub にあるオープンソースのソーシャル エンジニアリング ライブラリ プロジェクトですhttps://github.com/Leezj9671/socialdb_vue_flask、socialdb_vue_flask ですが、これも単なる技術的な説明であり、データは提供されていません。また、小さな修正が加えられています:カスケード検索が追加され、複数のプロセスがデータベースに同時に書き込まれています。
環境: フロントエンド Nodejs Vue
バックエンドPythonフラスコ
データベースMongoDB
Flask + Bootstrap などの従来の小規模な Web プロジェクトは、フロントエンドとバックエンドがインターフェースに同意した後に独立して開発およびデバッグし、その後デプロイしてプロジェクトに統合できる点が異なります。
オリジナル著者のフロントエンドページ
バックエンドインターフェース
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メソッドに取得リクエストのビジネスロジックを記述します。
フロントエンドも非常に合理化されており、荒削りではありますが、search.vue (検索) と Analysis.vue (分析) の 2 つのコンポーネントのみを備えています。
このうち、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 を参照します。元の作成者は Netease 52G データを使用してテストし、私は Q を使用してデータをバインドしてテストしました。対応する ID 情報 - 携帯電話番号、ID 情報コレクション名は info、特定のフィールド名はフロントエンド ページにあり、ID 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>
Github リンク: https://github.com/wdsjxh/socialdb_vue_flask_new