四、从零开始-优化用户管理新增

四、从零开始-优化用户管理新增

异常统一处理,异常日志,前置校验,接口防刷

导航

上一章地址:三、从零开始-填坑

下一章地址:五、从零开始-接口幂等处理

说明

  1. 贴代码时会贴整个类的全量代码,防止找不到我改的是哪个
  2. 创建的类在哪个包下不会单独说明,因为全量代码里有 package 直接看这个就可以了
  3. gitee 代码存放地址 https://gitee.com/mxw13579/study-scaffold 可以直接下下来看更加清晰

四、从零开始-优化用户管理新增

前面的话实际上我们主要是在处理读取的问题,而对写入问题基本上是没有去管的,这章就来搞定写入问题

4.1、插入填充

之前我们对插入的处理是直接 save 进去的,但是由于有一些字段比如说 createdTime 这种应该是要自动填充的,而我们没有写,所以对这个进行一下操作

package com.lzl.study.scaffold.studyscaffold.common.mybatis;

import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;

/**
 * @ClassName MyMetaObjectHandler
 * @Author lizelin
 * @Description 自动填充
 * @Date 2023-10-09 20:33
 * @Version 1.0
 */
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {

    /**
     * @param metaObject
     * @Description 插入填充
     * @author lizelin
     * @date 2023-09-25 20:41
     **/
    @Override
    public void insertFill(MetaObject metaObject) {
        //写入创建时间
        if (metaObject.hasGetter("createdTime")) {
            setFieldValByName("createdTime", LocalDateTime.now(), metaObject);
        }
        //写入创建人
        if (metaObject.hasGetter("createdBy")) {
            //这里实际上应该是当前用户的 ID 但是现在没有接入鉴权,所以写死为 1
            setFieldValByName("createdBy", 1L, metaObject);
        }
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        //写入修改时间
        if (metaObject.hasGetter("updatedTime")) {
            setFieldValByName("updatedTime", LocalDateTime.now(), metaObject);
        }
        //写入修改人
        if (metaObject.hasGetter("updatedBy")) {
            //这里实际上应该是当前用户的 ID 但是现在没有接入鉴权,所以写死为 1
            setFieldValByName("updatedBy", 1L, metaObject);
        }
    }
}

通过 MetaObjectHandler 将自己的处理逻辑给搞进去,这样我们也就搞定了插入填充

然后我们去研究一下他是怎么做到的,看看我们还能做一些什么

首先找到 MybatisPlusAutoConfiguration.java 中的 sqlSessionFactory()

@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
    //..........省略各种校验

    // TODO 修改源码支持定义 TransactionFactory
    this.getBeanThen(TransactionFactory.class, factory::setTransactionFactory);

    // TODO 对源码做了一定的修改(因为源码适配了老旧的mybatis版本,但我们不需要适配)
    Class<? extends LanguageDriver> defaultLanguageDriver = this.properties.getDefaultScriptingLanguageDriver();
    if (!ObjectUtils.isEmpty(this.languageDrivers)) {
        factory.setScriptingLanguageDrivers(this.languageDrivers);
    }
    Optional.ofNullable(defaultLanguageDriver).ifPresent(factory::setDefaultScriptingLanguageDriver);

    // TODO 自定义枚举包
    if (StringUtils.hasLength(this.properties.getTypeEnumsPackage())) {
        factory.setTypeEnumsPackage(this.properties.getTypeEnumsPackage());
    }
    // TODO 此处必为非 NULL
    GlobalConfig globalConfig = this.properties.getGlobalConfig();
    // TODO 注入填充器
    this.getBeanThen(MetaObjectHandler.class, globalConfig::setMetaObjectHandler);
    // TODO 注入主键生成器
    this.getBeanThen(IKeyGenerator.class, i -> globalConfig.getDbConfig().setKeyGenerator(i));
    // TODO 注入sql注入器
    this.getBeanThen(ISqlInjector.class, globalConfig::setSqlInjector);
    // TODO 注入ID生成器
    this.getBeanThen(IdentifierGenerator.class, globalConfig::setIdentifierGenerator);
    // TODO 设置 GlobalConfig 到 MybatisSqlSessionFactoryBean
    factory.setGlobalConfig(globalConfig);
    return factory.getObject();
}

private <T> void getBeanThen(Class<T> clazz, Consumer<T> consumer) {
        if (this.applicationContext.getBeanNamesForType(clazz, false, false).length > 0) {
            consumer.accept(this.applicationContext.getBean(clazz));
        }
}

我们省略掉各种校验之后,看关键代码,说白了就是在 Spring 容器里面看看有没有实现类,有的话拿出来

在这里我们看到了一个 ID 生成器这个东西,如果我们是在分布式的情况下是需要 分布式 ID 的,虽然现在没有,但是以后肯定要有

因为这玩意后面还要改分布式的 所以我直接给写上,实际上当前项目可以不需要

4.2、自定义 ID

说白了,按他的源码逻辑,我们只需要实现一下 IdentifierGenerator 接口,就可以搞定了,但是考虑到如果要分布式 ID 的情况下,所以得将生成器给抽离出去,然后在去调用

先搞定发号器

package com.lzl.study.scaffold.studyscaffold.common.config;

import cn.hutool.core.lang.Snowflake;
import cn.hutool.core.util.IdUtil;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @ClassName SnowflakeConfig
 * @Author lizelin
 * @Description 发号器
 * @Date 2023-10-09 20:05
 * @Version 1.0
 */
@Configuration
public class SnowflakeConfig {

    /**
     * @return cn.hutool.core.lang.Snowflake
     * @Description 发号器
     * 这里发号器的参数实际上来讲,应该是从配置文件去读取的,但是由于当前项目没必要,先这样写
     * @author lizelin
     * @date 2023-10-13 15:19
     **/
    @Bean
    public Snowflake snowflake() {
        return IdUtil.getSnowflake(1, 1);
    }
}

然后注入进 MybatisPlus

package com.lzl.study.scaffold.studyscaffold.common.mybatis;

import cn.hutool.core.lang.Snowflake;
import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

/**
 * @ClassName SnowflakeIdenti
 * @Author lizelin
 * @Description 雪花标识 发号器
 * @Date 2023-10-09 20:13
 * @Version 1.0
 */
