为什么引入验证码:
用户注册时:复杂的验证码一定程度上可以防止“恶意注册”,用户在注册时,先校验其验证码
是否正确,若验证码错误,则注册失败;
免密登录时:复杂的验证码一定程度上可以有效防止用户无限点击发送短信验证码接口进行
免密登录,造成无用的短信扣费;
流程:
1:用户第一次进入注册页面时,前端产生一个“唯一标识(此标识作为验证码存放在Redis中的
Key)”并且前端将该标识存储在sessionStorage中,在注册页面加载时将标识传给后端,
后端生成验证码将验证码的值作为value存放在Redis中,并将生成的图片验证码Base64数据
返回给前端渲染;
2:假如用户刷新注册页面(非第一次进入注册页面),前端则先去sessionStorage中判断是否已
经存在的“唯一标识”,如果有则将该标识传入后端,后端重新生成验证码并存放到Redis中
(Redis对已有的Key执行覆盖操作),后端再将新生成的图片验证码Base64数据返回给前端
渲染;
3:用户提交注册信息时,不仅要提交用户本身的信息如姓名,性别等信息,还要将用户输入
的验证码值,和前端生成的验证码标识一起提交给后端,后端根据前端提交过来的验证码标
识,去Redis中取验证码数据与用户填写的验证码数据做对比,如对比异常则提示“验证码错
误或失效”,如果验证码校验通过则进一步校验用户信息完成注册;
注意:
1:为什么要前端产生验证码“唯一标识”作为Redis的Key,而不是后端产生?
假设验证码的唯一标识在后端产生,用户进入注册界面时,后端产生一个验证码存入Redis并返回给前端,假如用户频繁刷新注册页面则,Redis中会存放很多无用验证码,此方式对于后端来说不是很方便将用户验证码的Key与用户多次操作对应起来(未登录状态);
唯一标识交由前端产生,可以将同一个用户的多次操作(刷新等操作)与后台Redis库中已有的验证码Key对应起来,Redis会根据Key相同进行覆盖value值,不会产生多个无效验证码;
2:为什么前端产生的“唯一标识”要存放在sessionStorage中而不是存放localStorage中?
存放在localStorage中的数据,不手动去清除会一直存在,而存放在sessionStorage中的
数据会在当前会话结束时自动清除里面的数据;
3:保存在Redis中的验证码信息,需设置过期时间,比如5分钟,如果不设置过期时间则
Redis中会一直存放很多已经验证过的验证码,占用Redis空间;
4:生产环境中可能不止一个业务会用到验证码,比如登录业务也可能会用到验证码,建议前
端生成的验证码唯一标识带一个验证码类型,用来标识该验证码是什么业务类型的;
如:type01代表的是注册业务的验证码,type02代表是登录业务的验证码(#作为分隔
符,#前数据表示业务类型,#后数据表示验证码的唯一标识);
type01#9527
type02#9816
5:生成的图片验证码中有大小写字母,建议后台生成验证码值时全转小写或全转大写再存入
Redis,在接收到用户输入的验证码进行对比时,同样对用户输入的验证码全转小写或全转
大写进行比较;
如何生成图片验证码:
图片验证码的实质:
一串随机字符串 + 背景图 + 干扰线
图片验证码生成工具类:
package cn.test.util;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Base64;
import java.util.Random;
/**
* 验证码生成工具类
*/
public class VerifyCodeUtils {
//使用到Algerian字体,系统里没有的话需要安装字体,字体只显示大写,去掉了1,0,i,o容易混淆的字符
public static final String VERIFY_CODES = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
private static Random random = new Random();
/**
* 使用系统默认字符源生成验证码
*
* @param verifySize 验证码长度
* @return
*/
public static String generateVerifyCode(int verifySize) {
return generateVerifyCode(verifySize, VERIFY_CODES);
}
/**
* 使用指定源生成验证码
*
* @param verifySize 验证码长度
* @param sources 验证码字符源
* @return
*/
public static String generateVerifyCode(int verifySize, String sources) {
if (sources == null || sources.length() == 0) {
sources = VERIFY_CODES;
}
int codesLen = sources.length();
Random rand = new Random(System.currentTimeMillis());
StringBuilder verifyCode = new StringBuilder(verifySize);
for (int i = 0; i < verifySize; i++) {
verifyCode.append(sources.charAt(rand.nextInt(codesLen - 1)));
}
return verifyCode.toString();
}
/**
* 输出指定验证码图片流
*/
public static void outputImage(int w, int h, OutputStream os, String code) throws IOException {
int verifySize = code.length();
BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
Random rand = new Random();
Graphics2D g2 = image.createGraphics();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
Color[] colors = new Color[5];
Color[] colorSpaces = new Color[]{Color.WHITE, Color.CYAN,
Color.GRAY, Color.LIGHT_GRAY, Color.MAGENTA, Color.ORANGE,
Color.PINK, Color.YELLOW};
float[] fractions = new float[colors.length];
for (int i = 0; i < colors.length; i++) {
colors[i] = colorSpaces[rand.nextInt(colorSpaces.length)];
fractions[i] = rand.nextFloat();
}
Arrays.sort(fractions);
g2.setColor(Color.GRAY);// 设置边框色
g2.fillRect(0, 0, w, h);
Color c = getRandColor(200, 250);
g2.setColor(c);// 设置背景色
g2.fillRect(0, 2, w, h - 4);
//绘制干扰线
Random random = new Random();
g2.setColor(getRandColor(160, 200));// 设置线条的颜色
for (int i = 0; i < 20; i++) {
int x = random.nextInt(w - 1);
int y = random.nextInt(h - 1);
int xl = random.nextInt(6) + 1;
int yl = random.nextInt(12) + 1;
g2.drawLine(x, y, x + xl + 40, y + yl + 20);
}
// 添加噪点
float yawpRate = 0.05f;// 噪声率
int area = (int) (yawpRate * w * h);
for (int i = 0; i < area; i++) {
int x = random.nextInt(w);
int y = random.nextInt(h);
int rgb = getRandomIntColor();
image.setRGB(x, y, rgb);
}
shear(g2, w, h, c);// 使图片扭曲
g2.setColor(getRandColor(100, 160));
int fontSize = h - 4;
Font font = new Font("Algerian", Font.ITALIC, fontSize);
g2.setFont(font);
char[] chars = code.toCharArray();
for (int i = 0; i < verifySize; i++) {
AffineTransform affine = new AffineTransform();
affine.setToRotation(Math.PI / 4 * rand.nextDouble() * (rand.nextBoolean() ? 1 : -1), (w / verifySize) * i + fontSize / 2, h / 2);
g2.setTransform(affine);
g2.drawChars(chars, i, 1, ((w - 10) / verifySize) * i + 5, h / 2 + fontSize / 2 - 10);
}
g2.dispose();
ImageIO.write(image, "jpg", os);
}
private static Color getRandColor(int fc, int bc) {
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);
}
private static int getRandomIntColor() {
int[] rgb = getRandomRgb();
int color = 0;
for (int c : rgb) {
color = color << 8;
color = color | c;
}
return color;
}
private static int[] getRandomRgb() {
int[] rgb = new int[3];
for (int i = 0; i < 3; i++) {
rgb[i] = random.nextInt(255);
}
return rgb;
}
private static void shear(Graphics g, int w1, int h1, Color color) {
shearX(g, w1, h1, color);
shearY(g, w1, h1, color);
}
private static void shearX(Graphics g, int w1, int h1, Color color) {
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.setColor(color);
g.drawLine((int) d, i, 0, i);
g.drawLine((int) d + w1, i, w1, i);
}
}
}
private static void shearY(Graphics g, int w1, int h1, Color color) {
int period = random.nextInt(40) + 10; // 50;
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.setColor(color);
g.drawLine(i, (int) d, i, 0);
g.drawLine(i, (int) d + h1, i, h1);
}
}
}
/**
* 获取随机验证码及其加密图片
*/
public static String VerifyCode(int w, int h, int size) throws IOException {
Base64.Encoder encoder = Base64.getEncoder();
String code = generateVerifyCode(size).toLowerCase(); // 指定size长度验证码
System.out.println("验证码:" + code);
ByteArrayOutputStream data = new ByteArrayOutputStream();
outputImage(w, h, data, code);
return encoder.encodeToString(data.toByteArray());
}
public static void main(String[] args) throws Exception{
System.out.println("图片验证码转Base64字符串数据:" + VerifyCode(100, 30, 6)); // 指定图片验证码 宽、高、位数
}
}
执行工具类中main方法如下结果:
验证码为:ltxjtm
验证码转Base64数据:
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
将Base64数据响应给前端渲染:
测试Html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Base64字符串数据转图片验证码</title>
</head>
<body>
<img src="data:text/html;base64,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">
</body>
</html>
明月万年无前身,照见古今独醒人
公子王孙何必问,虚度我青春;