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 #1481

Merged
merged 11 commits into from
May 2, 2022
Merged
Show file tree
Hide file tree
Changes from 6 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
29 changes: 21 additions & 8 deletions common/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import traceback

import simplejson as json
from django.contrib.sessions.backends.db import SessionStore
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.password_validation import validate_password
from django.core.exceptions import ValidationError
Expand All @@ -12,7 +13,7 @@

from common.config import SysConfig
from common.utils.ding_api import get_ding_user_id
from sql.models import Users, ResourceGroup
from sql.models import Users, ResourceGroup, TwoFactorAuthConfig

logger = logging.getLogger('default')

Expand Down Expand Up @@ -64,7 +65,6 @@ def authenticate(self):
if authenticated_user:
# ldap 首次登录逻辑
init_user(authenticated_user)
login(self.request, authenticated_user)
return {'status': 0, 'msg': 'ok', 'data': authenticated_user}
else:
return {'status': 1, 'msg': '用户名或密码错误,请重新输入!', 'data': ''}
Expand All @@ -90,7 +90,6 @@ def authenticate(self):
if authenticated_user:
if not authenticated_user.last_login:
init_user(authenticated_user)
login(self.request, authenticated_user)
return {'status': 0, 'msg': 'ok', 'data': authenticated_user}
user.failed_login_count += 1
user.last_login_failed_at = datetime.datetime.now()
Expand All @@ -104,11 +103,25 @@ def authenticate_entry(request):
new_auth = ArcheryAuth(request)
result = new_auth.authenticate()
if result['status'] == 0:
# 从钉钉获取该用户的 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}
authenticated_user = result['data']
twofa_enabled = TwoFactorAuthConfig.objects.filter(user=authenticated_user)
if twofa_enabled:
# 用户设置了2fa的情况需要进一步验证
auth_type = twofa_enabled[0].auth_type
# 设置无登录状态cookie
s = SessionStore()
s['user'] = authenticated_user.username
s['auth_type'] = auth_type
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
1 change: 1 addition & 0 deletions common/middleware/check_login_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

IGNORE_URL = [
'/login/',
'/login/2fa/',
'/authenticate/',
'/signup/',
'/api/info'
Expand Down
122 changes: 122 additions & 0 deletions common/templates/2fa.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
<!DOCTYPE html>
<html>
<head>
<title>Archery - 两步验证</title>
{% load static %}
<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 'dist/css/login.css' %}" rel="stylesheet">
<!-- HTML5 Shim 和 Respond.js 用于让 IE8 支持 HTML5元素和媒体查询 -->
<!-- 注意: 如果通过 file:// 引入 Respond.js 文件,则该文件无法起效果 -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
<script src="https://oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
<![endif]-->
<link rel="shortcut icon" href="{% static 'img/favicon.ico' %}" />
</head>
<body onload="document.getElementById('otpCode').focus()" style="background-color:#edeff1;">
<div class="row lsb-login">
<div class="col-sm-2 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>

</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 %}
<input type="text" style="display:none">
</form>
</div>
</div>

<!--底部部分 -->
<div class="user-bottom-div">
<p><strong>&copy; Archery</strong>&nbsp;(v{{ archery_version }})</p>
</div>
<script src="{% static 'jquery/jquery.min.js' %}"></script>
<script src="{% static 'bootstrap/js/bootstrap.min.js' %}"></script>
</body>
<!-- 解决CSRF-->
<script>
$(function () {
$.ajaxSetup({
headers: {"X-CSRFToken": getCookie("csrftoken")}
});
});