@Component
@AllArgsConstructor
@Slf4j
public class SnowflakeIdentifierGenerator implements IdentifierGenerator {

    private final Snowflake snowflake;

    /**
     * @param entity
     * @return java.lang.Long
     * @Description 生成 ID
     * @author lizelin
     * @date 2023-10-09 20:32
     **/
    @Override
    public Long nextId(Object entity) {
        log.info("生成ID");
        return snowflake.nextId();
    }

}

然后我们测试一下

image-20231013152349385

可以看到问题就搞定了,参用的是我们自定义的发号器,然后发号器通过设置 workerId 和 datacenterId 这样就可以做到不同机器间的 ID 不重复了

那么思考一下我们接下来新增的时候还差一些什么东西

image-20231013153100105

我们在重复添加的时候,异常会直接向前端展示,一个是这肯定不符合我们之前定义的返回规范,另外一个是这个异常不应该用户能看到,肯定不友好,你给用户看这个是怎么个事

所以我们需要一个异常拦截器,对异常进行封装处理

并且我们应该还需要一个自定义异常来向外抛出业务异常

4.3、异常统一处理

首先我们做一个自定义异常

package com.lzl.study.scaffold.studyscaffold.common.exception;

/**
 * @ClassName CustomException
 * @Author lizelin
 * @Description 自定义异常
 * @Date 2023-10-13 15:36
 * @Version 1.0
 */
public class CustomException extends RuntimeException {

    public CustomException() {

    }

    public CustomException(String msg) {
        super(msg);
    }
}

然后去常量类添加一点参数

package com.lzl.study.scaffold.studyscaffold.common.constant;

/**
 * @ClassName ConmmonEnum
 * @Author lizelin
 * @Description String 类型常用 常量
 * @Date 2023-10-10 16:46
 * @Version 1.0
 */
public class StringCommonConstant {

    private StringCommonConstant() {
        throw new IllegalStateException("工具类不允许实例化");
    }

    /**
     * 手机号正则
     */
    public static final String PHONE_REGEXP = "^((13[0-9])|(14[579])|(15([0-3]|[5-9]))|(16[56])|(17[0-8])|(18[0-9])|(19[1589]))\\d{8}$";
    /**
     * 身份证正则
     */
    public static final String ID_CARD_REGEXP = "^[1-9]\\d{5}(18|19|20)\\d{2}(0\\d|10|11|12)([0-2]\\d|30|31)\\d{3}[0-9Xx]$";
    /**
     * 邮箱正则
     */
    public static final String EMAIL_REGEXP = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$";
    /**
     * 唯一约束异常 返回信息
     */
    public static final String SQL_INTEGRITY_CONSTRAINT_VIOLATION_EXCEPTION_MSG_FORMAT = "{} 已存在 ,不允许重复添加";
    /**
     * 唯一约束异常 格式
     */
    public static final String SQL_INTEGRITY_CONSTRAINT_VIOLATION_EXCEPTION_CONTAINS = "Duplicate entry";
    /**
     * 包目录
     */
    public static final String PROJECT_PACKAGE_NAME = "com.lzl.study.scaffold.studyscaffold.user";

}

然后对异常进行统一处理

package com.lzl.study.scaffold.studyscaffold.common.exception;

import cn.hutool.core.text.CharSequenceUtil;
import com.lzl.study.scaffold.studyscaffold.common.constant.StringCommonConstant;
import com.lzl.study.scaffold.studyscaffold.common.entity.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.sql.SQLIntegrityConstraintViolationException;

/**
 * @ClassName GlobalExceptionHandler
 * @Author lizelin
 * @Description 全局异常拦截器
 * @Date 2023-10-13 15:35
 * @Version 1.0
 */
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {


    /**
     * @param ex
     * @return com.lzl.study.scaffold.studyscaffold.common.entity.R
     * @Description 唯一索引重复添加异常处理
     * @author lizelin
     * @date 2023-10-13 15:42
     **/
    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
    public R sqlIntegrityConstraintViolationExceptionHandler(SQLIntegrityConstraintViolationException ex) {
        StackTraceElement[] stackTraces = ex.getStackTrace();
        StackTraceElement carryOutStackTrace = null;
        for (StackTraceElement stackTraceElement : stackTraces) {
            String className = stackTraceElement.getClassName();
            if (CharSequenceUtil.startWith(className, StringCommonConstant.PROJECT_PACKAGE_NAME)) {
                carryOutStackTrace = stackTraceElement;
                break;
            }
        }
        //异常执行点
        String carryOutMethodName = carryOutStackTrace.getMethodName();
        String carryOutClassName = carryOutStackTrace.getClassName();
        int carryOutLineNumber = carryOutStackTrace.getLineNumber();
        log.error("异常执行在文件 {} 中的方法 {} 的第 {} 行,异常信息: {}", carryOutClassName, carryOutMethodName, carryOutLineNumber, ex.getMessage());
        //异常抛出点
        StackTraceElement stackTrace = ex.getStackTrace()[0];
        String methodName = stackTrace.getMethodName();
        String className = stackTrace.getClassName();
        int lineNumber = stackTrace.getLineNumber();
        log.error("异常发生在文件 {} 中的方法 {} 的第 {} 行,异常信息: {}", className, methodName, lineNumber, ex.getMessage());
        if (ex.getMessage().contains(StringCommonConstant.SQL_INTEGRITY_CONSTRAINT_VIOLATION_EXCEPTION_CONTAINS)) {
            String[] split = ex.getMessage().split(" ");
            String msg = CharSequenceUtil.format(StringCommonConstant.SQL_INTEGRITY_CONSTRAINT_VIOLATION_EXCEPTION_MSG_FORMAT, split[2]);
            return R.error(msg);
        }
        return R.error("未知错误");
    }

    /**
     * @param ex
     * @return com.lzl.study.scaffold.studyscaffold.common.entity.R
     * @Description 自定义异常捕获
     * @author lizelin
     * @date 2023-10-13 15:50
     **/
    @ExceptionHandler(CustomException.class)
    public R customExceptionHandler(CustomException ex) {
        log.error(ex.getMessage());
        return R.error(ex.getMessage());
    }

}

