增加注册登录 geetest 验证,增加免费次数下载,增加速率限制功能

pull/12/head
youngS 4 years ago
parent c2ecb1aa3e
commit 4b2a3c7508
  1. 357
      fir_client/src/assets/gt.js
  2. 2
      fir_client/src/components/FirAppInfostimeline.vue
  3. 56
      fir_client/src/components/FirApps.vue
  4. 12
      fir_client/src/components/FirDownload.vue
  5. 84
      fir_client/src/components/FirLogin.vue
  6. 111
      fir_client/src/components/FirRegist.vue
  7. 19
      fir_client/src/components/ShortDownload.vue
  8. 3
      fir_client/src/restful/download.js
  9. 5
      fir_client/src/restful/index.js
  10. 40
      fir_client/src/utils/index.js
  11. 18
      fir_ser/api/migrations/0024_auto_20210324_1751.py
  12. 2
      fir_ser/api/models.py
  13. 12
      fir_ser/api/utils/crontab/run.py
  14. 149
      fir_ser/api/utils/geetest/geetest_lib.py
  15. 15
      fir_ser/api/utils/geetest/geetest_lib_result.py
  16. 82
      fir_ser/api/utils/geetest/geetest_utils.py
  17. 8
      fir_ser/api/utils/serializer.py
  18. 67
      fir_ser/api/utils/storage/caches.py
  19. 70
      fir_ser/api/utils/throttle.py
  20. 27
      fir_ser/api/views/download.py
  21. 49
      fir_ser/api/views/login.py
  22. 33
      fir_ser/fir_ser/settings.py

File diff suppressed because one or more lines are too long

