且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

【通用行业开发部】用户组件之图片验证码

更新时间:2022-03-07 19:15:37

一、效果图:

【通用行业开发部】用户组件之图片验证码

二、实现思路:

1.使用BufferedImage用于在内存中存储生成的验证码图片;

2.使用Graphics来进行验证码图片的绘制,设置图片颜色、图片中字体大小、颜色等;

3.将绘制的图片返回给前端,同时将图片中的验证码存放到session中用于后续验证;

4.最后通过ImageIO将生成的图片进行输出;

5.通过页面提交的验证码和存放在session中的验证码对比来进行校验;

6.前端通过调用验证码接口刷新验证码。

三、生成验证码

1.验证码工具类

@Getter
@Setter
@Slf4j
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;
    }

    @Data
    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
     */
    @ResponseBody
    @RequestMapping(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打包导致字体不可用等,这里直接上正确的代码配置了):

@Configuration
@Slf4j
public class LoadFontsRunner implements ApplicationRunner {

    @Override
    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缺少字体组件,注册字体时依然报错)。