springboot圖片驗(yàn)證碼功能模塊
前言:
大家好!我是小??!今天我們用五分鐘來用springboot實(shí)現(xiàn)我們常用的圖形驗(yàn)證碼功能模塊!
用戶登錄幾乎是一個(gè)線上系統(tǒng)必不可少且使用相對比較頻繁的一個(gè)模塊,為了防止惡意暴力嘗試,防止洪水攻擊、防止腳本自動(dòng)提交等,驗(yàn)證碼是一個(gè)較為便捷且行之有效的預(yù)防手段。
具體效果如下:

第一步:工具類
該工具類為生成驗(yàn)證碼圖片的核心,直接拷貝到項(xiàng)目即可,無需做修改;可個(gè)性化的參數(shù)全部對外提供的API,比如 字體大小,背景顏色,干擾線數(shù)量,高寬等都可以根據(jù)自己的需求設(shè)置對應(yīng)參數(shù);
代碼幾乎每一行都加了詳細(xì)的注釋;如果遇上特殊的個(gè)性化需求,調(diào)整一下這個(gè)工具類即可實(shí)現(xiàn)。
package com.feng.util;
/**
* @return null
* @author Ladidol
* @description
* @date 2022/4/11 22:15
*/
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ù):
//生成圖片驗(yàn)證碼 Object[] verify = 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)目中:
需要引入的maven依賴:
<!--redis相關(guān)配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- redis 連接池 -->
<!--新版本連接池lettuce-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- 圖形驗(yàn)證碼 -->
<dependency>
<groupId>net.jodah</groupId>
<artifactId>expiringmap</artifactId>
<version>0.5.10</version>
</dependency>
獲取相關(guān)的驗(yàn)證碼:
service層:
package com.feng.service;
import org.cuit.epoch.result.Result;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @return null
* @author Ladidol
* @description
* @date 2022/4/11 22:15
*/
public interface VerifyService {
/**
* 創(chuàng)建圖片驗(yàn)證碼
* @param response
* @param request
* @throws IOException
*/
void createCode(HttpServletResponse response, HttpServletRequest request) throws IOException;
/**
* 檢查圖片驗(yàn)證碼
* @param
* @param
* @throws IOException
*/
Result<String> checkCode(String verificationCode);
}
serviceimpl層:
package com.feng.service.impl;
import com.feng.service.VerifyService;
import com.feng.util.RedisServiceImpl;
import com.google.common.net.HttpHeaders;
import com.feng.util.VerifyUtil;
import org.springframework.http.ResponseCookie;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.OutputStream;
import java.time.Duration;
/**
* @return null
* @author Ladidol
* @description
* @date 2022/4/11 22:15
*/
@Service
public class VerifyServiceImpl implements VerifyService {
@Resource
RedisServiceImpl redisUtil;
/**
* 生成圖片驗(yàn)證碼
* @param response
* @param request
* @throws IOException
*/
@Override
public void createCode(HttpServletResponse response, HttpServletRequest request) throws IOException {
//獲取session
HttpSession session = request.getSession();
//獲得sessionId
String id = session.getId();
System.out.println();
ResponseCookie cookie = ResponseCookie.from("JSESSIONID",id)
.secure(true)
.domain("")
.path("/")
.maxAge(Duration.ofHours(1))
.sameSite("None")
.build();
//清除之前緩存的圖片驗(yàn)證碼
if (!String.valueOf(request.getSession().getAttribute("SESSION_VERIFY_CODE_"+id)).isEmpty()){
String getVerify = String.valueOf(request.getSession().getAttribute("SESSION_VERIFY_CODE_"+id));
redisUtil.del(getVerify);
System.out.println("清除成功");
}
//生成圖片驗(yàn)證碼,用的默認(rèn)參數(shù)
Object[] verify = VerifyUtil.newBuilder().build().createImage();
//將驗(yàn)證碼存入session
session.setAttribute("SESSION_VERIFY_CODE_" + id, verify[0]);
//打印驗(yàn)證碼
System.out.println(verify[0]);
//將驗(yàn)證碼存入redis
redisUtil.set((String) verify[0],id,5*60);
//將圖片傳給瀏覽器
BufferedImage image = (BufferedImage) verify[1];
response.setContentType("image/png");
response.setHeader(HttpHeaders.SET_COOKIE,cookie.toString());
OutputStream ops = response.getOutputStream();
ImageIO.write(image,"png",ops);
}
@Override
public Result<String> checkCode(String verificationCode){
if (!redisUtil.hasKey(verificationCode)){
return new Result<>(false,"驗(yàn)證碼錯(cuò)誤");
}
redisUtil.del(verificationCode);
return R.success();
}
}
這里面還會(huì)用到redis相關(guān)的工具類,我就不列出來了,想要的話可以看我以前的文章工具類戳這里
controller層:
這里有用到@RequiredArgsConstructor, 就是簡單的注入而已, 如果想要詳細(xì)了解戳這里
package com.feng.controller;
import lombok.RequiredArgsConstructor;
import com.feng.annotation.LimitRequest;
import com.feng.service.VerifyService;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @return null
* @author Ladidol
* @description 這里主要就是多種驗(yàn)證碼和登錄相關(guān)的東西
* @date 2022/4/11 21:46
*/
@RestController
@RequestMapping("/verify")
@RequiredArgsConstructor//這是在lombok工具給的注入方式,真帥
public class VerifyController {
private final VerifyService verifyService;
/**
* 獲取圖片驗(yàn)證碼
*/
@LimitRequest(count = 5)//這個(gè)注解就是表示, 你在限制時(shí)間里(我們這里默認(rèn)是六秒鐘), 只能請求五次
@GetMapping("/getCode")
public void getCode(HttpServletResponse response, HttpServletRequest request) throws IOException {
verifyService.createCode(response, request);
}
@LimitRequest(count = 5)//這個(gè)注解就是表示, 你在限制時(shí)間里(我們這里默認(rèn)是六秒鐘), 只能請求五次
@GetMapping("/checkCode")
public Result<String> checkCode(String code){
return verifyService.checkCode(code);
}
}
這里為了不被一直無限制的訪問該服務(wù), 我們用了一個(gè)限制ip訪問次數(shù)的注解@LimitRequest
annotion包下的注解類:
package com.feng.annotation;
import java.lang.annotation.*;
/**
* @return null
* @author Ladidol
* @description 限制ip訪問次數(shù)注解
* @date 2022/4/11 22:15
*/
@Documented
@Target(ElementType.METHOD) // 說明該注解只能放在方法上面
@Retention(RetentionPolicy.RUNTIME)
public @interface LimitRequest {
long time() default 6000; // 限制時(shí)間 單位:毫秒
int count() default 3; // 允許請求的次數(shù)
}
aspect包下的切面類:
package com.feng.aspect;
import net.jodah.expiringmap.ExpirationPolicy;
import net.jodah.expiringmap.ExpiringMap;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import com.feng.annotation.LimitRequest;
import org.cuit.epoch.exception.AppException;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
/**
* @return null
* @author Ladidol
* @description
* @date 2022/4/11 22:15
*/
@Aspect
@Component
public class LimitRequestAspect {
private static ConcurrentHashMap<String, ExpiringMap<String, Integer>> book = new ConcurrentHashMap<>();
// 定義切點(diǎn)
// 讓所有有@LimitRequest注解的方法都執(zhí)行切面方法
@Pointcut("@annotation(limitRequest)")
public void excudeService(LimitRequest limitRequest) {
}
@Around("excudeService(limitRequest)")
public Object doAround(ProceedingJoinPoint pjp, LimitRequest limitRequest) throws Throwable {
// 獲得request對象
RequestAttributes ra = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes sra = (ServletRequestAttributes) ra;
HttpServletRequest request = sra.getRequest();
// 獲取Map對象, 如果沒有則返回默認(rèn)值
// 第一個(gè)參數(shù)是key, 第二個(gè)參數(shù)是默認(rèn)值
ExpiringMap<String, Integer> uc = book.getOrDefault(request.getRequestURI(), ExpiringMap.builder().variableExpiration().build());
Integer uCount = uc.getOrDefault(request.getRemoteAddr(), 0);
if (uCount >= limitRequest.count()) { // 超過次數(shù),不執(zhí)行目標(biāo)方法
System.out.println("接口請求超過次數(shù)!");
throw new AppException("接口請求超過次數(shù)!");
} else if (uCount == 0) { // 第一次請求時(shí),設(shè)置有效時(shí)間
//
uc.put(request.getRemoteAddr(), uCount + 1, ExpirationPolicy.CREATED, limitRequest.time(), TimeUnit.MILLISECONDS);
} else { // 未超過次數(shù), 記錄加一
uc.put(request.getRemoteAddr(), uCount + 1);
}
book.put(request.getRequestURI(), uc);
// result的值就是被攔截方法的返回值
Object result = pjp.proceed();
return result;
}
}
為了捕獲全局的異常拋出, 且符合restful規(guī)范我們加一個(gè)這個(gè)處理類:
handle包下面的全局異常類:
package org.cuit.epoch.handler;
import lombok.extern.log4j.Log4j2;
import org.cuit.epoch.exception.AppException;
import org.cuit.epoch.result.R;
import org.cuit.epoch.result.Result;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
@ControllerAdvice
@Log4j2
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
@ResponseBody
public Result error(Exception e) {
log.error(e.getMessage());
e.printStackTrace();
return R.fail(e.getMessage());
}
@ExceptionHandler(AppException.class)
@ResponseBody
public Result error(AppException e) {
log.error(e.getMessage());
e.printStackTrace();
return R.fail(e.getMessage());
}
}
application.yaml文件:
spring:
cache:
type:
redis
redis: #redis連接配置
host: 自己redis的ip地址
port: redis端口
password: 密碼
jedis:
pool:
max-active: 8
max-wait: -1ms
max-idle: 500
min-idle: 0
lettuce:
shutdown-timeout: 0ms
最終項(xiàng)目結(jié)構(gòu)如下:

