Skip to content

Commit

Permalink
登录支持2FA (#1481)
Browse files Browse the repository at this point in the history
* 新增2FA验证

* 修改登录流程

* 新增认证接口

* 更新依赖&单元测试

* 优化二维码图片删除

* 删除多余引入

* 敏感字段加密

* 修改菜单名称

* 修改菜单名称2

* 优化

1.二维码不保存在本地
2.保存2FA配置前先进行一次验证
  • Loading branch information
nick2wang authored May 2, 2022
1 parent e66c77f commit 21de2d2
Show file tree
Hide file tree
Showing 18 changed files with 817 additions and 16 deletions.
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>
Loading

0 comments on commit 21de2d2

Please sign in to comment.