这里通过代码讲述了使用`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 中,欢迎体验并指正。
以下是所有源代码的地址:
🪐祝您好运🪐
浙公网安备 33010602011771号