mirror of
https://github.com/QingdaoU/OnlineJudge.git
synced 2024-09-21 08:23:20 +00:00
增加两步验证基础代码
This commit is contained in:
parent
fbbc3558b9
commit
2222a56d0a
25
account/migrations/0016_auto_20151211_2230.py
Normal file
25
account/migrations/0016_auto_20151211_2230.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9 on 2015-12-11 14:30
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('account', '0015_userprofile_student_id'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='tfa_token',
|
||||||
|
field=models.CharField(blank=True, max_length=10, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='two_factor_auth',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
@ -40,6 +40,9 @@ class User(AbstractBaseUser):
|
|||||||
reset_password_token_create_time = models.DateTimeField(blank=True, null=True)
|
reset_password_token_create_time = models.DateTimeField(blank=True, null=True)
|
||||||
# 论坛授权token
|
# 论坛授权token
|
||||||
auth_token = models.CharField(max_length=40, blank=True, null=True)
|
auth_token = models.CharField(max_length=40, blank=True, null=True)
|
||||||
|
# 是否开启两步验证
|
||||||
|
two_factor_auth = models.BooleanField(default=False)
|
||||||
|
tfa_token = models.CharField(max_length=10, blank=True, null=True)
|
||||||
|
|
||||||
USERNAME_FIELD = 'username'
|
USERNAME_FIELD = 'username'
|
||||||
REQUIRED_FIELDS = []
|
REQUIRED_FIELDS = []
|
||||||
|
@ -84,3 +84,7 @@ class UserProfileSerializer(serializers.ModelSerializer):
|
|||||||
model = UserProfile
|
model = UserProfile
|
||||||
fields = ["avatar", "blog", "mood", "hduoj_username", "bestcoder_username", "codeforces_username",
|
fields = ["avatar", "blog", "mood", "hduoj_username", "bestcoder_username", "codeforces_username",
|
||||||
"rank", "accepted_number", "submissions_number", "problems_status", "phone_number", "school", "student_id"]
|
"rank", "accepted_number", "submissions_number", "problems_status", "phone_number", "school", "student_id"]
|
||||||
|
|
||||||
|
|
||||||
|
class ApplyTwoFactorAuthSerializer(serializers.Serializer):
|
||||||
|
code = serializers.IntegerField()
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
# coding=utf-8
|
# coding=utf-8
|
||||||
import codecs
|
import codecs
|
||||||
|
import qrcode
|
||||||
|
import StringIO
|
||||||
from django import http
|
from django import http
|
||||||
from django.contrib import auth
|
from django.contrib import auth
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponse
|
||||||
from django.core.exceptions import MultipleObjectsReturned
|
from django.core.exceptions import MultipleObjectsReturned
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
|
|
||||||
@ -15,6 +17,7 @@ from utils.shortcuts import (serializer_invalid_response, error_response,
|
|||||||
success_response, error_page, paginate, rand_str)
|
success_response, error_page, paginate, rand_str)
|
||||||
from utils.captcha import Captcha
|
from utils.captcha import Captcha
|
||||||
from utils.mail import send_email
|
from utils.mail import send_email
|
||||||
|
from utils.otp_auth import OtpAuth
|
||||||
|
|
||||||
from .decorators import login_required
|
from .decorators import login_required
|
||||||
from .models import User, UserProfile
|
from .models import User, UserProfile
|
||||||
@ -23,7 +26,8 @@ from .serializers import (UserLoginSerializer, UserRegisterSerializer,
|
|||||||
UserChangePasswordSerializer,
|
UserChangePasswordSerializer,
|
||||||
UserSerializer, EditUserSerializer,
|
UserSerializer, EditUserSerializer,
|
||||||
ApplyResetPasswordSerializer, ResetPasswordSerializer,
|
ApplyResetPasswordSerializer, ResetPasswordSerializer,
|
||||||
SSOSerializer, EditUserProfileSerializer, UserProfileSerializer)
|
SSOSerializer, EditUserProfileSerializer,
|
||||||
|
UserProfileSerializer, ApplyTwoFactorAuthSerializer)
|
||||||
|
|
||||||
from .decorators import super_admin_required
|
from .decorators import super_admin_required
|
||||||
|
|
||||||
@ -151,9 +155,9 @@ class EmailCheckAPIView(APIView):
|
|||||||
检测邮箱是否存在,用状态码标识结果
|
检测邮箱是否存在,用状态码标识结果
|
||||||
---
|
---
|
||||||
"""
|
"""
|
||||||
#这里是为了适应前端表单验证空间的要求
|
# 这里是为了适应前端表单验证空间的要求
|
||||||
reset = request.GET.get("reset", None)
|
reset = request.GET.get("reset", None)
|
||||||
#如果reset为true说明该请求是重置密码页面发出的,要返回的状态码应正好相反
|
# 如果reset为true说明该请求是重置密码页面发出的,要返回的状态码应正好相反
|
||||||
if reset:
|
if reset:
|
||||||
existed = 200
|
existed = 200
|
||||||
does_not_existed = 400
|
does_not_existed = 400
|
||||||
@ -375,3 +379,41 @@ def reset_password_page(request, token):
|
|||||||
if (now() - user.reset_password_token_create_time).total_seconds() > 30 * 60:
|
if (now() - user.reset_password_token_create_time).total_seconds() > 30 * 60:
|
||||||
return error_page(request, u"链接已过期")
|
return error_page(request, u"链接已过期")
|
||||||
return render(request, "oj/account/reset_password.html", {"user": user})
|
return render(request, "oj/account/reset_password.html", {"user": user})
|
||||||
|
|
||||||
|
|
||||||
|
class TwoFactorAuthAPIView(APIView):
|
||||||
|
@login_required
|
||||||
|
def get(self, request):
|
||||||
|
"""
|
||||||
|
获取绑定二维码
|
||||||
|
"""
|
||||||
|
user = request.user
|
||||||
|
if user.two_factor_auth:
|
||||||
|
return error_response(u"已经开启两步验证了")
|
||||||
|
token = rand_str()
|
||||||
|
user.tfa_token = token
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
image = qrcode.make(OtpAuth(token).to_uri("totp", "OnlineJudge", "OnlineJudge"))
|
||||||
|
buf = StringIO.StringIO()
|
||||||
|
image.save(buf, 'gif')
|
||||||
|
|
||||||
|
return HttpResponse(buf.getvalue(), 'image/gif')
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def post(self, request):
|
||||||
|
"""
|
||||||
|
开启两步验证
|
||||||
|
"""
|
||||||
|
serializer = ApplyTwoFactorAuthSerializer(data=request.data)
|
||||||
|
if serializer.is_valid():
|
||||||
|
code = serializer.data["code"]
|
||||||
|
user = request.user
|
||||||
|
if OtpAuth(user.tfa_token).valid_totp(code):
|
||||||
|
user.two_factor_auth = True
|
||||||
|
user.save()
|
||||||
|
return success_response(u"开启两步验证成功")
|
||||||
|
else:
|
||||||
|
return error_response(u"验证码错误")
|
||||||
|
else:
|
||||||
|
return serializer_invalid_response(serializer)
|
@ -6,7 +6,8 @@ from django.views.generic import TemplateView
|
|||||||
from account.views import (UserLoginAPIView, UsernameCheckAPIView, UserRegisterAPIView,
|
from account.views import (UserLoginAPIView, UsernameCheckAPIView, UserRegisterAPIView,
|
||||||
UserChangePasswordAPIView, EmailCheckAPIView,
|
UserChangePasswordAPIView, EmailCheckAPIView,
|
||||||
UserAdminAPIView, UserInfoAPIView, ResetPasswordAPIView,
|
UserAdminAPIView, UserInfoAPIView, ResetPasswordAPIView,
|
||||||
ApplyResetPasswordAPIView, SSOAPIView, UserProfileAPIView)
|
ApplyResetPasswordAPIView, SSOAPIView, UserProfileAPIView,
|
||||||
|
TwoFactorAuthAPIView)
|
||||||
|
|
||||||
from announcement.views import AnnouncementAdminAPIView
|
from announcement.views import AnnouncementAdminAPIView
|
||||||
|
|
||||||
@ -132,7 +133,8 @@ urlpatterns = [
|
|||||||
url(r'^account/sso/$', SSOAPIView.as_view(), name="sso_api"),
|
url(r'^account/sso/$', SSOAPIView.as_view(), name="sso_api"),
|
||||||
url(r'^api/account/userprofile/$', UserProfileAPIView.as_view(), name="userprofile_api"),
|
url(r'^api/account/userprofile/$', UserProfileAPIView.as_view(), name="userprofile_api"),
|
||||||
url(r'^reset_password/$', TemplateView.as_view(template_name="oj/account/apply_reset_password.html"), name="apply_reset_password_page"),
|
url(r'^reset_password/$', TemplateView.as_view(template_name="oj/account/apply_reset_password.html"), name="apply_reset_password_page"),
|
||||||
url(r'^reset_password/t/(?P<token>\w+)/$', "account.views.reset_password_page", name="reset_password_page")
|
url(r'^reset_password/t/(?P<token>\w+)/$', "account.views.reset_password_page", name="reset_password_page"),
|
||||||
|
url(r'^api/two_factor_auth/$', TwoFactorAuthAPIView.as_view(), name="two_factor_auth_api"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
208
utils/otp_auth.py
Normal file
208
utils/otp_auth.py
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
otpauth
|
||||||
|
~~~~~~~
|
||||||
|
|
||||||
|
Implements two-step verification of HOTP/TOTP.
|
||||||
|
|
||||||
|
:copyright: (c) 2013 - 2015 by Hsiaoming Yang.
|
||||||
|
:license: BSD, see LICENSE for more details.
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import hmac
|
||||||
|
import base64
|
||||||
|
import struct
|
||||||
|
import hashlib
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
|
||||||
|
if sys.version_info[0] == 3:
|
||||||
|
PY2 = False
|
||||||
|
string_type = str
|
||||||
|
else:
|
||||||
|
PY2 = True
|
||||||
|
string_type = unicode
|
||||||
|
range = xrange
|
||||||
|
|
||||||
|
|
||||||
|
__author__ = 'Hsiaoming Yang <me@lepture.com>'
|
||||||
|
__homepage__ = 'https://github.com/lepture/otpauth'
|
||||||
|
__version__ = '1.0.1'
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ['OtpAuth', 'HOTP', 'TOTP', 'generate_hotp', 'generate_totp']
|
||||||
|
|
||||||
|
|
||||||
|
HOTP = 'hotp'
|
||||||
|
TOTP = 'totp'
|
||||||
|
|
||||||
|
|
||||||
|
class OtpAuth(object):
|
||||||
|
"""One Time Password Authentication.
|
||||||
|
|
||||||
|
:param secret: A secret token for the authentication.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, secret):
|
||||||
|
self.secret = secret
|
||||||
|
|
||||||
|
def hotp(self, counter=4):
|
||||||
|
"""Generate a HOTP code.
|
||||||
|
|
||||||
|
:param counter: HOTP is a counter based algorithm.
|
||||||
|
"""
|
||||||
|
return generate_hotp(self.secret, counter)
|
||||||
|
|
||||||
|
def totp(self, period=30, timestamp=None):
|
||||||
|
"""Generate a TOTP code.
|
||||||
|
|
||||||
|
A TOTP code is an extension of HOTP algorithm.
|
||||||
|
|
||||||
|
:param period: A period that a TOTP code is valid in seconds
|
||||||
|
:param timestamp: Create TOTP at this given timestamp
|
||||||
|
"""
|
||||||
|
return generate_totp(self.secret, period, timestamp)
|
||||||
|
|
||||||
|
def valid_hotp(self, code, last=0, trials=100):
|
||||||
|
"""Valid a HOTP code.
|
||||||
|
|
||||||
|
:param code: A number that is less than 6 characters.
|
||||||
|
:param last: Guess HOTP code from last + 1 range.
|
||||||
|
:param trials: Guest HOTP code end at last + trials + 1.
|
||||||
|
"""
|
||||||
|
if not valid_code(code):
|
||||||
|
return False
|
||||||
|
|
||||||
|
code = bytes(int(code))
|
||||||
|
for i in range(last + 1, last + trials + 1):
|
||||||
|
if compare_digest(bytes(self.hotp(counter=i)), code):
|
||||||
|
return i
|
||||||
|
return False
|
||||||
|
|
||||||
|
def valid_totp(self, code, period=30, timestamp=None):
|
||||||
|
"""Valid a TOTP code.
|
||||||
|
|
||||||
|
:param code: A number that is less than 6 characters.
|
||||||
|
:param period: A period that a TOTP code is valid in seconds
|
||||||
|
:param timestamp: Validate TOTP at this given timestamp
|
||||||
|
"""
|
||||||
|
if not valid_code(code):
|
||||||
|
return False
|
||||||
|
return compare_digest(
|
||||||
|
bytes(self.totp(period, timestamp)),
|
||||||
|
bytes(int(code))
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def encoded_secret(self):
|
||||||
|
secret = base64.b32encode(to_bytes(self.secret))
|
||||||
|
# bytes to string
|
||||||
|
secret = secret.decode('utf-8')
|
||||||
|
# remove pad string
|
||||||
|
return secret.strip('=')
|
||||||
|
|
||||||
|
def to_uri(self, type, label, issuer, counter=None):
|
||||||
|
"""Generate the otpauth protocal string.
|
||||||
|
|
||||||
|
:param type: Algorithm type, hotp or totp.
|
||||||
|
:param label: Label of the identifier.
|
||||||
|
:param issuer: The company, the organization or something else.
|
||||||
|
:param counter: Counter of the HOTP algorithm.
|
||||||
|
"""
|
||||||
|
type = type.lower()
|
||||||
|
|
||||||
|
if type not in ('hotp', 'totp'):
|
||||||
|
raise ValueError('type must be hotp or totp')
|
||||||
|
|
||||||
|
if type == 'hotp' and not counter:
|
||||||
|
raise ValueError('HOTP type authentication need counter')
|
||||||
|
|
||||||
|
# https://code.google.com/p/google-authenticator/wiki/KeyUriFormat
|
||||||
|
url = ('otpauth://%(type)s/%(label)s?secret=%(secret)s'
|
||||||
|
'&issuer=%(issuer)s')
|
||||||
|
dct = dict(
|
||||||
|
type=type, label=label, issuer=issuer,
|
||||||
|
secret=self.encoded_secret, counter=counter
|
||||||
|
)
|
||||||
|
ret = url % dct
|
||||||
|
if type == 'hotp':
|
||||||
|
ret = '%s&counter=%s' % (ret, counter)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def to_google(self, type, label, issuer, counter=None):
|
||||||
|
"""Generate the otpauth protocal string for Google Authenticator.
|
||||||
|
|
||||||
|
.. deprecated:: 0.2.0
|
||||||
|
Use :func:`to_uri` instead.
|
||||||
|
"""
|
||||||
|
warnings.warn('deprecated, use to_uri instead', DeprecationWarning)
|
||||||
|
return self.to_uri(type, label, issuer, counter)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_hotp(secret, counter=4):
|
||||||
|
"""Generate a HOTP code.
|
||||||
|
|
||||||
|
:param secret: A secret token for the authentication.
|
||||||
|
:param counter: HOTP is a counter based algorithm.
|
||||||
|
"""
|
||||||
|
# https://tools.ietf.org/html/rfc4226
|
||||||
|
msg = struct.pack('>Q', counter)
|
||||||
|
digest = hmac.new(to_bytes(secret), msg, hashlib.sha1).digest()
|
||||||
|
|
||||||
|
ob = digest[19]
|
||||||
|
if PY2:
|
||||||
|
ob = ord(ob)
|
||||||
|
|
||||||
|
pos = ob & 15
|
||||||
|
base = struct.unpack('>I', digest[pos:pos + 4])[0] & 0x7fffffff
|
||||||
|
token = base % 1000000
|
||||||
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
def generate_totp(secret, period=30, timestamp=None):
|
||||||
|
"""Generate a TOTP code.
|
||||||
|
|
||||||
|
A TOTP code is an extension of HOTP algorithm.
|
||||||
|
|
||||||
|
:param secret: A secret token for the authentication.
|
||||||
|
:param period: A period that a TOTP code is valid in seconds
|
||||||
|
:param timestamp: Current time stamp.
|
||||||
|
"""
|
||||||
|
if timestamp is None:
|
||||||
|
timestamp = time.time()
|
||||||
|
counter = int(timestamp) // period
|
||||||
|
return generate_hotp(secret, counter)
|
||||||
|
|
||||||
|
|
||||||
|
def to_bytes(text):
|
||||||
|
if isinstance(text, string_type):
|
||||||
|
# Python3 str -> bytes
|
||||||
|
# Python2 unicode -> str
|
||||||
|
text = text.encode('utf-8')
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def valid_code(code):
|
||||||
|
code = string_type(code)
|
||||||
|
return code.isdigit() and len(code) <= 6
|
||||||
|
|
||||||
|
|
||||||
|
def compare_digest(a, b):
|
||||||
|
func = getattr(hmac, 'compare_digest', None)
|
||||||
|
if func:
|
||||||
|
return func(a, b)
|
||||||
|
|
||||||
|
# fallback
|
||||||
|
if len(a) != len(b):
|
||||||
|
return False
|
||||||
|
|
||||||
|
rv = 0
|
||||||
|
if PY2:
|
||||||
|
from itertools import izip
|
||||||
|
for x, y in izip(a, b):
|
||||||
|
rv |= ord(x) ^ ord(y)
|
||||||
|
else:
|
||||||
|
for x, y in zip(a, b):
|
||||||
|
rv |= x ^ y
|
||||||
|
return rv == 0
|
Loading…
Reference in New Issue
Block a user