增加微信公众号绑定扫码登录功能

dependabot/npm_and_yarn/fir_admin/tmpl-1.0.5
youngS 4 years ago
parent d6ce64c3b6
commit 9b8412304f
  1. 118
      fir_client/src/components/FirLogin.vue
  2. 265
      fir_client/src/components/user/FirUserProfileInfo.vue
  3. 34
      fir_client/src/restful/index.js
  4. 30
      fir_ser/api/migrations/0005_thirdwechatuserinfo.py
  5. 15
      fir_ser/api/models.py
  6. 7
      fir_ser/api/tasks.py
  7. 7
      fir_ser/api/urls.py
  8. 5
      fir_ser/api/utils/app/iossignapi.py
  9. 29
      fir_ser/api/utils/app/supersignutils.py
  10. 12
      fir_ser/api/utils/crontab/ctasks.py
  11. 5
      fir_ser/api/utils/mp/__init__.py
  12. 5
      fir_ser/api/utils/mp/chat/__init__.py
  13. 86
      fir_ser/api/utils/mp/chat/receive.py
  14. 61
      fir_ser/api/utils/mp/chat/reply.py
  15. 20
      fir_ser/api/utils/mp/ierror.py
  16. 220
      fir_ser/api/utils/mp/utils.py
  17. 119
      fir_ser/api/utils/mp/wechat.py
  18. 6
      fir_ser/api/utils/serializer.py
  19. 30
      fir_ser/api/utils/storage/caches.py
  20. 4
      fir_ser/api/views/domain.py
  21. 61
      fir_ser/api/views/login.py
  22. 176
      fir_ser/api/views/thirdlogin.py
  23. 13
      fir_ser/config.py
  24. 10
      fir_ser/fir_ser/settings.py

