Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

新增强制全局2FA开关 #1490

Merged
merged 1 commit into from
May 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 28 additions & 9 deletions common/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,23 +105,42 @@ def authenticate_entry(request):
if result['status'] == 0:
authenticated_user = result['data']
twofa_enabled = TwoFactorAuthConfig.objects.filter(user=authenticated_user)
if twofa_enabled:
# 用户设置了2fa的情况需要进一步验证
auth_type = twofa_enabled[0].auth_type
# 是否开启全局2fa
if SysConfig().get('enforce_2fa'):
# 用户是否配置过2fa
if twofa_enabled:
auth_type = twofa_enabled[0].auth_type
verify_mode = 'verify_only'
else:
auth_type = 'totp'
verify_mode = 'verify_config'
# 设置无登录状态cookie
s = SessionStore()
s['user'] = authenticated_user.username
s['auth_type'] = auth_type
s['verify_mode'] = verify_mode
s.set_expiry(300)
s.create()
result = {'status': 0, 'msg': 'ok', 'data': s.session_key}
else:
# 未设置2fa直接登录
login(request, authenticated_user)
# 从钉钉获取该用户的 dingding_id,用于单独给他发消息
if SysConfig().get("ding_to_person") is True and "admin" not in request.POST.get('username'):
get_ding_user_id(request.POST.get('username'))
result = {'status': 0, 'msg': 'ok', 'data': None}
# 用户是否配置过2fa
if twofa_enabled:
auth_type = twofa_enabled[0].auth_type
# 设置无登录状态cookie
s = SessionStore()
s['user'] = authenticated_user.username
s['auth_type'] = auth_type
s['verify_mode'] = 'verify_only'
s.set_expiry(300)
s.create()
result = {'status': 0, 'msg': 'ok', 'data': s.session_key}
else:
# 未设置2fa直接登录
login(request, authenticated_user)
# 从钉钉获取该用户的 dingding_id,用于单独给他发消息
if SysConfig().get("ding_to_person") is True and "admin" not in request.POST.get('username'):
get_ding_user_id(request.POST.get('username'))
result = {'status': 0, 'msg': 'ok', 'data': None}

return HttpResponse(json.dumps(result), content_type='application/json')

