这里通过代码讲述了使用python生成以及校验图片验证码,增强用户认证安全性的过程。
客户端则使用 vue3vuetify3 框架使用后台API生成的图片验证码。

图片验证码

生成验证码

主要思路是:随机生成字母和数字,使用随机的颜色创建白色背景上的验证码图片,再增加随机颜色的干扰线、干扰点以及干扰圆圈。主要逻辑代码如下:

from PIL import Image, ImageDraw, ImageFont
from random import randint, choices
from datetime import datetime

def _generate_captcha_text(length=5):
    return ''.join(choices("ABCDEFGHJKLMNPQRSTUVWXYZ23456789", k=length))

# 生成验证码
def generate_captcha():
    # 生成唯一ID作为验证码标识
    captcha_id = f"{int(datetime.now().timestamp() * 1000)}{randint(1000, 9999)}"
    captcha_text = _generate_captcha_text()
    captcha_image = _generate_captcha_image(captcha_text)
    return captcha_id,captcha_text,captcha_image

# 创建随机颜色
def _random_color():
    """
        生成随机颜色
        :return:
        """
    return randint(150, 235), randint(150, 235), randint(150, 235)

def _generate_captcha_image(captcha_text):
    
    image_width, image_height = 150, 40
    font_size: int = 25
    mode: str = 'RGB'
    character_length = len(captcha_text)

    # 创建一个白色背景的图像
    image = Image.new(mode, (image_width, image_height), 'white')
    draw = ImageDraw.Draw(image)
    font = ImageFont.load_default(size=font_size)

    # 绘制验证码文字
    for i, char in enumerate(captcha_text):
        x = 5 + i * (image_width-5)/(character_length)
        y = randint(-5, 5)
        draw.text((x, y), text=char, font=font, fill=_random_color())

    # 添加干扰线
    for _ in range(10):  
        start = (randint(0, image_width), randint(0, image_height))
        end = (randint(0, image_width), randint(0, image_height))
        draw.line([start, end], fill=_random_color(), width=1)

    # 写干扰点
    for _ in range(150):        
        draw.point([randint(0, image_width), randint(0, image_height)], fill=_random_color())

    for _ in range(10):
        # 写干扰圆圈
        x = randint(0, image_width)
        y = randint(0, image_height)
        radius = randint(2, 4)
        draw.arc((x-radius, y-radius, x + radius, y + radius), 0, 360, fill=_random_color())

    return image

您也可以直接下载完整代码: gitee | github | gitcode

在fastAPI中生成和校验验证码

  1. 生成验证码
from util.captcha import generate_captcha
from util.ttlcache import Cache,Error
_cache = Cache(max_size=300, ttl=300)    # 300个缓存,每个缓存5分钟

@app.get("/captcha")
def get_captcha():

    if _cache.is_full():
        raise HTTPException(status_code=status.HTTP_429_TOO_MANY_REQUESTS, detail="Too many requests")
    
    captcha_id,captcha_text,captcha_image =  generate_captcha()
    print(f"生成的验证码: {captcha_id} {captcha_text}")
    result = _cache.add(captcha_id,(captcha_text,captcha_image))
    if result != Error.OK:
        raise HTTPException(status_code=status.HTTP_429_TOO_MANY_REQUESTS, detail="Too many requests")

    # 返回图片流
    buffer = BytesIO()
    captcha_image.save(buffer, format="PNG")
    buffer.seek(0)
    headers = {custom_header_name: captcha_id,"Cache-Control": "no-store"}
    #print(headers)
    return StreamingResponse(buffer, headers=headers, media_type="image/png")

生成验证码时使用了缓存,每个验证码缓存5分钟后自动清除。
这个缓存在 实现可以自动清除过期条目的缓存 中有介绍。

  1. 校验图片验证码 我们在登录接口中增加了参数:aptcha_id 和 captcha_input,用以接受客户端传来的验证码。
@app.post("/token")
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(),remember: bool|None=Body(None),
    captcha_id: str|None=Body(None), captcha_input: str|None=Body(None),log_details: None = Depends(log_request_details))-> Token:
    '''
    OAuth2PasswordRequestForm 是用以下几项内容声明表单请求体的类依赖项:

    username
    password
    scope、grant_type、client_id等可选字段。
    '''

    # 校验验证码    
    error,value = _cache.get(captcha_id)
    if error != Error.OK:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid or expired captcha ID")
    
    captcha_text = value[0]

    if not captcha_text:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid or expired captcha ID")

    if captcha_text.upper() != captcha_input.upper():
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Incorrect captcha")

在服务端要合理设定 CORS ,允许跨域访问以及自定义header,否则客户端可能无法访问生成的图片验证码或者无法获取通过header携带的captcha ID,相关代码如下:

custom_header_name = "X-Captcha-ID"

# 允许跨域访问
from fastapi.middleware.cors import CORSMiddleware
origins = config["origins"]
app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
    expose_headers=[custom_header_name,"Cache-Control"],  # 允许前端访问的头部,不如此设置客户端获取不到这些头信息
)

在客户端使用图片验证码

这里包含页面打开后自动获取验证码,以及点击图片时自动刷新验证码。

在请求验证码时,务必设定 responseType: “blob”,否则无法显示二维码。

显示验证码的图片控件:

<v-container>
  <v-row>
    <v-text-field
      v-model="form_data.capchaText"
      label="输入验证码"
      variant="solo"
      :rules="[rules.required, rules.max]"
    ></v-text-field
    ><v-img
      :src="imageSrc"
      alt="验证码"
      class="mb-4"
      max-height="60"
      @click="refreshCaptcha"
      style="cursor: pointer"
    >
    </v-img>
  </v-row>
</v-container>

相关的 vuejs 脚本:

import { ref, onMounted } from "vue";
import axios from "axios";
const capcha_url = "http://127.0.0.1:8000/captcha";

const imageSrc = ref("");

//表单数据
const form_data = ref({
  username: "",
  password: "",
  remember: false,
  capchaId: "",
  capchaText: "",
});

// 获取验证码
const fetchCaptcha = async () => {
  try {
    let img_url = capcha_url + "?t=" + Date.now();
    const response = await axios(img_url, { responseType: "blob" });  // 响应类型为 blob,非常重要!
    console.log("获取验证码成功:", response);

    form_data.value.capchaId = response.headers["x-captcha-id"]; // 验证码唯一标识符
    console.log("验证码ID:", form_data.value.capchaId);
    imageSrc.value = URL.createObjectURL(response.data);
  } catch (error) {
    console.log("获取验证码失败:", error);
    if (error.code == "ERR_NETWORK") {
      error_msg.value = "网络错误,无法连接到服务器。";
    } else {
      error_msg.value = error.response.data.detail;
    }
  }

  if (error_msg.value != "") {
    error.value = true;
  }
};

// 刷新验证码
const refreshCaptcha = () => {
  fetchCaptcha();
  form_data.value.capchaText = ""; // 清空用户输入
};

onMounted(() => {
  fetchCaptcha();
});

总结

通过给登录功能增加图片验证码,可提升用户认证的安全性。
以上所述功能已经应用在 langchain+llama3+Chroma RAG demo 中,欢迎体验并指正。
以下是所有源代码的地址: gitee | github | gitcode


🪐祝您好运🪐