免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

验证码子系统总体设计:从需求分析到Redis防刷架构的完整实践

验证码子系统总体设计:从需求分析到Redis防刷架构的完整实践 如果你写过超过十个对外接口的后端项目大概率碰到过这么一件事注册接口突然被打满无意义的账号在一夜之间多了几千个。这时候大家第一个想到的防御手段往往都是验证码。验证码这个老家伙在互联网里存在了二十多年但直到今天依然是成本最低、见效最快的人机防线。我最近把“验证码案例”当成一个完整的综合练习来做从需求、设计、编码一路走到联调和自测。这个系列的第一篇我打算先把总体设计讲透。验证码看起来只是“出一张图、比一个字符串”但真要做成能扛住真实业务压力的子系统里面有大量写代码时才会暴露出来的细节问题。这篇文章会覆盖需求分析、方案选型、架构拆分、核心流程、缓存与安全设计、常见坑点以及后续编码阶段的工程目录规划。如果你正准备在项目里自研验证码模块或者想学一套完整的设计思路这篇值得你认真看一遍。1. 项目概述与需求分析1.1 一句话说清楚这个案例要做什么这个案例不是做一个“能画出图片的验证码”而是把验证码当成一个完整的安全子系统来设计。最终形态是前端页面有一个图形验证码组件用户输入验证码之后后端可以做到一次性校验、自动过期、接口防刷、并发安全并且能同时支持图形验证码和后续扩展短信验证码。项目的技术背景是SPA前后端分离架构登录链路用JWT维护会话状态。验证码系统作为登录、注册等敏感操作的前置人机校验不负责“用户是谁”只负责“当前操作是不是真人”。这个边界在一开始就要明确否则后面容易把业务逻辑和安全逻辑搅成一锅粥。1.2 需求清单验证码到底要解决哪些问题我在动手设计前会把需求拆成三类。第一类是核心功能需求第二类是非功能性需求第三类是扩展增强需求。很多团队做验证码翻车不是因为不会写绘制代码而是需求根本没理清楚。分类需求项说明核心功能图形验证码生成根据校验码生成图片返回给前端展示核心功能一次性校验校验成功后立即失效不能重复使用同一个验证码核心功能自动过期验证码存活时间可配置过期后必须重新获取核心功能刷新机制用户点击图片或校验失败后可以重新获取验证码非功能并发安全同一验证码被并发请求校验时只能有一个成功非功能防刷能力对IP、设备、账号维度做频率限制非功能性能要求生成和校验接口响应时间不能拖垮主业务流程扩展短信验证码支持预留短信验证码的接入能力场景可切换扩展行为验证码支持后续可以接入滑块、点选等行为验证服务这里有两个细节容易被忽略但恰恰是最容易埋坑的地方。第一个是“一次性校验”很多人做完验证码校验后没有删Key同一个验证码能被反复提交结果就是被脚本批量刷接口。第二个是“并发安全”前端网络重试、用户双击提交都会导致同一个验证码被同时提交多次如果不在Redis层面做原子处理就会出现两个请求都校验通过的情况。1.3 动手设计前必须想清楚的三件事第一件事用图形验证码还是短信验证码这两个东西的定位完全不同。图形验证码是防脚本和批量操作的成本低用户体验相对一般短信验证码是验证手机号归属的有真实成本也涉及短信服务商选择。登录注册场景通常先用图形验证码挡住机器流量再进行短信发送避免短信费用被恶意刷爆。第二件事验证码数据放哪里数据库、进程内存、Redis三个选项各有适用场景。这个案例选了Redis原因很直接验证码是短时效数据天然适合TTL过期机制同时后端服务多实例部署时进程内存无法共享数据库又太重了。第三件事校验成功之后要不要立即删除我的答案是必须删。不删的后果让人非常头疼脚本拿到一个有效验证码后可以反复重放等于验证码形同虚设。删除时机还要考虑并发不能先查再删要做到“读取比对并删除”这两个动作原子完成。2. 方案选型与技术栈设计2.1 自研图形验证码还是接入第三方验证服务这是一个每次都要纠结的选择。我个人的决策标准很简单项目处于学习阶段、内部系统、对成本敏感就自研如果是面向公网的高风险业务比如注册、支付、营销活动建议优先考虑接入成熟的第三方行为验证服务。对比维度自研图形验证码第三方行为验证服务接入成本中等需要自己写绘制、缓存、校验低前端组件加后端SDK即可定制空间高样式、算法、校验规则完全可控低依赖服务商能力防破解能力较弱容易被OCR批量识别较强滑块轨迹、无感验证等方案更难绕过费用基本为零按调用量计费稳定性依赖自身系统依赖第三方服务SLA适用场景内部系统、教学项目、登录辅助验证高价值业务入口、强对抗场景这个案例之所以选择自研是因为我要把整个设计链路走通验证码子系统本身就是要练的核心知识点。实际业务里很多团队最终会采用“自研图形验证码 第三方行为验证”的组合比如登录时先出图形验证码风控识别到高风险后再升级滑块或点选验证。这套组合思路在后续做扩展时会非常有用。2.2 技术栈SPA前端 Spring Boot Redis JWT这个案例的技术栈选型遵循“主流、稳定、可替换”的原则。前端用Vue或React都行组件化封装验证码区域后端用Spring Boot作为主框架缓存层用Redis会话层用JWT。这里需要厘清验证码和JWT的关系。验证码校验通过之后业务系统才继续走登录逻辑登录成功后签发JWT。验证码系统只管第一步的人机校验JWT管登录后的身份认证两者是上下游关系。很多初学者会把“验证码校验”和“登录认证”混在一起写导致一个Controller里面塞了太多职责后面完全没法维护。Redis在这个项目里的地位很重。除了存验证码本身还要存防刷计数、校验失败次数等状态。要注意的是Redis里存的验证码只是用于比对的临时数据不能把验证码当成用户信息持久化存储否则既浪费内存又埋下数据泄露隐患。2.3 校验码生成为什么不能用Math.random写验证码生成逻辑时第一个最容易踩的坑就是用Math.random()来生成随机字符。Math.random生成的是伪随机数虽然看起来随机但在某些场景下存在可预测性安全性不够。生成验证码属于安全敏感场景必须用密码学安全的随机数生成器。Java后端我习惯用SecureRandom示例代码是这样的SecureRandom random new SecureRandom(); char[] chars ABCDEFGHJKLMNPQRSTUVWXYZ23456789.toCharArray(); StringBuilder code new StringBuilder(); for (int i 0; i 4; i) { code.append(chars[random.nextInt(chars.length)]); }注意我把容易混淆的字符去掉了比如0和O、1和I这是提升用户识别率的细节。验证码长度在这个案例里先用4位至于为什么不是6位下一章会专门从概率角度讲清楚。2.4 图形、短信、行为验证码的分工这个案例虽然第一版实现的是图形验证码但设计上必须预留短信和行为验证码的扩展空间。很多人问为什么有的地方验证码是4位数字有的是6位数字这个问题背后其实是场景差异。验证码类型典型位数适用场景核心特点图形验证码4-6位登录、注册、提交表单前的人机校验防止脚本批量操作无发送成本短信验证码6位手机号绑定、找回密码、支付确认验证手机号归属有成本避免短信轰炸行为验证码无固定位数高风险操作、风控升级环节通过滑块、点选行为特征判断人机图形验证码用4位是因为它只需要挡住脚本不需要人工记忆4位字符加上扭曲和干扰线OCR识别的成本已经足够高。短信验证码用6位是因为短信有成本被暴力破解的空间必须压缩得更小。数字验证码由于只用了10个字符理论上空间比字母数字混排小这也是为什么纯数字验证码普遍比混合字符验证码更长。3. 架构设计与核心组件拆分3.1 完整请求链路先走一遍验证码的请求链路没有多复杂但每一步都要职责清晰。我先描述一遍申请验证码的流程你再看后面的组件拆分就会很顺。浏览器发起验证码申请请求后端Controller接收后调用验证码服务。验证码服务首先生成一个全局唯一的验证码ID也就是captchaId然后用SecureRandom生成明文校验码把这组映射关系写入Redis并设置过期时间。接着根据明文校验码生成图片通过Base64编码返回给前端。前端拿到图片数据后渲染出来给用户看。校验流程方向相反。用户输入验证码后前端把captchaId和用户输入值一起提交给后端。后端根据captchaId从Redis取出正确的校验码与用户输入做比对比对完成后无论成功失败都会删除这个Key防止重放攻击。3.2 核心组件清单与责任边界我把组件拆成七个每个组件只做一件事用表格列出来会很清楚组件职责关键点CaptchaController暴露申请接口、校验接口只做参数接收和响应封装CaptchaService编排生成、校验、刷新流程核心业务逻辑不关心图片如何画CodeGenerator生成随机校验码必须使用SecureRandomCacheStore封装验证码的存取和删除屏蔽Redis命令细节ImageRenderer根据校验码绘制图片处理字体、干扰线、扭曲RateLimiter基于IP、场景做频控独立于验证码核心逻辑DTO对象请求和响应的数据结构前端字段与后端字段解耦很多项目做着做着就乱了原因是Service层直接依赖了RedisTemplate同时画图逻辑也写在了Service里。我建议从设计阶段就把这些组件边界固定下来后续写代码时只需要对着组件清单填实现。3.3 为什么要把“发放”和“校验”拆成独立阶段有人会问验证码生成和校验这么简单的两个过程有必要拆成两个接口吗我的回答是不仅接口要拆服务层的方法也要拆。原因有三点。第一两个阶段的调用方不同。申请验证码的是前端页面校验验证码的可能是登录接口、注册接口、找回密码接口等多个业务系统。如果校验逻辑被埋进某个业务接口里其他业务要用时就得复制粘贴。第二频率限制策略不同。申请验证码需要严格限制防止有人写脚本不停刷新图片消耗服务器资源校验验证码也要限制防止有人暴力穷举。两个阶段的频控参数往往不一样拆开了才好独立配置。第三便于做超时处理。验证码过期、校验失败次数过多、用户手动刷新这些状态变化都集中在“发放”和“校验”之间的时间窗口。拆成独立阶段后每个状态都能被单独追踪和响应。3.4 与JWT的边界划分我见过不少项目把验证码校验结果直接塞进JWT的claims里这是个设计误区。验证码校验是一次性动作它的结果只在当前请求上下文里有效。JWT的作用是长期的会话凭证两者生命周期完全不同。正确做法是前端提交带验证码的登录表单后端先校验验证码验证码通过后继续执行用户名密码比对或短信验证码校验全部通过后再生成JWT返回给前端。如果验证码校验失败直接返回错误后续业务逻辑根本不会执行。这套流程划分清楚验证码系统和认证系统都能独立演化和扩展。4. 核心流程与关键接口设计4.1 流程一验证码申请申请验证码的接口设计成GET请求比较合理因为这是一个无副作用的查询操作。请求路径为/api/captcha响应里包含captchaId、base64图片数据、过期时间。一个容易忽略的点是如果前端点击图片一秒钟刷新十次后端不可能无限生成。所以在申请接口上也要做频控同一IP在一分钟内的申请次数不能超过合理阈值。频控不在这里做的话服务器会被刷图片接口直接打满。4.2 流程二验证码校验校验接口设计成POST请求路径为/api/captcha/validate。请求体里带着captchaId和inputCode两个字段。后端执行校验的逻辑看起来简单真正实现时要特别注意并发问题。校验步骤拆开是这样的根据captchaId查Redis取到正确校验码。如果取不到说明验证码已过期或已被使用直接返回无效。对比用户输入与正确校验码比对使用固定时间比较方式避免时序攻击。无论比对成功与否立即删除Redis中的Key。返回校验结果成功和失败都需要附带明确的提示信息。第3步提到的时序攻击可能很多人没听说过。如果字符串比较时按字符逐个比对遇到匹配到不同位置会提前返回攻击者可以通过响应时间的细微差异猜出字符。虽然验证码场景里攻击成本很高但既然是做安全模块从一开始就用MessageDigest.isEqual这类固定时间比较方法更稳妥。4.3 流程三验证码刷新与过期处理用户看不清验证码、输错验证码、验证码超时这些场景都需要刷新机制。最简单的实现是前端重新调用一次申请接口拿到新图片替换旧图片。这里的核心问题是旧验证码怎么处理。如果旧验证码还在Redis里存着攻击者可以拿旧验证码继续尝试留下安全隐患。我的做法是用户请求新验证码时后端如果检测到该会话存在旧captchaId就主动删掉旧Key同时再生成新的。这需要在会话上下文里维护一个当前生效的captchaId字段。过期处理则完全交给Redis的TTL机制。图形验证码的生命周期我建议设置成2分钟太短用户还没输完就过期了体验很差太长又增加了被穷举的时间窗口。这个数值可以做成配置项方便不同业务按需调整。4.4 接口定义与数据约定接口定义直接决定前后端联调的效率我把接口文档放在这里后面实现时对照这个来就行。接口方法请求参数响应/api/captchaGET无captchaId、imageBase64、expiresIn/api/captcha/validatePOSTcaptchaId、inputCodevalid、message申请接口的响应体设计成JSON结构如下{ captchaId: a3f9c2d1-7b4e-4f8a-9d62-9b3e1c8f5a2d, imageBase64: data:image/png;base64,iVBORw0KGgo..., expiresIn: 120 }校验接口的请求体和响应体设计成两组JSON请求{ captchaId: a3f9c2d1-7b4e-4f8a-9d62-9b3e1c8f5a2d, inputCode: K2X9 } 响应{ valid: true, message: 校验通过 }有个小细节需要注意imageBase64字段我直接返回了带data:image/png;base64,前缀的完整格式这样前端拿到之后可以直接赋给image标签的src属性不需要再拼接前缀。这种细节虽然不影响后端逻辑但能给前端省不少事。4.5 并发校验的原子性处理前面反复提到并发安全这里给出具体方案。同一个captchaId被多个请求同时提交可能会出现两个请求都读到同一个验证码、都校验通过的情况。原因是“读取比对”和“删除”不是原子操作。解决方案有两个。Redis 6.2以上版本可以用GETDEL命令一条命令完成取值和删除GETDEL captcha:img:a3f9c2d1-7b4e-4f8a-9d62-9b3e1c8f5a2d如果Redis版本比较旧就使用Lua脚本把取值、比对、删除三个动作放在服务端原子执行。Lua脚本类似这样local code redis.call(GET, KEYS[1]) if code ARGV[1] then redis.call(DEL, KEYS[1]) return true end return false这样处理之后同一验证码只能被一个请求成功消费另一个请求再来就会因为Key已经不存在而校验失败。实际项目里我强烈建议在测试环境写一个并发脚本压一下这个场景数据会说话。5. 数据与安全设计5.1 Redis数据结构与Key设计验证码场景用到的Redis数据结构主要是字符串类型但Key命名规范非常值得讲究。命名不规范的话项目做到后面会出现大量难以排查的脏数据。Key前缀用途示例TTLcaptcha:img:{captchaId}图形验证码captcha:img:uuid120秒captcha:sms:{scene}:{phone}短信验证码captcha:sms:login:13800001111300秒rate:captcha:{ip}申请频控rate:captcha:192.168.1.160秒rate:validate:{ip}校验频控rate:validate:192.168.1.160秒lock:captcha:{captchaId}校验锁lock:captcha:uuid10秒Key的命名模式可以统一成“业务域:子域:标识”。业务域表示这是验证码相关的数据子域区分图形还是短信标识指向具体对象。这套规范同样可以套用在整个项目的其他模块上维护成本会低很多。5.2 校验码位数的安全逻辑回到那个经典问题为什么图形验证码用4位短信验证码用6位这背后是安全强度和用户体验的权衡。4位字母数字混合码的字符集去掉易混淆字符后有32个字符理论空间是32的4次方也就是1048576种组合。如果验证码有效期是2分钟防刷策略限制同一个IP最多尝试3次那么暴力破解成功的概率大约是3除以1048576极低。纯数字4位验证码的空间只有10000种组合安全性就弱了很多所以纯数字场景通常把长度拉长到6位。短信验证码用6位数字空间是1000000且短信发送有成本攻击者不可能无限尝试。位数不是拍脑袋定的它是空间大小、有效期、尝试次数三者共同作用的结果。5.3 防刷与风控设计防刷设计是验证码系统能否真正抗住压力的关键。很多系统上线后被人用脚本刷爆不是验证码算法不行而是防刷机制没做够。第一层防刷是申请接口频控。同一IP在单位时间内的申请次数要限制这个限制要设置得相对宽松因为局域网出口IP下可能有大量正常用户。我通常设置为每IP每分钟申请验证码不超过30次这个数值可以根据业务调整。第二层防刷是校验接口频控。同一IP的单位时间校验失败次数要限制比如连续失败15次后锁定15分钟。校验失败的计数和验证码本身不一样它不能随着验证码过期而清空否则攻击者可以一直换验证码来试。第三层防刷是业务流程维度的控制。比如注册场景同一手机号一天内只能发5次短信验证码同一设备指纹一天的注册次数不超过3次。这一层需要业务方配合提供设备指纹、手机号等上下文信息。我在做防刷时踩过一个坑就是只做了IP维度的限制。后来发现攻击者可以通过代理池换IP绕过。后面加了设备指纹和账号维度才真正把刷量压下去。验证码的防刷一定要做多维度组合别指望单一维度万无一失。5.4 图形绘制设计要点图形验证码的绘制效果直接影响用户识别率和安全性。画得太简单容易被OCR识别画得太复杂用户也认不出来这个平衡很难拿捏。我的经验是把握几个原则。字体上选择笔画粗细不均匀的字体更抗识别但必须保证可读性。颜色上每个字符可以随机取不同颜色背景色和字符色的对比度要足够别搞成“高级灰”让用户看了半天看不清。干扰线上画几条随机的曲线或直线再加一部分噪点就足够了不要把整张图糊满。扭曲处理上对字符做轻微倾斜或者波浪形变形能在不破坏可读性的前提下显著提升OCR难度。绘制后的图片直接输出PNG格式PNG无压缩损失、边缘清晰在前端展示效果最好。图片尺寸建议不要太大宽120到150像素、高40到50像素比较常见。另外生成图片的代码要关注性能。每张图片的绘制要控制在几毫秒级别如果因为字体资源加载或者复杂滤镜处理导致单张图片耗时上百毫秒在高并发下会直接把CPU打满。6. 设计阶段常见的坑与排查思路6.1 图片生成后前端显示一片空白这个坑我遇到过不止一次现象是接口正常返回了Base64字符串但前端图片就是不显示。排查思路是先看响应数据有没有带data:image/png;base64,前缀。如果前缀缺失前端需要自己拼很多人不知道这个规则就会一直白屏。另一种可能是字体问题。服务端如果在没有安装所需字体的环境中运行绘制图片时不会报错但最终画出来的是空白或者乱码。这种情况下建议把字体文件打成资源包放进项目里而不是依赖服务器系统字体。6.2 并发校验时验证码疯狂失效一个用户提交登录前端网络重试机制自动发了两三次请求结果验证码第一次就成功后面几次全部提示失效。用户会认为系统有问题因为明明输入是对的。这个现象其实就是一次性校验的副作用。解决方案是在前端做提交按钮防抖发送请求期间禁用提交按钮避免重复提交。后端也不能因为前端做了防抖就放松并发控制两者要同时处理。6.3 防刷策略误伤正常用户防刷策略设置太激进会把同一出口IP下的所有正常用户都挡在门外。尤其是公司、学校、机场这类场景几十个人共享一个公网IP触发频控后大家集体不能用。应对方案是设置不同维度的阈值。IP维度阈值放宽账号维度阈值收紧校验失败次数单独计数不要跟申请次数混在一起。还可以在触发频控后增加人机升级验证比如让用户完成滑块验证后再继续操作而不是直接拒绝服务。6.4 设计期最容易忽略的四个细节有些坑不在报错里出现只会在线上冒出来我把它们归纳成一张速查表供你在设计阶段就对照检查。坑点产生原因后果设计方案校验成功后不删Key没有理解一次性语义验证码可被重放攻击校验流程强制删除Redis用GETDEL保证原子性刷新接口不限流只对申请接口做了频控刷新接口可被刷爆申请和刷新共用同一频控逻辑日志里打印明文验证码方便排查问题用户验证码泄露日志只记录captchaId不记录校验码测试环境关闭验证码方便测试生产配置误同步导致防线失效使用环境变量控制不允许硬编码测试环境关闭验证码这点需要特别提醒。有些团队为了测试方便在代码里写死一个开关跳过验证码一旦发布配置忘改生产环境等于裸奔。应该用配置中心或环境变量控制并且生产环境的配置要经过单独的发布审批。7. 项目目录规划与开发计划7.1 后端工程目录与模块划分设计阶段最后一步是把目录结构落下来。我是做Java后端出身习惯用Spring Boot的包结构来组织。下面这个结构可以作为参考com.example.captcha ├── controller │ ├── CaptchaController.java ├── service │ ├── CaptchaService.java │ ├── impl │ │ └── CaptchaServiceImpl.java ├── component │ ├── CodeGenerator.java │ ├── ImageRenderer.java │ ├── RateLimiter.java ├── repository │ ├── CaptchaCache.java ├── model │ ├── dto │ │ ├── CaptchaGenerateResponse.java │ │ ├── CaptchaValidateRequest.java │ │ └── CaptchaValidateResponse.java ├── config │ ├── RedisConfig.java │ └── CaptchaProperties.java └── common ├── exception └── result我特意把组件相关的类放在component包而不是都堆在service包里是为了强调每个组件的独立职责。CaptchaCache负责隔离Redis操作这样如果以后把Redis换成其他存储中间件只需要改动这一个类。7.2 前端组件目录与交互设计前端部分我对Vue框架更熟悉所以拿Vue的结构举例。核心是把验证码封装成独立组件而不是在每个页面散落着复制粘贴的代码。src/components/captcha ├── CaptchaField.vue ├── useCaptcha.js └── types.tsCaptchaField组件负责展示图片、渲染输入框、点击图片刷新等交互逻辑。useCaptcha是组合式函数封装申请验证码、校验验证码、处理刷新状态的业务逻辑。组件的props接收场景标识比如login、register这样同一个组件可以复用在多个场景而频控和配置可以按场景区分。前端交互有一个细节容易忽略就是“发送短信验证码”按钮点击之前的图形验证码前置逻辑。通常流程是用户先输入图形验证码并校验通过才允许发送短信验证码。这个顺序能有效防止短信接口被脚本直接刷掉。7.3 开发里程碑与验证标准设计再好最终要看能不能落地。我给这个案例规划了四个开发阶段每个阶段都有明确的交付物和验证标准。阶段目标关键产出验证标准阶段一搭建工程骨架实现图形验证码生成可访问的申请接口返回Base64图片浏览器能正确显示验证码图片阶段二实现缓存存储与校验闭环校验接口可正常工作一次性语义生效同一验证码重复校验第二次必然失败阶段三前端组件集成与交互完善验证码组件封装完成可嵌入登录页刷新、过期、错误提示交互完备阶段四防护加固与性能压测频控、防刷、并发原子性处理完成用JMeter压测单机撑住每秒200并发申请这里要强调不要把频控和防刷放到最后才做。我在之前的项目里试过先把正常链路跑通再回头补防刷结果发现很多接口设计在加防刷时变得非常别扭。防刷应该在阶段二就跟着校验流程一起实现哪怕初版参数粗糙一点后期再调整都来得及。7.4 留到编码前再确认的四个边界场景设计文档写得再完整编码前还是要把几个边界场景拿出来和团队对一遍。这四个场景是我在多个项目里反复遇到过的每次都能引发一堆讨论。第一个用户获取验证码后2分钟内没有使用过期后前端怎么提示。是自动刷新一张新图还是提示用户手动点击刷新两种交互各有优劣。我倾向于自动刷新能减少用户操作成本。第二个校验接口被频繁调用触发频控后前端应该给出什么反馈。反馈文案要明确告诉用户“操作过于频繁请稍后再试”而不是笼统地返回“验证码错误”否则用户会误以为自己输错了。第三个多个浏览器标签页同时打开每个标签页都有验证码用户在一个标签页输入成功另一个标签页的验证码是否还算有效。我的设计是每个标签页独立申请captchaId互不影响但字段校验失败后需要重新申请。第四个验证码申请接口是否需要登录状态。我强烈建议不需要。用户本来就是为了登录才看验证码如果验证码申请也需要登录就成了死循环。这些边界场景看起来细微但设计文档里有没有写清楚直接决定后面联调测试要返工几次。与其在开发中不断地口头约定不如在设计阶段就把结论敲定写进文档里留个存档。我个人在实际项目里最大的体会是验证码这个模块最困难的部分从来不在画图而在于怎么和业务边界、缓存、安全策略做结合。你把总体设计这部分想透了后续的编码工作会顺利得多。下一篇我会进入实现阶段把Service层的骨架代码、Redis缓存细节、图片绘制算法逐个讲清楚到时候会有更多可以直接抄作业的东西。
返回列表