我们来看一下效果如何

控制台效果

image-20231013163919506

APIFOX 效果

image-20231013163949054

可以看到,非常的合理,然后我们将异常信息存入数据库中,在线上排查的时候方便找问题

4.4、异常管理

将异常信息存入数据库中,方便线上排查问题,并且在前端展示,所以得做一套增删改查出来

当然目前只存,就不写 controller 了

4.4.1、数据库


/* --------------- 创建表 --------------- */
DROP TABLE IF EXISTS t_exception_manage;
CREATE TABLE t_exception_manage(
    `id` BIGINT    COMMENT '主键' ,
    `carry_out_method_name` VARCHAR(128)    COMMENT '异常执行点方法名称' ,
    `carry_out_class_name` VARCHAR(512)    COMMENT '异常执行点文件名称' ,
    `carry_out_line_number` INT    COMMENT '异常执行点行数' ,
    `method_name` VARCHAR(128)    COMMENT '异常抛出点方法名称' ,
    `class_name` VARCHAR(512)    COMMENT '异常抛出点文件名称' ,
    `line_number` INT    COMMENT '异常抛出点行数' ,
    `exception_message` TEXT    COMMENT '异常信息' ,
    `tenant_id` INT    COMMENT '租户号' ,
    `revision` INT    COMMENT '乐观锁' ,
    `created_by` BIGINT    COMMENT '创建人' ,
    `created_time` DATETIME    COMMENT '创建时间' ,
    `updated_by` BIGINT    COMMENT '更新人' ,
    `updated_time` DATETIME    COMMENT '更新时间' 
)  COMMENT = '异常管理';

4.4.2、entity

package com.lzl.study.scaffold.studyscaffold.exception.entity;

/**
 * @ClassName ExceptionMangeEntity
 * @Author lizelin
 * @Description TODO
 * @Date 2023-10-13 16:51
 * @Version 1.0
 */

import com.baomidou.mybatisplus.annotation.TableName;
import com.lzl.study.scaffold.studyscaffold.common.entity.BaseEntity;
import lombok.Builder;
import lombok.Data;


/**
 * @ClassName SysUser
 * @Author lizelin
 * @Description 异常管理 entity
 * @Date 2023-10-13 16:52
 * @Version 1.0
 */
@TableName("t_exception_manage")
@Data
@Builder
public class ExceptionManageEntity extends BaseEntity {
    /**
     * 异常执行点方法名称
     */
    private String carryOutMethodName;
    /**
     * 异常执行点文件名称
     */
    private String carryOutClassName;
    /**
     * 异常执行点行数
     */
    private Integer carryOutLineNumber;
    /**
     * 异常抛出点方法名称
     */
    private String methodName;
    /**
     * 异常抛出点文件名称
     */
    private String className;
    /**
     * 异常抛出点行数
     */
    private Integer lineNumber;
    /**
     * 异常信息
     */
    private String exceptionMessage;
    /**
     * 租户号
     */
    private Integer tenantId;
    /**
     * 乐观锁
     */
    private Integer revision;

}

4.4.3、service

package com.lzl.study.scaffold.studyscaffold.exception.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.lzl.study.scaffold.studyscaffold.exception.entity.ExceptionManageEntity;

/**
 * @ClassName ExceptionManageService
 * @Author lizelin
 * @Description 异常管理 service
 * @Date 2023-10-13 16:53
 * @Version 1.0
 */
public interface ExceptionManageService extends IService<ExceptionManageEntity> {

}

4.4.4、mapper

package com.lzl.study.scaffold.studyscaffold.exception.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.lzl.study.scaffold.studyscaffold.exception.entity.ExceptionManageEntity;
import org.apache.ibatis.annotations.Mapper;


/**
 * @ClassName ExceptionManageMapper
 * @Author lizelin
 * @Description 异常管理 mapper
 * @Date 2023-10-13 16:55
 * @Version 1.0
 */
@Mapper
public interface ExceptionManageMapper extends BaseMapper<ExceptionManageEntity> {

}

4.4.5、serviceimpl

package com.lzl.study.scaffold.studyscaffold.exception.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.lzl.study.scaffold.studyscaffold.exception.entity.ExceptionManageEntity;
import com.lzl.study.scaffold.studyscaffold.exception.mapper.ExceptionManageMapper;
import com.lzl.study.scaffold.studyscaffold.exception.service.ExceptionManageService;
import org.springframework.stereotype.Service;

/**
 * @ClassName ExceptionManageServiceImpl
 * @Author lizelin
 * @Description 异常管理 serviceImpl
 * @Date 2023-10-13 16:54
 * @Version 1.0
 */
@Service
public class ExceptionManageServiceImpl extends ServiceImpl<ExceptionManageMapper, ExceptionManageEntity> implements ExceptionManageService {

}

4.4.6、修改 异常拦截器

package com.lzl.study.scaffold.studyscaffold.common.exception;

import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.StrUtil;
import com.lzl.study.scaffold.studyscaffold.common.constant.StringCommonConstant;
import com.lzl.study.scaffold.studyscaffold.common.entity.R;
import com.lzl.study.scaffold.studyscaffold.common.state.ResultExceptionStateEnum;
import com.lzl.study.scaffold.studyscaffold.exception.entity.ExceptionManageEntity;
import com.lzl.study.scaffold.studyscaffold.exception.service.ExceptionManageService;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.sql.SQLIntegrityConstraintViolationException;

/**
 * @ClassName GlobalExceptionHandler
 * @Author lizelin
 * @Description 全局异常拦截器
 * @Date 2023-10-13 15:35
 * @Version 1.0
 */
@RestControllerAdvice
@Slf4j
@Component
@AllArgsConstructor
public class GlobalExceptionHandler {

    private final ExceptionManageService exceptionManageService;

