项目要求修改原本验证码登录为顺序点击图片验证码登录
验证码登录:
顺序点击汉字验证码登录:
这里就不讲解前端样式如何做的了,因为我是前后端分离,所以只做后端接口开发,将后台生成的图片返回给前台,前台监控鼠标,按照点击所获得的坐标进行保存,并发送给后台,后台生成图片验证码时会将汉字的坐标存储到redis或者ehcache中做一个缓存,当校验的请求进入之后,对前后端的坐标进行校验,成功后,然后校验密码,如果密码正确登录成功。
设计图如下:(涉及到一些业务场景不完全相同)
直接使用流将图片返回给前台,前台使用如下示例展示图片
<img data-v-d649224e="" src="/demo/drawChinesePhote?time=1610359826578"" class="loginverifyimg" style="cursor: pointer;">
controller中返回图片的代码
BufferedImage image = drawChineseService.draw(codeCount, token, iWidth, iHeight);
ehcacheLoginTimesService.addLoginTime(ip, times == null ? 1 : times + 1);
ByteArrayOutputStream stream = new ByteArrayOutputStream();
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");
try {
ImageIO.write(image, "png", stream);
BASE64Encoder encoder = new BASE64Encoder();
base64 = encoder.encodeBuffer(stream.toByteArray());
base64 = base64.replaceAll("\n", "").replaceAll("\r", "");
object.put("data", "data:image/png;base64," + base64);
object.put("code", "200");
object.put("number", codeCount);
response.getWriter().write(object.toString());
} catch (IOException e) {
response.getWriter().write("");
} finally {
if (stream != null) {
stream.flush();
stream.close();
response.getWriter().close();
}
}
调用service层解决业务逻辑
public BufferedImage draw(int count, String token, int iWidth, int iHeight) throws IOException {
if (logger.isDebugEnabled()) {
logger.debug("生成图片的iWidth:" + iWidth + "------iHeight:" + iHeight);
}
//开始画图返回image和坐标对象
DrawChinese drawChinese = DrawUtil.createChineseImage(count, iWidth, iHeight,chineseErrorSize);
//获取坐标list
List<Coords> coordsList = drawChinese.getCoordsList();
if (logger.isDebugEnabled()) {
logger.debug("coordsList存取前:" + coordsList);
}
//坐标list存入redis
loginService.saveXYZList(token, coordsList);
return drawChinese.getImage();
}
生成图片的工具类
public static DrawChinese createChineseImage(int count, int iWidth, int iHeight,int chineseErrorSize) throws IOException {
DrawChinese drawChinese = new DrawChinese();
Random random = new Random();
BufferedImage image = new BufferedImage(iWidth, iHeight, BufferedImage.TYPE_INT_RGB);
Graphics2D g = (Graphics2D) image.getGraphics();
ClassPathResource classPathResource = new ClassPathResource("png" + File.separator + "picture.png");
g.setBackground(Color.white);
//File picPath= classPathResource.getFile(); //读取本地图片,做背景图片
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
InputStream inputStream = classPathResource.getInputStream();
//生成目标文件
File somethingFile = File.createTempFile("DailyReportTemplate", ".png");
try {
FileUtils.copyInputStreamToFile(inputStream, somethingFile);
} finally {
IOUtils.closeQuietly(inputStream);
}
g.drawImage(ImageIO.read(somethingFile), 0, 0, iWidth, iHeight, null); //将背景图片从高度20开始
//g.setColor(Color.white); //设置颜色
g.drawRect(0, 0, iWidth - 1, iHeight - 1); //画边框
AffineTransform affineTransform = new AffineTransform();
Font font = new Font("华文行楷", Font.PLAIN, 40);
Integer x = null, y = null; //用于记录坐标
String target = null; // 用于记录文字
List<Coords> coordsList = new ArrayList<Coords>();
int a = 0;
int b = 0;
for (int i = 0; i < count; i++) { //随机产生4个文字,坐标,颜色都不同
//g.setColor(new Color(random.nextInt(50) + 200, random.nextInt(150) + 100, random.nextInt(50) + 200));
g.setColor(new Color(random.nextInt(50) + 100, random.nextInt(200), random.nextInt(200)));
String str = getRandomChineseChar();
int listx, listy;
int j = 0;
Coords coords = new Coords();
boolean flag = true;
//校验,如果字体坐标重复在一起影响美观
while (true) {
if (flag) {
a = random.nextInt(iWidth - 80) + 40;
b = random.nextInt(iHeight - 80) + 40;
flag = false;
}
if (j < coordsList.size()) {
listx = coordsList.get(j).getX();
listy = coordsList.get(j).getY();
//判断坐标如果相等直接重新生成
if (listx == a && listx == b) {
flag = true;
}
//判断字体是否会覆盖
if (!((listx - chineseErrorSize <= a) && (listx + chineseErrorSize >= a) &&
(listy - chineseErrorSize <= b) && (listy + chineseErrorSize >= b))) {
if (j == coordsList.size() - 1) {
coords.setX(a);
coords.setY(b);
//保存坐标
coordsList.add(coords);
break;
}
j++;
} else {
flag = true;
}
} else {
coords.setX(a);
coords.setY(b);
//保存坐标
coordsList.add(coords);
break;
}
}
Font rotatedFont = font.deriveFont(affineTransform);
affineTransform.rotate(Math.toRadians(random.nextInt(45)), 0, 0);
g.setFont(rotatedFont);
g.drawLine(a + 5, b - 4, random.nextInt(iWidth - 100) + 50, random.nextInt(iHeight - 70) + 55); //设置干扰线的坐标
if (target == null) {
target = str; //记录第一个文字
} else {
target = target + str;
}
g.drawString(str, a, b);
}
g.setColor(Color.black);
g.setFont(new Font("华文仿宋", Font.BOLD, 20)); //设置字体
g.drawString(target, 0, 17);//写入验证码第一行文字 “点击..”
//5.释放资源
g.dispose();
drawChinese.setCoordsList(coordsList);
drawChinese.setImage(image);
return drawChinese;
}
返回的drawChinese是一个DrawChinese对象,
public class DrawChinese {
private List<Coords> coordsList;
private BufferedImage image;
public List<Coords> getCoordsList() {
return coordsList;
}
public BufferedImage getImage() {
return image;
}
public void setCoordsList(List<Coords> coordsList) {
this.coordsList = coordsList;
}
public void setImage(BufferedImage image) {
this.image = image;
}
@Override
public String toString() {
return "DrawChinese{" +
"coordsList=" + coordsList +
'}';
}
}
以上为生成步骤
下面为校验步骤,chineseErrorSize为误差范围,用户点击验证码必然有误差,不可能百分之百正确。
@Override
public Boolean checkXYZ(String token, List<Coords> userCoords, int chineseErrorSize) throws IOException {
List<String> coords = loginService.getXYZList(token);
System.out.println("coords:" + coords);
if (logger.isDebugEnabled()) {
logger.debug("登陆的Coords:" + coords);
}
List<Coords> redisCoords = new ArrayList<>();
for (int i = 0; i < coords.size(); i++) {
String coordString = coords.get(i);
Coords coord = JsonUtil.getBean(coordString, Coords.class);
redisCoords.add(coord);
}
if (logger.isDebugEnabled()) {
logger.debug("redis内Coords:" + redisCoords);
}
if (!(redisCoords.size() == userCoords.size())) {
return false;
}
for (int i = 0; i < redisCoords.size(); i++) {
Coords redis = redisCoords.get(i);
Coords user = userCoords.get(i);
if (!(user.getX() < redis.getX() + chineseErrorSize && user.getX() > redis.getX() - chineseErrorSize
&& redis.getY() + chineseErrorSize > user.getY() && redis.getY() - chineseErrorSize < user.getY())) {
return false;
}
}
return true;
}