项目介绍
业务功能
登陆 上传文件(断点续传、大文件的分片上传、秒传、文件预览) 文件分级、文件删除、文件下载、文件移动、回收站还原 分享文件(被分享到的文件可以保存到自己的网盘,如果是自己分享的文件就不需要,可以取消分享) 管理员:用户管理(分配空间)
生成验证码
先用验证码生成器随机生成验证码,并存进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());
邮箱发送验证码
- 先校验普通验证码,通过后再进行下一步
- 新建一张表存放emailcode(email,code,status(使用过或未使用))
- 生成邮箱验证码发送出去
- 再禁用表里这个邮箱的其他验证码(status置为1)
- 最后将这个邮箱验证码存入表中。
- 结束时将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);
}
}