一、问题
前后端分离,数据交互是无状态的。验证码实现,验证码需要存储在后台。可以利用redis存储,也可以存session(不好的策略,只能在浏览器环境下使用,而且违背了无状态),更有甚者,存储在RDB中,当然,我觉得都太麻烦了,自己写一个Util有什么不好的吗?
二、实现原理
项目启动时,池会被加载到静态方法区,初始化池的大小,设置最大存储量,量满了自动清除过期内容(没有定时清除)。
三、代码
VerifyCodePool.java
//import some packages
/**
* 存放验证码的池子
* 全局只有一个 所有验证码存到这里
*/
public abstract class VerifyCodePool {
private VerifyCodePool(){} //与abstract的约束是一致的,可以去掉
//验证码池
//怎么存? <验证码,过期时间>
private static Map<String,Date> verifycodes = new ConcurrentHashMap<>(); //饿汉
//验证么最大有效时间 30 分钟
private final static long MAX_VALID_TIME = 1800l * 1000;
//计数器最大值,达到这个值就清理一次map中的值。
private static final long MAX_COUNT = 10000l;
//计数器 每隔
private static Integer count = 0;
/**
* 放入一个验证码
* @param code
*/
public static void setVerifyCode(String code){
count++;
verifycodes.put(code, new Date());
}
/**
* 验证并删除一个验证码
* @param code
* @return
*/
public static boolean verify(String code){
if(code == null) {
return false;
}
code = code.toLowerCase();
cleanPool();
Date date = verifycodes.get(code);
if(null == date){
//验证码不存在
return false;
}else {
//验证码是存在的
//验证码是否过期
boolean outOfDate = new Date().getTime() > date.getTime() + MAX_VALID_TIME;
//已经验证过了就要把验证码删除掉
verifycodes.remove(code);
return outOfDate?false:true;
}
}
/**
* 一万次清理一次池子
*/
private static void cleanPool(){
if (count >= MAX_COUNT){
count = 0;
// 清理验证码时需要锁定池子,不能使用的时候清理
//这里可能会造成过多垃圾,对内存不友好
synchronized (VerifyCodePool.class){
Map newMap = new ConcurrentHashMap<>();
Set<String> set = verifycodes.keySet();
for (String key :
set) {
Date date = verifycodes.get(key);
if (date.getTime() + MAX_VALID_TIME > new Date().getTime()){
// 只保留有效的
newMap.put(key, date);
}
}
//老的池子消灭了?
//新的池子
verifycodes = newMap;
}
}
}
}
VerifyCodeUtil.java
public class VerifyCodeUtil {
// 验证码字符集
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'};
// 字符数量
private static final int SIZE = 7;
// 干扰线数量
private static final int LINES = 5;
// 宽度
private static final int WIDTH = 140;
// 高度
private static final int HEIGHT = 40;
// 字体大小
private static final int FONT_SIZE = 30;
/**
* 生成随机验证码及图片
* Object[0]:验证码字符串;
* Object[1]:验证码图片。
*/
public static Object[] createImage() {
StringBuffer sb = new StringBuffer();
// 1.创建空白图片
BufferedImage image = new BufferedImage(
WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB);
// 2.获取图片画笔
Graphics graphic = image.getGraphics();
// 3.设置画笔颜色
graphic.setColor(Color.LIGHT_GRAY);
// 4.绘制矩形背景
graphic.fillRect(0, 0, WIDTH, HEIGHT);
// 5.画随机字符
Random ran = new Random();
for (int i = 0; i <SIZE; i++) {
// 取随机字符索引
int n = ran.nextInt(chars.length);
// 设置随机颜色
graphic.setColor(getRandomColor());
// 设置字体大小
graphic.setFont(new Font(
null, Font.BOLD + Font.ITALIC, FONT_SIZE));
// 画字符
graphic.drawString(
chars[n] + "", i * WIDTH / SIZE, HEIGHT*2/3);
// 记录字符
sb.append(chars[n]);
}
// 6.画干扰线
for (int i = 0; i < LINES; i++) {
// 设置随机颜色
graphic.setColor(getRandomColor());
// 随机画线
graphic.drawLine(ran.nextInt(WIDTH), ran.nextInt(HEIGHT),
ran.nextInt(WIDTH), ran.nextInt(HEIGHT));
}
// 7.返回验证码和图片
return new Object[]{sb.toString(), image};
}
/**
* 随机取色
*/
public static Color getRandomColor() {
Random ran = new Random();
Color color = new Color(ran.nextInt(256),
ran.nextInt(256), ran.nextInt(256));
return color;
}
public static void setVerifyCode(String code){
VerifyCodePool.setVerifyCode(code);
}
public static boolean verify(String code){
return VerifyCodePool.verify(code);
}
}
四、食用方式
4.1生验证码
@ApiOperation("生成验证码")
@GetMapping("/verifycode")
public void getCode(HttpServletResponse response) throws Exception {
//利用图片工具生成图片
//第一个参数是生成的验证码,第二个参数是生成的图片
Object[] objs = VerifyCodeUtil.createImage();
//将验证码存入验证码池
VerifyCodeUtil.setVerifyCode(objs[0].toString().toLowerCase());
//将图片输出给浏览器
BufferedImage image = (BufferedImage) objs[1];
response.setContentType("image/png");
OutputStream os = response.getOutputStream();
ImageIO.write(image, "png", os);
}
4.2 验验证码
@PostMapping("/login")
public ResponseVO login(String account, String password, String loginType, String code) {
// 如果没有验证码
if(null == code){
return ControllerUtil.getFalseResultMsgBySelf("verify code cannot be null");
}
//
if(!VerifyCodeUtil.verify(code.toLowerCase())){
return ControllerUtil.getFalseResultMsgBySelf("verify code is wrong,please retry after refresh page");
}else {
//业务逻辑
}
}
实际使用效果良好,但是目前存在如下问题:
- 没法区分验证码究竟是谁的,因为该系统设计时只考虑了同时间内有上千学生登录,所以七位验证码几乎不会重复,因此没有做区分。
- 验证码验证机会只有一次,验证完毕就会被丢掉,所以前端需要做一些逻辑处理,验证过的验证码需要重新刷新验证码。