文章目录
- 需求
- 后台实现
- 前台实现
- 彩蛋
需求
最近做的一个管理后台项目中,手机号登录是需要发送手机验证码的,然后需求方提出想要一个前台的图片验证码,只有输入正确后才可发送手机验证码。很简单很普通的一个需求但是实际做的时候,碰到了一个小坑,其他人有很多都没说出这点,千篇一律的。特此记录。
后台实现
图片的生成实现,这个部分网上已经有很多例子了,各种各样的实现方案和结果,大家找一个改改就应该可以了。
/**
* Captcha
* 图形验证码
*
* @author rysis
* @version 1.00
* @Date 2020/6/5 18:54
*/
public class Captcha {
private static final char MAP_TABLE[] = {
'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',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'
};
private static final int CHAR_NUM = 5; // 显示的字符数目
private static final int DISTURB_LINES_NUM = 168; // 干扰线数目
/**
* 生成图片验证码相关信息
*
* @param width 图片的宽度
* @param height 图片的高度
* @return
*/
public static Map<String, Object> getImageCode(int width, int height) {
if (width <= 0) {
width = 60;
}
if (height <= 0) {
height = 20;
}
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
// 获取图形上下文
Graphics g = image.getGraphics();
// 生成随机类
Random random = new Random();
// 设定背景色
g.setColor(getRandColor(200, 250));
g.fillRect(0, 0, width, height);
// 设定字体
g.setFont(new Font("Times New Roman", Font.PLAIN, 18));
// 随机产生 DISTURB_LINES_NUM 条干扰线,使图象中的认证码不易被其它程序探测到
g.setColor(getRandColor(160, 200));
for (int i = 0; i < DISTURB_LINES_NUM; i++) {
int x = random.nextInt(width);
int y = random.nextInt(height);
int xl = random.nextInt(12);
int yl = random.nextInt(12);
g.drawLine(x, y, x + xl, y + yl);
}
// 取随机产生的码
StringBuilder code = new StringBuilder();
// CHAR_NUM代表CHAR_NUM位验证码,如果要生成更多位的认证码,则加大数值
for (int i = 0; i < CHAR_NUM; ++i) {
code.append(MAP_TABLE[(int) (MAP_TABLE.length * Math.random())]);
// 将认证码显示到图象中
g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
// 直接生成
String str = code.substring(i, i + 1);
// 设置随便码在背景图图片上的位置
g.drawString(str, 13 * i + 20, 25);
}
// 释放图形上下文
g.dispose();
Map<String, Object> returnMap = new HashMap<>();
returnMap.put("image", image);
returnMap.put("code", code.toString());
return returnMap;
}
/**
* 给定范围获得随机颜色
*
* @param fc
* @param bc
* @return
*/
private static Color getRandColor(int fc, int bc) {
Random random = new Random();
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);
}
}
Java 服务部分使用了 spring-boot。然后提供了两个接口:一个是用于给前台返回二进制图片的;另一个接口是 用于校验图片验证码接口。
@RestController
@RequestMapping(value = "/api")
public class CaptchaController {
/**
* 获取图片验证码接口
*
* @return
*/
@PostMapping(value = "/captcha/image")
public String imageCode() {
// 返回验证码和图片的map
Map<String, Object> map = Captcha.getImageCode(90, 37);
// session 存入验证码和时间
HttpSession session = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest().getSession();
session.setAttribute(SIMPLE_CAPTCHA_KEY, map.get("code").toString().toLowerCase());
session.setAttribute(CAPTCHA_TIME_KEY, new Date().getTime());
// response
HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getResponse();
response.setDateHeader("Expires", 0);
response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
response.addHeader("Cache-Control", "post-check=0, pre-check=0");
response.setHeader("Pragma", "no-cache");
response.setContentType("image/jpeg"); // 这里要指定响应的类型
OutputStream outs = null;
try {
outs = response.getOutputStream();
ImageIO.write((BufferedImage) map.get("image"), "jpg", outs);
} catch (IOException e) {
logger.error("CaptchaController.imageCode:图片验证码流输出异常:", e.getMessage());
return "";
} finally {
if (outs != null) {
try {
outs.flush();
outs.close();
} catch (IOException e) {
logger.error("CaptchaController.imageCode:图片验证码流关闭异常:", e.getMessage());
}
}
}
return null;
}
/**
* 检查会话验证码信息是否正确
*
* @param paramJson 请求参数
* @return
*/
@PostMapping(value = "/captcha/image/check")
public ResponseUtil.ResponseResult checkCode(@RequestBody JSONObject paramJson) {
// session 验证码信息(这里是抽象到BaseController的一个获取方法)
HttpSession session = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest().getSession();
Object simpleCaptcha = session.getAttribute(SIMPLE_CAPTCHA_KEY); // 验证码信息
Object captchaTime = session.getAttribute(CAPTCHA_TIME_KEY); // 验证码时间
if (simpleCaptcha == null || captchaTime == null) {
return ResponseUtil.failed("图形验证码已失效,请重新获取!", 2);
}
String captcha = simpleCaptcha.toString();
Long createTime = Long.valueOf(captchaTime.toString());
// 参数信息
String checkCode = paramJson.getString("code");
// 这里是我封装的一个类(类似:scala.util.Either),可以使用Map代替。
Either<String, String> result = checkCode(checkCode, captcha, createTime);
Map<String, Object> data = new HashMap<>();
data.put("success", result.isRight());
if (result.isRight()) {
session.removeAttribute(SIMPLE_CAPTCHA_KEY);
return ResponseUtil.success("success", data);
} else {
return ResponseUtil.success(result.getLeftData().orElse("图形验证码错误"), data);
}
}
/**
* 检查会话验证码信息是否正确(这个我是放到service中的)
*
* @param checkCode 前台的验证码参数
* @param captcha session的验证码
* @param createTime 验证码创建时间
* @return
*/
private Either<String, String> checkCode(String checkCode, String captcha, Long createTime) {
Date now = new Date();
logger.debug(String.format("CaptchaController.checkCode:图片验证码J检查:checkCode=%s, captcha=%s", checkCode, captcha));
if (StringUtils.isEmpty(checkCode) || StringUtils.isEmpty(captcha) || !(checkCode.equalsIgnoreCase(captcha))) {
return new Left<>("图形验证码错误!");
} else if ((now.getTime() - createTime) > 5 * 60 * 1000) {
// 验证码有效时长为5分钟
return new Left<>("图形验证码已超时,请重新获取!");
} else {
return new Right<>("");
}
}
}
前台实现
前台使用了Vue来进行页面的开发。我这里主要的坑就是两个点:
- 由于我使用的axios守卫进行响应的拦截,我之前拦截是根据响应是数据内容进行判断成功与否。但是这里获取图片的时候是二进制的内容,无法直接进行判断,所以要根据
content-type
进行过滤判断; - 响应的对图片无法使用
<img>
标签进行显示,但是使用 postman 和浏览器 network 却都能看见返回的图片,这里是需要在请求后端时要在header上面进行响应结果的标注responseType: 'arraybuffer'
。
以上两点我都会在代码里面进行提到的。其中有些内容自行进行替换即可,这里我使用了element-ui的组件。
api.js 文件部分代码:
/**
* 获取图片验证码
* @param data
*/
getImageCode: (data) => {
return request({
url: `${_baseUrl}/api/captcha/image`,
method: 'post',
responseType: 'arraybuffer', // 注意点2
data
})
}
/**
* 检查图片验证码
* @param data
*/
checkImageCode: (data) => {
return request({
url: `${_baseUrl}/api/captcha/image/check`,
method: 'post',
data
})
}
login.vue 文件中的部分代码:
<template>
<img :src="imageCodeData" @click="getImageCode">
</template>
<script>
/**
* 获取图片验证码
*/
getImageCode() {
getImageCode().then(resp => {
return 'data:image/png;base64,' + btoa(new Uint8Array(resp).reduce((data, byte) => data + String.fromCharCode(byte), ''))
}).then(data => {
this.imageCodeData = data
})
},
/**
* 检查图片验证码
*/
checkImageCode() {
return checkImageCode({ code: this.loginForm.imageCode }).then(resp => {
this.authCodeBtnLock = !resp.data.success
this.authCodeBtnLock && this.$message.warning(resp.msg || '图片验证码错误')
return this.authCodeBtnLock
}).catch(() => {
this.authCodeBtnLock = true
return this.authCodeBtnLock
})
}
</script>
到这里就结束了,如果有不明确的,欢迎留言!
附上以前可以破解滑动验证码例子:https://github.com/RysisLiang/Trial_Jigsaw
彩蛋
附上我的Either类的实现代码。只是个人的癖好,没有什么太多的实际意义。
package com.demo.utils.either;
import java.util.Optional;
/**
* EitherAbstract
* 简单实现 scala.util.Either
*
* @author Rysis
* @version 1.00
* @Date 2020/2/8 20:04
*/
public interface Either<A, B> {
Optional<B> getRightData();
Optional<A> getLeftData();
boolean isRight();
boolean isLeft();
}
/**
* Left
*
* @author Rysis
* @version 1.00
* @Date 2020/2/8 20:05
*/
public class Left<A, B> implements Either<A, B> {
private final boolean type = false;
private A data;
public Left(A data) {
this.data = data;
}
@Override
public Optional<B> getRightData() {
return Optional.empty();
}
@Override
public Optional<A> getLeftData() {
return Optional.ofNullable(data);
}
@Override
public boolean isRight() {
return type;
}
@Override
public boolean isLeft() {
return !type;
}
public A getData() {
return data;
}
public void setData(A data) {
this.data = data;
}
}
/**
* Right
*
* @author Rysis
* @version 1.00
* @Date 2020/2/8 20:05
*/
public class Right<A, B> implements Either<A, B> {
private final boolean type = true;
private B data;
public Right(B data) {
this.data = data;
}
@Override
public Optional<B> getRightData() {
return Optional.ofNullable(data);
}
@Override
public Optional<A> getLeftData() {
return Optional.empty();
}
@Override
public boolean isRight() {
return type;
}
@Override
public boolean isLeft() {
return !type;
}
public B getData() {
return data;
}
public void setData(B data) {
this.data = data;
}
}