141 lines
4.9 KiB
Python
141 lines
4.9 KiB
Python
# _*_ coding : UTF-8 _*_
|
||
# @Time : 2025/01/26 20:59
|
||
# @UpdateTime : 2025/01/26 20:59
|
||
# @Author : sonder
|
||
# @File : captcha.py
|
||
# @Software : PyCharm
|
||
# @Comment : 本程序
|
||
import base64
|
||
import io
|
||
import os
|
||
import random
|
||
import string
|
||
|
||
from PIL import Image, ImageDraw, ImageFont
|
||
from fastapi import Request
|
||
|
||
from config.constant import RedisKeyConfig
|
||
|
||
|
||
class Captcha:
|
||
"""
|
||
验证码类
|
||
"""
|
||
|
||
@classmethod
|
||
async def create_captcha(cls, captcha_type: str = "0"):
|
||
"""
|
||
生成验证码
|
||
:param captcha_type: 验证码类型,0为数字,1为数字和字母
|
||
:return: 验证码图片和验证码base64字符串
|
||
"""
|
||
# 创建空白图像
|
||
image = Image.new('RGB', (120, 40), color='#EAEAEA')
|
||
draw = ImageDraw.Draw(image)
|
||
|
||
# 设置字体
|
||
font_path = os.path.join(os.path.abspath(os.getcwd()), 'assets', 'font', 'MiSans-Medium.ttf')
|
||
font = ImageFont.truetype(font_path, size=25)
|
||
|
||
if captcha_type == '0':
|
||
# 生成两个0-9之间的随机整数
|
||
num1 = random.randint(0, 9)
|
||
num2 = random.randint(0, 9)
|
||
# 从运算符列表中随机选择一个
|
||
operational_character_list = ['+', '-', '*']
|
||
operational_character = random.choice(operational_character_list)
|
||
# 根据选择的运算符进行计算
|
||
if operational_character == '+':
|
||
result = num1 + num2
|
||
elif operational_character == '-':
|
||
result = num1 - num2
|
||
else:
|
||
result = num1 * num2
|
||
# 生成算术题文本
|
||
text = f'{num1} {operational_character} {num2} = ?'
|
||
# 计算文本宽度以居中显示
|
||
text_width = draw.textlength(text, font=font)
|
||
x = (120 - text_width) / 2
|
||
draw.text((x, 5), text, fill='blue', font=font)
|
||
else:
|
||
# 生成随机字母和数字组合
|
||
result = ''.join(random.choices(string.ascii_letters + string.digits, k=4))
|
||
# 绘制每个字符,并添加随机旋转和倾斜
|
||
x = 10
|
||
for char in result:
|
||
# 创建单个字符的图像
|
||
char_image = Image.new('RGBA', (25, 40), color=(234, 234, 234, 0))
|
||
char_draw = ImageDraw.Draw(char_image)
|
||
char_draw.text((0, 0), char, font=font, fill=(0, 0, 255))
|
||
# 随机旋转字符
|
||
char_image = char_image.rotate(random.randint(-40, 40), expand=1)
|
||
# 随机倾斜字符
|
||
char_image = char_image.transform(char_image.size, Image.AFFINE,
|
||
(1, random.uniform(-0.3, 0.3), 0, 0, 1, 0))
|
||
# 将字符粘贴到主图像上
|
||
image.paste(char_image, (x, 0), char_image)
|
||
x += 25
|
||
# 添加干扰元素
|
||
cls._add_noise(image)
|
||
cls._add_lines(image)
|
||
|
||
# 将图像数据保存到内存中
|
||
buffer = io.BytesIO()
|
||
image.save(buffer, format='PNG')
|
||
|
||
# 将图像数据转换为base64字符串
|
||
base64_string = base64.b64encode(buffer.getvalue()).decode()
|
||
|
||
return [base64_string, result]
|
||
|
||
@staticmethod
|
||
def _add_noise(image):
|
||
"""
|
||
添加噪点干扰
|
||
"""
|
||
draw = ImageDraw.Draw(image)
|
||
for _ in range(150): # 添加100个噪点
|
||
x = random.randint(0, 120)
|
||
y = random.randint(0, 40)
|
||
draw.point((x, y), fill=(random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)))
|
||
|
||
@staticmethod
|
||
def _add_lines(image):
|
||
"""
|
||
添加干扰线
|
||
"""
|
||
draw = ImageDraw.Draw(image)
|
||
for _ in range(10): # 添加5条干扰线
|
||
x1 = random.randint(0, 120)
|
||
y1 = random.randint(0, 40)
|
||
x2 = random.randint(0, 120)
|
||
y2 = random.randint(0, 40)
|
||
draw.line((x1, y1, x2, y2),
|
||
fill=(random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)),
|
||
width=1)
|
||
|
||
@classmethod
|
||
async def verify_code(cls, request: Request, code: str, session_id: str) -> dict:
|
||
"""
|
||
验证验证码
|
||
:param request
|
||
:param code: 验证码
|
||
:param session_id: 会话ID
|
||
"""
|
||
redis_code = await request.app.state.redis.get(f"{RedisKeyConfig.CAPTCHA_CODES.key}:{session_id}")
|
||
if redis_code is None:
|
||
return {
|
||
"status": False,
|
||
"msg": "验证码已过期"
|
||
}
|
||
if str(redis_code).lower() == code.lower():
|
||
await request.app.state.redis.delete(f"{RedisKeyConfig.CAPTCHA_CODES.key}:{session_id}")
|
||
return {
|
||
"status": True,
|
||
"msg": "验证码正确"
|
||
}
|
||
return {
|
||
"status": False,
|
||
"msg": "验证码错误"
|
||
}
|