function getCookie(name) {
var cookieValue = null;
if (document.cookie && document.cookie !== '') {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = jQuery.trim(cookies[i]);
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
</script>
<script>

//回车键提交验证
$(document).ready(function () {
$(document).keydown(function (event) {
//keycode==13为回车键
if (event.keyCode === 13) {
let otp = $('#otpCode').val();
authOTP(otp);
}
});
});

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

function authOTP(otp) {
$.ajax({
type: "post",
url: "/api/v1/user/2fa/verify/",
dataType: "json",
data: {
engineer: '{{ username }}',
otp: otp
},
complete: function () {
},
success: function (data) {
if (data.status === 0) {
$(location).attr('href', '/index/');
} else {
alert(data.msg)
}
},
error: function (XMLHttpRequest, textStatus, errorThrown) {
alert(errorThrown + ' : ' + XMLHttpRequest.responseText)
}
});
};
</script>
</html>
142 changes: 142 additions & 0 deletions common/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
<a target="_blank" href="/admin"><i class="fa fa-sitemap fa-fw"></i> 管理后台</a>
</li>
{% endif %}
<li><a id="2fa-menu" href="javascript:;"><i class="fa fa-credit-card fa-fw"></i> 双重认证</a></li>
<li><a href="/admin/password_change/"><i class="fa fa-user fa-fw"></i> 修改密码</a></li>
<li><a href="/logout/"><i class="fa fa-gear fa-fw"></i> 退出</a></li>
</ul>
Expand Down Expand Up @@ -279,6 +280,59 @@
<!-- /.navbar-static-side -->
</nav>

<!-- 双重认证模态框-->
<div class="modal fade" id="2fa" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
<div class="modal-dialog modal-sm" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span
aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="myModalLabel1">双重认证</h4>
</div>
<div class="modal-body">
<div class="form-group">
<label for="auth_type">认证方式:</label>
<select id="auth_type" class="form-control show-tick selectpicker" name="instances"
title="选择额外认证方式:"
data-live-search="true">
<option value="disabled">关闭双重认证</option>
<option value="totp" selected="selected">Google身份验证器</option>
</select>
</div>
<div class="form-group">
<label for="passwd">需输入密码验证:</label>
<input type="password" class="form-control" id="passwd" placeholder="确认密码" required>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">取消</button>
<button id="btnConfirm" type="submit" class="btn btn-success">确认</button>
</div>
</div>
</div>
</div>

<!-- 二维码模态框-->
<div class="modal fade bs-example-modal-sm" id="qrcode" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
<div class="modal-dialog modal-sm" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span
aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="myModalLabel2">请使用Google身份验证器扫码</h4>
</div>
<div class="modal-body">
<div class="form-group">
<img id="qrcode-img" src="">
</div>
</div>
<div class="modal-footer">
<button type="button" data-dismiss="modal" class="btn btn-success">完成</button>
</div>
</div>
</div>
</div>

<!-- Page Content -->
<div id="page-wrapper">
<div class="clearfix">
Expand Down Expand Up @@ -319,6 +373,94 @@
}
};
</script>
<script>
function twofa(auth_type) {
// 配置2fa
let result;
$.ajax({
type: "post",
url: "/api/v1/user/2fa/",
dataType: "json",
data: {
engineer: '{{ user.username }}',
auth_type: auth_type
},
async: false,
complete: function () {
},
success: function (data) {
if (data.status === 0) {
result = data
$("#2fa").modal('hide');
} else {
alert(data.msg);
}
},
error: function (XMLHttpRequest, textStatus, errorThrown) {
alert(errorThrown + ' : ' + XMLHttpRequest.responseText);
}
})
return result
}

function auth(username, password) {
// 用户认证校验
let result = false;
$.ajax({
type: "post",
url: "/api/v1/user/auth/",
dataType: "json",
data: {
engineer: username,
password: password
},
async: false,
complete: function () {
},
success: function (data) {
if (data.status === 0) {
$("#2fa").modal('hide');
result = true
} else {
alert(data.msg);
}
},
error: function (XMLHttpRequest, textStatus, errorThrown) {
alert(errorThrown + ' : ' + XMLHttpRequest.responseText);
}
});
return result
}

$("#2fa-menu").click(function () {
$("#2fa").modal('show');
})

$("#btnConfirm").click(function () {
let auth_type = $("#auth_type").val();
let password = $("#passwd").val();
password = password.replace(/(^\s*)|(\s*$)/g, "");
if (!password) {
alert('请输入密码!')
return
}
$("#passwd").val('');
let isAuthenticated = auth('{{ user.username }}', password);
if (isAuthenticated) {
let data = twofa(auth_type);
if (data.status === 0) {
alert("配置成功!")
if (auth_type === 'totp') {
// 展示二维码
$("#qrcode-img").attr("src", "/static/" + data.src)
$("#qrcode").modal("show");
}
} else {
alert(data.msg)
}
}
})
</script>
</body>
<script>
<!-- 解决CSRF-->
Expand Down
9 changes: 7 additions & 2 deletions common/templates/login.html
Original file line number Diff line number Diff line change
Expand Up @@ -216,8 +216,13 @@ <h4 class="modal-title" id="myModalLabel">
$('#btnLogin').prop('disabled', false);
},
success: function (data) {
if (data.status == 0) {
$(location).attr('href', '/index/');
if (data.status === 0) {
if (data.data) {
document.cookie = "sessionid=" + data.data
$(location).attr('href', '/login/2fa/');
} else {
$(location).attr('href', '/index/');
}
} else {
$('#wrongpwd-modal-body').html(data.msg);
$('#wrongpwd-modal').modal({
Expand Down
Loading