@ -136,6 +136,8 @@
getdownloadurl(res => {
if (res.code === 1000) {
window.location.href = res.data.download_url;
} else {
this.$message.error(res.msg);
}
}, {
'data': {

@ -201,9 +201,9 @@
</el-col>
</el-row>
</el-col>
<el-col :span="8" class="surplus-card">
<el-col :span="5" class="surplus-card">
<el-row>
<el-col :span="8">
<el-col :span="10">
<div>
<span class="name">iOS应用</span>
<el-divider direction="vertical"></el-divider>
@ -214,7 +214,7 @@
</div>
</el-col>
<el-col :span="8">
<el-col :span="12">
<div>
<span class="name">Android应用</span>
<el-divider direction="vertical"></el-divider>
@ -224,57 +224,75 @@
<el-divider direction="vertical"></el-divider>
</div>
</el-col>
</el-row>
</el-col>
<el-col :span="8">
<el-col :span="5" class="surplus-card">
<el-row>
<el-col :span="12">
<div>
<span class="name">总应用</span>
<span class="name">今日苹果下载次数</span>
<el-divider direction="vertical"></el-divider>
</div>
<div>
<span class="value">{{ hdata.ios_count + hdata.android_count}}</span>
<span class="value">{{ hdata.ios_today_hits_count }}</span>
<el-divider direction="vertical"></el-divider>
</div>
</el-col>
</el-row>
</el-col>
<el-col :span="8" class="surplus-card">
<el-row>
<el-col :span="8">
<el-col :span="12">
<div>
<span class="name">今日苹果下载次数</span>
<span class="name">今日安卓下载次数</span>
<el-divider direction="vertical"></el-divider>
</div>
<div>
<span class="value">{{ hdata.ios_today_hits_count }}</span>
<span class="value">{{ hdata.android_today_hits_count }}</span>
<el-divider direction="vertical"></el-divider>
</div>
</el-col>
<el-col :span="8">
<!-- <el-col :span="8">-->
<!-- <div>-->
<!-- <span class="name">总共下载次数</span>-->
<!-- <el-divider direction="vertical"></el-divider>-->
<!-- </div>-->
<!-- <div>-->
<!-- <span class="value">{{ hdata.all_hits_count}}</span>-->
<!-- <el-divider direction="vertical"></el-divider>-->
<!-- </div>-->
<!-- </el-col>-->
</el-row>
</el-col>
<el-col :span="6" class="surplus-card">
<el-row>
<el-col :span="12">
<div>
<span class="name">今日安卓下载次数</span>
<span class="name">今日剩余免费下载次数</span>
<el-divider direction="vertical"></el-divider>
</div>
<div>
<span class="value">{{ hdata.android_today_hits_count }}</span>
<span class="value">{{$store.state.userinfo.free_download_times }}</span>
<el-divider direction="vertical"></el-divider>
</div>
</el-col>
<el-col :span="8">
<el-col :span="12">
<div>
<span class="name">总共下载次数</span>
<span class="name">剩余付费下载次数</span>
<el-divider direction="vertical"></el-divider>
</div>
<div>
<span class="value">{{ hdata.all_hits_count}}</span>
<span class="value">{{$store.state.userinfo.download_times }}</span>
<el-divider direction="vertical"></el-divider>
</div>
</el-col>
</el-row>
</el-col>
</el-row>
</el-header>

@ -62,7 +62,8 @@
</p>
</div>
<div id="actions" class="actions" v-if="agent !==''">
<div id="actions" class="actions" v-if="agent !==''"
style="margin-top: 20%;margin-bottom: 20%">
<el-button type="info" round v-if="agent === 'wxandroid' || agent === 'wxapple'">
不支持在微信内下载
@ -383,6 +384,15 @@
}
window.location.href = this.downloadurl;
} else if (res.code === 1009) {
this.isdownload = false;
document.title = res.msg;
this.wrong = true;
this.msg = res.msg;
this.$message({
message: res.msg,
type: 'error',
});
} else {
this.isdownload = false;
this.$message({

@ -57,9 +57,12 @@
</el-row>
</el-form-item>
<el-form-item>
<div id="captcha" ref="captcha"></div>
</el-form-item>
<el-form-item>
<el-button type="danger" @click="onSubmit">登录</el-button>
<el-button type="danger" :disabled="login_disable" @click="onSubmit">登录</el-button>
</el-form-item>
<el-form-item v-if="register_enable">
@ -77,7 +80,7 @@
<script>
import {loginFun, set_auth_token} from "../restful";
import {checkEmail, checkphone} from "../utils";
import {checkEmail, checkphone, geetest} from "../utils";
export default {
name: "FirLogin",
@ -94,7 +97,7 @@
rutitle: '',
rctitle: '',
register_enable: false,
login_disable: false,
}
},
methods: {
@ -150,38 +153,21 @@
if (password.length > 6) {
loginFun(data => {
if (data.code === 1000) {
this.$message({
message: '登录成功',
type: 'success'
});
this.$cookies.remove("auth_token");
this.$cookies.set("token", data['token'], 3600 * 24 * 30);
this.$cookies.set("username", data.userinfo.username, 3600 * 24 * 30);
this.$cookies.set("first_name", data.userinfo.first_name, 3600 * 24 * 30);
this.$store.dispatch("doUserinfo", data.userinfo);
set_auth_token();
this.$router.push({name: 'FirApps'})
} else {
this.$message({
message: data.msg,
type: 'error'
});
this.get_auth_code();
}
}, {
"methods": "POST",
"data": {
"username": email,
"password": password,
"authcode": authcode,
"cptch_key": this.cptch.cptch_key,
"login_type": login_type,
}
});
let params = {
"username": email,
"password": password,
"authcode": authcode,
"cptch_key": this.cptch.cptch_key,
"login_type": login_type,
};
this.login_disable = true;
if (this.cptch.geetest) {
geetest(this, params, (n_params) => {
this.do_login(n_params);
})
} else {
this.do_login(params)
}
} else {
this.$message({
message: '密码长度过短',
@ -195,6 +181,33 @@
});
}
},
do_login(params) {
loginFun(data => {
if (data.code === 1000) {
this.$message({
message: '登录成功',
type: 'success'
});
this.$cookies.remove("auth_token");
this.$cookies.set("token", data['token'], 3600 * 24 * 30);
this.$cookies.set("username", data.userinfo.username, 3600 * 24 * 30);
this.$cookies.set("first_name", data.userinfo.first_name, 3600 * 24 * 30);
this.$store.dispatch("doUserinfo", data.userinfo);
set_auth_token();
this.$router.push({name: 'FirApps'})
} else {
this.$message({
message: data.msg,
type: 'error'
});
this.get_auth_code();
}
this.login_disable = false;
}, {
"methods": "POST",
"data": params
});
},
onRegister() {
this.$router.push({name: 'FirRegist'})
},
@ -299,5 +312,4 @@
.el-header div:after {
margin: 0 -50% 0 0
}
</style>

@ -67,7 +67,11 @@
</el-form-item>
<el-form-item>
<el-button type="danger" @click="onRegist">注册</el-button>
<div id="captcha" ref="captcha"></div>
</el-form-item>
<el-form-item>
<el-button type="danger" @click="onRegist" :disabled="register_disable">注册</el-button>
</el-form-item>
<el-form-item>
@ -84,7 +88,7 @@
<script>
import {getAuthTokenFun, registerFun} from "../restful";
import {checkEmail, checkphone} from "../utils";
import {checkEmail, checkphone, geetest} from "../utils";
export default {
name: "FirRegist",
@ -106,6 +110,7 @@
allow_r: false,
allow_ways: {},
rutitle: '',
register_disable: false,
}
},
methods: {
@ -147,6 +152,23 @@
"data": {}
});
},
do_get_auth_token(params) {
getAuthTokenFun(data => {
if (data.code === 1000) {
this.$notify({
title: '验证码',
message: '您正在进行注册,验证码已经发送您',
type: 'success'
});
this.form.auth_token = data.data.auth_token;
} else {
this.$message({
message: data.msg,
type: 'error'
});
}
}, {"methods": 'POST', 'data': params})
},
getphonecode() {
let act = 'up';
if (!this.docheck()) {
@ -169,22 +191,14 @@
"cptch_key": this.cptch.cptch_key,
"icode": this.form.icode,
};
getAuthTokenFun(data => {
if (data.code === 1000) {
this.$notify({
title: '验证码',
message: '您正在进行注册,验证码已经发送您',
type: 'success'
});
this.form.auth_token = data.data.auth_token;
} else {
this.$message({
message: data.msg,
type: 'error'
});
}
}, {"methods": 'POST', 'data': {'act': act, 'target': this.form.email, 'ext': picode}})
let params = {'act': act, 'target': this.form.email, 'ext': picode};
if (this.cptch.geetest) {
geetest(this, params, (n_params) => {
this.do_get_auth_token(n_params);
})
} else {
this.do_get_auth_token(params);
}
} else {
this.$message({
message: '图片验证码有误',
@ -234,6 +248,27 @@
}
return 1
},
do_register(params) {
registerFun(data => {
if (data.code === 1000) {
this.$message({
message: '注册成功',
type: 'success'
});
this.$router.push({name: 'FirLogin'})
} else {
this.$message({
message: data.msg,
type: 'error'
});
this.get_auth_code();
}
}, {
"methods": "POST",
"data": params
});
this.register_disable = false;
},
onRegist() {
let email = this.form.email;
let password = this.form.password;
@ -248,30 +283,22 @@
}
if (cptch_flag) {
if (password === password2 && password.length >= 6) {
registerFun(data => {
if (data.code === 1000) {
this.$message({
message: '注册成功',
type: 'success'
});
this.$router.push({name: 'FirLogin'})
} else {
this.$message({
message: data.msg,
type: 'error'
});
this.get_auth_code();
}
}, {
"methods": "POST",
"data": {
"username": email,
"password": password,
"password2": password2,
"auth_token": this.form.auth_token,
"auth_key": this.form.seicode
}
});
let params = {
"username": email,
"password": password,
"password2": password2,
"auth_token": this.form.auth_token,
"auth_key": this.form.seicode
};
this.register_disable = true;
if (this.cptch.geetest) {
geetest(this, params, (n_params) => {
this.do_register(n_params);
})
} else {
this.do_register(params);
}
} else {
this.$message({
message: '密码不一致或者密码长度小于6',

@ -83,7 +83,8 @@
</p>
</div>
<div id="actions" class="actions" v-if="agent !==''">
<div id="actions" class="actions" v-if="agent !==''"
style="margin-top: 20%;margin-bottom: 20%">
<button type="info" round v-if="agent === 'wxandroid' || agent === 'wxapple'">
不支持在微信内下载
@ -233,8 +234,8 @@
<div v-if='iserror' class="main">
<div class="error-container">
<h1>404 - Not Found</h1>
<header>您访问的 应用 不存在
<h1>{{ error_msg.head }}</h1>
<header>{{ error_msg.body }}
</header>
</div>
</div>
@ -287,6 +288,7 @@
downloadurl: "",
isdownload: false,
iserror: false,
error_msg: {head: '404 - Not Found', body: '您访问的 应用 不存在'},
udid: "",
wxeasytypeflag: false,
timer: '',
@ -376,6 +378,11 @@
}
window.location.href = this.downloadurl;
} else if (res.code === 1009) {
this.isdownload = false;
document.title = res.msg;
this.wrong = true;
this.msg = res.msg;
} else {
this.isdownload = false;
alert("密码错误,或者下载链接失效");
@ -534,6 +541,12 @@
}
} else {
this.iserror = true;
if (data.code === 1009) {
this.error_msg.head = '温馨提示';
}
if (data.code === 1004) {
this.error_msg.head = '403 - Forbidden';
}
if (data.msg) {
document.title = data.msg;
}

@ -57,6 +57,9 @@ function getData(url, params = {}, callBack) {
case 408:
error.message = '请求超时(408)';
break;
case 429:
error.message = '您的 IP 访问频繁,请稍后再次尝试';
break;
case 500:
error.message = '服务器错误(500)';
break;

@ -63,6 +63,9 @@ function ErrorMsg(error) {
case 408:
error.message = '请求超时(408)';
break;
case 429:
error.message = '您的 IP 访问频繁,请稍后再次尝试';
break;
case 500:
error.message = '服务器错误(500)';
break;
@ -82,7 +85,7 @@ function ErrorMsg(error) {
error.message = 'HTTP版本不受支持(505)';
break;
default:
error.message = `连接出错(${error.response.status})!`;
error.message = `连接出错(${error.response.status}, ${JSON.stringify(error.response.data)})!`;
}
} else {
// eslint-disable-next-line no-console

@ -171,7 +171,7 @@ export function uploadaliyunoss(file, certinfo, app, successcallback, processcal
}
import {uploadstorage} from '../restful'
import {loginFun, uploadstorage} from '../restful'
export function uploadlocalstorage(file, certinfo, app, successcallback, processcallback) {
uploadstorage(certinfo, file, successcallback, processcallback)
@ -289,3 +289,41 @@ export function ImgToBase64(url, callback) {
return callback ? callback(dataURL) : null;
};
}
export function geetest(self, params, callback) {
loginFun(res => {
if (res.code === 1000) {
let data = res.data;
// eslint-disable-next-line no-undef
initGeetest({
gt: data.gt,
challenge: data.challenge,
new_captcha: data.new_captcha, // 用于宕机时表示是新验证码的宕机
offline: !data.success, // 表示用户后台检测极验服务器是否宕机,一般不需要关注
product: "float", // 产品形式,包括:float,popup
width: "100%"
}, (captchaObj) => {
self.$refs.captcha.innerHTML = '';
captchaObj.appendTo("#captcha");
captchaObj.onReady(() => {
//your code
}).onSuccess(() => {
params.geetest = captchaObj.getValidate();
callback(params);
}).onError(() => {
captchaObj.destroy();
});
});
} else {
self.$message({
message: res.msg,
type: 'error'
});
}
}, {
"methods": "PUT",
"data": {user_id: self.form.email}
});
}

@ -0,0 +1,18 @@
# Generated by Django 3.0.3 on 2021-03-24 17:51
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('api', '0023_auto_20210316_1602'),
]
operations = [
migrations.AlterField(
model_name='userinfo',
name='download_times',
field=models.PositiveIntegerField(default=0, verbose_name='可用下载次数,需要用户充值'),
),
]

@ -39,7 +39,7 @@ class UserInfo(AbstractUser):
memo = models.TextField('备注', blank=True, null=True, default=None, )
date_joined = models.DateTimeField(auto_now_add=True, verbose_name="注册时间")
download_times = models.IntegerField(default=100, verbose_name="可用下载次数")
download_times = models.PositiveIntegerField(default=0, verbose_name="可用下载次数,需要用户充值")
all_download_times = models.BigIntegerField(default=0, verbose_name="总共下载次数")
domain_name = models.CharField(verbose_name="下载页面域名", blank=True, null=True, max_length=64)
history_release_limit = models.IntegerField(default=10, verbose_name="app 历史记录版本", blank=True, null=True)

@ -6,11 +6,12 @@
from apscheduler.schedulers.background import BackgroundScheduler
from django_apscheduler.jobstores import DjangoJobStore, register_events, register_job
from fir_ser.settings import SYNC_CACHE_TO_DATABASE
from fir_ser.settings import SYNC_CACHE_TO_DATABASE, GEETEST_CYCLE_TIME
from api.utils.crontab.ctasks import sync_download_times, auto_clean_upload_tmp_file, auto_delete_job_log, \
auto_delete_tmp_file, auto_check_ios_developer_active
import logging
from api.utils.storage.storage import get_local_storage
from api.utils.geetest.geetest_utils import check_bypass_status
logger = logging.getLogger(__file__)
@ -42,6 +43,11 @@ try:
sync_download_times()
@register_job(scheduler, "interval", seconds=GEETEST_CYCLE_TIME)
def check_bypass_status_job():
check_bypass_status()
@register_job(scheduler, "interval", seconds=SYNC_CACHE_TO_DATABASE.get("auto_clean_tmp_file_times"))
def auto_clean_upload_tmp_file_job():
auto_clean_upload_tmp_file()
@ -62,7 +68,9 @@ try:
register_events(scheduler)
scheduler.start()
get_local_storage(True)
# 启动服务的时候,同时执行下面操作
get_local_storage(clean_cache=True)
check_bypass_status()
except Exception as e:
logger.error("scheduler failed,so shutdown it Exception:%s" % (e))

@ -0,0 +1,149 @@
import random
import json
import requests
import hmac
import hashlib
from .geetest_lib_result import GeetestLibResult
# sdk lib包,核心逻辑。
class GeetestLib:
IS_DEBUG = False # 调试开关,是否输出调试日志
API_URL = "http://api.geetest.com"
REGISTER_URL = "/register.php"
VALIDATE_URL = "/validate.php"
JSON_FORMAT = "1"
NEW_CAPTCHA = True
HTTP_TIMEOUT_DEFAULT = 5 # 单位:秒
VERSION = "python-flask:3.1.1"
GEETEST_CHALLENGE = "geetest_challenge" # 极验二次验证表单传参字段 chllenge
GEETEST_VALIDATE = "geetest_validate" # 极验二次验证表单传参字段 validate
GEETEST_SECCODE = "geetest_seccode" # 极验二次验证表单传参字段 seccode
def __init__(self, geetest_id, geetest_key):
self.geetest_id = geetest_id
self.geetest_key = geetest_key
self.libResult = GeetestLibResult()
def gtlog(self, message):
if self.IS_DEBUG:
print("gtlog: " + message)
# 验证初始化
def register(self, digestmod, param_dict):
self.gtlog("register(): 开始验证初始化, digestmod={0}.".format(digestmod))
origin_challenge = self.request_register(param_dict)
self.build_register_result(origin_challenge, digestmod)
self.gtlog("register(): 验证初始化, lib包返回信息={0}.".format(self.libResult))
return self.libResult
def request_register(self, param_dict):
param_dict.update({"gt": self.geetest_id, "sdk": self.VERSION, "json_format": self.JSON_FORMAT})
register_url = self.API_URL + self.REGISTER_URL
self.gtlog("requestRegister(): 验证初始化, 向极验发送请求, url={0}, params={1}.".format(register_url, param_dict))
try:
res = requests.get(register_url, params=param_dict, timeout=self.HTTP_TIMEOUT_DEFAULT)
res_body = res.text if res.status_code == requests.codes.ok else ""
self.gtlog("requestRegister(): 验证初始化, 与极验网络交互正常, 返回码={0}, 返回body={1}.".format(res.status_code, res_body))
res_dict = json.loads(res_body)
origin_challenge = res_dict["challenge"]
except Exception as e:
self.gtlog("requestRegister(): 验证初始化, 请求异常,后续流程走宕机模式, " + repr(e))
origin_challenge = ""
return origin_challenge
def local_init(self):
self.build_register_result("", "")
self.gtlog("local_init(): bypass当前状态为fail,后续流程将进入宕机模式, " + self.libResult.data)
return self.libResult
# 构建验证初始化返回数据
def build_register_result(self, origin_challenge, digestmod):
# origin_challenge为空或者值为0代表失败
if not origin_challenge or origin_challenge == "0":
# 本地随机生成32位字符串
challenge = "".join(random.sample('abcdefghijklmnopqrstuvwxyz0123456789', 32))
data = json.dumps(
{"success": 0, "gt": self.geetest_id, "challenge": challenge, "new_captcha": self.NEW_CAPTCHA})
self.libResult.set_all(0, data, "bypass当前状态为fail,后续流程走宕机模式")
else:
if digestmod == "md5":
challenge = self.md5_encode(origin_challenge + self.geetest_key)
elif digestmod == "sha256":
challenge = self.sha256_endode(origin_challenge + self.geetest_key)
elif digestmod == "hmac-sha256":
challenge = self.hmac_sha256_endode(origin_challenge, self.geetest_key)
else:
challenge = self.md5_encode(origin_challenge + self.geetest_key)
data = json.dumps(
{"success": 1, "gt": self.geetest_id, "challenge": challenge, "new_captcha": self.NEW_CAPTCHA})
self.libResult.set_all(1, data, "")
# 正常流程下(即验证初始化成功),二次验证
def successValidate(self, challenge, validate, seccode, param_dict={}):
self.gtlog(
"successValidate(): 开始二次验证 正常模式, challenge={0}, validate={1}, seccode={2}.".format(challenge, validate,
seccode))
if not self.check_param(challenge, validate, seccode):
self.libResult.set_all(0, "", "正常模式,本地校验,参数challenge、validate、seccode不可为空")
else:
response_seccode = self.requestValidate(challenge, validate, seccode, param_dict)
if not response_seccode:
self.libResult.set_all(0, "", "请求极验validate接口失败")
elif response_seccode == "false":
self.libResult.set_all(0, "", "极验二次验证不通过")
else:
self.libResult.set_all(1, "", "")
self.gtlog("successValidate(): 二次验证 正常模式, lib包返回信息={0}.".format(self.libResult))
return self.libResult
# 异常流程下(即验证初始化失败,宕机模式),二次验证
# 注意:由于是宕机模式,初衷是保证验证业务不会中断正常业务,所以此处只作简单的参数校验,可自行设计逻辑。
def failValidate(self, challenge, validate, seccode):
self.gtlog(
"failValidate(): 开始二次验证 宕机模式, challenge={0}, validate={1}, seccode={2}.".format(challenge, validate,
seccode))
if not self.check_param(challenge, validate, seccode):
self.libResult.set_all(0, "", "宕机模式,本地校验,参数challenge、validate、seccode不可为空.")
else:
self.libResult.set_all(1, "", "")
self.gtlog("failValidate(): 二次验证 宕机模式, lib包返回信息={0}.".format(self.libResult))
return self.libResult
# 向极验发送二次验证的请求,POST方式
def requestValidate(self, challenge, validate, seccode, param_dict):
param_dict.update(
{"seccode": seccode, "json_format": self.JSON_FORMAT, "challenge": challenge, "sdk": self.VERSION,
"captchaid": self.geetest_id})
validate_url = self.API_URL + self.VALIDATE_URL
self.gtlog("requestValidate(): 二次验证 正常模式, 向极验发送请求, url={0}, params={1}.".format(validate_url, param_dict))
try:
res = requests.post(validate_url, data=param_dict, timeout=self.HTTP_TIMEOUT_DEFAULT)
res_body = res.text if res.status_code == requests.codes.ok else ""
self.gtlog(
"requestValidate(): 二次验证 正常模式, 与极验网络交互正常, 返回码={0}, 返回body={1}.".format(res.status_code, res_body))
res_dict = json.loads(res_body)
seccode = res_dict["seccode"]
except Exception as e:
self.gtlog("requestValidate(): 二次验证 正常模式, 请求异常, " + repr(e))
seccode = ""
return seccode
# 校验二次验证的三个参数,校验通过返回true,校验失败返回false
def check_param(self, challenge, validate, seccode):
return not (
challenge is None or challenge.isspace() or validate is None or validate.isspace() or seccode is None or seccode.isspace())
def md5_encode(self, value):
md5 = hashlib.md5()
md5.update(value.encode("utf-8"))
return md5.hexdigest()
def sha256_endode(self, value):
sha256 = hashlib.sha256()
sha256.update(value.encode("utf-8"))
return sha256.hexdigest()
def hmac_sha256_endode(self, value, key):
return hmac.new(key.encode("utf-8"), value.encode("utf-8"), digestmod=hashlib.sha256).hexdigest()

@ -0,0 +1,15 @@
# sdk lib包的返回结果信息。
class GeetestLibResult:
def __init__(self):
self.status = 0 # 成功失败的标识码,1表示成功,0表示失败
self.data = '' # 返回数据,json格式
self.msg = '' # 备注信息,如异常信息等
def set_all(self, status, data, msg):
self.status = status
self.data = data
self.msg = msg
def __str__(self):
return "GeetestLibResult{{status={0}, data={1}, msg={2}}}".format(self.status, self.data, self.msg)

@ -0,0 +1,82 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# project: 3月
# author: NinEveN
# date: 2021/3/25
import requests
import json
import logging
from django.core.cache import cache as redis_connect
from fir_ser.settings import GEETEST_ID, GEETEST_KEY, GEETEST_BYPASS_URL, \
GEETEST_BYPASS_STATUS_KEY
from api.utils.geetest.geetest_lib import GeetestLib
logger = logging.getLogger(__file__)
# 发送bypass请求,获取bypass状态并进行缓存(如何缓存可根据自身情况合理选择,这里是使用redis进行缓存)
def check_bypass_status():
response = ""
params = {"gt": GEETEST_ID}
try:
response = requests.get(url=GEETEST_BYPASS_URL, params=params)
except Exception as e:
logger.error("check_bypass_status failed Exception:%s" % e)
if response and response.status_code == 200:
logger.debug("check_bypass_status success %s" % response.content)
bypass_status_str = response.content.decode("utf-8")
bypass_status = json.loads(bypass_status_str).get("status")
redis_connect.set(GEETEST_BYPASS_STATUS_KEY, bypass_status)
else:
bypass_status = "fail"
redis_connect.set(GEETEST_BYPASS_STATUS_KEY, bypass_status)
logger.debug("bypass状态已经获取并存入redis,当前状态为 {}".format(bypass_status))
# 从缓存中取出当前缓存的bypass状态(success/fail)
def get_bypass_cache():
bypass_status_cache = redis_connect.get(GEETEST_BYPASS_STATUS_KEY)
if not bypass_status_cache:
bypass_status_cache = 'fail'
return bypass_status_cache
# 验证初始化接口,GET请求
def first_register(user_id, ip_address):
# 必传参数
# digestmod 此版本sdk可支持md5、sha256、hmac-sha256,md5之外的算法需特殊配置的账号,联系极验客服
# 自定义参数,可选择添加
# user_id 客户端用户的唯一标识,确定用户的唯一性;作用于提供进阶数据分析服务,可在register和validate接口传入,不传入也不影响验证服务的使用;若担心用户信息风险,可作预处理(如哈希处理)再提供到极验
# client_type 客户端类型,web:电脑上的浏览器;h5:手机上的浏览器,包括移动应用内完全内置的web_view;native:通过原生sdk植入app应用的方式;unknown:未知
# ip_address 客户端请求sdk服务器的ip地址
bypass_status = get_bypass_cache()
gt_lib = GeetestLib(GEETEST_ID, GEETEST_KEY)
digestmod = "md5"
param_dict = {"digestmod": digestmod, "user_id": user_id, "client_type": "web", "ip_address": ip_address}
if bypass_status == "success":
result = gt_lib.register(digestmod, param_dict)
else:
result = gt_lib.local_init()
# 注意,不要更改返回的结构和值类型
return json.loads(result.data)
# 二次验证接口,POST请求
def second_validate(rdata):
challenge = rdata.get(GeetestLib.GEETEST_CHALLENGE, None)
validate = rdata.get(GeetestLib.GEETEST_VALIDATE, None)
seccode = rdata.get(GeetestLib.GEETEST_SECCODE, None)
bypass_status = get_bypass_cache()
gt_lib = GeetestLib(GEETEST_ID, GEETEST_KEY)
if bypass_status == "success":
result = gt_lib.successValidate(challenge, validate, seccode)
else:
result = gt_lib.failValidate(challenge, validate, seccode)
# 注意,不要更改返回的结构和值类型
if result.status == 1:
response = {"result": "success", "version": GeetestLib.VERSION}
else:
response = {"result": "fail", "version": GeetestLib.VERSION, "msg": result.msg}
logger.info("geetest second_validate info %s" % response)
return response

@ -5,6 +5,7 @@ from api.utils.TokenManager import DownloadToken
from api.utils.app.supersignutils import get_redirect_server_domain
from api.utils.storage.storage import Storage
from api.utils.utils import get_developer_udided
from api.utils.storage.caches import get_user_free_download_times
import os, json, logging
logger = logging.getLogger(__file__)
@ -44,7 +45,7 @@ class UserInfoSerializer(serializers.ModelSerializer):
model = models.UserInfo
# fields="__all__"
fields = ["username", "uid", "mobile", "job", "email", "domain_name", "last_login", "first_name",
'head_img', 'storage_active', 'supersign_active']
'head_img', 'storage_active', 'supersign_active', 'free_download_times', 'download_times']
head_img = serializers.SerializerMethodField()
@ -52,6 +53,11 @@ class UserInfoSerializer(serializers.ModelSerializer):
storage = Storage(obj)
return storage.get_download_url(obj.head_img)
free_download_times = serializers.SerializerMethodField()
def get_free_download_times(self, obj):
return get_user_free_download_times(obj.id)
class AppsSerializer(serializers.ModelSerializer):
class Meta:

@ -8,10 +8,12 @@ from django.core.cache import cache
from api.models import Apps, UserInfo, AppReleaseInfo, AppUDID, APPToDeveloper, APPSuperSignUsedInfo
import time, os
from django.utils import timezone
from fir_ser.settings import CACHE_KEY_TEMPLATE, SERVER_DOMAIN, SYNC_CACHE_TO_DATABASE, DEFAULT_MOBILEPROVISION
from fir_ser.settings import CACHE_KEY_TEMPLATE, SERVER_DOMAIN, SYNC_CACHE_TO_DATABASE, DEFAULT_MOBILEPROVISION, \
USER_FREE_DOWNLOAD_TIMES
from api.utils.storage.storage import Storage, LocalStorage
from api.utils.utils import file_format_path
import logging
from django.db.models import F
logger = logging.getLogger(__file__)
@ -287,3 +289,66 @@ def set_default_app_wx_easy(user_obj, only_clean_cache=False):
app_obj.wxeasytype = True
app_obj.save()
del_cache_response_by_short(app_obj.app_id)
def enable_user_download_times_flag(user_id):
set_user_download_times_flag(user_id, 1)
def disable_user_download_times_flag(user_id):
set_user_download_times_flag(user_id, 0)
def check_user_can_download(user_id):
return set_user_download_times_flag(user_id, 2)
def set_user_download_times_flag(user_id, act):
user_can_download_key = "_".join(
[CACHE_KEY_TEMPLATE.get("user_can_download_key"), str(user_id)])
if act == 2:
result = cache.get(user_can_download_key)
if result is None:
return True
return result
return cache.set(user_can_download_key, act, 3600 * 24)
def get_user_free_download_times(user_id, act='get'):
now = timezone.now()
user_free_download_times_key = "_".join(
[CACHE_KEY_TEMPLATE.get("user_free_download_times_key"), str(now.year), str(now.month), str(now.day),
str(user_id)])
user_free_download_times = cache.get(user_free_download_times_key)
if user_free_download_times is not None:
if act == 'set':
return cache.incr(user_free_download_times_key, -1)
else:
return user_free_download_times
else:
cache.set(user_free_download_times_key, USER_FREE_DOWNLOAD_TIMES, 3600 * 24)
if act == 'set':
return cache.incr(user_free_download_times_key, -1)
else:
return USER_FREE_DOWNLOAD_TIMES
def consume_user_download_times(user_id, app_id):
with cache.lock("%s_%s" % ('consume_user_download_times', user_id)):
if get_user_free_download_times(user_id) > 0:
get_user_free_download_times(user_id, 'set')
else:
if not check_user_can_download(user_id):
return False
try:
UserInfo.objects.filter(pk=user_id).update(download_times=F('download_times') - 1)
except Exception as e:
logger.error("%s download_times less then 0. Exception:%s" % (user_id, e))
disable_user_download_times_flag(user_id)
del_cache_response_by_short(app_id)
return False
return True
def check_user_has_all_download_times(user_id):
return get_user_free_download_times(user_id) > 0 or check_user_can_download(user_id)

@ -0,0 +1,70 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# project: 3月
# author: NinEveN
# date: 2021/3/25
from rest_framework.throttling import SimpleRateThrottle
import hashlib
from rest_framework.exceptions import Throttled
class VisitShortThrottle(SimpleRateThrottle):
"""短连接用户访问频率限制1"""
scope = "ShortAccessUser1"
def get_cache_key(self, request, view):
return 'short_access_' + self.get_ident(request) + hashlib.md5(
request.META.get('HTTP_USER_AGENT', '').encode("utf-8")).hexdigest()
class InstallShortThrottle(SimpleRateThrottle):
"""短连接用户访问频率限制2"""
scope = "ShortAccessUser2"
def get_cache_key(self, request, view):
return 'short_access_' + self.get_ident(request)
class LoginUserThrottle(SimpleRateThrottle):
"""登录用户访问频率限制"""
scope = "LoginUser"
def get_cache_key(self, request, view):
if hasattr(request.user, 'uid'):
return request.user.uid
else:
self.get_ident(request)
class VisitRegister1Throttle(SimpleRateThrottle):
"""注册或者登陆限速1"""
scope = "RegisterUser1"
def get_cache_key(self, request, view):
return 'login_register_' + self.get_ident(request)
class VisitRegister2Throttle(SimpleRateThrottle):
"""注册或者登陆限速2"""
scope = "RegisterUser2"
def get_cache_key(self, request, view):
return 'login_register_' + self.get_ident(request)
class GetAuthC1Throttle(SimpleRateThrottle):
"""注册或者登陆限速1"""
scope = "GetAuthC1"
def get_cache_key(self, request, view):
return 'get_auth_' + self.get_ident(request)
class GetAuthC2Throttle(SimpleRateThrottle):
"""注册或者登陆限速2"""
scope = "GetAuthC2"
def get_cache_key(self, request, view):
return 'get_auth_' + self.get_ident(request)

@ -13,7 +13,7 @@ from api.utils.app.apputils import make_resigned
from api.utils.app.supersignutils import make_sign_udid_mobileconfig, get_post_udid_url, get_redirect_server_domain
from api.utils.storage.storage import Storage, get_local_storage
from api.utils.storage.caches import get_app_instance_by_cache, get_download_url_by_cache, set_app_download_by_cache, \
del_cache_response_by_short
del_cache_response_by_short, consume_user_download_times, check_user_has_all_download_times
import os
from rest_framework_extensions.cache.decorators import cache_response
from api.utils.serializer import AppsShortSerializer
@ -21,6 +21,7 @@ from api.models import Apps, AppReleaseInfo, APPToDeveloper, APPSuperSignUsedInf
from django.http import FileResponse
import logging
from api.utils.utils import get_profile_full_path
from api.utils.throttle import VisitShortThrottle, InstallShortThrottle
logger = logging.getLogger(__file__)
@ -138,6 +139,7 @@ class DownloadView(APIView):
class ShortDownloadView(APIView):
throttle_classes = [VisitShortThrottle, InstallShortThrottle]
'''
根据下载短链接获取应用信息
'''
@ -152,13 +154,24 @@ class ShortDownloadView(APIView):
res.code = 1003
res.msg = "该应用不存在"
return Response(res.dict)
if udid:
del_cache_response_by_short(app_obj.app_id, udid=udid)
if not check_user_has_all_download_times(app_obj.user_id_id):
res.code = 1009
res.msg = "可用下载额度不足,请联系开发者"
return Response(res.dict)
if not app_obj.isshow:
res.code = 1004
res.msg = "您没有权限访问该应用"
return Response(res.dict)
if udid:
if not app_obj.issupersign:
res.code = 1002
res.msg = "参数有误"
return Response(res.dict)
del_cache_response_by_short(app_obj.app_id, udid=udid)
app_serializer = AppsShortSerializer(app_obj, context={"key": "ShortDownloadView", "release_id": release_id,
"storage": Storage(app_obj.user_id)})
res.data = app_serializer.data
@ -183,6 +196,7 @@ class ShortDownloadView(APIView):
class InstallView(APIView):
throttle_classes = [VisitShortThrottle, InstallShortThrottle]
'''
安装操作前端通过token 认证认证成功之后返回 下载连接并且 让下载次数加一
'''
@ -220,12 +234,17 @@ class InstallView(APIView):
res.data = {"download_url": download_url, "extra_url": extra_url}
if download_url != "" and "mobileconifg" not in download_url:
set_app_download_by_cache(app_id)
if request.META.get('HTTP_X_FORWARDED_FOR', None):
ip = request.META['HTTP_X_FORWARDED_FOR']
else:
ip = request.META['REMOTE_ADDR']
logger.info("remote ip %s short %s download_url %s app_obj %s" % (ip, short, download_url, app_obj))
if not consume_user_download_times(app_obj.get("user_id"), app_id):
res.code = 1009
res.msg = "可用下载额度不足"
del res.data
return Response(res.dict)
set_app_download_by_cache(app_id)
return Response(res.dict)
else:
res.code = 1004

@ -1,5 +1,5 @@
from django.contrib import auth
from api.models import Token, UserInfo, Apps
from api.models import Token, UserInfo
from rest_framework.response import Response
from api.utils.serializer import UserInfoSerializer
from django.core.cache import cache
@ -14,6 +14,8 @@ from api.utils.response import BaseResponse
from fir_ser.settings import CACHE_KEY_TEMPLATE, SERVER_DOMAIN, REGISTER, LOGIN
from api.utils.storage.caches import login_auth_failed, set_default_app_wx_easy
import logging
from api.utils.geetest.geetest_utils import first_register, second_validate
from api.utils.throttle import VisitRegister1Throttle, VisitRegister2Throttle, GetAuthC1Throttle, GetAuthC2Throttle
logger = logging.getLogger(__name__)
@ -169,6 +171,8 @@ def CheckChangeUerinfo(target, act, key, user):
class LoginView(APIView):
throttle_classes = [VisitRegister1Throttle, VisitRegister2Throttle]
def generate_key(self):
return binascii.hexlify(os.urandom(32)).decode()
@ -181,6 +185,15 @@ class LoginView(APIView):
else:
is_valid = True
if is_valid:
if LOGIN.get("geetest"):
geetest = receive.get("geetest", None)
if geetest and second_validate(geetest).get("result", "") == "success":
pass
else:
response.code = 1001
response.msg = "geetest 验证有误"
return Response(response.dict)
login_type = receive.get("login_type", None)
if login_auth_failed("get", username):
password = receive.get("password")
@ -231,13 +244,25 @@ class LoginView(APIView):
response.data = {}
if LOGIN.get("captcha"):
response.data = get_captcha()
if LOGIN.get("geetest"):
response.data['geetest'] = True
response.data['login_type'] = get_login_type()
allow_f = REGISTER.get("enable")
response.data['register_enable'] = allow_f
return Response(response.dict)
def put(self, request):
import hashlib
response = BaseResponse()
user_id = request.data.get('user_id', None)
sha = hashlib.sha1(user_id.encode("utf-8"))
response.data = first_register(sha.hexdigest(), request.META.get('REMOTE_ADDR'))
return Response(response.dict)
class RegistView(APIView):
throttle_classes = [VisitRegister1Throttle, VisitRegister2Throttle]
def generate_key(self):
return binascii.hexlify(os.urandom(32)).decode()
@ -261,6 +286,15 @@ class RegistView(APIView):
response.msg = "不允许该类型注册"
return Response(response.dict)
if REGISTER.get("geetest"):
geetest = request.data.get("geetest", None)
if geetest and second_validate(geetest).get("result", "") == "success":
pass
else:
response.code = 1018
response.msg = "geetest验证有误"
return Response(response.dict)
is_valid, target = is_valid_sender_code(act, receive.get("auth_token", None), receive.get("auth_key", None))
if is_valid and str(target) == str(username):
if login_auth_failed("get", username):
@ -302,6 +336,8 @@ class RegistView(APIView):
if allow_f:
if REGISTER.get("captcha"):
response.data = get_captcha()
if REGISTER.get("geetest"):
response.data['geetest'] = True
response.data['register_type'] = get_register_type()
response.data['enable'] = allow_f
return Response(response.dict)
@ -447,6 +483,7 @@ class UserInfoView(APIView):
class AuthorizationView(APIView):
throttle_classes = [GetAuthC1Throttle, GetAuthC2Throttle]
def post(self, request):
res = BaseResponse()
@ -472,6 +509,15 @@ class AuthorizationView(APIView):
res.msg = "图片验证码有误"
return Response(res.dict)
if REGISTER.get("geetest"):
geetest = request.data.get("geetest", None)
if geetest and second_validate(geetest).get("result", "") == "success":
pass
else:
res.code = 1018
res.msg = "geetest验证有误"
return Response(res.dict)
res = CheckRegisterUerinfo(target, act, 'register')
return Response(res.dict)
@ -479,6 +525,7 @@ class AuthorizationView(APIView):
class ChangeAuthorizationView(APIView):
authentication_classes = [ExpiringTokenAuthentication, ]
throttle_classes = [GetAuthC1Throttle, GetAuthC2Throttle]
def post(self, request):
res = BaseResponse()

@ -116,7 +116,19 @@ AUTH_PASSWORD_VALIDATORS = [
REST_FRAMEWORK = {
'DEFAULT_RENDERER_CLASSES': (
'rest_framework.renderers.JSONRenderer',
)
),
'DEFAULT_THROTTLE_CLASSES': [
'api.utils.throttle.LoginUserThrottle',
],
'DEFAULT_THROTTLE_RATES': {
'ShortAccessUser1': '80/m',
'ShortAccessUser2': '1000/h',
'LoginUser': '200/m',
'RegisterUser1': '30/m',
'RegisterUser2': '300/h',
'GetAuthC1': '8/m',
'GetAuthC2': '30/h',
}
}
# Internationalization
# https://docs.djangoproject.com/en/3.0/topics/i18n/
@ -156,7 +168,8 @@ CACHES = {
"CONNECTION_POOL_KWARGS": {"max_connections": 100},
"PASSWORD": "",
"DECODE_RESPONSES": True
}
},
"TIMEOUT": 60 * 15
},
}
@ -168,10 +181,18 @@ REST_FRAMEWORK_EXTENSIONS = {
'DEFAULT_USE_CACHE': 'default',
}
# geetest 配置信息
GEETEST_ID = "d3f3cf73200a70bd3a0c32ea22e5003a"
GEETEST_KEY = "6f4ab6185c230a4b1f8430f28ebe4804"
GEETEST_CYCLE_TIME = 10
GEETEST_BYPASS_STATUS_KEY = "gt_server_bypass_status"
GEETEST_BYPASS_URL = "http://bypass.geetest.com/v1/bypass_status.php"
# 注册方式,如果启用sms或者email 需要配置 THIRD_PART_CONFIG.sender 信息
REGISTER = {
"enable": True,
"captcha": True, # 是否开启注册字母验证码
"captcha": False, # 是否开启注册字母验证码
"geetest": True, # 是否开启geetest验证,如要开启请先配置geetest
"register_type": {
'sms': False, # 短信注册
'email': True, # 邮件注册
@ -182,6 +203,7 @@ REGISTER = {
# 信息修改也会使用该配置
LOGIN = {
"captcha": False, # 是否开启登录字母验证码
"geetest": True, # 是否开启geetest验证
"login_type": {
'sms': True, # 短信登录
'email': True, # 邮件登录
@ -200,7 +222,7 @@ THIRD_PART_CONFIG = {
# 正式环境需要填写正式的访问域名,如果配置cdn,可以填写cdn的域名,仅支持阿里云 cdn,
# 开启cdn之后,如果该域名和服务器域名不相同,需要设置阿里云cdn 缓存配置,自定义HTTP响应头 添加 Access-Control-Allow-Origin * 才可以
'is_https': True,
'download_auth_type': 0, # 0:不开启token 1:本地token 2:cdn 开启cdn,并且使用本地存储,使用阿里云cdn进行url鉴权,
'download_auth_type': 1, # 0:不开启token 1:本地token 2:cdn 开启cdn,并且使用本地存储,使用阿里云cdn进行url鉴权,
'cnd_auth_key': '', # 当cdn为阿里云并且 download_auth_type=2 的时候 生效,需要 开启阿里云OSS私有Bucket回源
},
'active': True
@ -289,6 +311,7 @@ THIRD_PART_CONFIG = {
}
CACHE_KEY_TEMPLATE = {
'user_can_download_key': 'user_can_download',
'download_times_key': 'app_download_times',
'make_token_key': 'make_token',
'download_short_key': 'download_short',
@ -300,11 +323,13 @@ CACHE_KEY_TEMPLATE = {
'developer_auth_code_key': 'developer_auth_code',
'upload_file_tmp_name_key': 'upload_file_tmp_name',
'login_failed_try_times_key': 'login_failed_try_times',
'user_free_download_times_key': 'user_free_download_times',
'super_sign_failed_send_msg_times_key': 'super_sign_failed_send_msg_times'
}
DATA_DOWNLOAD_KEY = "d_token"
FILE_UPLOAD_TMP_KEY = ".tmp"
USER_FREE_DOWNLOAD_TIMES = 10
SYNC_CACHE_TO_DATABASE = {
'download_times': 10, # 下载次数同步时间

Loading…
Cancel
Save