Skip to content

Commit

Permalink
新增强制全局2FA开关 (hhyo#1490)
Browse files Browse the repository at this point in the history
  • Loading branch information
nick2wang authored May 7, 2022
1 parent 346f799 commit a5c090b
Show file tree
Hide file tree
Showing 8 changed files with 178 additions and 31 deletions.
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

0 comments on commit a5c090b

Please sign in to comment.