Java21 + SpringBoot3集成easy-captcha实现验证码显示和登录校验

文章目录

    • 前言
    • 相关技术简介
      • easy-captcha
      • 实现步骤
        • 引入maven依赖
        • 定义实体类
        • 定义登录服务类
        • 定义登录控制器
        • 前端登录页面实现
        • 测试和验证
        • 总结
        • 附录
          • 使用`Session`缓存验证码
          • 前端登录页面实现代码

            前言

            近日心血来潮想做一个开源项目,目标是做一款可以适配多端、功能完备的模板工程,包含后台管理系统和前台系统,开发者基于此项目进行裁剪和扩展来完成自己的功能开发。

            本项目为前后端分离开发,后端基于Java21和SpringBoot3开发,后端使用Spring Security、JWT、Spring Data JPA等技术栈,前端提供了vue、angular、react、uniapp、微信小程序等多种脚手架工程。

            本文主要介绍在SpringBoot3项目中如何集成easy-captcha生成验证码,JDK版本是Java21,前端使用Vue3开发。

            项目地址:https://gitee.com/breezefaith/fast-alden

            相关技术简介

            easy-captcha

            easy-captcha是生成图形验证码的Java类库,支持gif、中文、算术等类型,可用于Java Web、JavaSE等项目。

            参考地址:

            • Github:https://github.com/whvcse/EasyCaptcha

              实现步骤

              引入maven依赖

              在pom.xml中添加easy-captcha以及相关依赖,并引入Lombok用于简化代码。

                 com.github.whvcse easy-captcha 1.6.2    org.openjdk.nashorn nashorn-core 15.4    org.projectlombok lombok 1.18.30 true 

              笔者使用的JDK版本是Java21,SpringBoot版本是3.2.0,如果不引入nashorn-core,生成验证码时会报错java.lang.NullPointerException: Cannot invoke "javax.script.ScriptEngine.eval(String)" because "engine" is null。有开发者反馈使用Java 17时也遇到了同样的问题,手动引入nashorn-core后即可解决该问题。

              详细堆栈和截图如下:

              java.lang.NullPointerException: Cannot invoke "javax.script.ScriptEngine.eval(String)" because "engine" is null
              	at com.wf.captcha.base.ArithmeticCaptchaAbstract.alphas(ArithmeticCaptchaAbstract.java:42) ~[easy-captcha-1.6.2.jar:na]
              	at com.wf.captcha.base.Captcha.checkAlpha(Captcha.java:156) ~[easy-captcha-1.6.2.jar:na]
              	at com.wf.captcha.base.Captcha.text(Captcha.java:137) ~[easy-captcha-1.6.2.jar:na]
              	at com.fast.alden.admin.service.impl.AuthServiceImpl.generateVerifyCode(AuthServiceImpl.java:72) ~[classes/:na]
                ......
              

              定义实体类

              为了方便后端校验,获取验证码的请求除了要返回验证码图片本身,还要返回一个验证码的唯一标识,所以笔者定义了一个实体类VerifyCodeEntity。

              /**
               * 验证码实体
               */
              @Data
              @NoArgsConstructor
              @AllArgsConstructor
              public class VerifyCodeEntity implements Serializable { /**
                   * 验证码Key
                   */
                  private String key;
                  /**
                   * 验证码图片,base64压缩后的字符串
                   */
                  private String image;
                  /**
                   * 验证码文本值
                   */
                  @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
                  private String text;
              }
              

              使用@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)注解可以使text属性不会被序列化后返回给前端。

              为实现登录功能,还要定义一个登录参数类LoginParam。

              @Data
              public class LoginParam { /**
                   * 用户名
                   */
                  private String username;
                  /**
                   * 密码
                   */
                  private String password;
                  /**
                   * 验证码Key
                   */
                  private String verifyCodeKey;
                  /**
                   * 验证码
                   */
                  private String verifyCode;
              }
              

              定义登录服务类

              在登录服务类中,我们需要定义以下方法:

              1. 生成验证码

                在该方法中使用easy-captcha生成一个验证码,生成的验证码除了要返回给前端,还需要在后端进行缓存,这样才能实现前后端的验证码校验。本文中给出了两种缓存验证码的方式,一种是基于RedisTemplate缓存至Redis,一种是缓存至Session,读者可根据需要选择性使用,推荐使用**Redis**。在本文附录中给出了缓存至Session的实现方式。

              2. 登录

                在登录方法中首先校验验证码是否正确,然后再校验用户名和密码是否正确,校验通过后生成Token返回给前端。本文中该方法仅给出验证码校验相关的逻辑,其他逻辑请自行实现。

              @Service
              public class AuthService { private final RedisTemplate redisTemplate;
                  public AuthService(
                      RedisTemplate redisTemplate
                  ) { this.redisTemplate = redisTemplate;
                  }
                  public VerifyCodeEntity generateVerifyCode() throws IOException { // 创建验证码对象
                      Captcha captcha = new ArithmeticCaptcha();
                      // 生成验证码编号
                      String verifyCodeKey = UUID.randomUUID().toString();
                      String verifyCode = captcha.text();
                      // 获取验证码图片,构造响应结果
                      VerifyCodeEntity verifyCodeEntity = new VerifyCodeEntity(verifyCodeKey, captcha.toBase64(), verifyCode);
                      // 存入Redis,设置120s过期
                      redisTemplate.opsForValue().set(verifyCodeKey, verifyCode, 120, TimeUnit.SECONDS);
                      return verifyCodeEntity;
                  }
                  public String login(LoginParam param) { // 校验验证码
                      // 获取用户输入的验证码
                      String actual = param.getVerifyCode();
                      // 判断验证码是否过期
                      if (redisTemplate.getExpire(param.getVerifyCodeKey(), TimeUnit.SECONDS) < 0) { throw new RuntimeException("验证码过期");
                      }
                      // 从redis读取验证码并删除缓存
                      String expect = (String) redisTemplate.opsForValue().get(param.getVerifyCodeKey());
                      redisTemplate.delete(param.getVerifyCodeKey());
                      // 比较用户输入的验证码和缓存中的验证码是否一致,不一致则抛错
                      if (!StringUtils.hasText(expect) || !StringUtils.hasText(actual) || !actual.equalsIgnoreCase(expect)) { throw new RuntimeException("验证码错误");
                      }
                      // 校验用户名和密码,校验成功后生成token返回给前端,具体逻辑省略
                      String token = "";
                      return token;
                  }
              }
              

              定义登录控制器

              /**
               * 登录控制器
               */
              @RestController("/auth")
              public class AuthController { private final AuthService authService;
                  public AuthController(AuthService authService) { this.authService = authService;
                  }
                  /**
                   * 获取验证码
                   */
                  @GetMapping("/verify-code")
                  public VerifyCodeEntity generateVerifyCode() throws IOException { return authService.generateVerifyCode();
                  }
                  /**
                   * 登录
                   */
                  @PostMapping("/login")
                  public String login(@RequestBody @Validated LoginParam param) { return authService.login(param);
                  }
              }
              

              前端登录页面实现

              此前端页面基于Vue3的组合式API和Element Plus开发,使用Axios向后端发送请求,因代码较长,将其放在附录中,请移步至附录查看。

              测试和验证

              总结

              本文介绍了如何基于Java21和SpringBoot3集成easy-captcha实现验证码显示和登录校验,给出了详细的实现代码,如有错误,还望批评指正。

              在后续实践中我也是及时更新自己的学习心得和经验总结,希望与诸位看官一起进步。

              附录

              使用Session缓存验证码

              使用Session缓存验证码时还需要借助ScheduledExecutorService、Timer、Quartz等实现一个延迟任务,用于从Session中删除超时的验证码。

              @Service
              public class AuthService { private final ScheduledExecutorService scheduledExecutorService;
                  public AuthService(
                      ScheduledExecutorService scheduledExecutorService
                  ) { this.scheduledExecutorService = scheduledExecutorService;
                  }
                  public VerifyCodeEntity generateVerifyCode() throws IOException { // 创建验证码对象
                      Captcha captcha = new ArithmeticCaptcha();
                      // 生成验证码编号
                      String verifyCodeKey = UUID.randomUUID().toString();
                      String verifyCode = captcha.text();
                      // 获取验证码图片,构造响应结果
                      VerifyCodeEntity verifyCodeEntity = new VerifyCodeEntity(verifyCodeKey, captcha.toBase64(), verifyCode);
                      // 存入session,设置120s过期
                      ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
                      HttpSession session = attributes.getRequest().getSession();
                      session.setAttribute(verifyCodeKey, verifyCode);
                      // 超时后删除验证码缓存
                      // 以下是使用ScheduledExecutorService实现
                      scheduledExecutorService.schedule(() -> { session.removeAttribute(verifyCode);
                      }, 120, TimeUnit.SECONDS);
                      // // 以下是使用Timer实现超时后删除验证码
                      // Timer timer = new Timer();
                      // timer.schedule(new TimerTask() { //     @Override
                      //     public void run() { //         session.removeAttribute(verifyCode);
                      //     }
                      // }, 120 * 1000L);
                      return verifyCodeEntity;
                  }
                  public String login(LoginParam param) { // 校验验证码
                      // 获取用户输入的验证码
                      String actual = param.getVerifyCode();
                      // 从Session读取验证码并删除缓存
                      ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
                      HttpSession session = attributes.getRequest().getSession();
                      String expect = (String) session.getAttribute(param.getVerifyCodeKey());
                      session.removeAttribute(param.getVerifyCodeKey());
                      // 比较用户输入的验证码和缓存中的验证码是否一致,不一致则抛错
                      if (!StringUtils.hasText(expect) || !StringUtils.hasText(actual) || !actual.equalsIgnoreCase(expect)) { throw new RuntimeException("验证码错误");
                      }
                      // 校验用户名和密码,校验成功后生成token返回给前端,具体逻辑省略
                      String token = "";
                      return token;
                  }
              }
              

              以上代码中使用ScheduledExecutorService设置了一个延迟任务,120s后从Session中删除验证码,还需要声明一个ScheduledExecutorService的Bean。

              /**
               * 线程池配置
               */
              @Configuration
              public class ThreadPoolConfig { /**
                   * 核心线程池大小
                   */
                  private final int corePoolSize = 50;
                  @Bean
                  public ScheduledExecutorService scheduledExecutorService() { return new ScheduledThreadPoolExecutor(corePoolSize);
                  }
              }
              

              前端登录页面实现代码