    /**
     * @param ex
     * @return com.lzl.study.scaffold.studyscaffold.common.entity.R
     * @Description 唯一索引重复添加异常处理
     * @author lizelin
     * @date 2023-10-13 15:42
     **/
    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
    public R sqlIntegrityConstraintViolationExceptionHandler(SQLIntegrityConstraintViolationException ex) {
        //记录异常日志
        recordsExceptionLog(ex);
        if (ex.getMessage().contains(StringCommonConstant.SQL_INTEGRITY_CONSTRAINT_VIOLATION_EXCEPTION_CONTAINS)) {
            String[] split = ex.getMessage().split(" ");
            String msg = CharSequenceUtil.format(ResultExceptionStateEnum.SQL_INTEGRITY_CONSTRAINT_VIOLATION_EXCEPTION_MSG_FORMAT.getMsg(), split[2]);
            return R.error(msg);
        }
        return R.error("未知错误");
    }

    /**
     * @param ex
     * @return com.lzl.study.scaffold.studyscaffold.common.entity.R
     * @Description 自定义异常捕获
     * @author lizelin
     * @date 2023-10-13 15:50
     **/
    @ExceptionHandler(CustomException.class)
    public R customExceptionHandler(CustomException ex) {
        String codeStr = ex.getMessage();
        //取出枚举中的错误定义
        ResultExceptionStateEnum msgByCode = ResultExceptionStateEnum.getEnumByCode(codeStr);

        log.error("msgByCode:{}", msgByCode);
        return R.error(msgByCode.getMsg(), msgByCode.getCode());
    }

    /**
     * @param ex
     * @return com.lzl.study.scaffold.studyscaffold.common.entity.R
     * @Description 方法参数无效异常
     * @author lizelin
     * @date 2023-10-13 19:38
     **/
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public R methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException ex) {
        FieldError fieldError = ex.getFieldError();
        String field = fieldError.getField();
        String defaultMessage = fieldError.getDefaultMessage();
        String format = CharSequenceUtil.format(ResultExceptionStateEnum.METHOD_ARGUMENT_NOTVALID_EXCEPTION.getMsg(), field, defaultMessage);
        recordsExceptionLog(ex,format);
        log.error(format);
        return R.error(format,ResultExceptionStateEnum.METHOD_ARGUMENT_NOTVALID_EXCEPTION.getCode());
    }


    /**
     * @param ex
     * @Description 记录异常日志
     * @author lizelin
     * @date 2023-10-13 19:51
     **/
    private void recordsExceptionLog(Exception ex) {
        recordsExceptionLog(ex, null);
    }

    /**
     * @param ex      异常
     * @param massage 异常信息
     * @Description 记录异常日志
     * @author lizelin
     * @date 2023-10-13 17:05
     **/
    private void recordsExceptionLog(Exception ex, String massage) {
        StackTraceElement[] stackTraces = ex.getStackTrace();
        StackTraceElement carryOutStackTrace = null;
        for (StackTraceElement stackTraceElement : stackTraces) {
            String className = stackTraceElement.getClassName();
            if (CharSequenceUtil.startWith(className, StringCommonConstant.PROJECT_PACKAGE_NAME)) {
                carryOutStackTrace = stackTraceElement;
                break;
            }
        }
        if (StrUtil.isEmpty(massage)) {
            massage = ex.getMessage();
        }

        ExceptionManageEntity.ExceptionManageEntityBuilder builder = ExceptionManageEntity.builder();
        //异常抛出点
        StackTraceElement stackTrace = ex.getStackTrace()[0];
        String methodName = stackTrace.getMethodName();
        String className = stackTrace.getClassName();
        int lineNumber = stackTrace.getLineNumber();
        log.error("异常发生在文件 {} 中的方法 {} 的第 {} 行,异常信息: {}", className, methodName, lineNumber, ex.getMessage());
        builder.methodName(methodName)
                .className(className)
                .lineNumber(lineNumber)
                .exceptionMessage(massage);
        if (carryOutStackTrace != null) {
            //可能存在不业务代码中抛出的异常
            //异常执行点
            String carryOutMethodName = carryOutStackTrace.getMethodName();
            String carryOutClassName = carryOutStackTrace.getClassName();
            int carryOutLineNumber = carryOutStackTrace.getLineNumber();
            log.error("异常执行在文件 {} 中的方法 {} 的第 {} 行,异常信息: {}", carryOutClassName, carryOutMethodName, carryOutLineNumber, ex.getMessage());
            builder.carryOutClassName(carryOutClassName)
                    .carryOutLineNumber(carryOutLineNumber)
                    .carryOutMethodName(carryOutMethodName);
        }
        //记录到异常日志
        ExceptionManageEntity build = builder.build();
        exceptionManageService.save(build);
    }

}

这里实际上如果感觉流量比较大服务器压力大,请求比较多的话,可以将异常写入修改为异步处理,因为异常信息丢一些也是无所谓的,当然这个具体看业务需求的

接下来我们思考一些问题,新增的时候发现重复之后返回异常,但是这个时候是有问题的

一、因为无论你的新增是否成功,这个请求他都已经打到了数据库,那么我们假设别人在攻击的情况下,就算数据没有写进去,但是他的请求到达了数据库,那么无疑是会增加数据库的一个压力。

二、并且用户将信息都填完了你告诉他重复,这样的体验怕是不太好,正常情况下应该是输入完用户名之后,然后就会去提示用户名是否重复才对,所以我们需要提供一个接口去给前端进行校验,但是这个时候如果是攻击的情况下,我可以直接刷接口呀,不需要过你的前端校验,所以接口也得好好设计一下,因为这种情况下可能是攻击,也有可能是用户鼠标有问题,点一次发了两次请求,这个我线上就遇到过,不过这种情况下前端做一下提交防刷也就可以了

综合下来实际上就是

  1. 校验需要前置,避免加大数据库压力
  2. 接口防刷

4.5、新增前置校验

我们来思考一下这个前置校验需要怎么做,首先用户传入用户名然后我们去查询数据库看是否存在,存在则不允许新增

package com.lzl.study.scaffold.studyscaffold.user.controller;

import com.lzl.study.scaffold.studyscaffold.common.entity.R;
import com.lzl.study.scaffold.studyscaffold.user.entity.dto.SysUserPageDto;
import com.lzl.study.scaffold.studyscaffold.user.entity.dto.SysUserSaveDto;
import com.lzl.study.scaffold.studyscaffold.user.service.SysUserService;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

/**
 * @ClassName SysUserController
 * @Author lizelin
 * @Description 用户管理 controller 层
 * @Date 2023-10-09 20:00
 * @Version 1.0
 */
