【Easy云盘】Day1 生成验证码、邮箱发送验证码、用AOP实现拦截器和参数校验

151 阅读5分钟

项目介绍

业务功能

登陆 上传文件(断点续传、大文件的分片上传、秒传、文件预览) 文件分级、文件删除、文件下载、文件移动、回收站还原 分享文件(被分享到的文件可以保存到自己的网盘,如果是自己分享的文件就不需要,可以取消分享) 管理员:用户管理(分配空间)

生成验证码

先用验证码生成器随机生成验证码,并存进session,之后每次验证码校验都和session里的验证码对比

        CreateImageCode vCode = new CreateImageCode(130, 38, 5, 10);
        String code = vCode.getCode();
        if (type == null || type == 0) {//普通验证码
            session.setAttribute(Constants.CHECK_CODE_KEY, code);
        } else {//邮箱验证码
            session.setAttribute(Constants.CHECK_CODE_KEY_EMAIL, code);
        }

设置响应体,禁用浏览器缓存(导致不生成新的验证码图片)

        response.setHeader("Pragma", "no-cache");
        response.setHeader("Cache-Control", "no-cache");
        response.setDateHeader("Expires", 0);
        response.setContentType("image/jpeg");
        vCode.write(response.getOutputStream());

邮箱发送验证码

  1. 先校验普通验证码,通过后再进行下一步
  2. 新建一张表存放emailcode(email,code,status(使用过或未使用))
  3. 生成邮箱验证码发送出去
  4. 再禁用表里这个邮箱的其他验证码(status置为1)
  5. 最后将这个邮箱验证码存入表中。
  6. 结束时将session中的邮箱验证码删掉。
 try {
            if (!checkCode.equalsIgnoreCase((String) session.getAttribute(Constants.CHECK_CODE_KEY_EMAIL))) {
                throw new BusinessException("图片验证码不正确");
            }
            emailCodeService.sendEmailCode(email, type);
            return getSuccessResponseVO(null);
        } finally {
            session.removeAttribute(Constants.CHECK_CODE_KEY_EMAIL);
        }
 @Override
    @Transactional(rollbackFor = Exception.class)
    public void sendEmailCode(String toEmail, Integer type) {
        //如果是注册,校验邮箱是否已存在
        if (type == Constants.ZERO) {
            UserInfo userInfo = userInfoMapper.selectByEmail(toEmail);
            if (null != userInfo) {
                throw new BusinessException("邮箱已经存在");
            }
        }

        String code = StringTools.getRandomNumber(Constants.LENGTH_5);
        sendEmailCode(toEmail, code);

        emailCodeMapper.disableEmailCode(toEmail);
        EmailCode emailCode = new EmailCode();
        emailCode.setCode(code);
        emailCode.setEmail(toEmail);
        emailCode.setStatus(Constants.ZERO);
        emailCode.setCreateTime(new Date());
        emailCodeMapper.insert(emailCode);
    }

发送逻辑如下: 先用spring的javaMailSender创建一个邮件,然后用MimeMessageHelper配置邮件内容(发件人、收件人、邮件内容)

private void sendEmailCode(String toEmail, String code) {

// 创建 MimeMessage 对象,表示一封待发送的邮件
MimeMessage message = javaMailSender.createMimeMessage();

// 使用 MimeMessageHelper 辅助类配置邮件内容
// 第二个参数 true 表示支持 multipart(多部分),可以添加附件
MimeMessageHelper helper = new MimeMessageHelper(message, true);

try {
    // 设置邮件的发件人地址,从配置中获取
    helper.setFrom(appConfig.getSendUserName());

    // 设置邮件的收件人
    helper.setTo(toEmail);

    // 从 Redis 中获取系统设置(例如邮件主题和内容模板)
    SysSettingsDto sysSettingsDto = redisComponent.getSysSettingsDto();

    // 设置邮件的主题,从 Redis 配置中获取
    helper.setSubject(sysSettingsDto.getRegisterEmailTitle());

    // 设置邮件的正文内容
    // 使用 String.format 将模板内容和动态参数(如验证码 code)格式化后填充到正文中
    helper.setText(String.format(sysSettingsDto.getRegisterEmailContent(), code));

    // 设置邮件的发送时间为当前时间
    helper.setSentDate(new Date());

    // 发送邮件,通过 JavaMailSender 的 send 方法
    javaMailSender.send(message);

    log.info("邮件发送成功,收件人:{}", Arrays.toString(toEmail));
} catch (Exception e) {
    log.error("邮件发送失败", e);
}
}

Redis配置

将一些系统信息放到redis中,之后可以从redis中获取数据,比如redisComponent.getSysSettingsDto()

先看redis中有无数据,如果没有,将SysSettingsDto(设置了系统信息)放入redis

    public SysSettingsDto getSysSettingsDto() {
        SysSettingsDto sysSettingsDto = (SysSettingsDto) redisUtils.get(Constants.REDIS_KEY_SYS_SETTING);
        if (sysSettingsDto == null) {
            sysSettingsDto = new SysSettingsDto();//新建一个SysSettingsDto
            redisUtils.set(Constants.REDIS_KEY_SYS_SETTING, sysSettingsDto);
        }
        return sysSettingsDto;
    }

用AOP实现拦截器和参数校验

看一个例子,比如对邮箱进行参数校验,这里设置2个注解,@GlobalInterceptor和@VerifyParam。前者用于指定哪些方法需要参数校验,后者用于指定哪些参数需要参数校验。

@GlobalInterceptor(checkLogin = false, checkParams = true)
    public ResponseVO sendEmailCode(HttpSession session,
                                    @VerifyParam(required = true, regex = VerifyRegexEnum.EMAIL, max = 150) String email,
                                    @VerifyParam(required = true) String checkCode,
                                    @VerifyParam(required = true) Integer type)