先得到一個(gè)驗(yàn)證碼:

驗(yàn)證一下是否成功:
成功結(jié)果:

驗(yàn)證失敗結(jié)果:

當(dāng)請求在規(guī)定時(shí)間內(nèi)的請求數(shù)超過規(guī)定的數(shù)量時(shí)或有報(bào)錯(cuò):

參考:
到此這篇關(guān)于springboot圖片驗(yàn)證碼的文章就介紹到這了,更多相關(guān)springboot圖片驗(yàn)證碼內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java使用Tess4J實(shí)現(xiàn)圖像識(shí)別方式
這篇文章主要介紹了Java使用Tess4J實(shí)現(xiàn)圖像識(shí)別方式,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-10-10
Spring實(shí)現(xiàn)擁有者權(quán)限驗(yàn)證的方法示例
這篇文章主要介紹了Spring實(shí)現(xiàn)擁有者權(quán)限驗(yàn)證的方法示例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-03-03
java 在圖片上寫字,兩個(gè)圖片合并的實(shí)現(xiàn)方法
下面小編就為大家?guī)硪黄猨ava 在圖片上寫字,兩個(gè)圖片合并的實(shí)現(xiàn)方法。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2016-11-11
JAVASE系統(tǒng)實(shí)現(xiàn)抽卡功能
這篇文章主要為大家詳細(xì)介紹了JAVASE系統(tǒng)實(shí)現(xiàn)抽卡功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-11-11
關(guān)于Jackson的JSON工具類封裝 JsonUtils用法
這篇文章主要介紹了關(guān)于Jackson的JSON工具類封裝 JsonUtils用法,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-09-09

