在登陆或注册的时候,为了防止程序恶意请求,网站一般都会要求输入验证码。之前也写了一篇关于验证码功能的文章 java springboot 使用Kaptcha组件实现验证码功能,但是感觉这个组件只有字符串验证码,功能不太强。于是准备自己写一个验证码,主要是实现算式验证码。所有功能已实现并上传只github,链接:https://github.com/wsJava/verification-code
首先是获取算式
因为使用 除 会产生小数问题,所以只有+-*
运算符。下面这个方法是生成一个指定运算符数量的算式。然后构建一个Equation 对象,其中包含算式和结果。
/**
* 获取算式
*
* @param operatorCount 运算符数量
* @return Equation 算式验证码对象
*/
public static Equation getEquation(int operatorCount) {
int[] nums = new int[operatorCount + 1];
char[] operates = new char[operatorCount];
char[] expression = new char[operatorCount * 2 + 1];
for (int i = 0; i < operatorCount + 1; i++) {
nums[i] = random.nextInt(10);
expression[i * 2] = (char) (nums[i] + 48);
}
for (int i = 0; i < operatorCount; i++) {
operates[i] = OPERATOR_CHARS.charAt(random.nextInt(OPERATOR_CHARS.length()));
expression[i * 2 + 1] = operates[i];
}
return new Equation(expression, getResult(nums, operates));
}
计算算式结果
主要使用堆栈的结构进行计算,将数字和运算符分别放入对应堆栈中,然后,对比前后两个运算符的优先级,如前相同或前一个优先级高,则将对应的数字和运算符取出来进行计算。再将结果存入数字栈中。代码如下:
/**
* 算术运算符优先级
*/
private static final Map<Character, Integer> operatorMap = new HashMap<>(5);
static {
operatorMap.put('+', 1);
operatorMap.put('-', 1);
operatorMap.put('x', 2);
}
/**
* 计算算式结果
*
* @param nums 数字数组
* @param operators 运算符数组
* @return 计算结果
*/
private static int getResult(int[] nums, char[] operators) {
LinkedList<Integer> numStack = new LinkedList<>();
LinkedList<Character> operatorStack = new LinkedList<>();
numStack.push(nums[0]);
numStack.push(nums[1]);
operatorStack.push(operators[0]);
for (int i = 1; i < operators.length; i++) {
if (operatorMap.get(operators[i]) <= operatorMap.get(operatorStack.peek())) {
int num2 = numStack.pop();
int num1 = numStack.pop();
numStack.push(computerResult(num1, num2, operatorStack.pop()));
}
numStack.push(nums[i + 1]);
operatorStack.push(operators[i]);
}
while (!operatorStack.isEmpty()) {
int num2 = numStack.pop();
int num1 = numStack.pop();
numStack.push(computerResult(num1, num2, operatorStack.pop()));
}
return numStack.pop();
}
private static int computerResult(Integer num1, Integer num2, Character operator) {
int result = 0;
switch (operator) {
case '+':
result = num1 + num2;
break;
case '-':
result = num1 - num2;
break;
case '*':
result = num1 * num2;
break;
default:
}
return result;
}
绘制验证码
主要是绘制干扰线条和验证码,这里算式验证码没有使用干扰线条。注意获取随机数使用的是 线程安全的ThreadLocalRandom
。这里代码不全,详细请参考 github 中的。
/**
* 获取验证码对象
*
* @return VerificationImage 验证码对象
*/
public VerificationImage getVerificationImage() {
BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_BGR);
char[] codes = null;
String rightCode = null;
Graphics graphics = bufferedImage.getGraphics();
graphics.fillRect(0, 0, width, height);
graphics.setFont(new Font("Times New Roman", Font.ROMAN_BASELINE, fontBasisSize));
if (codeType == CodeTypeEnum.CHAR) { //如果是字符类型验证码,则画干扰线
graphics.setFont(new Font("Times New Roman", Font.ROMAN_BASELINE, fontBasisSize));
for (int i = 0; i < lineCount; i++) {
graphics.setColor(getRandColor());
drawLine(graphics);
}
codes = getCharCode(codeSize);
rightCode = String.valueOf(codes);
} else if (codeType == CodeTypeEnum.EQUATION) { //如果是算式类型验证码
Equation equation = getEquation(operatorCount);
codes = equation.getExpression();
rightCode = String.valueOf(equation.getResult());
}
drawString(graphics, codes);
graphics.dispose();
return new VerificationImage(bufferedImage, rightCode);
}
/**
* 获取干扰线随机颜色
*
* @return 在 COLOR_BOUND 边界范围内随机生成的颜色
*/
private Color getRandColor() {
int r = random.nextInt(COLOR_BOUND);
int g = random.nextInt(COLOR_BOUND);
int b = random.nextInt(COLOR_BOUND);
return new Color(r, g, b);
}
/**
* 绘制干扰线
*
* @param graphics 图形
*/
private void drawLine(Graphics graphics) {
int x = random.nextInt(width);
int y = random.nextInt(height);
int x1 = random.nextInt(x + random.nextInt(width));
int y1 = random.nextInt(y + random.nextInt(height));
graphics.drawLine(x, y, x1, y1);
}
在response中设置验证码图片
这个地方可以改进,因为存储验证码结构的方式可以采用其他的。默认存到session中,但提供一个重载方法,提供Consumer接口,接收验证码结果自行存储,后续改进。
/**
* 在 http 中设置验证图片和验证码
*
* @param request http 请求, 在其中设置验证码
* @param response http 响应, 设置返回验证码图片
*/
public void setHttpVerificationCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.setContentType("image/jpeg");//设置相应类型,输出的内容为图片
response.setHeader("Pragma", "no-cache");//设置响应头信息,不要缓存此内容
response.setHeader("Cache-Control", "no-store");
response.setContentType("image/jpeg");
response.setDateHeader("Expires", 0);
HttpSession session = request.getSession();
session.removeAttribute(SESSION_CODE_KEY);
VerificationImage verificationImage = getVerificationImage();
session.setAttribute(SESSION_CODE_KEY, verificationImage.getRightCode());
ImageIO.write(verificationImage.getBufferedImage(), "JPEG", response.getOutputStream());
}