做一个简单的Web图形验证码
先来说说为什么会有验证码这么个反人类的玩意 每次输入验证码 365°的都是错误 然后重新输入 随着时间的推移 验证码这玩意 越来越高级越来越难 某班
的验证码还是汉字 简直了 但是呢 深处互联网时代 这个验证码可以说说 必不可少的 怎么说呢 举个简单的 🌰
现在有一个系统需要登录登出 那注册也是必不可少的吧 如果现在注册没有验证码 那注册账号 就显得简单多了 那么问题来了 注册是一个向服务器发起的请求 有人抓取注册的请求协议 直接可以写一个批量的注册机 一秒就可以注册上千个账号 是不是问题就来了
互联行为的注册、登录、发帖、领优惠券、投票等等应用场景,都有被机器刷造成各类损失的风险,如果不对各类机器垃圾的行为加以防范,灌水内容、垃圾注册、恶意登录、刷票、撞库、活动作弊、垃圾广告、爬虫、羊毛党等用户行为一旦发生,将对产品自身发展、用户体验造成极大的影响。
好了 废话不讲 直接上干货
为了防止机器识别 OCR之类 需要加点难度
- 噪点 干扰线
- 字体大小
- 字体风格
- 字体颜色
- 字体位置
- 字体旋转角度
我们从上面6个方面入手
先做干扰线 就是画线条
public void drawInterferingLind(Graphics g, int level) {
for (int i = 0; i < level; i++) {
int x = mRandom.nextInt(mImgWidth);
int y = mRandom.nextInt(mImgHeight);
int x1 = mRandom.nextInt(mImgWidth);
int y1 = mRandom.nextInt(mImgHeight);
g.setColor(getRandomColor());
g.drawLine(x, y, x1, y1);
}
}
随机字体大小 和 风格
private Font getRandomFontStyle() {
//随机字体名称
String fontName = mFont[mRandom.nextInt(mFont.length)];
//随机字体大小 初始值为最小26 最大36
int fontSize = mRandom.nextInt(10) + 26;
//给出2/3的概率设为字体加粗
int fontStyle = mRandom.nextInt(3) == 1 ? Font.PLAIN : Font.BOLD;
Font font = new Font(fontName, fontStyle, fontSize);
return font;
}
随机字体颜色
由于我们的背景是白色的 如果是完全随机验证的画 会一半的概率发现 验证码 看不起 难搞哦 所以对于文本 我们需要深色系列的字体 。RBG颜色控制在115以内 干扰线的话 就没那么讲究了 直接255 随机色值
/**
* 获取随机颜色 深色系列 用于文本显示
*
* @return
*/
private Color getDarkColor() {
return new Color(mRandom.nextInt(115), mRandom.nextInt(115), mRandom.nextInt(115));
}
综合上面 附加 字体位置 和 旋转角度
这里需要提的几个点
- 刚刚开始为了实现字体有一定夹角 我用了 Graphics 强转为2DGraphics 这样就可以达到旋转的效果了 但是发现他旋转的是整个画布 我们绘制是一个字符 一个字符绘制的 每次旋转多少 绘制完一个字符 就需要 逆转回去 不然就会出现 字符转没了 4个字符 需要旋转8次 还很麻烦 后来查了下 发现
font.deriveFont
可以设置 AffineTransform 这就很理想了 - 由于字符是平分 底图画板的宽度的 也就是X轴的位置差不多固定了 我有加了点随机偏移值 但是 如果带上旋转的话 会发现 首位2个字符 可能会消失一半 原因是 在旋转时 如果首字符逆时针旋转了 他就跑外面去了 底图就那么大 这里2种解决办法
- 1.一开始在绘制的时候 就限制掉绘制的首位字符的X轴 让他离边距产生点距离 保证旋转的时候不跑出去
- 2.让首位字符不产生夹角 也就是不旋转 那也就没有因为夹角过大消失的问题了 这里我选了第二种办法解决
/**
* 绘制验证码文本
*
* @param g
*/
private void drawCaptchaText(Graphics g, int textLength) {
//获取文本
String randomChars = getRandomChars(textLength);
mCaptchaText = randomChars;
//设置文本颜色
g.setColor(getDarkColor());
for (int i = 0; i < textLength; i++) {
//设置随机字体风格
Font font = getRandomFontStyle();
/*
首尾2个字符不进行旋转 如果旋转 可能会只出现半个字符 另一半旋转到边缘去了
*/
if (!(i == 0 || i == textLength - 1)) {
//设置随机旋转角度 在 -45~45°区间
Double angle = Math.toRadians(mRandom.nextInt(90) - 45);
//创建一个2D可旋转字体对象
AffineTransform affineTransform = new AffineTransform();
//设置旋转角度
affineTransform.rotate(angle);
//替换Font
font = font.deriveFont(affineTransform);
}
g.setFont(font);
//设置x的坐标 将布局空间分配个字符串加上一点随机偏移量
int x = mImgWidth / textLength * i + mRandom.nextInt(mImgWidth / 4 / 2);
//设置y的坐标 在绘制文本以文本底变为y的坐标 设置一个最低高度 以免文本太靠上消失
int y = mRandom.nextInt(mImgHeight - font.getSize()) + font.getSize();
g.drawString(String.valueOf(randomChars.charAt(i)), x, y);
}
}
最后附上效果
应该还可以 差不多了
总结
验证码还是很有用的 上面的代码也都很简单 问题可能就在于 随机夹角吧 其他应该都还可以 最后附上Captcha类
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.util.Random;
/**
* @author Bee
*/
public class Captcha {
private Random mRandom = new Random();
/**
* 随机字体字体
*/
private String[] mFont = {"宋体", "楷体", "黑体", "微软雅黑", "仿宋"};
/**
* 随机字符
*/
private String mCodes = "qwertyuipasdfghjklxcvbnm1234567890QWERTYUPASDFGHJKLZXCVBNM";
/**
* 验证码文本
*/
private String mCaptchaText = "";
/**
* 验证码图形宽度
*/
private int mImgWidth = 0;
/**
* 验证码图形高度
*/
private int mImgHeight = 0;
public String getCaptchaText() {
return mCaptchaText.toLowerCase();
}
public int getImgWidth() {
return mImgWidth;
}
public void setImgWidth(int mImgWidth) {
this.mImgWidth = mImgWidth;
}
public int getImgHeight() {
return mImgHeight;
}
public void setImgHeight(int mImgHeight) {
this.mImgHeight = mImgHeight;
}
public Captcha(int imgWidth, int imgHeight) {
this.mImgWidth = imgWidth;
this.mImgHeight = imgHeight;
}
/**
* 获取随机字符
*
* @param length 长度
* @return 随机结果
*/
public String getRandomChars(int length) {
if (length < 1 || length > 20) {
length = 4;
}
StringBuffer sb = new StringBuffer();
for (int i = 0; i < length; i++) {
sb.append(mCodes.charAt(mRandom.nextInt(mCodes.length())));
}
return sb.toString();
}
/**
* 绘制干扰线
*
* @param g 图形
* @param level 干扰线等级
*/
public void drawInterferingLind(Graphics g, int level) {
for (int i = 0; i < level; i++) {
int x = mRandom.nextInt(mImgWidth);
int y = mRandom.nextInt(mImgHeight);
int x1 = mRandom.nextInt(mImgWidth);
int y1 = mRandom.nextInt(mImgHeight);
g.setColor(getRandomColor());
g.drawLine(x, y, x1, y1);
}
}
/**
* 获取随机颜色
*
* @return
*/
private Color getRandomColor() {
return new Color(mRandom.nextInt(255), mRandom.nextInt(255), mRandom.nextInt(255));
}
/**
* 获取随机颜色 深色系列 用于文本显示
*
* @return
*/
private Color getDarkColor() {
return new Color(mRandom.nextInt(115), mRandom.nextInt(115), mRandom.nextInt(115));
}
/**
* 获取 BufferedImage
*
* @return BufferedImage
*/
private BufferedImage getImg() {
//创建一个BufferedImage对象
BufferedImage img = new BufferedImage(mImgWidth, mImgHeight, BufferedImage.TYPE_INT_BGR);
Graphics g = img.getGraphics();
//绘制背景
g.setColor(Color.WHITE);
g.fillRect(0, 0, img.getWidth(), img.getHeight());
return img;
}
/**
* 绘制验证码文本
*
* @param g
*/
private void drawCaptchaText(Graphics g, int textLength) {
//获取文本
String randomChars = getRandomChars(textLength);
mCaptchaText = randomChars;
//设置文本颜色
g.setColor(getDarkColor());
for (int i = 0; i < textLength; i++) {
//设置随机字体风格
Font font = getRandomFontStyle();
/*
首尾2个字符不进行旋转 如果旋转 可能会只出现半个字符 另一半旋转到边缘去了
*/
if (!(i == 0 || i == textLength - 1)) {
//设置随机旋转角度 在 -45~45°区间
Double angle = Math.toRadians(mRandom.nextInt(90) - 45);
//创建一个2D可旋转字体对象
AffineTransform affineTransform = new AffineTransform();
//设置旋转角度
affineTransform.rotate(angle);
//替换Font
font = font.deriveFont(affineTransform);
}
g.setFont(font);
//设置x的坐标 将布局空间分配个字符串加上一点随机偏移量
int x = mImgWidth / textLength * i + mRandom.nextInt(mImgWidth / 4 / 2);
//设置y的坐标 在绘制文本以文本底变为y的坐标 设置一个最低高度 以免文本太靠上消失
int y = mRandom.nextInt(mImgHeight - font.getSize()) + font.getSize();
g.drawString(String.valueOf(randomChars.charAt(i)), x, y);
}
}
/**
* 随机字体风格
*
* @return
*/
private Font getRandomFontStyle() {
//随机字体名称
String fontName = mFont[mRandom.nextInt(mFont.length)];
//随机字体大小 初始值为最小26 最大36
int fontSize = mRandom.nextInt(10) + 26;
//给出2/3的概率设为字体加粗
int fontStyle = mRandom.nextInt(3) == 1 ? Font.PLAIN : Font.BOLD;
Font font = new Font(fontName, fontStyle, fontSize);
return font;
}
/**
* 获取验证码图片
*
* @return
*/
public BufferedImage getCaptchaImage(int length) {
BufferedImage img = getImg();
//获取图形
Graphics g = img.getGraphics();
//绘制干扰线 及干扰线等级
drawInterferingLind(g, 30);
//绘制文本 及文本个数
drawCaptchaText(g, length);
return img;
}
}