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

 

生成验证码

主要思路是:随机生成字母和数字,使用随机的颜色创建白色背景上的验证码图片,再增加随机颜色的干扰线、干扰点以及干扰圆圈。主要逻辑代码如下:
 1 from PIL import Image, ImageDraw, ImageFont
 2 from random import randint, choices
 3 from datetime import datetime
 4 
 5 def _generate_captcha_text(length=5):
 6     return ''.join(choices("ABCDEFGHJKLMNPQRSTUVWXYZ23456789", k=length))
 7 
 8 # 生成验证码
 9 def generate_captcha():
10     # 生成唯一ID作为验证码标识
11     captcha_id = f"{int(datetime.now().timestamp() * 1000)}{randint(1000, 9999)}"
12     captcha_text = _generate_captcha_text()
13     captcha_image = _generate_captcha_image(captcha_text)
14     return captcha_id,captcha_text,captcha_image
15 
16 # 创建随机颜色
17 def _random_color():
18     """
19         生成随机颜色
20         :return:
21         """
22     return randint(150, 235), randint(150, 235), randint(150, 235)
23 
24 def _generate_captcha_image(captcha_text):
25     
26     image_width, image_height = 150, 40
27     font_size: int = 25
28     mode: str = 'RGB'
29     character_length = len(captcha_text)
30 
31     # 创建一个白色背景的图像
32     image = Image.new(mode, (image_width, image_height), 'white')
33     draw = ImageDraw.Draw(image)
34     font = ImageFont.load_default(size=font_size)
35 
36     # 绘制验证码文字
37     for i, char in enumerate(captcha_text):
38         x = 5 + i * (image_width-5)/(character_length)
39         y = randint(-5, 5)
40         draw.text((x, y), text=char, font=font, fill=_random_color())
41 
42     # 添加干扰线
43     for _ in range(10):  
44         start = (randint(0, image_width), randint(0, image_height))
45         end = (randint(0, image_width), randint(0, image_height))
46         draw.line([start, end], fill=_random_color(), width=1)
47 
48     # 写干扰点
49     for _ in range(150):        
50         draw.point([randint(0, image_width), randint(0, image_height)], fill=_random_color())
51 
52     for _ in range(10):
53         # 写干扰圆圈
54         x = randint(0, image_width)
55         y = randint(0, image_height)
56         radius = randint(2, 4)
57         draw.arc((x-radius, y-radius, x + radius, y + radius), 0, 360, fill=_random_color())
58 
59     return image

 

您也可以直接下载完整代码:

在fastAPI中生成和校验验证码


1. 生成验证码
 1 from util.captcha import generate_captcha
 2 from util.ttlcache import Cache,Error
 3 _cache = Cache(max_size=300, ttl=300)    # 300个缓存,每个缓存5分钟
 4 
 5 @app.get("/captcha")
 6 def get_captcha():
 7 
 8     if _cache.is_full():
 9         raise HTTPException(status_code=status.HTTP_429_TOO_MANY_REQUESTS, detail="Too many requests")
10     
11     captcha_id,captcha_text,captcha_image =  generate_captcha()
12     print(f"生成的验证码: {captcha_id} {captcha_text}")
13     result = _cache.add(captcha_id,(captcha_text,captcha_image))
14     if result != Error.OK:
15         raise HTTPException(status_code=status.HTTP_429_TOO_MANY_REQUESTS, detail="Too many requests")
16 
17     # 返回图片流
18     buffer = BytesIO()
19     captcha_image.save(buffer, format="PNG")
20     buffer.seek(0)
21     headers = {custom_header_name: captcha_id,"Cache-Control": "no-store"}
22     #print(headers)
23     return StreamingResponse(buffer, headers=headers, media_type="image/png")

生成验证码时使用了缓存,每个验证码缓存5分钟后自动清除。  


