从零开始的SpringBoot前后端分离入门级项目(六)

生成验证码

验证码是一个保障接口和用户密码安全的良好工具,在项目中进行一些比较“危险”的操作的时候我们需要让输入验证码才能进入到下一步,话不多说,先来编写一个验证码工具类:

新建验证码实体类

在bean子包下新建VerifyCode实体类:

/**
 * @Author Alfalfa99
 * @Date 2020/9/19 22:13
 * @Version 1.0
 * 验证码实体类
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class VerifyCode {
    
    
    private String code;
    private byte[] imgBytes;
    private long expireTime;
}

新建验证码工具类

在util包下新建一个VerifyCodeUtil:

import Echo.alfalfa.MemberManager.model.bean.VerifyCode;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Random;

/**
 * @Author Alfalfa99
 * @Date 2020/9/19 22:10
 * @Version 1.0
 * 验证码生成工具类
 */


public class VerifyCodeUtil extends org.apache.commons.lang3.RandomUtils {
    
    

    private static final char[] CODE_SEQ = {
    
    'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J',
            'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W',
            'X', 'Y', 'Z', '2', '3', '4', '5', '6', '7', '8', '9'};

    private static final char[] NUMBER_ARRAY = {
    
    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};

    private static Random random = new Random();

    public static String randomString(int length) {
    
    
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < length; i++) {
    
    
            sb.append(String.valueOf(CODE_SEQ[random.nextInt(CODE_SEQ.length)]));
        }
        return sb.toString();
    }

    public static String randomNumberString(int length) {
    
    
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < length; i++) {
    
    
            sb.append(String.valueOf(NUMBER_ARRAY[random.nextInt(NUMBER_ARRAY.length)]));
        }
        return sb.toString();
    }

    public static Color randomColor(int fc, int bc) {
    
    
        int f = fc;
        int b = bc;
        Random random = new Random();
        if (f > 255) {
    
    
            f = 255;
        }
        if (b > 255) {
    
    
            b = 255;
        }
        return new Color(f + random.nextInt(b - f), f + random.nextInt(b - f),
                f + random.nextInt(b - f));
    }

    public static int nextInt(int bound) {
    
    
        return random.nextInt(bound);
    }

    private static final String[] FONT_TYPES = {
    
     "\u5b8b\u4f53", "\u65b0\u5b8b\u4f53", "\u9ed1\u4f53",
            "\u6977\u4f53", "\u96b6\u4e66" };

    //验证码字符位数
    private static final int VALICATE_CODE_LENGTH = 5;

    /**
     * 设置背景颜色及大小,干扰线
     *
     * @param graphics
     * @param width
     * @param height
     */
    private static void fillBackground(Graphics graphics, int width, int height) {
    
    
        // 填充背景
        graphics.setColor(Color.WHITE);
        //设置矩形坐标x y 为0
        graphics.fillRect(0, 0, width, height);

        // 加入干扰线条
        for (int i = 0; i < 8; i++) {
    
    
            //设置随机颜色算法参数
            graphics.setColor(randomColor(40, 150));
            Random random = new Random();
            int x = random.nextInt(width);
            int y = random.nextInt(height);
            int x1 = random.nextInt(width);
            int y1 = random.nextInt(height);
            graphics.drawLine(x, y, x1, y1);
        }
    }

    /**
     * 生成随机字符
     *
     * @param width 宽度
     * @param height 高度
     * @param os
     * @return
     * @throws IOException
     */
    public String generate(int width, int height, OutputStream os) throws IOException {
    
    
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        Graphics graphics = image.getGraphics();
        fillBackground(graphics, width, height);
        String randomStr = randomString(VALICATE_CODE_LENGTH);
        createCharacter(graphics, randomStr);
        graphics.dispose();
        //设置JPEG格式
        ImageIO.write(image, "JPEG", os);
        return randomStr;
    }

    /**
     * 验证码生成
     *
     * @param width
     * @param height
     * @return
     */
    public VerifyCode generate(int width, int height) {
    
    
        VerifyCode verifyCode = null;
        try (
                //将流的初始化放到这里就不需要手动关闭流
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ) {
    
    
            String code = generate(width, height, baos);
            verifyCode = new VerifyCode();
            verifyCode.setCode(code);
            verifyCode.setImgBytes(baos.toByteArray());
        } catch (IOException e) {
    
    
            verifyCode = null;
        }
        return verifyCode;
    }

    /**
     * 设置字符颜色大小
     *
     * @param g
     * @param randomStr
     */
    private void createCharacter(Graphics g, String randomStr) {
    
    
        char[] charArray = randomStr.toCharArray();
        for (int i = 0; i < charArray.length; i++) {
    
    
            //设置RGB颜色算法参数
            g.setColor(new Color(50 + nextInt(100),
                    50 + nextInt(100), 50 + nextInt(100)));
            //设置字体大小,类型
            g.setFont(new Font(FONT_TYPES[nextInt(FONT_TYPES.length)], Font.BOLD, 26));
            //设置x y 坐标
            g.drawString(String.valueOf(charArray[i]), 15 * i + 5, 19 + nextInt(8));
        }
    }
}

