用戶登錄幾乎是一個(gè)線上系統(tǒng)必不可少且使用相對比較頻繁的一個(gè)模塊,為了防止惡意暴力嘗試,防止洪水攻擊、防止腳本自動(dòng)提交等,驗(yàn)證碼是一個(gè)較為便捷且行之有效的預(yù)防手段,下面使用三個(gè)簡單的步驟輕松實(shí)現(xiàn)一個(gè)驗(yàn)證碼功能,具體的效果如下:

第一步:工具類
該工具類為生成驗(yàn)證碼圖片的核心,直接拷貝到項(xiàng)目即可,無需做修改;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.util.Random;
/**
* 圖形驗(yàn)證碼生成
*/
public class VerifyUtil {
// 默認(rèn)驗(yàn)證碼字符集
private static final char[] chars = {
'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',
'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'};
// 默認(rèn)字符數(shù)量
private final Integer SIZE;
// 默認(rèn)干擾線數(shù)量
private final int LINES;
// 默認(rèn)寬度
private final int WIDTH;
// 默認(rèn)高度
private final int HEIGHT;
// 默認(rèn)字體大小
private final int FONT_SIZE;
// 默認(rèn)字體傾斜
private final boolean TILT;
private final Color BACKGROUND_COLOR;
/**
* 初始化基礎(chǔ)參數(shù)
*
* @param builder
*/
private VerifyUtil(Builder builder) {
SIZE = builder.size;
LINES = builder.lines;
WIDTH = builder.width;
HEIGHT = builder.height;
FONT_SIZE = builder.fontSize;
TILT = builder.tilt;
BACKGROUND_COLOR = builder.backgroundColor;
}
/**
* 實(shí)例化構(gòu)造器對象
*
* @return
*/
public static Builder newBuilder() {
return new Builder();
}
/**
* @return 生成隨機(jī)驗(yàn)證碼及圖片
* Object[0]:驗(yàn)證碼字符串;
* Object[1]:驗(yàn)證碼圖片。
*/
public Object[] createImage() {
StringBuffer sb = new StringBuffer();
// 創(chuàng)建空白圖片
BufferedImage image = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB);
// 獲取圖片畫筆
Graphics2D graphic = image.createGraphics();
// 設(shè)置抗鋸齒
graphic.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// 設(shè)置畫筆顏色
graphic.setColor(BACKGROUND_COLOR);
// 繪制矩形背景
graphic.fillRect(0, 0, WIDTH, HEIGHT);
// 畫隨機(jī)字符
Random ran = new Random();
//graphic.setBackground(Color.WHITE);
// 計(jì)算每個(gè)字符占的寬度,這里預(yù)留一個(gè)字符的位置用于左右邊距
int codeWidth = WIDTH / (SIZE + 1);
// 字符所處的y軸的坐標(biāo)
int y = HEIGHT * 3 / 4;
for (int i = 0; i < SIZE; i++) {
// 設(shè)置隨機(jī)顏色
graphic.setColor(getRandomColor());
// 初始化字體
Font font = new Font(null, Font.BOLD + Font.ITALIC, FONT_SIZE);
if (TILT) {
// 隨機(jī)一個(gè)傾斜的角度 -45到45度之間
int theta = ran.nextInt(45);
// 隨機(jī)一個(gè)傾斜方向 左或者右
theta = (ran.nextBoolean() == true) ? theta : -theta;
AffineTransform affineTransform = new AffineTransform();
affineTransform.rotate(Math.toRadians(theta), 0, 0);
font = font.deriveFont(affineTransform);
}
// 設(shè)置字體大小
graphic.setFont(font);
// 計(jì)算當(dāng)前字符繪制的X軸坐標(biāo)
int x = (i * codeWidth) + (codeWidth / 2);
// 取隨機(jī)字符索引
int n = ran.nextInt(chars.length);
// 得到字符文本
String code = String.valueOf(chars[n]);
// 畫字符
graphic.drawString(code, x, y);
// 記錄字符
sb.append(code);
}
// 畫干擾線
for (int i = 0; i < LINES; i++) {
// 設(shè)置隨機(jī)顏色
graphic.setColor(getRandomColor());
// 隨機(jī)畫線
graphic.drawLine(ran.nextInt(WIDTH), ran.nextInt(HEIGHT), ran.nextInt(WIDTH), ran.nextInt(HEIGHT));
}
// 返回驗(yàn)證碼和圖片
return new Object[]{sb.toString(), image};
}
/**
* 隨機(jī)取色
*/
private Color getRandomColor() {
Random ran = new Random();
Color color = new Color(ran.nextInt(256), ran.nextInt(256), ran.nextInt(256));
return color;
}
/**
* 構(gòu)造器對象
*/
public static class Builder {
// 默認(rèn)字符數(shù)量
private int size = 4;
// 默認(rèn)干擾線數(shù)量
private int lines = 10;
// 默認(rèn)寬度
private int width = 80;
// 默認(rèn)高度
private int height = 35;
// 默認(rèn)字體大小
private int fontSize = 25;
// 默認(rèn)字體傾斜
private boolean tilt = true;
//背景顏色
private Color backgroundColor = Color.LIGHT_GRAY;
public Builder setSize(int size) {
this.size = size;
return this;
}
public Builder setLines(int lines) {
this.lines = lines;
return this;
}
public Builder setWidth(int width) {
this.width = width;
return this;
}
public Builder setHeight(int height) {
this.height = height;
return this;
}
public Builder setFontSize(int fontSize) {
this.fontSize = fontSize;
return this;
}
public Builder setTilt(boolean tilt) {
this.tilt = tilt;
return this;
}
public Builder setBackgroundColor(Color backgroundColor) {
this.backgroundColor = backgroundColor;
return this;
}
public VerifyUtil build() {
return new VerifyUtil(this);
}
}
}
第二步:圖片生成
- 使用默認(rèn)參數(shù)
// 返回的數(shù)組第一個(gè)參數(shù)是生成的驗(yàn)證碼,第二個(gè)參數(shù)是生成的圖片
Object[] objs = VerifyUtil.newBuilder().build().createImage();
- 自定義參數(shù)
// 這個(gè)根據(jù)自己的需要設(shè)置對應(yīng)的參數(shù)來實(shí)現(xiàn)個(gè)性化
// 返回的數(shù)組第一個(gè)參數(shù)是生成的驗(yàn)證碼,第二個(gè)參數(shù)是生成的圖片
Object[] objs = VerifyUtil.newBuilder()
.setWidth(120) //設(shè)置圖片的寬度
.setHeight(35) //設(shè)置圖片的高度
.setSize(6) //設(shè)置字符的個(gè)數(shù)
.setLines(10) //設(shè)置干擾線的條數(shù)
.setFontSize(25) //設(shè)置字體的大小
.setTilt(true) //設(shè)置是否需要傾斜
.setBackgroundColor(Color.WHITE) //設(shè)置驗(yàn)證碼的背景顏色
.build() //構(gòu)建VerifyUtil項(xiàng)目
.createImage(); //生成圖片
第三步:整合至SpringBoot項(xiàng)目
- 引入redis相關(guān)依賴
用于保存驗(yàn)證碼驗(yàn)證重試次數(shù)及驗(yàn)證碼失效等數(shù)據(jù)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
- 獲取及驗(yàn)證相關(guān)代碼
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.OutputStream;
import java.util.concurrent.TimeUnit;
//測試Controller
@RestController
@RequestMapping("verify")
public class VerifyController {
@Autowired
RedisTemplate redisTemplate;
/**
* 生成驗(yàn)證碼的接口
*
* @param response Response對象
* @param request Request對象
* @throws Exception
*/
@PostMapping("/getcode")
public void getCode(HttpServletResponse response, HttpServletRequest request) throws Exception {
// 獲取到session
HttpSession session = request.getSession();
// 取到sessionid
String id = session.getId();
// 利用圖片工具生成圖片
// 返回的數(shù)組第一個(gè)參數(shù)是生成的驗(yàn)證碼,第二個(gè)參數(shù)是生成的圖片
Object[] objs = VerifyUtil.newBuilder()
.setWidth(120) //設(shè)置圖片的寬度
.setHeight(35) //設(shè)置圖片的高度
.setSize(6) //設(shè)置字符的個(gè)數(shù)
.setLines(10) //設(shè)置干擾線的條數(shù)
.setFontSize(25) //設(shè)置字體的大小
.setTilt(true) //設(shè)置是否需要傾斜
.setBackgroundColor(Color.LIGHT_GRAY) //設(shè)置驗(yàn)證碼的背景顏色
.build() //構(gòu)建VerifyUtil項(xiàng)目
.createImage(); //生成圖片
// 將驗(yàn)證碼存入Session
session.setAttribute("SESSION_VERIFY_CODE_" + id, objs[0]);
// 打印驗(yàn)證碼
System.out.println(objs[0]);
// 設(shè)置redis值的序列化方式
redisTemplate.setValueSerializer(new StringRedisSerializer());
// 在redis中保存一個(gè)驗(yàn)證碼最多嘗試次數(shù)
redisTemplate.opsForValue().set(("VERIFY_CODE_" + id), "3", 5 * 60, TimeUnit.SECONDS);
// 將圖片輸出給瀏覽器
BufferedImage image = (BufferedImage) objs[1];
response.setContentType("image/png");
OutputStream os = response.getOutputStream();
ImageIO.write(image, "png", os);
}
/**
* 業(yè)務(wù)接口包含了驗(yàn)證碼的驗(yàn)證
*
* @param code 前端傳入的驗(yàn)證碼
* @param request Request對象
* @return
*/
@GetMapping("/checkcode")
public String checkCode(String code, HttpServletRequest request) {
HttpSession session = request.getSession();
String id = session.getId();
// 將redis中的嘗試次數(shù)減一
String verifyCodeKey = "VERIFY_CODE_" + id;
long num = redisTemplate.opsForValue().decrement(verifyCodeKey);
// 如果次數(shù)次數(shù)小于0 說明驗(yàn)證碼已經(jīng)失效
if (num < 0) {
return "驗(yàn)證碼失效!";
}
// 將session中的取出對應(yīng)session id生成的驗(yàn)證碼
String serverCode = (String) session.getAttribute("SESSION_VERIFY_CODE_" + id);
// 校驗(yàn)驗(yàn)證碼
if (null == serverCode || null == code || !serverCode.toUpperCase().equals(code.toUpperCase())) {
return "驗(yàn)證碼錯(cuò)誤!";
}
// 驗(yàn)證通過之后手動(dòng)將驗(yàn)證碼失效
redisTemplate.delete(verifyCodeKey);
// 這里做具體業(yè)務(wù)相關(guān)
return "驗(yàn)證碼正確!";
}
}
-
PostMan測試
-
正常的
異常失效的
-
-
HTML方式測試
- 代碼
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="initial-scale=1.0,width=device-width, user-scalable=no"/> <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script> <title>Title</title> </head> <body> <p>方式一</p> <img alt="驗(yàn)證碼1" id="code1" onclick="check1()"/> <p>方式二</p> <img alt="驗(yàn)證碼2" id="code2" onclick="check2()"/> </body> </html> <script> getCode1(); getCode2(); // 點(diǎn)擊事件 function check1() { getCode1(); } // 點(diǎn)擊事件 function check2() { getCode2(); } // 響應(yīng)類型以blob的方式 function getCode1() { var url = "http://127.0.0.1/verify/getcode"; var xhr = new XMLHttpRequest(); xhr.open('POST', url, true); xhr.responseType = "blob"; xhr.onload = function () { if (this.status === 200) { var res = this.response; $("#code1").attr("src", window.URL.createObjectURL(res)); } }; xhr.send(); } // 響應(yīng)類型以arraybuffer的方式 function getCode2() { var url = "http://127.0.0.1/verify/getcode"; var xhr = new XMLHttpRequest(); xhr.open('POST', url, true); xhr.responseType = "arraybuffer"; xhr.onload = function () { if (this.status === 200) { var res = this.response; $("#code2").attr("src", "data:image/png;base64," + btoa( new Uint8Array(res).reduce(function (data, byte) { console.log(data); return data + String.fromCharCode(byte) }, '') )); } }; xhr.send(); } </script>- 運(yùn)行結(jié)果