2. 校验图片验证码
我们在登录接口中增加了参数:aptcha_id 和 captcha_input,用以接受客户端传来的验证码。
 1 @app.post("/token")
 2 async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(),remember: bool|None=Body(None),
 3     captcha_id: str|None=Body(None), captcha_input: str|None=Body(None),log_details: None = Depends(log_request_details))-> Token:
 4     '''
 5     OAuth2PasswordRequestForm 是用以下几项内容声明表单请求体的类依赖项:
 6 
 7     username
 8     password
 9     scope、grant_type、client_id等可选字段。
10     '''
11 
12     # 校验验证码    
13     error,value = _cache.get(captcha_id)
14     if error != Error.OK:
15         raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid or expired captcha ID")
16     
17     captcha_text = value[0]
18 
19     if not captcha_text:
20         raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid or expired captcha ID")
21 
22     if captcha_text.upper() != captcha_input.upper():
23         raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Incorrect captcha")

 

在服务端要合理设定 `CORS` ,允许跨域访问以及自定义header,否则客户端可能无法访问生成的图片验证码或者无法获取通过header携带的captcha ID,相关代码如下:
 1 custom_header_name = "X-Captcha-ID"
 2 
 3 # 允许跨域访问
 4 from fastapi.middleware.cors import CORSMiddleware
 5 origins = config["origins"]
 6 app.add_middleware(
 7     CORSMiddleware,
 8     allow_origins=origins,
 9     allow_credentials=True,
10     allow_methods=["*"],
11     allow_headers=["*"],
12     expose_headers=[custom_header_name,"Cache-Control"],  # 允许前端访问的头部,不如此设置客户端获取不到这些头信息

 

在客户端使用图片验证码

这里包含页面打开后自动获取验证码,以及点击图片时自动刷新验证码。  
> 在请求验证码时,务必设定 responseType: "blob",否则无法显示二维码。

显示验证码的图片控件:
 1 <v-container>
 2   <v-row>
 3     <v-text-field
 4       v-model="form_data.capchaText"
 5       label="输入验证码"
 6       variant="solo"
 7       :rules="[rules.required, rules.max]"
 8     ></v-text-field
 9     ><v-img
10       :src="imageSrc"
11       alt="验证码"
12       class="mb-4"
13       max-height="60"
14       @click="refreshCaptcha"
15       style="cursor: pointer"
16     >
17     </v-img>
18   </v-row>
19 </v-container>

 

相关的 `vuejs` 脚本:
 1 import { ref, onMounted } from "vue";
 2 import axios from "axios";
 3 const capcha_url = "http://127.0.0.1:8000/captcha";
 4 
 5 const imageSrc = ref("");
 6 
 7 //表单数据
 8 const form_data = ref({
 9   username: "",
10   password: "",
11   remember: false,
12   capchaId: "",
13   capchaText: "",
14 });
15 
16 // 获取验证码
17 const fetchCaptcha = async () => {
18   try {
19     let img_url = capcha_url + "?t=" + Date.now();
20     const response = await axios(img_url, { responseType: "blob" });  // 响应类型为 blob,非常重要!
21     console.log("获取验证码成功:", response);
22 
23     form_data.value.capchaId = response.headers["x-captcha-id"]; // 验证码唯一标识符
24     console.log("验证码ID:", form_data.value.capchaId);
25     imageSrc.value = URL.createObjectURL(response.data);
26   } catch (error) {
27     console.log("获取验证码失败:", error);
28     if (error.code == "ERR_NETWORK") {
29       error_msg.value = "网络错误,无法连接到服务器。";
30     } else {
31       error_msg.value = error.response.data.detail;
32     }
33   }
34 
35   if (error_msg.value != "") {
36     error.value = true;
37   }
38 };
39 
40 // 刷新验证码
41 const refreshCaptcha = () => {
42   fetchCaptcha();
43   form_data.value.capchaText = ""; // 清空用户输入
44 };
45 
46 onMounted(() => {
47   fetchCaptcha();
48 });

 

总结

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

🪐祝您好运🪐