@Slf4j
@RestController
@RequestMapping("sysUser")
@AllArgsConstructor
public class SysUserController {

    private final SysUserService sysUserService;

    /**
     * @param dto
     * @return com.baomidou.mybatisplus.extension.plugins.pagination.Page
     * @Description 分页查询
     * @author lizelin
     * @date 2023-10-10 16:35
     **/
    @GetMapping("/page")
    public R page(SysUserPageDto dto) {
        return R.success(sysUserService.dtoPage(dto));
    }

    /**
     * @param sysUserSaveDto
     * @return com.lzl.study.scaffold.studyscaffold.common.entity.R
     * @Description 新增
     * @author lizelin
     * @date 2023-10-10 16:35
     **/
    @PostMapping
    public R add(@Validated @RequestBody SysUserSaveDto sysUserSaveDto) {
        return R.success(sysUserService.dtoSave(sysUserSaveDto));
    }


    /**
     * @param account
     * @return com.lzl.study.scaffold.studyscaffold.common.entity.R
     * @Description 校验用户名是否存在
     * @author lizelin
     * @date 2023-10-13 17:42
     **/
    @GetMapping("/verifyAccount")
    public R verifyAccount(@RequestParam("account") String account) {
        return R.success(sysUserService.verifyAccount(account));
    }
}

service 我就不写了 看实现也能知道他长什么样子

然后实现一下方法

package com.lzl.study.scaffold.studyscaffold.user.service.impl;

import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.lzl.study.scaffold.studyscaffold.common.mapstruct.MapStructConvertUtil;
import com.lzl.study.scaffold.studyscaffold.common.util.PageUtil;
import com.lzl.study.scaffold.studyscaffold.common.util.QueryWrapperBuildUtil;
import com.lzl.study.scaffold.studyscaffold.user.entity.SysUserEntity;
import com.lzl.study.scaffold.studyscaffold.user.entity.dto.SysUserPageDto;
import com.lzl.study.scaffold.studyscaffold.user.entity.dto.SysUserSaveDto;
import com.lzl.study.scaffold.studyscaffold.user.entity.vo.SysUserPageHomeVo;
import com.lzl.study.scaffold.studyscaffold.user.mapper.SysUserMapper;
import com.lzl.study.scaffold.studyscaffold.user.service.SysUserService;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

/**
 * @ClassName SysUserServiceImpl
 * @Author lizelin
 * @Description 用户管理 serviceImpl
 * @Date 2023-10-09 19:58
 * @Version 1.0
 */
@Service
@Slf4j
@AllArgsConstructor
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUserEntity> implements SysUserService {

    /**
     * @param sysUserSaveDto
     * @return java.lang.Boolean
     * @Description 通过 dto 新增
     * @author lizelin
     * @date 2023-10-10 0:08
     **/
    @Override
    public Boolean dtoSave(SysUserSaveDto sysUserSaveDto) {
        SysUserEntity entity = MapStructConvertUtil.mapStructConvert(sysUserSaveDto, SysUserEntity.class);
        return save(entity);
    }

    /**
     * @param dto
     * @return com.baomidou.mybatisplus.extension.plugins.pagination.Page<com.lzl.study.scaffold.studyscaffold.user.entity.vo.SysUserPageVo>
     * @Description 分页查询
     * @author lizelin
     * @date 2023-10-10 17:44
     **/
    @Override
    public Page<SysUserPageHomeVo> dtoPage(SysUserPageDto dto) {
        SysUserEntity entity = MapStructConvertUtil.mapStructConvert(dto, SysUserEntity.class);
        Page<SysUserEntity> page = MapStructConvertUtil.mapStructConvert(dto, Page.class);
        page(page, QueryWrapperBuildUtil.buildLambdaQueryWrapper(entity, SysUserPageHomeVo.class));
        //转换分页
        return PageUtil.convertPage(page, SysUserPageHomeVo.class);
    }

    /**
     * @param account
     * @return java.lang.Boolean
     * @Description 校验用户名是否存在
     * @author lizelin
     * @date 2023-10-13 17:46
     **/
    @Override
    public Boolean verifyAccount(String account) {
        int count = count(Wrappers.lambdaQuery(SysUserEntity.class).eq(SysUserEntity::getAccount, account));
        return count == 0 ? false : true;
    }
}

做成这样然后我们看一下效果

image-20231013175030785

这样也就可以在用户新增之前就做校验看用户名是否重复了

但是我们仔细一想,这不是纯纯掩耳盗铃么,他要是攻击的时候直接请求接口,这些前置校验不等于摆设了么

所以这个时候两个接口之间,我们必须要有交互

按上面这个逻辑来的话,我们将校验接口的时候成功的话生成一串随机数,然后将随机数丢入缓存中,在新增的时候判断随机数也就可以了,这里的话就有两种做法

一、新增用户的时候如果是必须登录了之后在管理后台新增账号,那么我们可以不用返回随机数给前端,直接以当前用户 ID 拼接一个 key,然后新增的时候直接读取就可以了,这也是常见的管理后台注册的校验逻辑

二、如果是允许前台直接注册的话,那么由于没有标识,所以必须要将随机数返回给前端,当然也可以通过 IP 的方式来做,但是 IP 这玩意吧,比如说你们在公司访问某些网站,实际上对方能拿到的只有公司的 IP 是没有办法对具体使用者进行区分的,所以一个 IP 下的随机数就得是多个,因为你总不能让人家公司同一时间只允许一个人注册吧

第一种方式比较简单就不写了

我们现在假设第二种场景来做

4.6、优化前置校验

首先修改 yml 文件来链接 redis

server:
  port: 2005
spring:
  profiles:
    active: @profiles.active@
  application:
    name: study-scaffold
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://192.168.1.106:3306/studyscaffold?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC
    username: root
    password: lzlStudy
  redis:
    port: 6379
    database: 0
    host: 192.168.1.106
# 开启mp的日志(输出到控制台)
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

4.6.1、配置一下 Redis

package com.lzl.study.scaffold.studyscaffold.common.config;

import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * @ClassName RedisConfig
 * @Author lizelin
 * @Description Redis 配置
 * @Date 2023-10-09 20:08
 * @Version 1.0
 */