Expand Down
3 changes: 2 additions & 1 deletion common/middleware/check_login_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@ def process_request(request):
"""
if not request.user.is_authenticated:
# 以下是不用跳转到login页面的url白名单
if request.path not in IGNORE_URL and re.match(IGNORE_URL_RE, request.path) is None:
if request.path not in IGNORE_URL and re.match(IGNORE_URL_RE, request.path) is None \
and not (re.match(r'/user/qrcode/\w+', request.path) and request.session.get('user')):
return HttpResponseRedirect('/login/')
124 changes: 110 additions & 14 deletions common/templates/2fa.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- 引入 Bootstrap -->
<link href="{% static 'bootstrap/css/bootstrap.min.css' %}" rel="stylesheet">
<link href="{% static 'bootstrap-select/css/bootstrap-select.min.css' %}" rel="stylesheet">
<link href="{% static 'dist/css/login.css' %}" rel="stylesheet">
<!-- HTML5 Shim 和 Respond.js 用于让 IE8 支持 HTML5元素和媒体查询 -->
<!-- 注意: 如果通过 file:// 引入 Respond.js 文件,则该文件无法起效果 -->
Expand All @@ -17,29 +18,55 @@
</head>
<body onload="document.getElementById('otpCode').focus()" style="background-color:#edeff1;">
<div class="row lsb-login">
<div class="col-sm-2 mypanalbox">
<div class="col-sm-3 mypanalbox">
<form class="login-form fade-in-effect" id="auth" method="post" role="form">
{% csrf_token %}
{% if auth_type == 'totp' %}
<div class="form-group is-focused">
<label class="control-label" for="otpCode">OTP验证码</label>
<input class="form-control ng-valid ng-dirty ng-touched" id="otpCode" name="otpCode" type="text"
oninput="value=value.replace(/[^\d]/g,'')" autocomplete="off" required>

{% if verify_mode == 'verify_config' %}
<div class="form-group">
<h4 style="font-weight: bold">启用两步验证</h4>
</div>
<div class="form-group">
<button id="btnAuth" type="button" class="btn btn-success btn-block"><i class="fa-lock"></i>验证</button>
<label for="auth_type">验证方式:</label>
<select id="auth_type" class="form-control show-tick selectpicker" name="instances"
title="选择额外验证方式:"
data-live-search="true">
<option value="totp" selected="selected">Google身份验证器</option>
</select>
</div>
{% else %}
<div class="form-group is-focused">
<label class="control-label" for="otpCode">验证码</label>
<div class="form-group" style="display: grid">
<label class="control-label" for="qrcode-img">1. 使用Google身份验证器扫码:</label>
<img id="qrcode-img" key="" src="">
</div>
<div class="form-group">
<label class="control-label" for="otpCode">2. 输入6位验证码完成验证:</label>
<input class="form-control ng-valid ng-dirty ng-touched" id="otpCode" name="otpCode" type="text"
oninput="value=value.replace(/[^\d]/g,'')" autocomplete="off" required>
</div>
<div class="form-group">
<button id="btnCaptcha" type="button" class="btn btn-default btn-block" >获取验证码</button>
<button id="btnAuth" type="button" class="btn btn-success btn-block" style="display: none"><i class="fa-lock"></i>验证</button>
<button id="btnAuth" type="button" class="btn btn-success btn-block"><i class="fa-lock"></i>验证</button>
</div>
{% else %}
{% if auth_type == 'totp' %}
<div class="form-group is-focused">
<label class="control-label" for="otpCode">OTP验证码</label>
<input class="form-control ng-valid ng-dirty ng-touched" id="otpCode" name="otpCode" type="text"
oninput="value=value.replace(/[^\d]/g,'')" autocomplete="off" required>

</div>
<div class="form-group">
<button id="btnAuth" type="button" class="btn btn-success btn-block"><i class="fa-lock"></i>验证</button>
</div>
{% else %}
<div class="form-group is-focused">
<label class="control-label" for="otpCode">验证码</label>
<input class="form-control ng-valid ng-dirty ng-touched" id="otpCode" name="otpCode" type="text"
oninput="value=value.replace(/[^\d]/g,'')" autocomplete="off" required>
</div>
<div class="form-group">
<button id="btnCaptcha" type="button" class="btn btn-default btn-block" >获取验证码</button>
<button id="btnAuth" type="button" class="btn btn-success btn-block" style="display: none"><i class="fa-lock"></i>验证</button>
</div>
{% endif %}
{% endif %}
<input type="text" style="display:none">
</form>
Expand All @@ -52,6 +79,8 @@
</div>
<script src="{% static 'jquery/jquery.min.js' %}"></script>
<script src="{% static 'bootstrap/js/bootstrap.min.js' %}"></script>
<script src="{% static 'bootstrap-select/js/bootstrap-select.min.js' %}"></script>
<script src="{% static 'bootstrap-select/js/i18n/defaults-zh_CN.min.js' %}"></script>
</body>
<!-- 解决CSRF-->
<script>
Expand Down Expand Up @@ -90,24 +119,91 @@
});
});

$(document).ready(function () {
if ('{{ verify_mode }}' === 'verify_config') {
let data = config_2fa();
$("#qrcode-img").attr("key", data.data.key)
$("#qrcode-img").attr("src", "/user/qrcode/" + data.data.key)
}
})

$('#btnAuth').click(function () {
let otp = $('#otpCode').val();
authOTP(otp);
});

function config_2fa() {
// 配置2fa
let result;
$.ajax({
type: "post",
url: "/api/v1/user/2fa/",
dataType: "json",
data: {
engineer: '{{ username }}',
auth_type: $("#auth_type").val()
},
async: false,
complete: function () {
},
success: function (data) {
if (data.status === 0) {
result = data
} else {
alert(data.msg);
}
},
error: function (XMLHttpRequest, textStatus, errorThrown) {
alert(errorThrown + ' : ' + XMLHttpRequest.responseText);
}
})
return result
}

function save(key) {
$.ajax({
type: "post",
url: "/api/v1/user/2fa/save/",
dataType: "json",
headers: {"X-CSRFToken": getCookie("csrftoken")},
data: {
engineer: '{{ username }}',
key: key,
},
complete: function () {
},
success: function (data) {
if (data.status === 0) {
alert("已开启两步验证!");
} else {
alert(data.msg)
}
},
error: function (XMLHttpRequest, textStatus, errorThrown) {
alert(errorThrown + ' : ' + XMLHttpRequest.responseText)
}
});
}

function authOTP(otp) {
let key = $("#qrcode-img").attr('key');
$.ajax({
type: "post",
url: "/api/v1/user/2fa/verify/",
dataType: "json",
data: {
engineer: '{{ username }}',
otp: otp
auth_type: $("#auth_type").val(),
otp: otp,
key: key
},
complete: function () {
},
success: function (data) {
if (data.status === 0) {
if ('{{ verify_mode }}' === 'verify_config') {
save(key);
}
$(location).attr('href', '/index/');
} else {
alert(data.msg)
Expand Down
13 changes: 13 additions & 0 deletions common/templates/config.html
Original file line number Diff line number Diff line change
Expand Up @@ -845,6 +845,19 @@ <h4 style="color: darkgrey"><b>其他配置</b></h4>
</div>
</div>
</div>
<div class="form-group">
<label for="enforce_2fa"
class="col-sm-4 control-label">ENFORCE_2FA</label>
<div class="col-sm-8">
<div class="switch switch-small">
<label>
<input id="enforce_2fa" key="enforce_2fa"
value="{{ config.enforce_2fa }}"
type="checkbox"> 登录是否强制使用2FA
</label>
</div>
</div>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-4 col-sm-10">
Expand Down
3 changes: 2 additions & 1 deletion common/twofa/totp.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,9 @@ def auth_type(self):

def generate_qrcode(request, data):
"""生成并返回二维码图片流"""
user = request.user

username = request.user.username
username = user.username if user.is_authenticated else request.session.get('user')
secret_key = data

# 生成二维码
Expand Down
3 changes: 2 additions & 1 deletion sql/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,11 @@ def twofa(request):
username = request.session.get('user')
if username:
auth_type = request.session.get('auth_type')
verify_mode = request.session.get('verify_mode')
else:
return HttpResponseRedirect('/login/')

return render(request, '2fa.html', context={'auth_type': auth_type, 'username': username})
return render(request, '2fa.html', context={'verify_mode': verify_mode, 'auth_type': auth_type, 'username': username})


@permission_required('sql.menu_dashboard', raise_exception=True)
Expand Down
18 changes: 14 additions & 4 deletions sql_api/api_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ class TwoFA(views.APIView):
"""
配置2fa
"""
permission_classes = [IsOwner]
permission_classes = [permissions.AllowAny]

@extend_schema(summary="配置2fa",
request=TwoFASerializer,
Expand All @@ -254,6 +254,13 @@ def post(self, request):
engineer = request.data['engineer']
auth_type = request.data['auth_type']
user = Users.objects.get(username=engineer)
request_user = request.session.get('user')

if request_user:
if request_user != engineer:
return Response({'status': 1, 'msg': '登录用户与校验用户不一致!'})
else:
return Response({'status': 1, 'msg': '需先校验用户密码!'})

if auth_type == 'disabled':
# 关闭2fa
Expand Down Expand Up @@ -317,7 +324,6 @@ def post(self, request):
user = Users.objects.get(username=engineer)
request_user = request.session.get('user')

print(request.user)
if not request.user.is_authenticated:
if request_user:
if request_user != engineer:
Expand All @@ -327,8 +333,12 @@ def post(self, request):

twofa_config = TwoFactorAuthConfig.objects.filter(user=user)
if not twofa_config:
return Response({'status': 1, 'msg': '用户未配置2FA!'})
auth_type = twofa_config[0].auth_type
if key:
auth_type = request.data['auth_type']
else:
return Response({'status': 1, 'msg': '用户未配置2FA!'})
else:
auth_type = twofa_config[0].auth_type
else:
auth_type = request.data['auth_type']

Expand Down
8 changes: 7 additions & 1 deletion sql_api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,15 @@ class TwoFAVerifySerializer(serializers.Serializer):

def validate(self, attrs):
engineer = attrs.get('engineer')
key = attrs.get('key')
auth_type = attrs.get('auth_type')

if key:
if not auth_type:
raise serializers.ValidationError({"errors": "缺少 auth_type"})

try:
user = Users.objects.get(username=engineer)
Users.objects.get(username=engineer)
except Users.DoesNotExist:
raise serializers.ValidationError({"errors": "不存在该用户"})

Expand Down