@GlobalInterceptor

@Target({ElementType.METHOD})//作用于方法上
@Retention(RetentionPolicy.RUNTIME)//必须加上,表示运行时使用
public @interface GlobalInterceptor {

    boolean checkLogin() default true
    boolean checkParams() default false;
    boolean checkAdmin() default false;
}

@VerifyParam

@Target({ElementType.PARAMETER, ElementType.FIELD})//作用在参数上
@Retention(RetentionPolicy.RUNTIME)
public @interface VerifyParam {
 
    VerifyRegexEnum regex() default VerifyRegexEnum.NO;//校验正则,默认不校验
    int min() default -1;
    int max() default -1;
    boolean required() default false;
}

正则校验枚举类

public enum VerifyRegexEnum {
    NO("", "不校验"),
    IP("([1-9]|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])(\\.(\\d|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])){3}", "IP地址"),
    POSITIVE_INTEGER("^[0-9]*[1-9][0-9]*$", "正整数"),
    NUMBER_LETTER_UNDER_LINE("^\\w+$", "由数字、26个英文字母或者下划线组成的字符串"),
    EMAIL("^[\\w-]+(\\.[\\w-]+)*@[\\w-]+(\\.[\\w-]+)+$", "邮箱"),
    PHONE("(1[0-9])\\d{9}$", "手机号码"),
    COMMON("^[a-zA-Z0-9_\\u4e00-\\u9fa5]+$", "数字,字母,中文,下划线"),
    PASSWORD("^(?=.*\\d)(?=.*[a-zA-Z])[\\da-zA-Z~!@#$%^&*_]{8,}$", "只能是数字,字母,特殊字符 8-18位"),
    ACCOUNT("^[0-9a-zA-Z_]{1,}$", "字母开头,由数字、英文字母或者下划线组成"),
    MONEY("^[0-9]+(.[0-9]{1,2})?$", "金额");

    private String regex;
    private String desc;

    VerifyRegexEnum(String regex, String desc) {
        this.regex = regex;
        this.desc = desc;
    }

    public String getRegex() {
        return regex;
    }

    public String getDesc() {
        return desc;
    }
}

下面是重点,即切面类 全局操作切面,拦截被 @GlobalInterceptor 注解的方法,处理登录校验、参数校验等逻辑

@Component
@Aspect
public class GlobalOperationAspect {

	//定义切点,匹配所有使用了 @GlobalInterceptor 注解的方法。
    @Pointcut("@annotation(com.easypan.annotation.GlobalInterceptor)")
    private void requestInterceptor() {
    }
    
	//作用于切点方法前,执行拦截逻辑,利用反射获取被拦截方法的所有信息(参数、注释等)
    @Before("requestInterceptor()")
    public void interceptorDo(JoinPoint point) throws BusinessException {
        try {
            // 获取拦截方法的相关信息
            Object target = point.getTarget();
            Object[] arguments = point.getArgs();
            String methodName = point.getSignature().getName();
            Class<?>[] parameterTypes = ((MethodSignature) point.getSignature()).getMethod().getParameterTypes();
            Method method = target.getClass().getMethod(methodName, parameterTypes);

            // 获取方法上的 @GlobalInterceptor 注解
            GlobalInterceptor interceptor = method.getAnnotation(GlobalInterceptor.class);
            if (interceptor == null) {
                return;
            }
            // 校验登录状态
            if (interceptor.checkLogin() || interceptor.checkAdmin()) {
                checkLogin(interceptor.checkAdmin());
            }
            // 校验方法参数
            if (interceptor.checkParams()) {
                validateParams(method, arguments);
            }
        } catch (BusinessException e) {
            logger.error("全局拦截器异常", e);
            throw e;
        } catch (Exception e) {
            logger.error("全局拦截器异常", e);
            throw new BusinessException(ResponseCodeEnum.CODE_500);
        }
    }
    }

参数校验:先获取传入方法的参数,遍历这些参数,获取参数类型,分成基本数据类型和对象类型校验

 private void validateParams(Method m, Object[] arguments) throws BusinessException {
        Parameter[] parameters = m.getParameters();
        for (int i = 0; i < parameters.length; i++) {
            Parameter parameter = parameters[i];
            Object value = arguments[i];

            // 获取参数上的 @VerifyParam 注解
            VerifyParam verifyParam = parameter.getAnnotation(VerifyParam.class);
            if (verifyParam == null) {
                continue;
            }

            // 校验基本数据类型的参数
            String paramType = parameter.getParameterizedType().getTypeName();
            if (TYPE_STRING.equals(paramType) || TYPE_LONG.equals(paramType) || TYPE_INTEGER.equals(paramType)) {
                checkValue(value, verifyParam);
            } else {
                // 校验对象类型的参数
                checkObjValue(parameter, value);
            }
        }
    }

基本数据类型的参数校验

private void checkValue(Object value, VerifyParam verifyParam) throws BusinessException {
        boolean isEmpty = value == null || StringTools.isEmpty(value.toString());
        int length = value == null ? 0 : value.toString().length();

        // 校验是否为空
        if (isEmpty && verifyParam.required()) {
            throw new BusinessException(ResponseCodeEnum.CODE_600);
        }

        // 校验长度范围
        if (!isEmpty && (verifyParam.max() != -1 && verifyParam.max() < length || verifyParam.min() != -1 && verifyParam.min() > length)) {
            throw new BusinessException(ResponseCodeEnum.CODE_600);
        }

        // 校验正则表达式
        if (!isEmpty && !StringTools.isEmpty(verifyParam.regex().getRegex()) && !VerifyUtils.verify(verifyParam.regex(), String.valueOf(value))) {
            throw new BusinessException(ResponseCodeEnum.CODE_600);
        }
    }