@ -71,6 +71,25 @@
<el-form-item> <el-form-item>
<el-link :underline="false" @click="$router.push({name: 'FirResetPwd'})" plain>忘记密码</el-link> <el-link :underline="false" @click="$router.push({name: 'FirResetPwd'})" plain>忘记密码</el-link>
</el-form-item> </el-form-item>
<div class="other-way">
<hr>
<span class="info">或使用以下账户登录</span>
<el-popover
placement="top"
trigger="manual"
title="微信扫码关注公众号登录"
v-model="wx_visible">
<div>
<el-image :src="wx_login_qr_url" style="width: 176px;height: 166px"/>
</div>
<el-button style="color: #1fc939;border: 1px solid rgba(31,201,57,.5); width: 110px"
slot="reference" size="small" @click="wxLogin">微信
</el-button>
</el-popover>
</div>
</el-form> </el-form>
@ -82,7 +101,7 @@
</template> </template>
<script> <script>
import {loginFun, set_auth_token} from "@/restful"; import {loginFun, set_auth_token, wxLoginFun} from "@/restful";
import {checkEmail, checkphone, geetest} from "@/utils"; import {checkEmail, checkphone, geetest} from "@/utils";
export default { export default {
@ -101,9 +120,75 @@
rctitle: '', rctitle: '',
register_enable: false, register_enable: false,
login_disable: false, login_disable: false,
wx_login_qr_url: '',
wx_visible: false,
loop_flag: false,
} }
}, },
methods: { methods: {
set_cookie_and_token(data) {
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'})
},
loop_get_wx_info(wx_login_ticket) {
if (wx_login_ticket && wx_login_ticket.length < 3) {
this.$message.error("获取登陆码失败,请稍后再试");
return
}
let c_count = 1;
// eslint-disable-next-line no-unused-vars
const loop_t = window.setInterval(res => {
if (!this.loop_flag) {
window.clearInterval(loop_t);
}
wxLoginFun(data => {
c_count += 1;
if (c_count > 120) {
window.clearInterval(loop_t);
}
if (data.code === 1000) {
window.clearInterval(loop_t);
this.set_cookie_and_token(data);
} else if (data.code === 1005) {
window.clearInterval(loop_t);
this.wx_visible = false;
this.loop_flag = false;
this.$message({
message: data.msg,
type: 'error',
duration: 30000
});
}
}, {
"methods": "POST",
data: {"ticket": wx_login_ticket}
})
}, 3000)
},
wxLogin() {
this.wx_visible = !this.wx_visible;
this.wx_login_qr_url = '';
if (this.wx_visible) {
wxLoginFun(data => {
if (data.code === 1000) {
this.wx_login_qr_url = data.data.qr;
this.loop_flag = true;
this.loop_get_wx_info(data.data.ticket);
}
}, {
"methods": "GET",
})
} else {
this.loop_flag = false;
}
},
is_cptch() { is_cptch() {
let cptch_flag = this.form.authcode.length === this.cptch.length; let cptch_flag = this.form.authcode.length === this.cptch.length;
if (this.cptch.cptch_key === '' || !this.cptch.cptch_key) { if (this.cptch.cptch_key === '' || !this.cptch.cptch_key) {
@ -189,13 +274,7 @@
message: '登录成功', message: '登录成功',
type: 'success' type: 'success'
}); });
this.$cookies.remove("auth_token"); this.set_cookie_and_token(data);
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 { } else {
this.$message({ this.$message({
message: data.msg, message: data.msg,
@ -260,6 +339,29 @@
<style scoped> <style scoped>
.other-way {
position: relative;
text-align: center;
}
.other-way hr {
height: 1px;
margin: 30px 0;
border: 0;
background-color: #e4e7ed;
}
.other-way span.info {
font-size: 12px;
line-height: 1;
position: absolute;
top: -6px;
left: 50%;
padding: 0 10px;
transform: translate(-50%, 0);
color: #9ba3af;
}
.el-container { .el-container {
margin: 10px auto; margin: 10px auto;
width: 1266px; width: 1266px;

@ -1,6 +1,85 @@
<template> <template>
<div> <div>
<el-dialog
:visible.sync="show_wx_visible"
width="780px"
center
:close-on-click-modal="false"
:close-on-press-escape="false">
<span slot="title" style="color: #313639;font-size: 28px; margin-bottom: 14px;">
微信授权登录用户信息
</span>
<el-tag style="margin-bottom: 10px">授权用户将可以使用微信扫码直接登录</el-tag>
<el-table
:data="wx_user_list"
stripe
border
style="width: 100%">
<el-table-column
label="昵称"
align="center"
width="180">
<template slot-scope="scope">
<el-popover trigger="hover" placement="top">
<p>昵称: {{ scope.row.nickname }}</p>
<p>性别: {{ scope.row.sex|sex_filter }}</p>
<p>住址: {{ scope.row.address }}</p>
<p>openid: {{ scope.row.openid }}</p>
<div slot="reference" class="name-wrapper">
<el-tag size="medium">{{ scope.row.nickname }}</el-tag>
</div>
</el-popover>
</template>
</el-table-column>
<el-table-column
label="头像"
align="center"
width="120">
<template slot-scope="scope">
<el-image :src="scope.row.head_img_url" style="width: 80px;height: 80px"/>
</template>
</el-table-column>
<el-table-column
label="关注公众号"
align="center"
width="100">
<template slot-scope="scope">
<div slot="reference" class="name-wrapper">
<el-tag size="medium" v-if="scope.row.subscribe"></el-tag>
<el-tag size="medium" v-else type="danger"></el-tag>
</div>
</template>
</el-table-column>
<el-table-column
label="授权时间"
align="center"
width="200">
<template slot-scope="scope">
<i class="el-icon-time"></i>
<span style="margin-left: 10px">{{ scope.row.created_time|format_time }}</span>
</template>
</el-table-column>
<el-table-column
label="操作"
align="center"
>
<template slot-scope="scope">
<el-button
size="mini"
type="danger"
@click="delete_wx_u(scope.row)">移除授权
</el-button>
</template>
</el-table-column>
</el-table>
</el-dialog>
<el-form ref="form" :model="userinfo" label-width="90px"> <el-form ref="form" :model="userinfo" label-width="90px">
<el-form-item label="用户名"> <el-form-item label="用户名">
<el-row :gutter="36"> <el-row :gutter="36">
@ -138,25 +217,6 @@
</el-row> </el-row>
</el-form-item> </el-form-item>
<!-- <el-form-item label="下载域名">-->
<!-- <el-row :gutter="36">-->
<!-- <el-col :span="16">-->
<!-- <el-input v-model="userinfo.domain_name" :readonly="editdomain_name !== true" ref="domain_name"-->
<!-- prefix-icon="el-icon-download"-->
<!-- placeholder="下载页域名" clearable/>-->
<!-- </el-col>-->
<!-- <el-col :span="1">-->
<!-- <el-button icon="el-icon-edit" @click="changeDomainValue">-->
<!-- </el-button>-->
<!-- </el-col>-->
<!-- <el-col :span="5" v-if="editdomain_name === true">-->
<!-- <el-button type="success" @click="saveDomain" plain-->
<!-- class="save-button">-->
<!-- 保存-->
<!-- </el-button>-->
<!-- </el-col>-->
<!-- </el-row>-->
<!-- </el-form-item>-->
<el-form-item label="下载域名"> <el-form-item label="下载域名">
<el-row :gutter="36"> <el-row :gutter="36">
<el-col :span="16"> <el-col :span="16">
@ -187,6 +247,28 @@
</el-row> </el-row>
</el-form-item> </el-form-item>
<div class="other-way">
<hr>
<span class="info">绑定第三方账户</span>
<el-row :gutter="20">
<el-col :span="6" :offset="6">
<el-popover
placement="top"
trigger="manual"
title="打开微信扫一扫进行绑定"
v-model="wx_visible">
<div>
<el-image :src="wx_login_qr_url" style="width: 176px;height: 166px"/>
</div>
<el-button slot="reference" size="small" @click="wxLogin">绑定微信</el-button>
</el-popover>
</el-col>
<el-col :span="6">
<el-button size="small" @click="get_wx_user_list">授权信息</el-button>
</el-col>
</el-row>
</div>
</el-form> </el-form>
@ -195,7 +277,7 @@
</template> </template>
<script> <script>
import {changeInfoFun, getAuthcTokenFun, userinfos} from '@/restful' import {changeInfoFun, getAuthcTokenFun, userinfos, wxLoginFun, wxutils} from '@/restful'
import {deepCopy, geetest} from "@/utils"; import {deepCopy, geetest} from "@/utils";
export default { export default {
@ -214,8 +296,110 @@
editposition: false, editposition: false,
cptch: {"cptch_image": '', "cptch_key": '', "length": 8, change_type: {email: false, sms: false}}, cptch: {"cptch_image": '', "cptch_key": '', "length": 8, change_type: {email: false, sms: false}},
form: {}, form: {},
wx_login_qr_url: '',
wx_visible: false,
loop_flag: false,
show_wx_visible: false,
wx_user_list: [],
pagination: {"currentPage": 1, "total": 0, "pagesize": 999},
} }
}, methods: { }, methods: {
delete_wx_u(wx_user_info) {
this.$confirm(`此操作将导致微信用户 “${wx_user_info.nickname}” 无法通过扫码登录, 是否继续删除?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.wxUtilsFun({
methods: 'DELETE',
data: {
"size": this.pagination.pagesize,
"page": this.pagination.currentPage,
"user_id": wx_user_info.user_id,
"openid": wx_user_info.openid,
}
})
}).catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
});
});
},
wxUtilsFun(params) {
wxutils(data => {
if (data.code === 1000) {
this.wx_user_list = data.data;
this.show_wx_visible = true
} else {
this.$message.error("获取授权列表失败")
}
}, params)
},
get_wx_user_list() {
this.wxUtilsFun({
methods: 'GET',
data: {
"size": this.pagination.pagesize,
"page": this.pagination.currentPage
}
})
},
loop_get_wx_info(wx_login_ticket) {
if (wx_login_ticket && wx_login_ticket.length < 3) {
this.$message.error("获取登陆码失败,请稍后再试");
return
}
let c_count = 1;
// eslint-disable-next-line no-unused-vars
const loop_t = window.setInterval(res => {
if (!this.loop_flag) {
window.clearInterval(loop_t);
}
wxLoginFun(data => {
c_count += 1;
if (c_count > 120) {
window.clearInterval(loop_t);
}
if (data.code === 1000) {
window.clearInterval(loop_t);
if (this.userinfo.uid === data.userinfo.uid) {
this.$message.success("绑定成功");
this.wx_visible = false;
this.loop_flag = false;
}
} else if (data.code === 1005) {
window.clearInterval(loop_t);
this.$message({
message: data.msg,
type: 'error',
duration: 30000
});
}
}, {
"methods": "POST",
data: {"ticket": wx_login_ticket}
})
}, 3000)
},
wxLogin() {
this.wx_visible = !this.wx_visible;
this.wx_login_qr_url = '';
if (this.wx_visible) {
userinfos(data => {
if (data.code === 1000) {
this.wx_login_qr_url = data.data.qr;
this.loop_flag = true;
this.loop_get_wx_info(data.data.ticket);
}
}, {
"methods": "POST",
})
} else {
this.loop_flag = false;
}
},
get_auth_code() { get_auth_code() {
changeInfoFun(data => { changeInfoFun(data => {
if (data.code === 1000) { if (data.code === 1000) {
@ -391,11 +575,52 @@
this.userinfo = this.$store.state.userinfo; this.userinfo = this.$store.state.userinfo;
this.orguserinfo = deepCopy(this.$store.state.userinfo); this.orguserinfo = deepCopy(this.$store.state.userinfo);
} }
}, filters: {
sex_filter: function (x) {
let ret = '未知';
if (x === 1) {
ret = '男'
} else if (x === 2) {
ret = '女'
}
return ret;
},
format_time: function (x) {
if (x) {
x = x.split(".")[0].split("T");
return x[0] + " " + x[1]
} else
return '';
}
} }
} }
</script> </script>
<style scoped> <style scoped>
.other-way {
position: relative;
text-align: center;
}
.other-way hr {
height: 1px;
margin: 30px 0;
border: 0;
background-color: #e4e7ed;
}
.other-way span.info {
font-size: 12px;
line-height: 1;
position: absolute;
top: -6px;
left: 50%;
padding: 0 10px;
transform: translate(-50%, 0);
color: #9ba3af;
}
.el-form { .el-form {
max-width: 500px; max-width: 500px;
margin: 0 auto; margin: 0 auto;

@ -216,6 +216,25 @@ export function loginFun(callBack, params, load = true) {
); );
} }
/**微信公众号关注登录 */
export function wxLoginFun(callBack, params, load = true) {
let g_url = 'third.wx.login';
if (params.methods === 'POST') {
g_url = 'third.wx.sync'
}
getData(
params.methods,
USERSEVER + '/' + g_url,
params.data,
data => {
callBack(data);
},
load,
true,
true
);
}
/**获取验证token */ /**获取验证token */
export function getAuthTokenFun(callBack, params, load = true) { export function getAuthTokenFun(callBack, params, load = true) {
getData( getData(
@ -628,3 +647,18 @@ export function developercert(callBack, params, load = true) {
true true
); );
} }
/**微信用户绑定 */
export function wxutils(callBack, params, load = true) {
getData(
params.methods,
USERSEVER + '/twx/info',
params.data,
data => {
callBack(data);
},
load,
true,
true
);
}

@ -0,0 +1,30 @@
# Generated by Django 3.2.3 on 2021-09-07 19:18
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('api', '0004_auto_20210705_1819'),
]
operations = [
migrations.CreateModel(
name='ThirdWeChatUserInfo',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('openid', models.CharField(max_length=64, unique=True, verbose_name='普通用户的标识,对当前公众号唯一')),
('nickname', models.CharField(blank=True, max_length=64, verbose_name='昵称')),
('sex', models.SmallIntegerField(default=0, help_text='值为1时是男性,值为2时是女性,值为0时是未知', verbose_name='性别')),
('subscribe_time', models.BigIntegerField(verbose_name='订阅时间')),
('head_img_url', models.CharField(blank=True, max_length=256, null=True, verbose_name='用户头像')),
('address', models.CharField(blank=True, max_length=128, null=True, verbose_name='地址')),
('subscribe', models.BooleanField(default=0, verbose_name='是否订阅公众号')),
('created_time', models.DateTimeField(auto_now_add=True, verbose_name='授权时间')),
('user_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL,
verbose_name='用户ID')),
],
),
]

@ -62,6 +62,21 @@ class UserInfo(AbstractUser):
super(UserInfo, self).save(*args, **kwargs) super(UserInfo, self).save(*args, **kwargs)
class ThirdWeChatUserInfo(models.Model):
user_id = models.ForeignKey(to="UserInfo", verbose_name="用户ID", on_delete=models.CASCADE)
openid = models.CharField(max_length=64, unique=True, verbose_name="普通用户的标识,对当前公众号唯一")
nickname = models.CharField(max_length=64, verbose_name="昵称", blank=True)
sex = models.SmallIntegerField(default=0, verbose_name="性别", help_text="值为1时是男性,值为2时是女性,值为0时是未知")
subscribe_time = models.BigIntegerField(verbose_name="订阅时间")
head_img_url = models.CharField(max_length=256, verbose_name="用户头像", blank=True, null=True)
address = models.CharField(max_length=128, verbose_name="地址", blank=True, null=True)
subscribe = models.BooleanField(verbose_name="是否订阅公众号", default=0)
created_time = models.DateTimeField(auto_now_add=True, verbose_name="授权时间")
def __str__(self):
return f"{self.user_id}-{self.nickname}-{self.openid}"
class Token(models.Model): class Token(models.Model):
""" """
The default authorization token model. The default authorization token model.

@ -12,7 +12,7 @@ from api.utils.storage.storage import get_local_storage
from api.models import Apps from api.models import Apps
from api.utils.app.supersignutils import IosUtils, resign_by_app_id from api.utils.app.supersignutils import IosUtils, resign_by_app_id
from api.utils.crontab.ctasks import sync_download_times, auto_clean_upload_tmp_file, auto_delete_ios_mobile_tmp_file, \ from api.utils.crontab.ctasks import sync_download_times, auto_clean_upload_tmp_file, auto_delete_ios_mobile_tmp_file, \
auto_check_ios_developer_active auto_check_ios_developer_active, sync_wx_access_token
from api.utils.geetest.geetest_utils import check_bypass_status from api.utils.geetest.geetest_utils import check_bypass_status
from fir_ser.celery import app from fir_ser.celery import app
@ -80,3 +80,8 @@ def auto_delete_tmp_file_job():
@app.task @app.task
def auto_check_ios_developer_active_job(): def auto_check_ios_developer_active_job():
auto_check_ios_developer_active() auto_check_ios_developer_active()
@app.task
def sync_wx_access_token_job():
sync_wx_access_token()

@ -16,7 +16,7 @@ Including another URLconf
from django.urls import re_path from django.urls import re_path
from api.views.login import LoginView, UserInfoView, RegistView, AuthorizationView, ChangeAuthorizationView, \ from api.views.login import LoginView, UserInfoView, RegistView, AuthorizationView, ChangeAuthorizationView, \
UserApiTokenView, CertificationView, ChangeInfoView UserApiTokenView, CertificationView, ChangeInfoView, WeChatLoginView, WeChatLoginCheckView
from api.views.logout import LogoutView from api.views.logout import LogoutView
from api.views.apps import AppsView, AppInfoView, AppReleaseInfoView from api.views.apps import AppsView, AppInfoView, AppReleaseInfoView
from api.views.download import ShortDownloadView from api.views.download import ShortDownloadView
@ -26,6 +26,7 @@ from api.views.receiveudids import IosUDIDView, TaskView
from api.views.order import PriceView, OrderView, PaySuccess from api.views.order import PriceView, OrderView, PaySuccess
from api.views.supersign import DeveloperView, SuperSignUsedView, AppUDIDUsedView, SuperSignCertView from api.views.supersign import DeveloperView, SuperSignUsedView, AppUDIDUsedView, SuperSignCertView
from api.views.domain import DomainCnameView from api.views.domain import DomainCnameView
from api.views.thirdlogin import ValidWxChatToken, ThirdWxAccount
# router=DefaultRouter() # router=DefaultRouter()
# router.register("apps", AppsView) # router.register("apps", AppsView)
@ -58,5 +59,9 @@ urlpatterns = [
re_path("^certification$", CertificationView.as_view()), re_path("^certification$", CertificationView.as_view()),
re_path(r"^pay_success/(?P<name>\w+)$", PaySuccess.as_view()), re_path(r"^pay_success/(?P<name>\w+)$", PaySuccess.as_view()),
re_path("^cname_domain$", DomainCnameView.as_view()), re_path("^cname_domain$", DomainCnameView.as_view()),
re_path("^mp.weixin$", ValidWxChatToken.as_view()),
re_path("^third.wx.login$", WeChatLoginView.as_view()),
re_path("^third.wx.sync$", WeChatLoginCheckView.as_view()),
re_path("^twx/info$", ThirdWxAccount.as_view()),
] ]

@ -264,7 +264,7 @@ class AppDeveloperApiV2(object):
return False, result return False, result
def get_profile(self, app_obj, udid_info, provisionName, auth, developer_app_id, def get_profile(self, app_obj, udid_info, provisionName, auth, developer_app_id,
device_id_list): device_id_list, err_callback):
result = {} result = {}
bundle_id = app_obj.bundle_id bundle_id = app_obj.bundle_id
app_id = app_obj.app_id app_id = app_obj.app_id
@ -301,6 +301,9 @@ class AppDeveloperApiV2(object):
except Exception as e: except Exception as e:
logger.error(f"app_id {app_obj.app_id} ios developer make profile Failed Exception:{e}") logger.error(f"app_id {app_obj.app_id} ios developer make profile Failed Exception:{e}")
result['return_info'] = "%s" % e result['return_info'] = "%s" % e
if "There are no current ios devices" in e:
err_callback()
return False, result return False, result
def del_profile(self, app_id): def del_profile(self, app_id):

@ -192,6 +192,13 @@ def get_apple_udid_key(auth):
return m_key return m_key
def err_callback(func, a, b):
def wrapper():
return func(a, b)
return wrapper
def get_server_domain_from_request(request, server_domain): def get_server_domain_from_request(request, server_domain):
if not server_domain or not server_domain.startswith("http"): if not server_domain or not server_domain.startswith("http"):
http_host = request.META.get('HTTP_HOST') http_host = request.META.get('HTTP_HOST')
@ -273,10 +280,10 @@ class IosUtils(object):
return None return None
return developer_obj return developer_obj
def download_profile(self, developer_app_id, device_id_list): # def download_profile(self, developer_app_id, device_id_list):
return get_api_obj(self.auth).get_profile(self.app_obj, self.udid_info, # return get_api_obj(self.auth).get_profile(self.app_obj, self.udid_info,
self.get_profile_full_path(), # self.get_profile_full_path(),
self.auth, developer_app_id, device_id_list) # self.auth, developer_app_id, device_id_list, )
# 开启超级签直接在开发者账户创建 # 开启超级签直接在开发者账户创建
# def create_app(self, app_obj): # def create_app(self, app_obj):
@ -327,7 +334,9 @@ class IosUtils(object):
status, result = get_api_obj(auth).get_profile(app_obj, udid_info, status, result = get_api_obj(auth).get_profile(app_obj, udid_info,
get_profile_full_path(developer_obj, app_obj), get_profile_full_path(developer_obj, app_obj),
auth, developer_app_id, device_id_lists) auth, developer_app_id, device_id_lists,
err_callback(IosUtils.get_device_from_developer,
developer_obj, developer_obj.user_id))
if add_did_flag and sync_device_obj: if add_did_flag and sync_device_obj:
result['did'] = sync_device_obj.serial result['did'] = sync_device_obj.serial
result['did_exists'] = True result['did_exists'] = True
@ -340,7 +349,7 @@ class IosUtils(object):
if sign_try_attempts != -1: if sign_try_attempts != -1:
logger.error(f"app {app_obj} developer {developer_obj} sign failed {result}") logger.error(f"app {app_obj} developer {developer_obj} sign failed {result}")
developer_obj.is_actived = False developer_obj.is_actived = False
developer_obj.save() developer_obj.save(update_fields=['is_actived'])
send_ios_developer_active_status(developer_obj.user_id, send_ios_developer_active_status(developer_obj.user_id,
MSGTEMPLATE.get('ERROR_DEVELOPER') % ( MSGTEMPLATE.get('ERROR_DEVELOPER') % (
developer_obj.user_id.first_name, app_obj.name, developer_obj.user_id.first_name, app_obj.name,
@ -404,7 +413,7 @@ class IosUtils(object):
storage_obj.delete_file(apptodev_obj.binary_file + ".ipa") storage_obj.delete_file(apptodev_obj.binary_file + ".ipa")
apptodev_obj.binary_file = random_file_name apptodev_obj.binary_file = random_file_name
apptodev_obj.release_file = release_obj.release_id apptodev_obj.release_file = release_obj.release_id
apptodev_obj.save() apptodev_obj.save(update_fields=['binary_file', 'release_file'])
else: else:
APPToDeveloper.objects.create(developerid=developer_obj, app_id=app_obj, APPToDeveloper.objects.create(developerid=developer_obj, app_id=app_obj,
binary_file=random_file_name, release_file=release_obj.release_id) binary_file=random_file_name, release_file=release_obj.release_id)
@ -419,7 +428,7 @@ class IosUtils(object):
developer_obj.use_number = developer_obj.use_number + 1 developer_obj.use_number = developer_obj.use_number + 1
logger.info( logger.info(
f"developer {developer_obj} use_number+1 now {developer_obj.use_number} udid {self.udid_info.get('udid')} app_id {self.app_obj}") f"developer {developer_obj} use_number+1 now {developer_obj.use_number} udid {self.udid_info.get('udid')} app_id {self.app_obj}")
developer_obj.save() developer_obj.save(update_fields=['use_number'])
if not appsupersign_obj.filter(app_id=self.app_obj, user_id=self.user_obj).first(): if not appsupersign_obj.filter(app_id=self.app_obj, user_id=self.user_obj).first():
APPSuperSignUsedInfo.objects.create(app_id=self.app_obj, user_id=self.user_obj, APPSuperSignUsedInfo.objects.create(app_id=self.app_obj, user_id=self.user_obj,
@ -602,7 +611,7 @@ class IosUtils(object):
if developer_obj.use_number > 0: if developer_obj.use_number > 0:
developer_obj.use_number = developer_obj.use_number - 1 developer_obj.use_number = developer_obj.use_number - 1
developer_obj.save() developer_obj.save(update_fields=['use_number'])
udid_obj.delete() udid_obj.delete()
@ -706,7 +715,7 @@ class IosUtils(object):
developer_obj.is_actived = True developer_obj.is_actived = True
else: else:
developer_obj.is_actived = False developer_obj.is_actived = False
developer_obj.save() developer_obj.save(update_fields=['certid', 'cert_expire_time', 'is_actived'])
return status, result return status, result
@staticmethod @staticmethod

@ -5,6 +5,7 @@
# date: 2020/4/7 # date: 2020/4/7
from api.models import Apps, UserInfo, AppIOSDeveloperInfo from api.models import Apps, UserInfo, AppIOSDeveloperInfo
from api.utils.mp.wechat import make_wx_auth_obj
from api.utils.storage.storage import Storage from api.utils.storage.storage import Storage
from api.utils.app.supersignutils import IosUtils from api.utils.app.supersignutils import IosUtils
from api.utils.utils import send_ios_developer_active_status from api.utils.utils import send_ios_developer_active_status
@ -82,3 +83,14 @@ def auto_check_ios_developer_active():
logger.error(msg) logger.error(msg)
send_ios_developer_active_status(userinfo, MSGTEMPLATE.get('AUTO_CHECK_DEVELOPER') % ( send_ios_developer_active_status(userinfo, MSGTEMPLATE.get('AUTO_CHECK_DEVELOPER') % (
userinfo.first_name, ios_developer.name)) userinfo.first_name, ios_developer.name))
def sync_wx_access_token():
wx_access_token_key = CACHE_KEY_TEMPLATE.get("wx_access_token_key")
access_token_info = cache.get(wx_access_token_key)
if not access_token_info:
access_token_info = make_wx_auth_obj().get_access_token()
expires_in = access_token_info.get('expires_in')
if expires_in:
cache.set(wx_access_token_key, access_token_info, expires_in - 60)
return access_token_info

@ -0,0 +1,5 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# project: 9月
# author: NinEveN
# date: 2021/9/6

@ -0,0 +1,5 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# project: 9月
# author: NinEveN
# date: 2021/9/6

@ -0,0 +1,86 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# project: 9月
# author: NinEveN
# date: 2021/9/6
import xml.etree.ElementTree as ET
def parse_xml(web_data):
if len(web_data) == 0:
return None
xml_data = ET.fromstring(web_data)
msg_type = xml_data.find('MsgType').text
if msg_type == 'event':
event_type = xml_data.find('Event').text
if event_type == 'CLICK':
return Click(xml_data)
elif event_type in ('subscribe', 'unsubscribe'):
return Subscribe(xml_data)
# elif event_type == 'VIEW':
# return View(xml_data)
# elif event_type == 'LOCATION':
# return LocationEvent(xml_data)
elif event_type == 'SCAN':
return Scan(xml_data)
elif msg_type == 'text':
return TextMsg(xml_data)
elif msg_type == 'image':
return ImageMsg(xml_data)
class Msg(object):
def __init__(self, xml_data):
self.ToUserName = xml_data.find('ToUserName').text
self.FromUserName = xml_data.find('FromUserName').text
self.CreateTime = xml_data.find('CreateTime').text
self.MsgType = xml_data.find('MsgType').text
self.MsgId = xml_data.find('MsgId').text
class TextMsg(Msg):
def __init__(self, xml_data):
Msg.__init__(self, xml_data)
self.Content = xml_data.find('Content').text.encode("utf-8")
class ImageMsg(Msg):
def __init__(self, xml_data):
Msg.__init__(self, xml_data)
self.PicUrl = xml_data.find('PicUrl').text
self.MediaId = xml_data.find('MediaId').text
class EventMsg(object):
def __init__(self, xml_data):
self.ToUserName = xml_data.find('ToUserName').text
self.FromUserName = xml_data.find('FromUserName').text
self.CreateTime = xml_data.find('CreateTime').text
self.MsgType = xml_data.find('MsgType').text
self.Event = xml_data.find('Event').text
self.Eventkey = xml_data.find('EventKey').text
self.Ticket = ''
class Click(EventMsg):
def __init__(self, xml_data):
EventMsg.__init__(self, xml_data)
class Scan(EventMsg):
def __init__(self, xml_data):
EventMsg.__init__(self, xml_data)
self.Ticket = xml_data.find('Ticket').text
class Subscribe(EventMsg):
def __init__(self, xml_data):
EventMsg.__init__(self, xml_data)
tick = xml_data.find('Ticket')
if tick is None:
self.Ticket = ''
else:
self.Ticket = tick.text

@ -0,0 +1,61 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# project: 9月
# author: NinEveN
# date: 2021/9/6
import time
class Msg(object):
def __init__(self):
pass
def send(self):
return "success"
class TextMsg(Msg):
def __init__(self, to_user_name, from_user_name, content):
super().__init__()
self.__dict = dict()
self.__dict['ToUserName'] = to_user_name
self.__dict['FromUserName'] = from_user_name
self.__dict['CreateTime'] = int(time.time())
self.__dict['Content'] = content
def send(self):
xml_form = """
<xml>
<ToUserName><![CDATA[{ToUserName}]]></ToUserName>
<FromUserName><![CDATA[{FromUserName}]]></FromUserName>
<CreateTime>{CreateTime}</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[{Content}]]></Content>
</xml>
"""
return xml_form.format(**self.__dict)
class ImageMsg(Msg):
def __init__(self, to_user_name, from_user_name, media_id):
super().__init__()
self.__dict = dict()
self.__dict['ToUserName'] = to_user_name
self.__dict['FromUserName'] = from_user_name
self.__dict['CreateTime'] = int(time.time())
self.__dict['MediaId'] = media_id
def send(self):
xml_form = """
<xml>
<ToUserName><![CDATA[{ToUserName}]]></ToUserName>
<FromUserName><![CDATA[{FromUserName}]]></FromUserName>
<CreateTime>{CreateTime}</CreateTime>
<MsgType><![CDATA[image]]></MsgType>
<Image>
<MediaId><![CDATA[{MediaId}]]></MediaId>
</Image>
</xml>
"""
return xml_form.format(**self.__dict)

@ -0,0 +1,20 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#########################################################################
# Author: jonyqin
# Created Time: Thu 11 Sep 2014 01:53:58 PM CST
# File Name: ierror.py
# Description:定义错误码含义
#########################################################################
WXBizMsgCrypt_OK = 0
WXBizMsgCrypt_ValidateSignature_Error = -40001
WXBizMsgCrypt_ParseXml_Error = -40002
WXBizMsgCrypt_ComputeSignature_Error = -40003
WXBizMsgCrypt_IllegalAesKey = -40004
WXBizMsgCrypt_ValidateAppid_Error = -40005
WXBizMsgCrypt_EncryptAES_Error = -40006
WXBizMsgCrypt_DecryptAES_Error = -40007
WXBizMsgCrypt_IllegalBuffer = -40008
WXBizMsgCrypt_EncodeBase64_Error = -40009
WXBizMsgCrypt_DecodeBase64_Error = -40010
WXBizMsgCrypt_GenReturnXml_Error = -40011

@ -0,0 +1,220 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# project: 9月
# author: NinEveN
# date: 2021/9/6
import base64
import logging
import string
import random
import struct
import hashlib
from Crypto.Cipher import AES
import xml.etree.cElementTree as ET
import time
import socket
from . import ierror
logger = logging.getLogger(__name__)
class XMLParse:
"""提供提取消息格式中的密文及生成回复消息格式的接口"""
# xml消息模板
AES_TEXT_RESPONSE_TEMPLATE = """<xml>
<Encrypt><![CDATA[%(msg_encrypt)s]]></Encrypt>
<MsgSignature><![CDATA[%(msg_signature)s]]></MsgSignature>
<TimeStamp>%(timestamp)s</TimeStamp>
<Nonce><![CDATA[%(nonce)s]]></Nonce>
</xml>"""
def extract(self, xml_text):
"""提取出xml数据包中的加密消息
@param xml_text: 待提取的xml字符串
@return: 提取出的加密消息字符串
"""
try:
xml_tree = ET.fromstring(xml_text)
encrypt = xml_tree.find("Encrypt")
to_user_name = xml_tree.find("ToUserName")
return ierror.WXBizMsgCrypt_OK, encrypt.text, to_user_name.text
except Exception as e:
logger.error(e)
return ierror.WXBizMsgCrypt_ParseXml_Error, None, None
def generate(self, encrypt, signature, timestamp, nonce):
"""生成xml消息
@param encrypt: 加密后的消息密文
@param signature: 安全签名
@param timestamp: 时间戳
@param nonce: 随机字符串
@return: 生成的xml字符串
"""
resp_dict = {
'msg_encrypt': encrypt,
'msg_signature': signature,
'timestamp': timestamp,
'nonce': nonce,
}
resp_xml = self.AES_TEXT_RESPONSE_TEMPLATE % resp_dict
return resp_xml
class PKCS7Encoder(object):
"""提供基于PKCS7算法的加解密接口"""
block_size = 32
def encode(self, text):
""" 对需要加密的明文进行填充补位
@param text: 需要进行填充补位操作的明文
@return: 补齐明文字符串
"""
text_length = len(text)
# 计算需要填充的位数
amount_to_pad = self.block_size - (text_length % self.block_size)
if amount_to_pad == 0:
amount_to_pad = self.block_size
# 获得补位所用的字符
pad = chr(amount_to_pad)
return text + pad * amount_to_pad
def decode(self, decrypted):
"""删除解密后明文的补位字符
@param decrypted: 解密后的明文
@return: 删除补位字符后的明文
"""
pad = ord(decrypted[-1])
if pad < 1 or pad > 32:
pad = 0
return decrypted[:-pad]
def get_random_str():
""" 随机生成16位字符串
@return: 16位字符串
"""
rule = string.ascii_letters + string.digits
return "".join(random.sample(rule, 16))
class Prpcrypt(object):
"""提供接收和推送给公众平台消息的加解密接口"""
def __init__(self, key):
# self.key = base64.b64decode(key+"=")
self.key = key
# 设置加解密模式为AES的CBC模式
self.mode = AES.MODE_CBC
def encrypt(self, text, app_id):
"""对明文进行加密
:param text: 需要加密的明文
:param app_id: 应用id
@return: 加密得到的字符串
"""
# 16位随机字符串添加到明文开头
text = get_random_str() + struct.pack("I", socket.htonl(len(text))).decode('utf-8') + text + app_id
# 使用自定义的填充方式对明文进行补位填充
pkcs7 = PKCS7Encoder()
text = pkcs7.encode(text)
# 加密
crypto = AES.new(self.key, self.mode, self.key[:16])
try:
ciphertext = crypto.encrypt(text)
# 使用BASE64对加密后的字符串进行编码
return ierror.WXBizMsgCrypt_OK, base64.b64encode(ciphertext)
except Exception as e:
logger.error(e)
return ierror.WXBizMsgCrypt_EncryptAES_Error, None
def decrypt(self, text, app_id):
""" 对解密后的明文进行补位删除
:param text: 密文
:param app_id: 应用id
:return: 删除填充补位后的明文
"""
try:
cryptor = AES.new(self.key, self.mode, self.key[:16])
# 使用BASE64对密文进行解码,然后AES-CBC解密
plain_text = cryptor.decrypt(base64.b64decode(text))
except Exception as e:
logger.error(e)
return ierror.WXBizMsgCrypt_DecryptAES_Error, None
try:
pad = plain_text[-1]
# 去掉补位字符串
# pkcs7 = PKCS7Encoder()
# plain_text = pkcs7.encode(plain_text)
# 去除16位随机字符串
content = plain_text[16:-pad]
xml_len = socket.ntohl(struct.unpack("I", content[: 4])[0])
xml_content = content[4: xml_len + 4]
from_app_id = content[xml_len + 4:].decode('utf-8')
except Exception as e:
logger.error(e)
return ierror.WXBizMsgCrypt_IllegalBuffer, None
if from_app_id != app_id:
return ierror.WXBizMsgCrypt_ValidateAppid_Error, None
return 0, xml_content
class WxMsgCryptBase(object):
def __init__(self, app_id, app_secret, token, encoding_aes_key):
try:
self.key = base64.b64decode(encoding_aes_key + "=")
assert len(self.key) == 32
except Exception as e:
logger.error(f"{encoding_aes_key} 解密失败 Exception:{e}")
raise Exception("[error]: EncodingAESKey invalid !")
self.token = token
self.app_id = app_id
def encrypt_msg(self, msg, nonce, timestamp=None):
"""
:param msg: 企业号待回复用户的消息xml格式的字符串
:param nonce: 随机串可以自己生成也可以用URL参数的nonce
:param timestamp: 时间戳可以自己生成也可以用URL参数的timestamp,如为None则自动用当前时间
:return: 成功0sEncryptMsg,失败返回对应的错误码None
"""
pc = Prpcrypt(self.key)
ret, encrypt = pc.encrypt(msg, self.app_id)
if ret != 0:
return ret, None
if timestamp is None:
timestamp = str(int(time.time()))
try:
sha = hashlib.sha1(("".join(sorted([self.token, timestamp, nonce, encrypt]))).encode('utf-8'))
return ret, XMLParse().generate(encrypt, sha.hexdigest(), timestamp, nonce)
except Exception as e:
logger.error(e)
return ierror.WXBizMsgCrypt_ComputeSignature_Error, None
def decrypt_msg(self, msg, msg_signature, timestamp, nonce):
"""
:param msg: 密文对应POST请求的数据
:param msg_signature: 签名串对应URL参数的msg_signature
:param timestamp: 时间戳对应URL参数的timestamp
:param nonce: 随机串对应URL参数的nonce
:return: 成功0失败返回对应的错误码
"""
if isinstance(msg, str):
ret, encrypt, _ = XMLParse().extract(msg)
if ret != 0:
return ret, None
else:
encrypt = msg.get('Encrypt')
try:
sha = hashlib.sha1(("".join(sorted([self.token, timestamp, nonce, encrypt]))).encode('utf-8'))
if not sha.hexdigest() == msg_signature:
return ierror.WXBizMsgCrypt_ValidateSignature_Error, None
pc = Prpcrypt(self.key)
ret, xml_content = pc.decrypt(encrypt, self.app_id)
return ret, xml_content
except Exception as e:
logger.error(e)
return ierror.WXBizMsgCrypt_ComputeSignature_Error, None

@ -0,0 +1,119 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# project: 9月
# author: NinEveN
# date: 2021/9/6
from hashlib import sha1
import requests
import logging
import json
from fir_ser.settings import THIRDLOGINCONF
from api.utils.mp.utils import WxMsgCryptBase
from api.utils.storage.caches import get_wx_access_token_cache
logger = logging.getLogger(__name__)
wx_login_info = THIRDLOGINCONF.wx_official
def create_menu():
menu_json = {
"button": [
{
"type": "click",
"name": "",
"key": "good"
},
{
"name": "分发平台",
"sub_button": [
{
"type": "view",
"name": "官方地址",
"url": "https://flyapps.cn"
},
{
"type": "view",
"name": "留言反馈",
"url": "https://flyapps.cn/gbook/"
},
]
},
{
"type": "media_id",
"name": "联系我们",
"media_id": "qvQxPuAb4GnUgjkxl2xVnbsnldxawf4DXM09biqgP30"
}
]
}
p_url = f"https://api.weixin.qq.com/cgi-bin/menu/create?access_token={get_wx_access_token_cache()}"
req = requests.post(url=p_url, data=json.dumps(menu_json, ensure_ascii=False).encode('utf-8'))
print(req.json())
def show_qrcode_url(ticket):
return f'https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket={ticket}'
def make_wx_login_qrcode(scene_str='web.login', expire_seconds=600):
"""
:param scene_str: 场景值ID字符串形式的ID字符串类型长度限制为1到64
:param expire_seconds: 该二维码有效时间以秒为单位 最大不超过2592000即30天此字段如果不填则默认有效期为30秒
:return: {
"ticket":"gQH47joAAAAAAAAAASxodHRwOi8vd2VpeGluLnFxLmNvbS9xL2taZ2Z3TVRtNzJXV1Brb3ZhYmJJAAIEZ23sUwMEmm3sUw==",
"expire_seconds":60,
"url":"http://weixin.qq.com/q/kZgfwMTm72WWPkovabbI"
}
https://developers.weixin.qq.com/doc/offiaccount/Account_Management/Generating_a_Parametric_QR_Code.html
"""
t_url = f'https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token={get_wx_access_token_cache()}'
data = {"expire_seconds": expire_seconds, "action_name": "QR_STR_SCENE",
"action_info": {"scene": {"scene_str": scene_str}}}
req = requests.post(t_url, json=data)
if req.status_code == 200:
return True, req.json()
logger.error(f"make wx login qrcode failed {req.status_code} {req.text}")
return False, req.text
def get_userinfo_from_openid(open_id):
t_url = f'https://api.weixin.qq.com/cgi-bin/user/info?access_token={get_wx_access_token_cache()}&openid={open_id}&lang=zh_CN'
req = requests.get(t_url)
if req.status_code == 200:
return True, req.json()
logger.error(f"get userinfo from openid failed {req.status_code} {req.text}")
return False, req.text
class WxOfficialBase(object):
def __init__(self, app_id, app_secret, token, encoding_aes_key):
self.app_id = app_id
self.app_secret = app_secret
self.token = token
self.encoding_aes_key = encoding_aes_key
def get_access_token(self):
t_url = f'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={self.app_id}&secret={self.app_secret}'
req = requests.get(t_url)
if req.status_code == 200:
return req.json()
logger.error(f"get access token failed {req.status_code} {req.text}")
return req.text
def make_wx_auth_obj():
return WxOfficialBase(**wx_login_info.get('auth'))
def check_signature(params):
tmp_list = sorted([wx_login_info.get('auth', {}).get('token'), params.get("timestamp"), params.get("nonce")])
tmp_str = "".join(tmp_list)
tmp_str = sha1(tmp_str.encode("utf-8")).hexdigest()
if tmp_str == params.get("signature"):
return int(params.get("echostr"))
return ''
class WxMsgCrypt(WxMsgCryptBase):
def __init__(self):
super().__init__(**wx_login_info.get('auth'))

@ -570,3 +570,9 @@ class AdminUserCertificationSerializer(serializers.ModelSerializer):
def update(self, instance, validated_data): def update(self, instance, validated_data):
return super(AdminUserCertificationSerializer, self).update(instance, validated_data) return super(AdminUserCertificationSerializer, self).update(instance, validated_data)
class ThirdWxSerializer(serializers.ModelSerializer):
class Meta:
model = models.ThirdWeChatUserInfo
exclude = ["id"]

@ -12,7 +12,7 @@ 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, AUTH_USER_FREE_DOWNLOAD_TIMES USER_FREE_DOWNLOAD_TIMES, AUTH_USER_FREE_DOWNLOAD_TIMES
from api.utils.storage.storage import Storage, LocalStorage from api.utils.storage.storage import Storage, LocalStorage
from api.utils.baseutils import get_app_d_count_by_app_id, get_app_domain_name, check_app_password # file_format_path, from api.utils.baseutils import get_app_d_count_by_app_id, get_app_domain_name, check_app_password
import logging import logging
from django.db.models import F from django.db.models import F
@ -281,7 +281,7 @@ def set_default_app_wx_easy(user_obj, only_clean_cache=False):
else: else:
if not get_app_domain_name(app_obj): if not get_app_domain_name(app_obj):
app_obj.wxeasytype = True app_obj.wxeasytype = True
app_obj.save() app_obj.save(update_fields=['wxeasytype'])
del_cache_response_by_short(app_obj.app_id) del_cache_response_by_short(app_obj.app_id)
@ -405,7 +405,7 @@ def update_order_status(out_trade_no, status):
order_obj = Order.objects.filter(order_number=out_trade_no).first() order_obj = Order.objects.filter(order_number=out_trade_no).first()
if order_obj: if order_obj:
order_obj.status = status order_obj.status = status
order_obj.save() order_obj.save(update_fields=['status'])
def update_order_info(user_id, out_trade_no, payment_number, payment_type): def update_order_info(user_id, out_trade_no, payment_number, payment_type):
@ -460,3 +460,27 @@ def check_app_permission(app_obj, res):
res.msg = "您没有权限访问该应用" res.msg = "您没有权限访问该应用"
return res return res
def get_wx_access_token_cache():
wx_access_token_key = CACHE_KEY_TEMPLATE.get("wx_access_token_key")
access_token = cache.get(wx_access_token_key)
if access_token:
return access_token.get('access_token')
return ''
def set_wx_ticket_login_info_cache(ticket, data=None, expire_seconds=600):
if data is None:
data = {}
wx_ticket_info_key = CACHE_KEY_TEMPLATE.get("wx_ticket_info_key")
cache.set("_".join([wx_ticket_info_key, ticket]), data, expire_seconds)
def get_wx_ticket_login_info_cache(ticket):
wx_ticket_info_key = CACHE_KEY_TEMPLATE.get("wx_ticket_info_key")
wx_t_key = "_".join([wx_ticket_info_key, ticket])
wx_ticket_info = cache.get(wx_t_key)
if wx_ticket_info:
cache.delete(wx_t_key)
return wx_ticket_info

@ -91,7 +91,7 @@ class DomainCnameView(APIView):
app_obj = Apps.objects.filter(app_id=app_id).first() app_obj = Apps.objects.filter(app_id=app_id).first()
if app_obj: if app_obj:
app_obj.wxeasytype = False app_obj.wxeasytype = False
app_obj.save() app_obj.save(update_fields=['wxeasytype'])
del_cache_response_by_short(app_obj.app_id) del_cache_response_by_short(app_obj.app_id)
else: else:
set_default_app_wx_easy(request.user, True) set_default_app_wx_easy(request.user, True)
@ -111,7 +111,7 @@ class DomainCnameView(APIView):
app_obj = Apps.objects.filter(app_id=app_id).first() app_obj = Apps.objects.filter(app_id=app_id).first()
if app_obj and not get_user_domain_name(request.user): if app_obj and not get_user_domain_name(request.user):
app_obj.wxeasytype = True app_obj.wxeasytype = True
app_obj.save() app_obj.save(update_fields=['wxeasytype'])
del_cache_response_by_short(app_obj.app_id) del_cache_response_by_short(app_obj.app_id)
else: else:
set_default_app_wx_easy(request.user) set_default_app_wx_easy(request.user)

@ -2,6 +2,8 @@ from django.contrib import auth
from api.models import Token, UserInfo, UserCertificationInfo, CertificationInfo from api.models import Token, UserInfo, UserCertificationInfo, CertificationInfo
from rest_framework.response import Response from rest_framework.response import Response
from api.utils.mp.wechat import make_wx_login_qrcode, show_qrcode_url
from api.utils.serializer import UserInfoSerializer, CertificationSerializer, UserCertificationSerializer from api.utils.serializer import UserInfoSerializer, CertificationSerializer, UserCertificationSerializer
from django.core.cache import cache from django.core.cache import cache
from rest_framework.views import APIView from rest_framework.views import APIView
@ -12,7 +14,7 @@ from api.utils.baseutils import is_valid_phone, is_valid_email, get_min_default_
from api.utils.auth import ExpiringTokenAuthentication from api.utils.auth import ExpiringTokenAuthentication
from api.utils.response import BaseResponse from api.utils.response import BaseResponse
from fir_ser.settings import REGISTER, LOGIN, CHANGER from fir_ser.settings import REGISTER, LOGIN, CHANGER
from api.utils.storage.caches import login_auth_failed from api.utils.storage.caches import login_auth_failed, set_wx_ticket_login_info_cache, get_wx_ticket_login_info_cache
import logging import logging
from api.utils.geetest.geetest_utils import first_register, second_validate from api.utils.geetest.geetest_utils import first_register, second_validate
from api.utils.throttle import VisitRegister1Throttle, VisitRegister2Throttle, GetAuthC1Throttle, GetAuthC2Throttle from api.utils.throttle import VisitRegister1Throttle, VisitRegister2Throttle, GetAuthC1Throttle, GetAuthC2Throttle
@ -398,6 +400,19 @@ class RegistView(APIView):
return Response(response.dict) return Response(response.dict)
def wx_qr_code_response(ret, code, qr_info):
if code:
logger.info(f"微信登录码获取成功, {qr_info}")
ticket = qr_info.get('ticket')
if ticket:
set_wx_ticket_login_info_cache(ticket)
ret.data = {'qr': show_qrcode_url(ticket), 'ticket': ticket}
else:
ret.code = code
ret.msg = "微信登录码获取失败,请稍后"
return Response(ret.dict)
class UserInfoView(APIView): class UserInfoView(APIView):
authentication_classes = [ExpiringTokenAuthentication, ] authentication_classes = [ExpiringTokenAuthentication, ]
@ -504,6 +519,12 @@ class UserInfoView(APIView):
return Response(res.dict) return Response(res.dict)
def post(self, request):
ret = BaseResponse()
uid = request.user.uid
code, qr_info = make_wx_login_qrcode(f"web.bind.{uid}")
return wx_qr_code_response(ret, code, qr_info)
class AuthorizationView(APIView): class AuthorizationView(APIView):
throttle_classes = [GetAuthC1Throttle, GetAuthC2Throttle] throttle_classes = [GetAuthC1Throttle, GetAuthC2Throttle]
@ -706,3 +727,41 @@ class ChangeInfoView(APIView):
response.data['change_type'] = CHANGER.get("change_type") response.data['change_type'] = CHANGER.get("change_type")
response.data['enable'] = allow_f response.data['enable'] = allow_f
return Response(response.dict) return Response(response.dict)
class WeChatLoginView(APIView):
throttle_classes = [VisitRegister1Throttle, VisitRegister2Throttle]
def get(self, request):
ret = BaseResponse()
code, qr_info = make_wx_login_qrcode()
return wx_qr_code_response(ret, code, qr_info)
class WeChatLoginCheckView(APIView):
def post(self, request):
ret = BaseResponse()
ticket = request.data.get("ticket")
if ticket:
wx_ticket_data = get_wx_ticket_login_info_cache(ticket)
if wx_ticket_data:
if wx_ticket_data == -1:
ret.msg = "还未绑定用户,请通过手机或者邮箱登录账户之后进行绑定"
ret.code = 1005
else:
user = UserInfo.objects.filter(pk=wx_ticket_data).first()
if user.is_active:
key, user_info = set_user_token(user)
serializer = UserInfoSerializer(user_info)
data = serializer.data
ret.msg = "验证成功!"
ret.userinfo = data
ret.token = key
else:
ret.msg = "用户被禁用"
ret.code = 1005
else:
ret.code = 1006
else:
ret.code = 1006
return Response(ret.dict)

@ -0,0 +1,176 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# project: 3月
# author: NinEveN
# date: 2021/3/29
from rest_framework.pagination import PageNumberPagination
from rest_framework.response import Response
import logging
import random
from rest_framework.views import APIView
from rest_framework_xml.parsers import XMLParser
from api.models import ThirdWeChatUserInfo, UserInfo
from api.utils.auth import ExpiringTokenAuthentication
from api.utils.mp.chat import reply, receive
from api.utils.mp.wechat import check_signature, WxMsgCrypt, get_userinfo_from_openid
from api.utils.response import BaseResponse
from api.utils.serializer import ThirdWxSerializer
from api.utils.storage.caches import set_wx_ticket_login_info_cache
logger = logging.getLogger(__name__)
GOOD_XX = [
'您是一位有恒心有毅力的人,我很佩服您!',
'越有内涵的人越虚怀若谷,像您这样有内涵的人我十分敬佩!',
'你像天上的月亮,也像那闪烁的星星,可惜我不是诗人,否则,当写一万首诗来形容你的美丽!',
'据考证,你是世界上最大的宝藏,里面藏满了金子、钻石和名画!',
'虽然你没有一簇樱唇两排贝齿,但你的谈吐高雅脱俗,机智过人,令我折服!',
'您是一位有恒心有毅力的人,我很佩服您!',
'我很荣幸,认识你这样有内涵的漂亮朋友!',
'春花秋月,是诗人们歌颂的情景,可是我对于它,却感到十分平凡。只有你嵌着梨涡的笑容,才是我眼中最美的景象!',
'你像一片轻柔的云在我眼前飘来飘去,你清丽秀雅的脸上荡漾着春天般美丽的笑容。在你那双又大又亮的眼睛里,我总能捕捉到你的宁静,你的热烈,你的聪颖,你的敏感!',
'人生旅程上,您丰富我的心灵,开发我得智力,为我点燃了希望的光芒,谢谢您!',
]
class TextXMLParser(XMLParser):
media_type = 'text/xml' # 微信解析的是 text/xml
def reply_login_msg(rec_msg, to_user, from_user, ):
content = f'还未绑定用户,请通过手机或者邮箱登录账户之后进行绑定'
u_data_id = -1
wx_user_obj = ThirdWeChatUserInfo.objects.filter(openid=to_user).first()
if wx_user_obj:
u_data_id = wx_user_obj.user_id.pk
content = f'用户 {wx_user_obj.user_id.first_name} 登录成功'
set_wx_ticket_login_info_cache(rec_msg.Ticket, u_data_id)
reply_msg = reply.TextMsg(to_user, from_user, content)
return reply_msg.send()
def update_or_create_wx_userinfo(to_user, user_obj):
code, wx_user_info = get_userinfo_from_openid(to_user)
logger.info(f"get openid:{to_user} info:{to_user} code:{code}")
if code:
wx_user_info = {
'openid': wx_user_info.get('openid'),
'nickname': wx_user_info.get('nickname'),
'sex': wx_user_info.get('sex'),
'subscribe_time': wx_user_info.get('subscribe_time'),
'head_img_url': wx_user_info.get('headimgurl'),
'address': f"{wx_user_info.get('country')}-{wx_user_info.get('province')}-{wx_user_info.get('city')}",
'subscribe': wx_user_info.get('subscribe'),
}
ThirdWeChatUserInfo.objects.update_or_create(user_id=user_obj, openid=to_user, defaults=wx_user_info)
class ValidWxChatToken(APIView):
parser_classes = (XMLParser, TextXMLParser)
def get(self, request):
params = request.query_params
return Response(check_signature(params))
def post(self, request):
params = request.query_params
data = request.data
encrypt_obj = WxMsgCrypt()
ret, encrypt_xml = encrypt_obj.decrypt_msg(data, params.get("msg_signature"), params.get("timestamp"),
params.get("nonce"))
logger.info(f"code:{ret}, result {encrypt_xml}")
result = "success"
if ret == 0:
content = '欢迎使用fly应用分发平台,感谢您的关注'
rec_msg = receive.parse_xml(encrypt_xml)
if isinstance(rec_msg, receive.Msg):
to_user = rec_msg.FromUserName
from_user = rec_msg.ToUserName
if rec_msg.MsgType == 'text':
content = random.choices([*GOOD_XX, content, rec_msg.Content.decode('utf-8')])[0]
reply_msg = reply.TextMsg(to_user, from_user, content)
result = reply_msg.send()
elif rec_msg.MsgType == 'image':
media_id = rec_msg.MediaId
reply_msg = reply.ImageMsg(to_user, from_user, media_id)
result = reply_msg.send()
else:
result = reply.Msg().send()
elif isinstance(rec_msg, receive.EventMsg):
to_user = rec_msg.FromUserName
from_user = rec_msg.ToUserName
if rec_msg.Event == 'CLICK': # 公众号点击事件
if rec_msg.Eventkey == 'good':
content = random.choices(GOOD_XX)[0]
reply_msg = reply.TextMsg(to_user, from_user, content)
result = reply_msg.send()
elif rec_msg.Event in ['subscribe', 'unsubscribe']: # 订阅
reply_msg = reply.TextMsg(to_user, from_user, content)
result = reply_msg.send()
if rec_msg.Eventkey == 'qrscene_web.login': # 首次关注,登录认证操作
if rec_msg.Event == 'subscribe':
result = reply_login_msg(rec_msg, to_user, from_user, )
if rec_msg.Event == 'unsubscribe':
ThirdWeChatUserInfo.objects.filter(openid=to_user).update(subscribe=False)
elif rec_msg.Event == 'SCAN':
if rec_msg.Eventkey == 'web.login': # 已经关注,然后再次扫码,登录认证操作
result = reply_login_msg(rec_msg, to_user, from_user, )
elif rec_msg.Eventkey.startswith('web.bind.'):
uid = rec_msg.Eventkey.split('.')[-1]
wx_user_obj = ThirdWeChatUserInfo.objects.filter(openid=to_user).first()
user_obj = UserInfo.objects.filter(uid=uid).first()
update_or_create_wx_userinfo(to_user, user_obj)
if wx_user_obj:
if user_obj and user_obj.uid == wx_user_obj.user_id.uid:
content = f'账户 {wx_user_obj.user_id.first_name} 已经绑定成功,感谢您的使用'
else:
content = f'账户已经被 {wx_user_obj.user_id.first_name} 绑定'
else:
content = f'账户绑定 {wx_user_obj.user_id.first_name} 成功'
set_wx_ticket_login_info_cache(rec_msg.Ticket, user_obj.pk)
reply_msg = reply.TextMsg(to_user, from_user, content)
result = reply_msg.send()
else:
logger.error('密文解密失败')
return Response(result)
class PageNumber(PageNumberPagination):
page_size = 10 # 每页显示多少条
page_size_query_param = 'size' # URL中每页显示条数的参数
page_query_param = 'page' # URL中页码的参数
max_page_size = None # 最大页码数限制
class ThirdWxAccount(APIView):
authentication_classes = [ExpiringTokenAuthentication, ]
def get(self, request):
res = BaseResponse()
# wx_open_id = request.query_params.get("openid", None)
wx_obj_lists = ThirdWeChatUserInfo.objects.filter(user_id=request.user)
# if wx_open_id:
# wx_obj_lists = wx_obj_lists.filter(openid=wx_open_id)
page_obj = PageNumber()
info_serializer = page_obj.paginate_queryset(queryset=wx_obj_lists.order_by("-subscribe_time"),
request=request,
view=self)
wx_user_info = ThirdWxSerializer(info_serializer, many=True, )
res.data = wx_user_info.data
res.count = wx_obj_lists.count()
return Response(res.dict)
def delete(self, request):
openid = request.query_params.get("openid")
user_id = request.query_params.get("user_id")
if openid and user_id:
ThirdWeChatUserInfo.objects.filter(user_id_id=user_id, openid=openid).delete()
return self.get(request)

@ -64,6 +64,19 @@ class CACHECONF(object):
password = '' password = ''
class THIRDLOGINCONF(object):
wx_official = {
'name': 'wx_official',
'auth': {
'app_id': 'we6',
'app_secret': '5bfb678',
'token': 'f0ae1b879b8',
'encoding_aes_key': '7b9URovp83gG',
},
'active': True
}
class AUTHCONF(object): class AUTHCONF(object):
# 注册方式,如果启用sms或者email 需要配置 THIRD_PART_CONFIG.sender 信息 # 注册方式,如果启用sms或者email 需要配置 THIRD_PART_CONFIG.sender 信息
REGISTER = { REGISTER = {

@ -209,7 +209,9 @@ CACHE_KEY_TEMPLATE = {
'upload_file_tmp_name_key': 'upload_file_tmp_name', 'upload_file_tmp_name_key': 'upload_file_tmp_name',
'login_failed_try_times_key': 'login_failed_try_times', 'login_failed_try_times_key': 'login_failed_try_times',
'user_free_download_times_key': 'user_free_download_times', 'user_free_download_times_key': 'user_free_download_times',
'super_sign_failed_send_msg_times_key': 'super_sign_failed_send_msg_times' 'super_sign_failed_send_msg_times_key': 'super_sign_failed_send_msg_times',
'wx_access_token_key': 'wx_basic_access_token',
'wx_ticket_info_key': 'wx_ticket_info',
} }
DATA_DOWNLOAD_KEY = "d_token" DATA_DOWNLOAD_KEY = "d_token"
@ -219,6 +221,7 @@ AUTH_USER_FREE_DOWNLOAD_TIMES = 60
SYNC_CACHE_TO_DATABASE = { SYNC_CACHE_TO_DATABASE = {
'download_times': 10, # 下载次数同步时间 'download_times': 10, # 下载次数同步时间
'wx_get_access_token_times': 60 * 10, # 微信access_token 自动获取时间
'try_login_times': (10, 12 * 60 * 60), # 当天登录失败次数,超过该失败次数,锁定24小时 'try_login_times': (10, 12 * 60 * 60), # 当天登录失败次数,超过该失败次数,锁定24小时
'auto_clean_tmp_file_times': 60 * 30, # 定时清理上传失误生成的临时文件 'auto_clean_tmp_file_times': 60 * 30, # 定时清理上传失误生成的临时文件
'auto_clean_local_tmp_file_times': 60 * 30, # 定时清理临时文件,现在包含超级签名描述临时文件 'auto_clean_local_tmp_file_times': 60 * 30, # 定时清理临时文件,现在包含超级签名描述临时文件
@ -377,6 +380,11 @@ CELERY_BEAT_SCHEDULE = {
'args': (), 'args': (),
'one_off': True 'one_off': True
}, },
'sync_wx_access_token_job': {
'task': 'api.tasks.sync_wx_access_token_job',
'schedule': SYNC_CACHE_TO_DATABASE.get("wx_get_access_token_times"),
'args': (),
},
} }
MSGTEMPLATE = { MSGTEMPLATE = {

Loading…
Cancel
Save