四、从零开始-优化用户管理新增
异常统一处理,异常日志,前置校验,接口防刷
导航
上一章地址:三、从零开始-填坑
下一章地址:五、从零开始-接口幂等处理
说明
- 贴代码时会贴整个类的全量代码,防止找不到我改的是哪个
- 创建的类在哪个包下不会单独说明,因为全量代码里有 package 直接看这个就可以了
- 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();
}
}
然后我们测试一下
可以看到问题就搞定了,参用的是我们自定义的发号器,然后发号器通过设置 workerId 和 datacenterId 这样就可以做到不同机器间的 ID 不重复了
那么思考一下我们接下来新增的时候还差一些什么东西
、
我们在重复添加的时候,异常会直接向前端展示,一个是这肯定不符合我们之前定义的返回规范,另外一个是这个异常不应该用户能看到,肯定不友好,你给用户看这个是怎么个事
所以我们需要一个异常拦截器,对异常进行封装处理
并且我们应该还需要一个自定义异常来向外抛出业务异常
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());
}
}
我们来看一下效果如何
控制台效果
APIFOX 效果
可以看到,非常的合理,然后我们将异常信息存入数据库中,在线上排查的时候方便找问题
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);
}
}
这里实际上如果感觉流量比较大服务器压力大,请求比较多的话,可以将异常写入修改为异步处理,因为异常信息丢一些也是无所谓的,当然这个具体看业务需求的
接下来我们思考一些问题,新增的时候发现重复之后返回异常,但是这个时候是有问题的
一、因为无论你的新增是否成功,这个请求他都已经打到了数据库,那么我们假设别人在攻击的情况下,就算数据没有写进去,但是他的请求到达了数据库,那么无疑是会增加数据库的一个压力。
二、并且用户将信息都填完了你告诉他重复,这样的体验怕是不太好,正常情况下应该是输入完用户名之后,然后就会去提示用户名是否重复才对,所以我们需要提供一个接口去给前端进行校验,但是这个时候如果是攻击的情况下,我可以直接刷接口呀,不需要过你的前端校验,所以接口也得好好设计一下,因为这种情况下可能是攻击,也有可能是用户鼠标有问题,点一次发了两次请求,这个我线上就遇到过,不过这种情况下前端做一下提交防刷也就可以了
综合下来实际上就是
- 校验需要前置,避免加大数据库压力
- 接口防刷
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;
}
}
做成这样然后我们看一下效果
这样也就可以在用户新增之前就做校验看用户名是否重复了
但是我们仔细一想,这不是纯纯掩耳盗铃么,他要是攻击的时候直接请求接口,这些前置校验不等于摆设了么
所以这个时候两个接口之间,我们必须要有交互
按上面这个逻辑来的话,我们将校验接口的时候成功的话生成一串随机数,然后将随机数丢入缓存中,在新增的时候判断随机数也就可以了,这里的话就有两种做法
一、新增用户的时候如果是必须登录了之后在管理后台新增账号,那么我们可以不用返回随机数给前端,直接以当前用户 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、效果展示
频繁展示
校验码不存在或者校验码错误
这样一做了之后,接口防刷也就有了
当然这里也留了个坑,如果有多个参数需要校验那改怎么办呢?