@Configuration
public class RedisConfig extends CachingConfigurerSupport {

    /**
     * @param redisConnectionFactory
     * @return org.springframework.data.redis.core.RedisTemplate<java.lang.String, java.lang.Object>
     * @Description 配置 redisTemplate
     * @author lizelin
     * @date 2023-09-25 23:09
     **/
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        ObjectMapper objectMapper = new ObjectMapper();
        serializeCfg(objectMapper);
        GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer(objectMapper);

        //值序列化器
        redisTemplate.setValueSerializer(genericJackson2JsonRedisSerializer);

        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    /**
     * @param objectMapper
     * @Description 添加 LocalDateTime 序列化 反序列化器,添加 Long 序列化 反序列化器
     * @author lizelin
     * @date 2023-10-09 20:08
     **/
    private void serializeCfg(ObjectMapper objectMapper) {
        JavaTimeModule javaTimeModule = new JavaTimeModule();

        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        javaTimeModule.addSerializer(LocalDateTime.class,
                new LocalDateTimeSerializer(dtf));
        javaTimeModule.addDeserializer(LocalDateTime.class,
                new LocalDateTimeDeserializer(dtf));
        objectMapper.registerModule(javaTimeModule);

        SimpleModule simpleModule = new SimpleModule();
        simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
        simpleModule.addSerializer(Long.TYPE, ToStringSerializer.instance);
        objectMapper.registerModule(simpleModule);
        //必须加上
        objectMapper.activateDefaultTyping(objectMapper.getPolymorphicTypeValidator(),
                ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);

        objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    }
}

4.6.2、RedisKey 常量类

package com.lzl.study.scaffold.studyscaffold.common.constant;

/**
 * @ClassName RedisKeyConstant
 * @Author lizelin
 * @Description redis key 常量
 * @Date 2023-10-13 18:12
 * @Version 1.0
 */
public class RedisKeyConstant {

    /**
     * 系统用户服务impl验证帐户 统计同一 IP 请求
     */
    public static final String SYS_USER_SERVICE_IMPL_VERIFY_ACCOUNT_COUNT = "sysUser:addUser:verifyAccount:count:{}";
    /**
     * 系统用户服务impl验证帐户 账号验证随机数
     */
    public static final String SYS_USER_SERVICE_IMPL_VERIFY_ACCOUNT_RANDOM_CODE = "sysUser:addUser:verifyAccount:random:code:{}:{}";

}

4.6.3、整型常量

package com.lzl.study.scaffold.studyscaffold.common.constant;

/**
 * @ClassName IntegerCommonConstant
 * @Author lizelin
 * @Description 整型常量
 * @Date 2023-10-13 19:02
 * @Version 1.0
 */
public class IntegerCommonConstant {

    /**
     * 验证账户随机数最小值
     */
    public final static Integer VERIFY_ACCOUNT_RANDOM_CODE_MIN = 1000000;
    /**
     * 验证账户随机数最大值
     */
    public final static Integer VERIFY_ACCOUNT_RANDOM_CODE_MAX = 9999999;

}

4.6.4、异常结果枚举

package com.lzl.study.scaffold.studyscaffold.common.state;

import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;

import java.util.HashMap;
import java.util.Map;

/**
 * @ClassName ResultErrorStateEnum
 * @Author lizelin
 * @Description 异常结果枚举
 * @Date 2023-10-13 18:41
 * @Version 1.0
 */
public enum ResultExceptionStateEnum {


    VERIFY_ACCOUNT_FREQUENTLY_EXCEPTION(1001, "当前 IP 校验请求过于频繁,请稍后再试", "验证账户频繁异常"),
    VERIFY_ACCOUNT_REPEAT_EXCEPTION(1002, "当前验证账户重复", "验证账户重复异常"),
    VERIFY_ACCOUNT_RANDOM_CDOE_INEQUALITY_EXCEPTION(1003, "校验码不正确,请从正确途径访问", "校验码不正确"),
    SQL_INTEGRITY_CONSTRAINT_VIOLATION_EXCEPTION_MSG_FORMAT(1004, "{}已存在 ,不允许重复添加", "唯一约束异常"),

    METHOD_ARGUMENT_NOTVALID_EXCEPTION(2001, "字段:{},{}", "参数绑定异常")

    ;

    private static Map<Integer, ResultExceptionStateEnum> resultExceptionStateEnumHashMap = new HashMap<>(64);

    /**
     * 启动时缓存方便取 枚举
     */
    static {
        resultExceptionStateEnumHashMap.put(VERIFY_ACCOUNT_FREQUENTLY_EXCEPTION.getCode(), VERIFY_ACCOUNT_FREQUENTLY_EXCEPTION);
        resultExceptionStateEnumHashMap.put(SQL_INTEGRITY_CONSTRAINT_VIOLATION_EXCEPTION_MSG_FORMAT.getCode(), SQL_INTEGRITY_CONSTRAINT_VIOLATION_EXCEPTION_MSG_FORMAT);
        resultExceptionStateEnumHashMap.put(VERIFY_ACCOUNT_REPEAT_EXCEPTION.getCode(), VERIFY_ACCOUNT_REPEAT_EXCEPTION);
        resultExceptionStateEnumHashMap.put(VERIFY_ACCOUNT_RANDOM_CDOE_INEQUALITY_EXCEPTION.getCode(), VERIFY_ACCOUNT_RANDOM_CDOE_INEQUALITY_EXCEPTION);
        resultExceptionStateEnumHashMap.put(METHOD_ARGUMENT_NOTVALID_EXCEPTION.getCode(), METHOD_ARGUMENT_NOTVALID_EXCEPTION);
    }

    private Integer code;
    private String msg;

    private String remark;


    ResultExceptionStateEnum(int code, String msg, String remark) {
        this.msg = msg;
        this.code = code;
        this.remark = remark;
    }

    public Integer getCode() {
        return code;
    }

    public String getMsg() {
        return msg;
    }

    public String getRemark() {
        return remark;
    }

    /**
     * @return java.lang.String
     * @Description code 转 string
     * @author lizelin
     * @date 2023-10-13 18:51
     **/
    public String getCodeStr() {
        return StrUtil.toString(code);
    }

