# captcah
**Repository Path**: xihe-xihexieh1212/captcah
## Basic Information
- **Project Name**: captcah
- **Description**: 🚀 通过Java和SpringBoot,创建了一个功能完整的验证码系统,包括随机字符串生成、图像绘制、前端展示以及后端验证,为应用增加了一层安全保护!🛡️
- **Primary Language**: Java
- **License**: MIT
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2024-05-05
- **Last Updated**: 2024-05-05
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# captcha
🚀 通过Java和SpringBoot,创建了一个功能完整的验证码系统,包括随机字符串生成、图像绘制、前端展示以及后端验证,为应用增加了一层安全保护!🛡️

# Java 验证码开发(一)

---


上述是`SpringBoot` 项目的搭建工作,搭建完成之后,我们开始本次项目的基础部分介绍。
## 前言
> **今天来介绍一下普通型验证码。** 1.首先生成一随机字符串。字符串的内容可以根据需求设置为英文小写、英文大写、数字、特殊字符、中文。特殊字符和中文不便输入,实际应用中只需英文小写和数字,这里还需除去一些容易容易识别的字符,如:‘1’、‘0’、‘o’、‘l’等。 2.将生成的字符串转为图片。
> (a)将每个字符都生成一张透明背景正方形的图片;
> (b)将每张小图片合并在底图上,小图片随机旋转、缩放、扭曲,清除字符之间的距离;
> (c)在生成的大图上设置 阴影、波纹等效果,加上干扰线、噪点等,扭曲图片,加边框。
> 3.将验证码字符串存入数据库或者缓存中,设置有效时间;将图片 base64 字符串和随机字符串 token 传给前端。
> 4.用户根据图片上的内容,输入验证码;提交时将验证码和 token 传入后端;后端根据 token 取出数据库中验证码,忽略大小比较与用户输入的验证码。
## 一、生成验证码字符串
```java
import java.util.Random;
/**
* @description 验证码文本生成类
**/
public class TextCreatorUtils {
/**
* 生成验证码所需字符,除去容易识别的字符
*/
private static char[] charSequence = {
'A', 'B', 'D', 'E', 'F', 'G',
'H', 'J', 'K', 'P', 'Q', 'R',
'T', 'Y', 'Z', '2', '3', '4',
'5', '6', '7', '8', '9', 'a',
'b', 'c', 'd', 'e', 'f', 'y',
'n', 'm', 'n', 'p', 'w', 'x'
};
/**
* 根据系统时间创建随机数
*/
private static Random random = new Random(System.currentTimeMillis());
/**
* 验证码长度
*/
private static final int LENGTH = 5;
private TextCreatorUtils(){
}
/**
* 产生验证码文本
* @return 验证码字符串
*/
public static String getText() {
StringBuilder text = new StringBuilder();
for (int i = 0; i < LENGTH; ++i) {
text.append(charSequence[random.nextInt(charSequence.length)]);
}
return text.toString();
}
}
```
## 二、字符串绘制图片
> 将字符串绘制成图片,每个字符自由旋转、缩放、扭曲,消除字符与字符间的间隙。
```Java
/**
* @description 文本渲染器,将验证码文字转为图片,能够使文字粘连、扭曲、旋转、缩放
**/
public class WordRendererUtils {
/* 对验证码各个字符进行变换时的缩放因子范围,包括x方向和y方向 */
private static double[] scaleRange = new double[]{0.5, 1};
/* 验证码各个字符进行变换时的旋转因子范围,单位为弧度 */
private static double[] rotateRange = new double[]{-Math.PI / 5, Math.PI / 5};
/* 验证码各个字符进行变换时的切变因子范围,包括x方向和y方向 */
private static double[] shearRange = new double[]{0, 0};
/* 字体 */
private static Font font = new Font("TimesRoman", Font.BOLD, 40);
/* 颜色 */
private static Color color = Color.BLUE;
private WordRendererUtils() {
}
/**
* 将验证码文本转为图片
*
* @param word 验证码文本
* @param width 图片宽度
* @param height 图片高度
* @return 验证码文字图像
*/
public static BufferedImage renderWord(String word, int width, int height) {
//将验证码字符串转为数组
char[] codeChars = word.toCharArray();
//每个字符生成一个图片
BufferedImage[] images = new BufferedImage[codeChars.length];
//将word中每个char转为图片
for (int i = 0; i < codeChars.length; i++) {
//创建一个height边长的正方形图片
images[i] = new BufferedImage(height, height, 2);
//获取画笔
Graphics2D graphics2D = images[i].createGraphics();
//设置字体
graphics2D.setFont(font);
//设置
graphics2D.setColor(color);
//设置抗锯齿
RenderingHints hints = new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
hints.add(new RenderingHints(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY));
graphics2D.setRenderingHints(hints);
//设置缩放(scale)、旋转(rotate)、错切(shear)
AffineTransform affineTransform = new AffineTransform();
affineTransform.scale(getRandomInRange(scaleRange), getRandomInRange(scaleRange));
affineTransform.rotate(getRandomInRange(rotateRange), height / 2.0, height / 2.0);
affineTransform.shear(getRandomInRange(shearRange), 0);
graphics2D.setTransform(affineTransform);
//文字在图片中的位置,x轴水平居中
int x = (height - graphics2D.getFontMetrics().stringWidth(Character.toString(codeChars[i]))) / 2;
//坐标轴是从左上角开始的,y轴向下,所以这里用height-x
int y = height - x;
//画出这个文字
graphics2D.drawString(Character.toString(codeChars[i]), x, y);
//释放
graphics2D.dispose();
}
//返回每个字符图像合并图像
return appendImages(images, width, height);
}
/**
* 拼接每个字符图片
*
* @param images 文字图片数组
* @param width 图片宽度
* @param height 图片高度
* @return 合并后的图片
*/
private static BufferedImage appendImages(BufferedImage[] images, int width, int height) {
//创建一个width*height大小的黑色背景图片
BufferedImage bgImage = new BufferedImage(width, height, 2);
//获得画笔
Graphics2D graphics2D = bgImage.createGraphics();
//当前正在处理的图片序号
int index = 0;
//计算字符与字符间的总空隙
int d = 0;
for (int i = 1; i < images.length; i++) {
int distance = calculateDistanceBetweenChar2(images[i - 1], images[i]);
d += distance;
}
//使验证码水平居中
int drawX = 0;
if (images.length * images[0].getWidth() - d < width) {
drawX = (width - (images.length * images[0].getWidth() - d)) / 2;
}
//将第一个文字图片画到底图上
graphics2D.drawImage(images[index], drawX, 0, images[index].getWidth(), images[0].getHeight(), null);
drawX += images[index].getWidth();
index++;
//将1-images.length个图片画到底图上。并取消文字间隙,使文字粘连在一起
while (index < images.length) {
//加上2是为了使粘连更紧凑
int distance = calculateDistanceBetweenChar2(images[index - 1], images[index]) + 2;
graphics2D.drawImage(images[index], drawX - distance, 0, images[index].getWidth(), images[0].getHeight(), null);
drawX += images[index].getWidth() - distance;
index++;
}
graphics2D.dispose();
return bgImage;
}
/**
* 按行扫描
*
* @param leftImage 左边图像
* @param rightImage 右边图像
* @return 两图片之间的空隙
*/
private static int calculateDistanceBetweenChar2(BufferedImage leftImage, BufferedImage rightImage) {
//左图每行右侧空白字符个数列表
int[][] left = calculateBlankNum(leftImage);
//右图每行左侧空白字符个数列表
int[][] right = calculateBlankNum(rightImage);
int[] tempArray = new int[leftImage.getHeight()];
for (int i = 0; i < left.length; i++) {
if (right[i][0] == 0) {
tempArray[i] = left[i][1] + leftImage.getWidth();
} else {
tempArray[i] = left[i][1] + right[i][0];
}
}
return min(tempArray);
}
/**
* 计算每个图片每行的左右空白像素个数,int[row][0]存储左边空白像素个数int[row][1]存储右边空白像素个数
*
* @param image 图像
* @return 空白字符个数数组
*/
private static int[][] calculateBlankNum(BufferedImage image) {
int width = image.getWidth();
int height = image.getHeight();
int[][] result = new int[height][2];
for (int i = 0; i < result.length; i++) {
result[i][0] = 0;
result[i][1] = width;
}
int[] colorArray = new int[4];
for (int i = 0; i < height; i++) {
for (int j = 0; j < width; j++) {
colorArray = image.getRaster().getPixel(j, i, colorArray);
if (!checkArray(colorArray, new int[]{0, 0, 0, 0})) {
if (result[i][0] == 0) {
result[i][0] = j;
}
result[i][1] = width - j - 1;
}
}
}
return result;
}
/**
* 检查两数组是否相同
*
* @param arrayA A数组
* @param arrayB B数组
* @return 是否相同
*/
private static boolean checkArray(int[] arrayA, int[] arrayB) {
if (arrayA == null || arrayB == null) {
return false;
}
if (arrayA.length != arrayB.length) {
return false;
}
for (int i = 0; i < arrayA.length; i++) {
if (arrayA[i] != arrayB[i]) {
return false;
}
}
return true;
}
/**
* 找出数组中最小元素
*
* @param array 数组
* @return 数组中最小值
*/
private static int min(int[] array) {
int result = Integer.MAX_VALUE;
for (int item : array) {
if (item < result) {
result = item;
}
}
return result;
}
/**
* 获取一个指定范围内的double随机数
*
* @param range 范围
* @return 随机数
*/
private static double getRandomInRange(double[] range) {
if (range == null || range.length != 2) {
throw new RuntimeException("至少包含两个元素");
}
return Math.random() * (range[1] - range[0]) + range[0];
}
}
```
## 三、产生验证码图片
> 给图片设置阴影、波纹等效果,加上干扰线、噪点等,扭曲图片,加边框。
```java
/**
* @description 图片产生类
**/
public class ImageCreatorUtils {
private static int width = 200;
private static int height = 50;
/* 边框颜色 */
private static Color borderColor = Color.gray;
/* 边框宽度 */
private static int borderThickness = 1;
/* 随机数 */
private static Random random = new Random();
/* 是否画边框 */
private static boolean isBorderDrawn = true;
/* 是否有阴影 */
private static boolean isShadow = false;
/* 是否有波纹 */
private static boolean isRipple = false;
/* 是否制造干扰 */
private static boolean isMakeNoise = false;
/* 图片是否扭曲 */
private static boolean isShear = false;
private ImageCreatorUtils() {
}
/**
* 产生验证码图片
*
* @param text 验证码文本
* @return 验证码图片
*/
public static BufferedImage createImage(String text) {
BufferedImage bi = WordRendererUtils.renderWord(text, width, height);
bi = getDistortedImage(bi);
bi = addBackground(bi);
Graphics2D graphics = bi.createGraphics();
if (isBorderDrawn) {
drawBox(graphics);
}
return bi;
}
/**
* 添加背景
*
* @param baseImage 基图
* @return 加了背景的图片
*/
private static BufferedImage addBackground(BufferedImage baseImage) {
Color colorFrom = Color.WHITE;
Color colorTo = Color.WHITE;
int width = baseImage.getWidth();
int height = baseImage.getHeight();
BufferedImage imageWithBackground = new BufferedImage(width, height, 1);
Graphics2D graph = (Graphics2D) imageWithBackground.getGraphics();
RenderingHints hints = new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
hints.add(new RenderingHints(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY));
hints.add(new RenderingHints(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY));
hints.add(new RenderingHints(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY));
graph.setRenderingHints(hints);
GradientPaint paint = new GradientPaint(0.0F, 0.0F, colorFrom, (float) width, (float) height, colorTo);
graph.setPaint(paint);
graph.fill(new Rectangle2D.Double(0.0D, 0.0D, (double) width, (double) height));
graph.drawImage(baseImage, 0, 0, (ImageObserver) null);
return imageWithBackground;
}
/**
* 添加边框
*
* @param graphics 画笔
*/
private static void drawBox(Graphics2D graphics) {
graphics.setColor(borderColor);
if (borderThickness != 1) {
BasicStroke stroke = new BasicStroke((float) borderThickness);
graphics.setStroke(stroke);
}
Line2D line1 = new Line2D.Double(0.0D, 0.0D, 0.0D, (double) width);
graphics.draw(line1);
Line2D line2 = new Line2D.Double(0.0D, 0.0D, (double) width, 0.0D);
graphics.draw(line2);
line2 = new Line2D.Double(0.0D, (double) (height - 1), (double) width, (double) (height - 1));
graphics.draw(line2);
line2 = new Line2D.Double((double) (width - 1), (double) (height - 1), (double) (width - 1), 0.0D);
graphics.draw(line2);
}
/**
* 制作干扰
*
* @param image
* @param factorOne
* @param factorTwo
* @param factorThree
* @param factorFour
*/
private static void makeNoise(BufferedImage image, float factorOne, float factorTwo, float factorThree, float factorFour) {
Color color = Color.BLUE;
int width = image.getWidth();
int height = image.getHeight();
Point2D[] pts = null;
CubicCurve2D cc = new CubicCurve2D.Float((float) width * factorOne, (float) height * random.nextFloat(), (float) width * factorTwo, (float) height * random.nextFloat(), (float) width * factorThree, (float) height * random.nextFloat(), (float) width * factorFour, (float) height * random.nextFloat());
PathIterator pi = cc.getPathIterator((AffineTransform) null, 2.0D);
Point2D[] tmp = new Point2D[200];
int i = 0;
while (!pi.isDone()) {
float[] coords = new float[6];
switch (pi.currentSegment(coords)) {
case 0:
case 1:
tmp[i] = new Point2D.Float(coords[0], coords[1]);
default:
++i;
pi.next();
}
}
pts = new Point2D[i];
System.arraycopy(tmp, 0, pts, 0, i);
Graphics2D graph = (Graphics2D) image.getGraphics();
graph.setRenderingHints(new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON));
graph.setColor(color);
for (i = 0; i < pts.length - 1; ++i) {
if (i < 3) {
graph.setStroke(new BasicStroke(0.9F * (float) (4 - i)));
}
graph.drawLine((int) pts[i].getX(), (int) pts[i].getY(), (int) pts[i + 1].getX(), (int) pts[i + 1].getY());
}
graph.dispose();
}
/**
* 图片样式
*
* @param baseImage 基图
* @return 改变样式后的图片
*/
private static BufferedImage getDistortedImage(BufferedImage baseImage) {
BufferedImage distortedImage = new BufferedImage(baseImage.getWidth(), baseImage.getHeight(), BufferedImage.TYPE_INT_ARGB);
Graphics2D graph = (Graphics2D) distortedImage.getGraphics();
if (isShadow) {
ShadowFilter shadowFilter = new ShadowFilter();
shadowFilter.setRadius(10);
shadowFilter.setDistance(3);
shadowFilter.setOpacity(1);
baseImage = shadowFilter.filter(baseImage, null);
}
if (isRipple) {
RippleFilter rippleFilter = new RippleFilter();
rippleFilter.setWaveType(RippleFilter.SINE);
rippleFilter.setXAmplitude(7.5f);
rippleFilter.setYAmplitude(0.5f);
rippleFilter.setXWavelength(random.nextInt(7) + 8);
rippleFilter.setYWavelength(random.nextInt(3) + 2);
rippleFilter.setEdgeAction(TransformFilter.BILINEAR);
baseImage = rippleFilter.filter(baseImage, null);
}
graph.drawImage(baseImage, 0, 0, null, null);
if (isShear) {
shear(graph, baseImage.getWidth(), baseImage.getHeight());
}
graph.dispose();
if (isMakeNoise) {
makeNoise(distortedImage, .1f, .3f, .6f, .9f);
}
return distortedImage;
}
/**
* 字符和干扰线扭曲
*
* @param g 绘制图形的java工具类
* @param w1 验证码图片宽
* @param h1 验证码图片高
*/
private static void shear(Graphics g, int w1, int h1) {
shearX(g, w1, h1);
shearY(g, w1, h1);
}
/**
* x轴扭曲
*
* @param g 绘制图形的java工具类
* @param w1 验证码图片宽
* @param h1 验证码图片高
*/
private static void shearX(Graphics g, int w1, int h1) {
int period = random.nextInt(2);
boolean borderGap = true;
int frames = 1;
int phase = random.nextInt(2);
for (int i = 0; i < h1; i++) {
double d = (double) (period >> 1) * Math.sin((double) i / (double) period + (6.2831853071795862D * (double) phase) / (double) frames);
g.copyArea(0, i, w1, 1, (int) d, 0);
if (borderGap) {
g.drawLine((int) d, i, 0, i);
g.drawLine((int) d + w1, i, w1, i);
}
}
}
/**
* y轴扭曲
*
* @param g 绘制图形的java工具类
* @param w1 验证码图片宽
* @param h1 验证码图片高
*/
private static void shearY(Graphics g, int w1, int h1) {
int period = random.nextInt(10) + 5;
boolean borderGap = true;
int frames = 20;
int phase = 7;
for (int i = 0; i < w1; i++) {
double d = (double) (period >> 1) * Math.sin((double) i / (double) period + (6.2831853071795862D * (double) phase) / (double) frames);
g.copyArea(i, 0, 1, h1, 0, (int) d);
if (borderGap) {
g.drawLine(i, (int) d, i, 0);
g.drawLine(i, (int) d + h1, i, h1);
}
}
}
}
```
## 四、SpringBoot 创建 captcha demo
### 1.依赖 `pom.xml`
```xml