生成图片验证码
问题描述
在项目中遇到有人恶意拉取图片资源,无限刷资源,导致阿里图片服务器流量暴涨(钱遭不住),使得带宽一直处于上限,正常用户不能好的访问。
解决办法
这个功能设计要点有两个:
1.接口限流使得不能一直拉取接口获取图片地址,针对同一用户单位时间内请求不能超过N次,如果超过N次属于不正常访问,ip列入黑名单
2.图片服务器处理,添加失效时间,访问图片时必须带个token
3.添加图片验证码,列入黑名单用户必须验证图片验证码才可解除黑名单,这里主要说明生成图片验证码
其他情况验证码使用:通常我们最登录的时候,为了防止多次尝试或攻击登录接口,我们需要弄一个验证码的功能,只有输入验证码正确的情况下,我们才会去做密码校验,这样就减少了密码可能会被试出来的可能。
代码实现
使用包:java.awt
生成验证码配置(CaptchaUtil工具类)
/** * 验证码工具 * * @author LiRui * @version 1.0 */
public final class CaptchaUtil {
public static final int WIDTH = 60;
public static final int HEIGHT = 20;
private static final char[] MAP_TABLE = {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6',
'7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'};
private CaptchaUtil() {
}
/** * 生成图片验证码 * * @param width 宽度 * @param height 高度 * @return */
public static Map<String, Object> getImageCode(int width, int height) {
Map<String, Object> returnMap = new HashMap<>(2);
width = (width <= 0 ? WIDTH : width);
height = (height <= 0 ? HEIGHT : height);
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
// 获取图形上下文
Graphics g = image.getGraphics();
//生成随机类
Random random = new Random();
// 设定背景色
g.setColor(getRandColor(200, 250));
g.fillRect(0, 0, width, height);
//设定字体
g.setFont(new Font("Times New Roman", Font.PLAIN, 18));
// 随机产生168条干扰线,使图象中的认证码不易被其它程序探测到
g.setColor(getRandColor(160, 200));
for (int i = 0; i < 168; i++) {
int x = random.nextInt(width);
int y = random.nextInt(height);
int xl = random.nextInt(12);
int yl = random.nextInt(12);
g.drawLine(x, y, x + xl, y + yl);
}
//取随机产生的码
StringBuilder strEnsure = new StringBuilder();
//4代表4位验证码,如果要生成更多位的认证码,则加大数值
for (int i = 0; i < 4; ++i) {
strEnsure.append(MAP_TABLE[(int) (MAP_TABLE.length * Math.random())]);
// 将认证码显示到图象中
g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110),
20 + random.nextInt(110)));
// 直接生成
String str = strEnsure.substring(i, i + 1);
// 设置随便码在背景图图片上的位置
g.drawString(str, 13 * i + 20, 25);
}
// 释放图形上下文
g.dispose();
String base64Img = "data:image/png;base64,";
returnMap.put("image", base64Img + encodeToString("jpg", image));
returnMap.put("strEnsure", strEnsure.toString());
return returnMap;
}
/** * 给定范围获得随机颜色 */
static Color getRandColor(int fc, int bc) {
Random random = new Random();
if (fc > 255) {
fc = 255;
}
if (bc > 255) {
bc = 255;
}
int r = fc + random.nextInt(bc - fc);
int g = fc + random.nextInt(bc - fc);
int b = fc + random.nextInt(bc - fc);
return new Color(r, g, b);
}
/** * 将图片转换成base64格式进行存储 * * @param formatName 文件格式 * @param image 图片流 * @return base64字符串 */
private static String encodeToString(String formatName, BufferedImage image) {
String imageString = null;
try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
ImageIO.write(image, formatName, bos);
byte[] imageBytes = bos.toByteArray();
imageString = new String(Base64.encodeBase64(imageBytes));
} catch (IOException e) {
e.printStackTrace();
}
return imageString;
}
}
我这里直接将图片转换成base64位字符串,前端可以直接展示,这样写也可方便ajax请求使用
调用生成验证码返回
/** * 用于生成带四位数字验证码的图片 */
@RequestMapping(value = "/captcha", method = RequestMethod.GET)
@ResponseBody
public HashMap<String, Object> imageCode() {
//返回验证码和图片的map
Map<String, Object> map = CaptchaUtil.getImageCode(86, 37);
final ValueOperations operations = redisTemplate.opsForValue();
//生成唯一key
final String verifyKey = getVerifyKey();
//设置过期时间
operations.set(verifyKey, map.get("strEnsure"), 5, TimeUnit.MINUTES);
//返回base64位图片字符串,唯一key(用于查找哪个验证码)
return JsonWrapper.successWrapper("image", map.get("image"), "key", verifyKey);
}
如果只是一个单体项目,可以存在session当中,如果集成了shiro,也可以放shiro的session中。
而在分布式系统当中,需要考虑验证码的共享功能。
1、可以存储在session中,如需要集成spring session,把session存到redis等存储中间件中session验证码共享功能。
2、shiro集成了redis的,就可以存在shiro session当中实现共享。
3、当然,你也可以直接把验证码存到redis等中间件中,不需要通过session,但是key就必须唯一。(当前使用)
验证验证码
/** * code验证码验证 * * @param checkCode 前端用户输入返回的验证码 * @param key 验证码唯一标识 */
@RequestMapping(value = "/verify", method = RequestMethod.POST)
@ResponseBody
public HashMap<String, Object> checkCode(HttpServletRequest request,
@RequestParam String checkCode, @RequestParam String key) {
if (!redisTemplate.hasKey(key)) {
return JsonWrapper.failureWrapperMsg("验证码失效,请重新获取");
}
final ValueOperations operations = redisTemplate.opsForValue();
final String code = (String) operations.get(verifyKey);
if (!code.equalsIgnoreCase(key)) {
return JsonWrapper.failureWrapperMsg("验证码错误,请重新输入");
}
return JsonWrapper.successWrapper();
}
另外吕一明老师使用了三方包实现图片验证码验证更加简洁、简单(这篇博客就是参照他的格式写的)图片验证码的需求分析、优雅实现
还没有评论,来说两句吧...