diff --git a/account/urls/oj.py b/account/urls/oj.py index 34b267c5..f9a3c7ca 100644 --- a/account/urls/oj.py +++ b/account/urls/oj.py @@ -4,6 +4,8 @@ from ..views.oj import (ApplyResetPasswordAPI, ResetPasswordAPI, UserChangePasswordAPI, UserRegisterAPI, UserLoginAPI, UserLogoutAPI) +from utils.captcha.views import CaptchaAPIView + urlpatterns = [ url(r"^login/?$", UserLoginAPI.as_view(), name="user_login_api"), url(r"^logout/?$", UserLogoutAPI.as_view(), name="user_logout_api"), @@ -11,5 +13,5 @@ urlpatterns = [ url(r"^change_password/?$", UserChangePasswordAPI.as_view(), name="user_change_password_api"), url(r"^apply_reset_password/?$", ApplyResetPasswordAPI.as_view(), name="apply_reset_password_api"), url(r"^reset_password/?$", ResetPasswordAPI.as_view(), name="apply_reset_password_api"), - url(r"^captcha/?$", "utils.captcha.views.show_captcha", name="show_captcha"), + url(r"^captcha/?$", CaptchaAPIView.as_view(), name="show_captcha"), ] diff --git a/utils/captcha/__init__.py b/utils/captcha/__init__.py index ee3fe25f..e360b720 100644 --- a/utils/captcha/__init__.py +++ b/utils/captcha/__init__.py @@ -1,12 +1,9 @@ """ -Copyright 2013 TY - +Copyright 2017 TY Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -15,95 +12,147 @@ limitations under the License. """ import os -import time import random - -from io import BytesIO -from django.http import HttpResponse +from math import ceil +from six import BytesIO from PIL import Image, ImageDraw, ImageFont +__version__ = '0.3.3' +current_path = os.path.normpath(os.path.dirname(__file__)) class Captcha(object): def __init__(self, request): + """ something init """ - 初始化,设置各种属性 - """ - self.django_request = request - self.session_key = "_django_captcha_key" - self.captcha_expires_time = "_django_captcha_expires_time" - # 验证码图片尺寸 - self.img_width = 90 + self.django_request = request + self.session_key = '_django_captcha_key' + self.words = ["hello", "word"] + + # image size (pix) + self.img_width = 150 self.img_height = 30 - def _get_font_size(self, code): - """ - 将图片高度的80%作为字体大小 - """ + self.type = 'number' + self.mode = 'number' + + def _get_font_size(self): s1 = int(self.img_height * 0.8) - s2 = int(self.img_width / len(code)) - return int(min((s1, s2)) + max((s1, s2)) * 0.05) + s2 = int(self.img_width/len(self.code)) + return int(min((s1, s2)) + max((s1, s2))*0.05) + + def _get_words(self): + """ words list + """ + + # TODO 扩充单词表 + + if self.words: + return set(self.words) + def _set_answer(self, answer): - """ - 设置答案和过期时间 - """ self.django_request.session[self.session_key] = str(answer) - self.django_request.session[self.captcha_expires_time] = time.time() + 60 - def _make_code(self): + def _generate(self): + # 英文单词验证码 + def word(): + code = random.sample(self._get_words(), 1)[0] + self._set_answer(code) + return code + + # 数字公式验证码 + def number(): + m, n = 1, 50 + x = random.randrange(m, n) + y = random.randrange(m, n) + + r = random.randrange(0, 2) + if r == 0: + code = "%s - %s = ?" % (x, y) + z = x - y + else: + code = "%s + %s = ?" % (x, y) + z = x + y + self._set_answer(z) + return code + + fun = eval(self.mode.lower()) + return fun() + + def get(self): + """ return captcha image bytes """ - 生成随机数或随机字符串 + + # font color + self.font_color = ['black', 'darkblue', 'darkred'] + + # background color + self.background = (random.randrange(230, 255), random.randrange(230, 255), random.randrange(230, 255)) + + # font path + self.font_path = os.path.join(current_path, 'timesbi.ttf') # or Menlo.ttc + + self.django_request.session[self.session_key] = '' + im = Image.new('RGB', (self.img_width, self.img_height), self.background) + self.code = self._generate() + + # set font size automaticly + self.font_size = self._get_font_size() + + # creat + draw = ImageDraw.Draw(im) + + # draw noisy point/line + if self.mode == 'word': + c = int(8/len(self.code)*3) or 3 + elif self.mode == 'number': + c = 4 + + for i in range(random.randrange(c-2, c)): + line_color = (random.randrange(0, 255), random.randrange(0, 255), random.randrange(0, 255)) + xy = (random.randrange(0, int(self.img_width*0.2)), random.randrange(0, self.img_height), + random.randrange(int(3*self.img_width/4), self.img_width), random.randrange(0, self.img_height)) + draw.line(xy, fill=line_color, width=int(self.font_size*0.1)) + + # main part + j = int(self.font_size*0.3) + k = int(self.font_size*0.5) + x = random.randrange(j, k) + + for i in self.code: + # 上下抖动量,字数越多,上下抖动越大 + m = int(len(self.code)) + y = random.randrange(1, 3) + + if i in ('+', '=', '?'): + # 对计算符号等特殊字符放大处理 + m = ceil(self.font_size*0.8) + else: + # 字体大小变化量,字数越少,字体大小变化越多 + m = random.randrange(0, int(45 / self.font_size) + int(self.font_size/5)) + + self.font = ImageFont.truetype(self.font_path.replace('\\', '/'), self.font_size + int(ceil(m))) + draw.text((x, y), i, font=self.font, fill=random.choice(self.font_color)) + x += self.font_size*0.9 + + del x + del draw + with BytesIO() as buf: + im.save(buf, 'gif') + buf_str = buf.getvalue() + return buf_str + + def validate(self, code): + """ user input validate """ - string = random.sample("abcdefghkmnpqrstuvwxyzABCDEFGHGKMNOPQRSTUVWXYZ23456789", 4) - self._set_answer("".join(string)) - return string - def display(self): - """ - 生成验证码图片 - """ - background = (random.randrange(200, 255), random.randrange(200, 255), random.randrange(200, 255)) - code_color = (random.randrange(0, 50), random.randrange(0, 50), random.randrange(0, 50), 255) - - font_path = os.path.join(os.path.normpath(os.path.dirname(__file__)), "timesbi.ttf") - - image = Image.new("RGB", (self.img_width, self.img_height), background) - code = self._make_code() - font_size = self._get_font_size(code) - draw = ImageDraw.Draw(image) - - # x是第一个字母的x坐标 - x = random.randrange(int(font_size * 0.3), int(font_size * 0.5)) - - for i in code: - # 字符y坐标 - y = random.randrange(1, 7) - # 随机字符大小 - font = ImageFont.truetype(font_path.replace("\\", "/"), font_size + random.randrange(-3, 7)) - draw.text((x, y), i, font=font, fill=code_color) - # 随机化字符之间的距离 字符粘连可以降低识别率 - x += font_size * random.randrange(6, 8) / 10 - - buf = BytesIO() - image.save(buf, "gif") - - self.django_request.session[self.session_key] = "".join(code) - return HttpResponse(buf.getvalue(), "image/gif") - - def check(self, code): - """ - 检查用户输入的验证码是否正确 - """ - _code = self.django_request.session.get(self.session_key) or "" - if not _code: - return False - expires_time = self.django_request.session.get(self.captcha_expires_time) or 0 - # 注意 如果验证之后不清除之前的验证码的话 可能会造成重复验证的现象 - del self.django_request.session[self.session_key] - del self.django_request.session[self.captcha_expires_time] - if _code.lower() == str(code).lower() and time.time() < expires_time: - return True - else: + if not code: return False + + code = code.strip() + _code = self.django_request.session.get(self.session_key) or '' + self.django_request.session[self.session_key] = '' + return _code.lower() == str(code).lower() + diff --git a/utils/captcha/views.py b/utils/captcha/views.py index ba420593..36258d3b 100644 --- a/utils/captcha/views.py +++ b/utils/captcha/views.py @@ -1,7 +1,11 @@ -from django.http import HttpResponse +from base64 import b64encode -from utils.captcha import Captcha +from . import Captcha +from ..api import APIView -def show_captcha(request): - return HttpResponse(Captcha(request).display(), content_type="image/gif") +class CaptchaAPIView(APIView): + def get(self, request): + img_prefix = "data:image/png;base64," + img = img_prefix + b64encode(Captcha(request).get()).decode("utf-8") + return self.success(img)