通过该类中的generate()方法我们就可以获得一个图片验证码了,我们需要在UserController中添加这个方法,将这个图片验证码返回给前端:

	@PostMapping("/verifyCode")
    public void verifyCode(HttpServletResponse response) throws IOException {
    
    
        VerifyCodeUtil verifyCodeUtil = new VerifyCodeUtil();
        //设置长宽
        VerifyCode verifyCode = verifyCodeUtil.generate(80, 28);
        //设置响应头
        response.setHeader("Pragma", "no-cache");
        //设置响应头
        response.setHeader("Cache-Control", "no-cache");
        //在代理服务器端防止缓冲
        response.setDateHeader("Expires", 0);
        //设置响应内容类型
        response.setContentType("image/jpeg");
        response.getOutputStream().write(verifyCode.getImgBytes());
        response.getOutputStream().flush();
    }

修改拦截器配置类

我们在InterceptorConfig拦截器配置类中要放行通向获取验证码接口的道路噢,不然就会陷入“死锁”–我要验证码才能登陆,我要登录才能获取验证码。

	@Override
    public void addInterceptors(InterceptorRegistry registry) {
    
    

        //拦截所有目录,除了通向login和register的接口
        registry.addInterceptor(tokenInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns("/**/login/**", "/**/register/**", "/**/verifyCode/**")
                .excludePathPatterns("/**/*.html", "/**/*.js", "/**/*.css");
    }

那么我们现在测试一下我们的验证码是否能成功的返回给前端
在这里插入图片描述
可以看到我们现在确实完成了验证码的生成,并且将其返回给了前端,但是问题来了,用户登录或注册时,如何携带这个验证码和验证其是否正确呢?我们先来解决第一个问题!

修改两个DTO

我们分别在UserLoginDTO和UserRegisterDTO中增加一个verifyCode字段即可完成:

//登录
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserLoginDTO {
    
    
    @ApiModelProperty(example = "登录名")
    @NotNull(message = "账号不允许为空")
    private String username;
    @ApiModelProperty(example = "密码")
    @NotNull(message = "密码不允许为空")
    private String password;
    @ApiModelProperty(example = "验证码")
    @NotNull(message = "验证码不允许为空")
    private String verifyCode;
}
//注册
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserRegisterDTO {
    
    
    @ApiModelProperty(example = "登录名")
    @NotNull(message = "用户名不允许为空")
    private String username;
    @ApiModelProperty(example = "密码")
    @NotNull(message = "密码不允许为空")
    private String password;
    @ApiModelProperty(example = "昵称")
    @NotNull(message = "昵称不允许为空")
    private String nickname;
    @ApiModelProperty(example = "验证码")
    @NotNull(message = "验证码不允许为空")
    private String verifyCode;
}

那么我们就要解决第二个问题了,如何服务端如何保存这个生成的验证码并验证其是否正确呢?

SpringBoot集成Redis

在本系列第一篇博客中,如果有细心的读者应该已经发现了,我们引入了SpringBoot集成Redis的相关依赖spring-boot-starter-data-redis,那么接下来对我们的代码进行更新吧!(redis的安装和开启过于简单,不在此处赘述了,一分钟就能启动)

更新yml配置文件

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: xxx
    password: xxx
    url: xxx
 #在下面追加如下内容
  redis:
    host: 127.0.0.1
    port: 6379

在yml中追加如下内容,我们连接本地的redis,监听端口为默认的6379。

修改UserController

首先我们先通过构造方法注入操作redis

private final UserService userService;
    private final RedisTemplate redisTemplate;

    public UserController(UserService userService, RedisTemplate redisTemplate) {
    
    
        this.userService = userService;
        this.redisTemplate = redisTemplate;
    }

RedisTemplate 是Spring框架为我们提供的用于操作redis的工具类,如果读者有使用过jedis,那么对其应该并不陌生。
然后我们前往生成验证码的方法中添加代码:

	@PostMapping("/verifyCode")
    public void verifyCode(HttpServletResponse response) throws IOException {
    
    
        VerifyCodeUtil verifyCodeUtil = new VerifyCodeUtil();
        //设置长宽
        VerifyCode verifyCode = verifyCodeUtil.generate(80, 28);
        //获取验证码中的字符
        String code = verifyCode.getCode();
        //将验证码中的字符写入redis、过期时间为300秒钟
        redisTemplate.opsForValue().set("vc_" + code, "1", 300, TimeUnit.SECONDS);
        //设置响应头
        response.setHeader("Pragma", "no-cache");
        //设置响应头
        response.setHeader("Cache-Control", "no-cache");
        //在代理服务器端防止缓冲
        response.setDateHeader("Expires", 0);
        //设置响应内容类型
        response.setContentType("image/jpeg");
        response.getOutputStream().write(verifyCode.getImgBytes());
        response.getOutputStream().flush();
    }

在这里我们存入redis的内容为{vc_code:1}(在此处不去处理两个验证码相同的情况,300秒过期时间还能相同那确实挺幸运的)

在登录和注册时,我们从相应的DTO中取出验证码,并与在Redis中进行查询,如果存在该验证码则进行下面的业务流程,如果不存在该验证码则抛出验证码错误异常(VerifyException)
请在exception包下新建该自定义异常类

/**
 * @Author Alfalfa99
 * @Date 2020/9/20 10:48
 * @Version 1.0
 */

public class VerifyException extends AuthenticationException {
    
    
    public VerifyException(String msg) {
    
    
        super(msg);
    }
    public VerifyException(String msg, Throwable t) {
    
    
        super(msg, t);
    }
}

之后我们去全局异常处理类中添加上这个异常

		```
		else if (e instanceof VerifyException){
    
    
            //验证码错误
            return new CommonResult<>(40006, "Error", e.getMessage());
        }
        ```

完成上述步骤之后回到我们的UserController中,对我们的登录以及注册方法进行验证码验证

	@PostMapping("/login")
    public CommonResult<String> login(@RequestBody UserLoginDTO ulDTO, HttpServletRequest request) {
    
    
        if (null == redisTemplate.opsForValue().get("vc_" + ulDTO.getVerifyCode())) {
    
    
            throw new VerifyException("验证码错误");
        }
        redisTemplate.delete("vc_" + ulDTO.getVerifyCode());
        String token = userService.userLogin(ulDTO);

        return new CommonResult<>(20000, "OK", token);
    }

    @PostMapping("/register")
    public CommonResult<String> register(@RequestBody UserRegisterDTO urDTO) {
    
    
        if (null == redisTemplate.opsForValue().get("vc_" + urDTO.getVerifyCode())) {
    
    
            throw new VerifyException("验证码错误");
        }
        userService.userRegister(urDTO);
        return new CommonResult<>(20000, "OK", "注册成功");
    }

我们通过redisTemplate.opsForValue().get()方法去获得相应的验证码,如果验证码存在,那么执行接下来的业务代码,如果验证码不存在,则抛出异常要求用户重新填写验证码。

我们这篇博客的内容到这里就告一段落了,在下一篇博客就是本系列的最后一篇博客,我们会讲解分页和排序相关的内容

本次博客的内容也到此为止了,如果对博客内容有疑问可以私信联系笔者,如果这篇文章对你有用希望你能点一个赞,谢谢~

猜你喜欢

转载自blog.csdn.net/Alfalfa99/article/details/108783676