    /**
     * @param codeStr
     * @return java.lang.String
     * @Description 通过 code 获取 msg
     * @author lizelin
     * @date 2023-10-13 18:56
     **/
    public static ResultExceptionStateEnum getEnumByCode(String codeStr) {
        int i = NumberUtil.parseInt(codeStr);
        ResultExceptionStateEnum stateEnum = resultExceptionStateEnumHashMap.get(i);
        return stateEnum;
    }
}

讲道理这里本来打算偷懒的,后来想了一下还是规范点,就把原先在 StringCommonConstant 中的SQL_INTEGRITY_CONSTRAINT_VIOLATION_EXCEPTION_MSG_FORMAT 拿到枚举类里面去了

4.6.5、修改 DTO

package com.lzl.study.scaffold.studyscaffold.user.entity.dto;

import com.lzl.study.scaffold.studyscaffold.common.constant.StringCommonConstant;
import com.lzl.study.scaffold.studyscaffold.common.entity.BaseDto;
import lombok.Data;

import javax.validation.constraints.*;

/**
 * @ClassName SysUserSaveDto
 * @Author lizelin
 * @Description 新增用户管理 DTO
 * @Date 2023-10-09 23:58
 * @Version 1.0
 */
@Data
public class SysUserSaveDto extends BaseDto {

    /**
     * 新增校验随机数校验码
     */
    @Min(value = 1000000, message = "校验码 格式不正确")
    @Max(value = 9999999, message = "校验码 格式不正确")
    @NotNull
    private Integer randomInt;
}

添加一个这个字段就好了

4.6.6、修改实现类

package com.lzl.study.scaffold.studyscaffold.user.service.impl;

import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.extra.servlet.ServletUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.lzl.study.scaffold.studyscaffold.common.constant.IntegerCommonConstant;
import com.lzl.study.scaffold.studyscaffold.common.constant.RedisKeyConstant;
import com.lzl.study.scaffold.studyscaffold.common.exception.CustomException;
import com.lzl.study.scaffold.studyscaffold.common.mapstruct.MapStructConvertUtil;
import com.lzl.study.scaffold.studyscaffold.common.state.ResultExceptionStateEnum;
import com.lzl.study.scaffold.studyscaffold.common.util.PageUtil;
import com.lzl.study.scaffold.studyscaffold.common.util.QueryWrapperBuildUtil;
import com.lzl.study.scaffold.studyscaffold.user.entity.SysUserEntity;
import com.lzl.study.scaffold.studyscaffold.user.entity.dto.SysUserPageDto;
import com.lzl.study.scaffold.studyscaffold.user.entity.dto.SysUserSaveDto;
import com.lzl.study.scaffold.studyscaffold.user.entity.vo.SysUserPageHomeVo;
import com.lzl.study.scaffold.studyscaffold.user.mapper.SysUserMapper;
import com.lzl.study.scaffold.studyscaffold.user.service.SysUserService;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletRequest;
import java.util.concurrent.TimeUnit;

/**
 * @ClassName SysUserServiceImpl
 * @Author lizelin
 * @Description 用户管理 serviceImpl
 * @Date 2023-10-09 19:58
 * @Version 1.0
 */
@Service
@Slf4j
@AllArgsConstructor
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUserEntity> implements SysUserService {

    private final RedisTemplate<String, Object> redisTemplate;

    /**
     * @param sysUserSaveDto
     * @return java.lang.Boolean
     * @Description 通过 dto 新增
     * @author lizelin
     * @date 2023-10-10 0:08
     **/
    @Override
    public Boolean dtoSave(SysUserSaveDto sysUserSaveDto, HttpServletRequest httpServletRequest) {
        //校验 校验码是否正确
        String account = sysUserSaveDto.getAccount();
        String clientIP = ServletUtil.getClientIP(httpServletRequest);
        Integer randomInt = sysUserSaveDto.getRandomInt();
        String randomCodeKey = CharSequenceUtil.format(RedisKeyConstant.SYS_USER_SERVICE_IMPL_VERIFY_ACCOUNT_RANDOM_CODE, clientIP, account);
        Integer randomRedis = (Integer) redisTemplate.opsForValue().get(randomCodeKey);
        //校验是否相等
        if (randomCodeKey == null || !NumberUtil.equals(randomInt, randomRedis)) {
            throw new CustomException(ResultExceptionStateEnum.VERIFY_ACCOUNT_RANDOM_CDOE_INEQUALITY_EXCEPTION.getCodeStr());
        }
        //计数 key
        String countKey = CharSequenceUtil.format(RedisKeyConstant.SYS_USER_SERVICE_IMPL_VERIFY_ACCOUNT_COUNT, clientIP);
        redisTemplate.opsForValue().decrement(countKey);
        //去除验证码
        redisTemplate.delete(randomCodeKey);
        SysUserEntity entity = MapStructConvertUtil.mapStructConvert(sysUserSaveDto, SysUserEntity.class);
        return save(entity);
    }

    /**
     * @param dto
     * @return com.baomidou.mybatisplus.extension.plugins.pagination.Page<com.lzl.study.scaffold.studyscaffold.user.entity.vo.SysUserPageVo>
     * @Description 分页查询
     * @author lizelin
     * @date 2023-10-10 17:44
     **/
    @Override
    public Page<SysUserPageHomeVo> dtoPage(SysUserPageDto dto) {
        SysUserEntity entity = MapStructConvertUtil.mapStructConvert(dto, SysUserEntity.class);
        Page<SysUserEntity> page = MapStructConvertUtil.mapStructConvert(dto, Page.class);
        page(page, QueryWrapperBuildUtil.buildLambdaQueryWrapper(entity, SysUserPageHomeVo.class));
        //转换分页
        return PageUtil.convertPage(page, SysUserPageHomeVo.class);
    }

    /**
     * @param account
     * @return java.lang.Boolean
     * @Description 校验用户名是否存在
     * @author lizelin
     * @date 2023-10-13 17:46
     **/
    @Override
    public Integer verifyAccount(String account, HttpServletRequest httpServletRequest) {
        //获取客户端 IP
        String clientIP = ServletUtil.getClientIP(httpServletRequest);
        //计数 key
        String countKey = CharSequenceUtil.format(RedisKeyConstant.SYS_USER_SERVICE_IMPL_VERIFY_ACCOUNT_COUNT, clientIP);

        Integer redisCount = (Integer) redisTemplate.opsForValue().get(countKey);

        if (redisCount == null) {
            //为空则初始化,10 分钟有效期
            redisTemplate.opsForValue().set(countKey, 0, 10 * 60L, TimeUnit.SECONDS);
        }

        if (randomRedis != null && redisCount >= 10) {
            //抛出频繁异常
            throw new CustomException(ResultExceptionStateEnum.VERIFY_ACCOUNT_FREQUENTLY_EXCEPTION.getCodeStr());
        }
        int count = count(Wrappers.lambdaQuery(SysUserEntity.class).eq(SysUserEntity::getAccount, account));
        if (count != 0) {
            //抛出账号重复异常
            throw new CustomException(ResultExceptionStateEnum.VERIFY_ACCOUNT_REPEAT_EXCEPTION.getCodeStr());
        }

        //随机数 key
        String randomCodeKey = CharSequenceUtil.format(RedisKeyConstant.SYS_USER_SERVICE_IMPL_VERIFY_ACCOUNT_RANDOM_CODE, clientIP, account);
        Integer randomInt = RandomUtil.randomInt(IntegerCommonConstant.VERIFY_ACCOUNT_RANDOM_CODE_MIN, IntegerCommonConstant.VERIFY_ACCOUNT_RANDOM_CODE_MAX);
        //写入随机数 10 分钟有效期
        redisTemplate.opsForValue().set(randomCodeKey, randomInt, 10 * 60L, TimeUnit.SECONDS);
        //计数加一
        redisTemplate.opsForValue().increment(countKey);
        return randomInt;
    }
}

