一、效果图:
二、实现思路:
1.使用BufferedImage用于在内存中存储生成的验证码图片;
2.使用Graphics来进行验证码图片的绘制,设置图片颜色、图片中字体大小、颜色等;
3.将绘制的图片返回给前端,同时将图片中的验证码存放到session中用于后续验证;
4.最后通过ImageIO将生成的图片进行输出;
5.通过页面提交的验证码和存放在session中的验证码对比来进行校验;
6.前端通过调用验证码接口刷新验证码。
三、生成验证码
1.验证码工具类
public class CaptchaUtils { private static Random random = new Random(); /** * 随机产生数字与字母组合的字符串 */ private static final String randString = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; /** * 图片宽 */ private static final int width = 80; /** * 图片高 */ private static final int height = 26; /** * 字体高度 */ private static final int fontHeight = 18; /** * 干扰线数量 */ private static final int lineSize = 40; /** * 随机产生字符数量 */ private static final int num = 4; /** * 生成随机图片 */ public static CaptchaDTO create() { // BufferedImage类(画板) BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_BGR); // Graphics类(画纸),该对象可以在图像上进行各种绘制操作 Graphics g = image.getGraphics(); g.fillRect(0, 0, width, height); // g.setFont(new Font("Times New Roman", Font.ROMAN_BASELINE, 18)); g.setColor(Color.WHITE); // 绘制随机字符 String captchaCode = ""; for (int i = 1; i <= num; i++) { captchaCode = drawString(g, captchaCode, i); } // 释放图形上下文 g.dispose(); // 构建返回数据 CaptchaDTO captchaDTO = new CaptchaDTO(); captchaDTO.setCaptchaCode(captchaCode); captchaDTO.setBufferedImage(image); return captchaDTO; } public static class CaptchaDTO { /** * 验证码 */ private String captchaCode; private BufferedImage bufferedImage; } /* * 获取随机颜色 */ private static Color getRandomColor(int r, int g, int b) { if (r > 255) { r = 255; } if (g > 255) { g = 255; } if (b > 255) { b = 255; } r = random.nextInt(r); g = random.nextInt(g); b = random.nextInt(b); return new Color(r, g, b); } /* * 绘制字符串 */ private static String drawString(Graphics g, String captchaCode, int i) { // 设置字体,字体的大小应该根据图片的高度来定。 g.setFont(new Font("Times New Roman", Font.CENTER_BASELINE, fontHeight)); g.setColor(getRandomColor(101,111,121)); String randomString = getRandomString(random.nextInt(randString.length())); captchaCode += randomString; g.translate(random.nextInt(3), random.nextInt(3)); g.drawString(randomString, 13 * i, 16); return captchaCode; } /* * 绘制干扰线 */ private static void drawLine(Graphics g) { int x = random.nextInt(width); int y = random.nextInt(height); int xl = random.nextInt(13); int yl = random.nextInt(15); g.drawLine(x, y, x + xl, y + yl); } /* * 获取随机的字符 */ private static String getRandomString(int num) { return String.valueOf(randString.charAt(num)); }
2.验证码接口
private static final String CAPTCHA_CODE = "captchaCode"; /** * 获取图片验证码 * @param request * @param response */ value = "/getCaptcha", method = {RequestMethod.GET}) ( public void getCaptcha(HttpServletRequest request, HttpServletResponse response) { CaptchaUtils.CaptchaDTO captchaDTO = CaptchaUtils.create(); HttpSession session = request.getSession(); session.setAttribute(CAPTCHA_CODE, captchaDTO.getCaptchaCode()); try { ImageIO.write(captchaDTO.getBufferedImage(), "png", response.getOutputStream()); } catch (Exception e) { log.error("验证码生成失败,e:{}",e); } }
四、校验验证码
// 登录接口中校验验证码 HttpSession session = request.getSession(); if(ObjectUtils.isEmpty(session.getAttribute(CAPTCHA_CODE))){ return BaseResult.buildError("验证码已过期,请刷新后重试!"); } String captchaCodeBySession = session.getAttribute(CAPTCHA_CODE).toString(); if(!captchaCodeBySession.equalsIgnoreCase(captchaCode)){ return BaseResult.buildError("验证码校验失败!"); } session.removeAttribute(CAPTCHA_CODE);
五、遇到的问题
本机(windows环境)测试正常,服务器(docker)报错
java.lang.NullPointerException at sun.awt.FontConfiguration.getVersion(FontConfiguration.java:1264) at sun.awt.FontConfiguration.readFontConfigFile(FontConfiguration.java:219) at sun.awt.FontConfiguration.init(FontConfiguration.java:107) at sun.awt.X11FontManager.createFontConfiguration(X11FontManager.java:774) at sun.font.SunFontManager$2.run(SunFontManager.java:431) at java.security.AccessController.doPrivileged(Native Method)
解决思路
1.本机正常,服务器报字体相关错误,在Dockerfile中添加如下命令,尝试安装字体后测试正常:
RUN apk add --update ttf-dejavu fontconfig && rm -rf /var/cache/apk/*
2.假如在服务器直接部署,每台服务器都要安装字体,比较麻烦,尝试集成到java项目中,字体可以从windows系统copy(集成到项目中时也遇到些问题,比如如何读取jar包中的文件、mvn打包导致字体不可用等,这里直接上正确的代码配置了):
public class LoadFontsRunner implements ApplicationRunner { public void run(ApplicationArguments args) throws Exception { registerFont(); } private static void registerFont() { Font font; try { String path = "fonts/times.ttf"; InputStream stream = Thread.currentThread().getContextClassLoader().getResourceAsStream(path); font = Font.createFont(Font.TRUETYPE_FONT, stream); GraphicsEnvironment.getLocalGraphicsEnvironment().registerFont(font); } catch (Exception e) { log.error("加载字体失败,e:{}", e); throw new BaseException(-1, "加载字体失败!"); } GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); ge.registerFont(font); } }
然而,windows系统下正常,docker服务器依然报错,但和第一次报错不太一样,报错信息如下:
java.io.IOException: Problem reading font data. at java.awt.Font.createFont0(Font.java:1000) at java.awt.Font.createFont(Font.java:877) at com.xxx.sso.server.LoadFontsRunner.registerFont(LoadFontsRunner.java:34) at com.xxx.sso.server.LoadFontsRunner.run(LoadFontsRunner.java:25) at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:804) at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:794) at org.springframework.boot.SpringApplication.run(SpringApplication.java:324) at org.springframework.boot.SpringApplication.run(SpringApplication.java:1260) at org.springframework.boot.SpringApplication.run(SpringApplication.java:1248)
使用idea远程连接服务器打断点查看报错后,发现实际报错信息和第一步是一样的,最后抛异常时换了异常信息,但这是在项目内注册字体时抛的异常,和第一步还是有区别,最后发现本机使用的Oracle JDK8,而docker镜像使用的OpenJDK8(FROM openjdk:8-jre-alpine)缺少字体组件导致报错,替换为如下基础镜像后,运行正常:
FROM java:8
总结
此次的验证码和之前做过的报表工具(jasper report)都曾遇到过本机开发正常,服务器部署报字体的错误,解决方法有两种:1.服务器安装字体2.项目集成字体(推荐,要用Oracle JDK,OpenJDK缺少字体组件,注册字体时依然报错)。