4.6.7、分析代码

主要的功能就是实现类的 verifyAccount()dtoSave()

依次来看看做了什么

verifyAccount

/**
 * @param account
 * @return java.lang.Boolean
 * @Description 校验用户名是否存在
 * @author lizelin
 * @date 2023-10-13 17:46
 **/
@Override
public Integer verifyAccount(String account, HttpServletRequest httpServletRequest) {
    //获取客户端 IP
    String clientIP = ServletUtil.getClientIP(httpServletRequest);
    //计数 key
    String countKey = CharSequenceUtil.format(RedisKeyConstant.SYS_USER_SERVICE_IMPL_VERIFY_ACCOUNT_COUNT, clientIP);

    Integer redisCount = (Integer) redisTemplate.opsForValue().get(countKey);

    if (redisCount == null) {
        //为空则初始化,10 分钟有效期
        redisTemplate.opsForValue().set(countKey, 0, 10 * 60L, TimeUnit.SECONDS);
    }

    if (redisCount != null && redisCount >= 10) {
        //抛出频繁异常
        throw new CustomException(ResultExceptionStateEnum.VERIFY_ACCOUNT_FREQUENTLY_EXCEPTION.getCodeStr());
    }
    int count = count(Wrappers.lambdaQuery(SysUserEntity.class).eq(SysUserEntity::getAccount, account));
    if (count != 0) {
        //抛出账号重复异常
        throw new CustomException(ResultExceptionStateEnum.VERIFY_ACCOUNT_REPEAT_EXCEPTION.getCodeStr());
    }

    //随机数 key
    String randomCodeKey = CharSequenceUtil.format(RedisKeyConstant.SYS_USER_SERVICE_IMPL_VERIFY_ACCOUNT_RANDOM_CODE, clientIP, account);
    Integer randomInt = RandomUtil.randomInt(IntegerCommonConstant.VERIFY_ACCOUNT_RANDOM_CODE_MIN, IntegerCommonConstant.VERIFY_ACCOUNT_RANDOM_CODE_MAX);
    //写入随机数 10 分钟有效期
    redisTemplate.opsForValue().set(randomCodeKey, randomInt, 10 * 60L, TimeUnit.SECONDS);
    //计数加一
    redisTemplate.opsForValue().increment(countKey);
    return randomInt;
}

说白了效果就是,十分钟只能请求十次 校验用户 接口,并且每次请求之后,同一个用户名下的随机数会被重置

并且没有限制账号,也就是同一个账号同一个 IP 请求十次,也会抛出异常,因为说白了,你请求两次三次我都算正常,同一个账号请求十次,我表示你这个怕不是在攻击我哦

dtoSave

public Boolean dtoSave(SysUserSaveDto sysUserSaveDto, HttpServletRequest httpServletRequest) {
    //校验 校验码是否正确
    String account = sysUserSaveDto.getAccount();
    String clientIP = ServletUtil.getClientIP(httpServletRequest);
    Integer randomInt = sysUserSaveDto.getRandomInt();
    String randomCodeKey = CharSequenceUtil.format(RedisKeyConstant.SYS_USER_SERVICE_IMPL_VERIFY_ACCOUNT_RANDOM_CODE, clientIP, account);
    Integer randomRedis = (Integer) redisTemplate.opsForValue().get(randomCodeKey);
    //校验是否相等
    if (randomRedis == null || !NumberUtil.equals(randomInt, randomRedis)) {
        throw new CustomException(ResultExceptionStateEnum.VERIFY_ACCOUNT_RANDOM_CDOE_INEQUALITY_EXCEPTION.getCodeStr());
    }
    //计数 key
    String countKey = CharSequenceUtil.format(RedisKeyConstant.SYS_USER_SERVICE_IMPL_VERIFY_ACCOUNT_COUNT, clientIP);
    redisTemplate.opsForValue().decrement(countKey);
    //去除验证码
    redisTemplate.delete(randomCodeKey);
    SysUserEntity entity = MapStructConvertUtil.mapStructConvert(sysUserSaveDto, SysUserEntity.class);
    return save(entity);
}

这里说白了就是看 Redis 里面存的和传的是否匹配,不匹配就抛异常,匹配就减少计数,就去除验证码

4.6.8、效果展示

频繁展示

image-20231013202247285

校验码不存在或者校验码错误

image-20231013202523965

这样一做了之后,接口防刷也就有了

当然这里也留了个坑,如果有多个参数需要校验那改怎么办呢?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值