文章目录
- 前言
- 效果演示
- 注册效果
- 登录
- 环境准备
- 邮箱准备
- 相关工具类
- 验证码工具类
- 日期工具类
- Redis 工具类
- 专门用来管理key的类
- JWT的封装
- 密码加密比对
- 接口与实体
- 接口
- 实体
- 信息枚举类
- 统一异常处理
- 登录流程
- 前端
- 后端
- 密码比对
- 防刷
- 完整代码
- 注册流程
- 前端
- 后端
- 邮箱服务
- 邮箱验证码
- 防刷
- 验证码
- 完整代码
- 注册
- 总结
前言
ok,我又来水博文了,今天的内容很简单,就是咱们的这个用户登录注册,加上邮箱验证,非常简单,为我们接下来的Auto2.0做好前置工作。因为这个没做好,那个也做不好。本文的内容可能比较多,但是都很简单。
效果演示
注册效果
ok,多说无益,我们先来看看完成后的效果咋样。
注册首页:
这块的话,咱们这边发送邮箱验证码之后,前端这边的验证码还会重新刷新一次,反正是在10分钟内完成操作。
登录
环境准备
邮箱准备
首先我们需要使用到邮箱服务,所以的话我们需要去申请到这个邮箱发送的权限,这个也简单,我们以QQ邮箱为例子,打开这个QQ邮箱的管理页面,然后开启下面的服务就好了,之后的话,会显示密钥,这个请记下来。
相关工具类
为了提高开发效率,我这里准备了几个工具类。用于方便后续的操作。
验证码工具类
这个工具类就是单纯用来产生验证码的。
public class CodeUtils { public static void main(String[] args) { String s = creatCode(4); System.out.println("随机验证码为:" + s); } //定义一个方法返回一个随机验证码 public static String creatCode(int n) { String code = ""; Random r = new Random(); //2.在方法内部使用for循环生成指定位数的随机字符,并连接起来 for (int i = 0; i <= n; i++) { //生成一个随机字符:大写 ,小写 ,数字(0 1 2) int type = r.nextInt(3); switch (type) { case 0: char ch = (char) (r.nextInt(26) + 65); code += ch; break; case 1: char ch1 = (char) (r.nextInt(26) + 97); code += ch1; break; case 2: code += r.nextInt(10); break; } } return code; }}
日期工具类
这个主要是对日期进行处理的,可以很方便得到YY-MM-DD HH:MM:SS 的日期。
public class DateUtils { /** * 获得当前日期 yyyy-MM-dd HH:mm:ss * */ public static String getCurrentTime() { // 小写的hh取得12小时,大写的HH取的是24小时 SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); Date date = new Date(); return df.format(date); } /** * 获取系统当前时间戳 * */ public static String getSystemTime() { String current = String.valueOf(System.currentTimeMillis()); return current; } /** * 获取当前日期 yy-MM-dd */ public static String getDateByString() { Date date = new Date(); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); return sdf.format(date); } /** * 得到两个时间差 格式yyyy-MM-dd HH:mm:ss */ public static long dateSubtraction(String start, String end) { SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); try { Date date1 = df.parse(start); Date date2 = df.parse(end); return date2.getTime() - date1.getTime(); } catch (ParseException e) { e.printStackTrace(); return 0; } } /** * 得到两个时间差 * * @param start 开始时间 * @param end 结束时间 * @return */ public static long dateTogether(Date start, Date end) { return end.getTime() - start.getTime(); } /** * 转化long值的日期为yyyy-MM-dd HH:mm:ss.SSS格式的日期 * * @param millSec 日期long值 5270400000 * @return 日期,以yyyy-MM-dd HH:mm:ss.SSS格式输出 1970-03-03 08:00:00.000 */ public static String transferLongToDate(String millSec) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); Date date = new Date(Long.parseLong(millSec)); return sdf.format(date); } /** * 获得当前日期 yyyy-MM-dd HH:mm:ss * * @return */ public static String getOkDate(String date) { try { if (StringUtils.isEmpty(date)) { return null; } Date date1 = new SimpleDateFormat("EEE MMM dd HH:mm:ss Z yyyy", Locale.ENGLISH).parse(date); //格式化 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return sdf.format(date1); } catch (Exception e) { e.printStackTrace(); } return null; } /** * 获取当前日期是一个星期的第几天 */ public static int getDayOfWeek() { Calendar cal = Calendar.getInstance(); cal.setTime(new Date()); return cal.get(Calendar.DAY_OF_WEEK) - 1; } /** * 判断当前时间是否在[startTime, endTime]区间,注意时间格式要一致 * @param nowTime 当前时间 * @param dateSection 时间区间 yy-mm-dd,yy-mm-dd */ public static boolean isEffectiveDate(Date nowTime, String dateSection) { try { String[] times = dateSection.split(","); String format = "yyyy-MM-dd"; Date startTime = new SimpleDateFormat(format).parse(times[0]); Date endTime = new SimpleDateFormat(format).parse(times[1]); if (nowTime.getTime() == startTime.getTime() || nowTime.getTime() == endTime.getTime()) { return true; } Calendar date = Calendar.getInstance(); date.setTime(nowTime); Calendar begin = Calendar.getInstance(); begin.setTime(startTime); Calendar end = Calendar.getInstance(); end.setTime(endTime); if (isSameDay(date, begin) || isSameDay(date, end)) { return true; } if (date.after(begin) && date.before(end)) { return true; } else { return false; } } catch (Exception e) { e.printStackTrace(); return false; } } public static boolean isSameDay(Calendar cal1, Calendar cal2) { if (cal1 != null && cal2 != null) { return cal1.get(0) == cal2.get(0) && cal1.get(1) == cal2.get(1) && cal1.get(6) == cal2.get(6); } else { throw new IllegalArgumentException("The date must not be null"); } } public static long getTimeByDate(String time) { SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); try { Date date = format.parse(time); //日期转时间戳(毫秒) return date.getTime(); } catch (Exception e) { e.printStackTrace(); return 0; } } /** * 获取当前小时 :2020-10-3 17 */ public static String getCurrentHour() { GregorianCalendar calendar = new GregorianCalendar(); int hour = calendar.get(Calendar.HOUR_OF_DAY); if (hour < 10) { return DateUtils.getCurrentTime() + " 0" + hour; } return DateUtils.getDateByString() + " " + hour; } /** * 获取当前时间一个小时前 */ public static String getCurrentHourBefore() { GregorianCalendar calendar = new GregorianCalendar(); int hour = calendar.get(Calendar.HOUR_OF_DAY); if (hour > 0) { hour = calendar.get(Calendar.HOUR_OF_DAY) - 1; if (hour < 10) { return DateUtils.getDateByString() + " 0" + hour; } return DateUtils.getDateByString() + " " + hour; } //获取当前日期前一天 return DateUtils.getBeforeDay() + " " + 23; } /** * 获取当前日期前一天 */ public static String getBeforeDay() { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); Date date = new Date(); Calendar calendar = Calendar.getInstance(); calendar.setTime(date); calendar.add(Calendar.DAY_OF_MONTH, -1); date = calendar.getTime(); return sdf.format(date); } /** * 获取最近七天 */ public static String getServen() { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); Calendar c = Calendar.getInstance(); c.add(Calendar.DATE, -7); Date monday = c.getTime(); String preMonday = sdf.format(monday); return preMonday; } /** * 获取最近一个月 */ public static String getOneMonth() { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); Calendar c = Calendar.getInstance(); c.add(Calendar.MONTH, -1); Date monday = c.getTime(); String preMonday = sdf.format(monday); return preMonday; } /** * 获取最近三个月 */ public static String getThreeMonth() { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); Calendar c = Calendar.getInstance(); c.add(Calendar.MONTH, -3); Date monday = c.getTime(); String preMonday = sdf.format(monday); return preMonday; } /** * 获取最近一年 */ public static String getOneYear() { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); Calendar c = Calendar.getInstance(); c.add(Calendar.YEAR, -1); Date start = c.getTime(); String startDay = sdf.format(start); return startDay; } private static int month = Calendar.getInstance().get(Calendar.MONTH) + 1; /** * 获取今年月份数据 * 说明 有的需求前端需要根据月份查询每月数据,此时后台给前端返回今年共有多少月份 * * @return [1, 2, 3, 4, 5, 6, 7, 8] */ public static List getMonthList(){ List list = new ArrayList(); for (int i = 1; i <= month; i++) { list.add(i); } return list; } /** * 返回当前年度季度list * 本年度截止目前共三个季度,然后根据1,2,3分别查询相关起止时间 * @return [1, 2, 3] */ public static List getQuartList(){ int quart = month / 3 + 1; List list = new ArrayList(); for (int i = 1; i <= quart; i++) { list.add(i); } return list; } public static void main(String[] args) { System.out.println(DateUtils.getQuartList()); }}
Redis 工具类
public class RedisUtils { @Autowired private RedisTemplate<String,Object> redisTemplate; /** * 指定缓存失效时间 * @param key 键 * @param time 时间(秒) * @return */ public boolean expire(String key, long time) { if (time > 0) { redisTemplate.expire(key, time, TimeUnit.SECONDS); return true; } else { throw new RuntimeException("超时时间小于0"); } } /** * 根据key 获取过期时间 * @param key 键 不能为null * @return 时间(秒) 返回0代表为永久有效 */ public long getExpire(String key) { return redisTemplate.getExpire(key, TimeUnit.SECONDS); } /** * 根据key 获取过期时间 * @param key 键 不能为null * @param tiemtype 时间类型 * @return 时间(秒) 返回0代表为永久有效 */ public long getExpire(String key,TimeUnit tiemtype) { return redisTemplate.getExpire(key, tiemtype); } /** * 判断key是否存在 * @param key 键 * @return true 存在 false不存在 */ public boolean hasKey(String key) { return Boolean.TRUE.equals(redisTemplate.hasKey(key)); } /** * 删除缓存 * @param key 可以传一个值 或多个 */ @SuppressWarnings("unchecked") public void del(String... key) { if (key != null && key.length > 0) { if (key.length == 1) { redisTemplate.delete(key[0]); } else { redisTemplate.delete(CollectionUtils.arrayToList(key)); } } }// ============================String============================= /** * 普通缓存获取 * @param key 键 * @return 值 */ public Object get(String key) { return key == null " />
实体
之后是我们前后端分类的实体类了。专门再提取出单独的Entity而不是直接使用数据库的原因很简单,首先提交的一些信息和那些实体类有些对不上,其次,我的实体类是按照数据库的字段来的,如果直接这样搞的话,很容易猜到我数据库的字段。所以我单独搞了一个专门和前端交互的Entity,同时使用JSR303校验美滋滋。
这些实体我是按照功能来划分的。
@Data@AllArgsConstructor@NoArgsConstructorpublic class LoginEntity { private String username; @NotEmpty(message = "用户密码不能为空") @Length(min = 6,max = 18,message="密码必须是6-18位") private String password;}
这个EmailCode主要是后面对Email的验证码进行处理的,这个是存在Redis里面的将来。
@Data@NoArgsConstructor@AllArgsConstructorpublic class EmailCodeEntity { private String emailCode; private String username; private String email; private int times;}
@Data@AllArgsConstructor@NoArgsConstructorpublic class GetEmailCodeEntity { @NotNull(message = "用户邮箱不能为空") @Email(message = "邮箱格式错位") private String email; @NotNull(message = "用户账号不能为空") private String username; @NotNull(message = "用户密码不能为空") @Size(min=6, max=15,message="密码长度必须在 6 ~ 15 字符之间!") @Pattern(regexp="^[a-zA-Z0-9|_]+$",message="密码必须由字母、数字、下划线组成!") private String password; @NotNull(message = "用户昵称不能为空") private String nickname;}
@Data@AllArgsConstructor@NoArgsConstructorpublic class RegisterEntity {// @Pattern(regexp = "^[1]([3-9])[0-9]{9}$",message = "手机号格式不正确") private String phone; @NotEmpty(message = "用户昵称不能为空") private String nickname; @NotEmpty(message = "用户账号不能为空") private String username; @NotEmpty(message = "用户密码不能为空") @Length(min = 6,max = 18,message="密码必须是6-18位") private String password; @NotEmpty(message = "用户邮箱不能为空") @Email(message = "邮箱格式错位") private String email; @NotEmpty(message = "邮箱验证码不能为空") private String emailCode;}
信息枚举类
同时的话,为了统一方便管理,这里专门做了一个枚举类。
public enum BizCodeEnum { UNKNOW_EXCEPTION(10000,"系统未知异常"), VAILD_EXCEPTION(10001,"参数格式校验失败"), HAS_USERNAME(10002,"已存在该用户"), OVER_REQUESTS(10003,"访问频次过多"), OVER_TIME(10004,"操作超时"), BAD_DOING(10005,"疑似恶意操作"), BAD_EMAILCODE_VERIFY(10007,"邮箱验证码错误"), REPARATION_GO(10008,"请重新操作"), NO_SUCHUSER(10009,"该用户不存在"), BAD_PUTDATA(10010,"信息提交错误,请重新检查"), SUCCESSFUL(200,"successful"); private int code; private String msg; BizCodeEnum(int code,String msg){ this.code = code; this.msg = msg; } public int getCode() { return code; } public String getMsg() { return msg; }}
这个是用来定义一些异常操作的。
当然还有我们还有R返回类。
public class R extends HashMap<String, Object> {private static final long serialVersionUID = 1L;public R() {put("code", 0);put("msg", "success");}public static R error() {return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, "未知异常,请联系管理员");}public static R error(String msg) {return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, msg);}public static R error(int code, String msg) {R r = new R();r.put("code", code);r.put("msg", msg);return r;}public static R ok(String msg) {R r = new R();r.put("msg", msg);return r;}public static R ok(Map<String, Object> map) {R r = new R();r.putAll(map);return r;}public static R ok() {return new R();}public R put(String key, Object value) {super.put(key, value);return this;}}
统一异常处理
之后是我们的异常处理了,这个直接交给切片去做。
这里的话复杂管理整个的Controller的异常
@Slf4j@RestControllerAdvice(basePackages = "com.huterox.whitehole.whiteholeuser.controller")public class UserExceptionControllerAdvice { @ExceptionHandler(value= MethodArgumentNotValidException.class) public R handleVaildException(MethodArgumentNotValidException e){ log.error("数据校验出现问题{},异常类型:{}",e.getMessage(),e.getClass()); BindingResult bindingResult = e.getBindingResult(); Map<String,String> errorMap = new HashMap<>(); bindingResult.getFieldErrors().forEach((fieldError)->{ errorMap.put(fieldError.getField(),fieldError.getDefaultMessage()); }); return R.error(BizCodeEnum.VAILD_EXCEPTION.getCode(),BizCodeEnum.VAILD_EXCEPTION.getMsg()).put("data",errorMap); } @ExceptionHandler(value = Throwable.class) public R handleException(Throwable throwable){ log.error("错误:",throwable); return R.error(BizCodeEnum.UNKNOW_EXCEPTION.getCode(),BizCodeEnum.UNKNOW_EXCEPTION.getMsg()); }}
登录流程
我们先从最简单的开始讲起吧。因为这个流程是最好搞的。
这个就是最简单的流程。
前端
这个前端没啥,主要就是使用axios发生请求。
this.axios({ url: "/user/user/login", method: 'post', data:{ "username":this.formLogin.username, "password":this.formLogin.password } }).then((res)=>{ res = res.data if (res.code===10001){ alert("请将对应信息填写完整!") }else if(res.code===0){ alert("登录成功") sessionStorage.setItem("loginToken",res.loginToken) this.$router.push("/userinfo") }else { this.$message.error(res.msg); } })
后端
首先,会先通过我们的校验,通过之后触发我们的流程。
我们这一块有几个点要做
密码比对
我们无法解密,所以我们只能比对,这里就用到了先前封装好的工具。
SecurityUtils.matchesPassword(password,User.getPassword())
防刷
由于我们每次在进行用户登录的时候都是需要查询数据库的,并且每个人访问的时候,请求的数据都不一样,所以很难存到缓存里面,因为可能几天就一次,除非是永久存储,但是这个内存消耗就太大了。所以只能直接查数据库,所以这里的话就可能存在恶意刷接口,导致mysql瘫痪的情况。所以需要做防刷,限制请求频次,最好的方案就是在redis里面记录一下。
只有访问接口,我们就这样
redisUtils.set(RedisTransKey.setLoginKey(username),1,20);
开始的时候在判断一下:
if(redisUtils.hasKey(RedisTransKey.getLoginKey(username))){ return R.error(BizCodeEnum.OVER_REQUESTS.getCode(),BizCodeEnum.OVER_REQUESTS.getMsg()); }
20s的话,可能太久了可以适当减少一点,但是如果是密码输错了的话可能很快就修改好了。当然这样做还是有漏洞的,我们只是根据这个username来的,实际上脚本换一个username就好了,只要随机生成username我们就一样不行,那么这个时候的话就要锁IP了,这个也有个问题,那就是有些地方是公共IP,也就是很多人共用一个IP,那就尴尬了,而且还有就是这个要做的话应该在网关去做,这样会更好一点,或者是拦截器去做。所以这里我就不做了,原理是一样的。
完整代码
public R Login(LoginEntity entity) { String username = entity.getUsername(); String password = entity.getPassword(); password=password.replaceAll(" ",""); if(redisUtils.hasKey(RedisTransKey.getLoginKey(username))){ return R.error(BizCodeEnum.OVER_REQUESTS.getCode(),BizCodeEnum.OVER_REQUESTS.getMsg()); } redisUtils.set(RedisTransKey.setLoginKey(username),1,20); UserEntity User = userService.getOne( new QueryWrapper<UserEntity>().eq("username", username) ); if(User!=null){ if(SecurityUtils.matchesPassword(password,User.getPassword())){ //登录成功,签发token String token = JwtTokenUtil.generateToken(User); redisUtils.set(RedisTransKey.setTokenKey(username),token,7, TimeUnit.DAYS); return R.ok(BizCodeEnum.SUCCESSFUL.getMsg()).put("loginToken",token); }else { return R.error(BizCodeEnum.BAD_PUTDATA.getCode(),BizCodeEnum.BAD_PUTDATA.getMsg()); } }else { return R.error(BizCodeEnum.NO_SUCHUSER.getCode(),BizCodeEnum.NO_SUCHUSER.getMsg()); } }
注册流程
前端
咱们这个前端也没啥,就是两个。
getEmailCode () { const that = this if (this.formRegister.email === '') { this.$message.error('请先输入邮箱再点击获取验证码') } else { let flag=true; let regEmail = /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/ if (!regEmail.test(this.formRegister.email)) { this.$message({showClose: true, message: '请输入格式正确有效的邮箱号!', type: 'error'}) flag=false; } else if(!this.formRegister.username){ this.$message.error('请填写账号'); this.refreshCode(); flag=false; } else if(!this.formRegister.password){ this.$message.error('请填写密码'); this.refreshCode(); flag=false; }else if(!this.formRegister.nickname){ this.$message.error('请填写用户昵称'); this.refreshCode(); flag=false; } if(flag){ // 这部分是发送邮箱验证码的玩意 this.axios({ url: "/user/user/emailcode", method: 'post', data:{ "email":this.formRegister.email, "username":this.formRegister.username, "password":this.formRegister.password, "nickname":this.formRegister.nickname, } }).then((res)=>{ res = res.data; if (res.code===10001){ alert("请将对应信息填写完整!") }else if(res.code===0){ alert("邮箱验证码发送成功,请及时查看,10分钟有效") }else { this.$message.error(res.msg); } }); //倒计时 if (!this.timer) { this.show = false this.timer = setInterval(() => { if (this.count > 0 && this.count <= this.TIME_COUNT) { this.count-- } else { this.show = true clearInterval(this.timer) this.timer = null } }, 1000) } } } },
还有这个:
submitForm(){ let flag = true; if (this.formRegister.code.toLowerCase() !== this.identifyCode.toLowerCase()) { this.$message.error('请填写正确验证码'); this.refreshCode(); flag=false; } else if(!this.formRegister.emailCode){ this.$message.error('请填写邮箱验证码'); this.refreshCode(); flag=false; } else if(!this.formRegister.email){ this.$message.error('已填写邮箱请勿删除或修改邮箱,恶意操作将在120分钟内禁止注册!'); this.refreshCode(); flag=false; } if(flag){ //这边后面做一个提交,提交对于消息 this.axios({ url: "/user/user/register", method: 'post', data:{ "nickname": this.formRegister.nickname, "phone": this.formRegister.phone, "username": this.formRegister.username, "password": this.formRegister.password, "email":this.formRegister.email, "emailCode":this.formRegister.emailCode } }).then((res)=>{ res = res.data; if (res.code===10001){ alert("请将对应信息填写完整!") }else if(res.code===0){ alert("注册成功") this.goLogin(); }else { this.$message.error(res.msg); } }); } },
后端
之后是我们的注册,注册也是分为两个部分的。
我们的流程如下:
邮箱服务
那么这个时候咱们就需要使用到咱们的邮箱服务了。首先是导入相关依赖。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> </dependency>
然后填写你的配置
spring: #邮箱基本配置 mail:# 配置在limit_time内,用户可以发送limit次验证码 limit: 2 这个是我额外的配置,结合邮箱服务用的 limitTime: 10 这个是我额外的配置 #配置smtp服务主机地址 # qq邮箱为smtp.qq.com 端口号465或587 # sina smtp.sina.cn # aliyun smtp.aliyun.com # 163 smtp.163.com 端口号465或994 host: smtp.qq.com #发送者邮箱 username: xxxxx@foxmail.com #配置密码,注意不是真正的密码,而是刚刚申请到的授权码 password: vmtwmkq6564651asd #端口号465或587 port: 587 #默认的邮件编码为UTF-8 default-encoding: UTF-8 #其他参数 properties: mail: #配置SSL 加密工厂 smtp: ssl: #本地测试,先放开ssl enable: false required: false #开启debug模式,这样邮件发送过程的日志会在控制台打印出来,方便排查错误 debug: true socketFactory: class: javax.net.ssl.SSLSocketFactory
之后就是咱们的服务了
public class MaliServiceImpl implements MailService { /** * 注入邮件工具类 */ @Autowired private JavaMailSenderImpl javaMailSender; @Value("${spring.mail.username}") private String sendMailer; /** * 检测邮件信息类 * @param to * @param subject * @param text */ private void checkMail(String to,String subject,String text){ if(StringUtils.isEmpty(to)){ throw new RuntimeException("邮件收信人不能为空"); } if(StringUtils.isEmpty(subject)){ throw new RuntimeException("邮件主题不能为空"); } if(StringUtils.isEmpty(text)){ throw new RuntimeException("邮件内容不能为空"); } } /** * 发送纯文本邮件 * @param to * @param subject * @param text */ @Override public void sendTextMailMessage(String to,String subject,String text){ try { //true 代表支持复杂的类型 MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(javaMailSender.createMimeMessage(),true); //邮件发信人 mimeMessageHelper.setFrom(sendMailer); //邮件收信人 1或多个 mimeMessageHelper.setTo(to.split(",")); //邮件主题 mimeMessageHelper.setSubject(subject); //邮件内容 mimeMessageHelper.setText(text); //邮件发送时间 mimeMessageHelper.setSentDate(new Date()); //发送邮件 javaMailSender.send(mimeMessageHelper.getMimeMessage()); System.out.println("发送邮件成功:"+sendMailer+"->"+to); } catch (MessagingException e) { e.printStackTrace(); System.out.println("发送邮件失败:"+e.getMessage()); } } /** * 发送html邮件 * @param to * @param subject * @param content */ @Override public void sendHtmlMailMessage(String to,String subject,String content){ content="\n" + "\n" + "\n" + "\n" + "邮件 \n" + "\n" + "\n" + "\t这是一封HTML邮件!
\n" + "\n" + ""; try { //true 代表支持复杂的类型 MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(javaMailSender.createMimeMessage(),true); //邮件发信人 mimeMessageHelper.setFrom(sendMailer); //邮件收信人 1或多个 mimeMessageHelper.setTo(to.split(",")); //邮件主题 mimeMessageHelper.setSubject(subject); //邮件内容 true 代表支持html mimeMessageHelper.setText(content,true); //邮件发送时间 mimeMessageHelper.setSentDate(new Date()); //发送邮件 javaMailSender.send(mimeMessageHelper.getMimeMessage()); System.out.println("发送邮件成功:"+sendMailer+"->"+to); } catch (MessagingException e) { e.printStackTrace(); System.out.println("发送邮件失败:"+e.getMessage()); } } /** * 发送带附件的邮件 * @param to 邮件收信人 * @param subject 邮件主题 * @param content 邮件内容 * @param filePath 附件路径 */ @Override public void sendAttachmentMailMessage(String to,String subject,String content,String filePath){ try { //true 代表支持复杂的类型 MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(javaMailSender.createMimeMessage(),true); //邮件发信人 mimeMessageHelper.setFrom(sendMailer); //邮件收信人 1或多个 mimeMessageHelper.setTo(to.split(",")); //邮件主题 mimeMessageHelper.setSubject(subject); //邮件内容 true 代表支持html mimeMessageHelper.setText(content,true); //邮件发送时间 mimeMessageHelper.setSentDate(new Date()); //添加邮件附件 FileSystemResource file = new FileSystemResource(new File(filePath)); String fileName = file.getFilename(); mimeMessageHelper.addAttachment(fileName, file); //发送邮件 javaMailSender.send(mimeMessageHelper.getMimeMessage()); System.out.println("发送邮件成功:"+sendMailer+"->"+to); } catch (MessagingException e) { e.printStackTrace(); System.out.println("发送邮件失败:"+e.getMessage()); } } /** * 发送邮箱验证码 * @param to * @param code */ @Override public void sendCodeMailMessage(String to, String code) { String subject = "WhiteHole邮箱验证码"; String text = "验证码10分钟内有效:"+code; sendTextMailMessage(to,subject,text); }}
支持发送多种格式的邮箱,不过咱们的验证码只需要文本的就够了,但是保不齐后面还有别的。比如我们可以搞一个更加复杂的一点的邮箱链接验证,这个时候可能需要加点东西了。
邮箱验证码
那么这个时候就是咱们的邮箱验证码服务了。
防刷
同样的我们最怕的就是防刷,前端我们是有60s倒数计时的,我们的逻辑是这样的,前端60s后才能去再次点击发送邮箱,一个邮箱的验证码的有效期是10分钟,如果用户填写错了邮箱,那么60s倒计时后,可以在前端再次点击发送邮箱,但是在10分钟内我们只允许发送2次。前端的只是用来糊弄不太懂的用户的,后端是为了校验各种恶心的脚本的。啥都不怕就怕脚本乱搞。所以的话,咱们这边就是这样设计的。
原理一样:
判断一下
if (redisUtils.hasKey(RedisTransKey.getEmailKey(username)))
成功后
这里多了点东西,主要是还需要计数。
验证码
这个我得说一下的就是,那个验证码的话,我是在前端做的,每次还是来骗骗不太“懂”的用户的。这里不是后端做,主要是因为,第一有了邮箱验证不需要再鉴别一次,我们在接口层就做了硬性指标只能访问多少次,不太再需要验证码防脚本了,之后也是降低服务端请求次数。
完整代码
public R emailCode(GetEmailCodeEntity entity) { String email = entity.getEmail(); String username = entity.getUsername(); //判断用户是不是恶意刷邮箱,在规定时间内进行的 if (redisUtils.hasKey(RedisTransKey.getEmailKey(username))) { Object o = redisUtils.get(RedisTransKey.getEmailKey(username)); EmailCodeEntity emailCodeEntity = JSON.parseObject(o.toString(), EmailCodeEntity.class); if (emailCodeEntity.getTimes() >= limit) { return R.error(BizCodeEnum.OVER_REQUESTS.getCode(), BizCodeEnum.OVER_REQUESTS.getMsg()); } else {// 这里就不去判断两次绑定的邮箱是不是一样的了,不排除第一次输入错了邮箱的情况 String emailCode = CodeUtils.creatCode(6); emailCodeEntity.setEmailCode(emailCode); emailCodeEntity.setTimes(emailCodeEntity.getTimes() + 1); long overTime = redisUtils.getExpire(username, TimeUnit.MINUTES); redisUtils.set(RedisTransKey.setEmailKey(username), emailCodeEntity, overTime, TimeUnit.MINUTES ); mailService.sendCodeMailMessage(email, emailCodeEntity.getEmailCode()); } } else { UserEntity User = userService.getOne( new QueryWrapper<UserEntity>().eq("username", username) ); if (User != null) { return R.error(BizCodeEnum.HAS_USERNAME.getCode(), BizCodeEnum.HAS_USERNAME.getMsg()); } else { String emailCode = CodeUtils.creatCode(6); // 我们这里做一件事情,那就是最多允许用户在10分钟内发送2次的邮箱验证 // 60s倒计时后用户可以再发送验证码,但是间隔在10分钟内只能再发送1次 EmailCodeEntity emailCodeEntity = new EmailCodeEntity( emailCode, username,email,1 ); redisUtils.set(RedisTransKey.setEmailKey(username), emailCodeEntity, limitTime, TimeUnit.MINUTES ); mailService.sendCodeMailMessage(email, emailCodeEntity.getEmailCode()); } } return R.ok(BizCodeEnum.SUCCESSFUL.getMsg()); }
注册
这个也是类似的,我们拿到这个验证码后,去校验就好了。
那么注册这里的话就不需要做防刷了,或者说已经做好了因为有邮箱验证码间接做好了。因为如果没有验证码,直接校验过不去,如果有验证码在Redis当中的对不到一样没法后序操作。其实这里的防刷都是消耗了服务器资源的,只是消耗了多少的问题,因为咱们这边都是拿Redis先顶住的。
public R register(RegisterEntity entity) { String username = entity.getUsername(); username = username.replaceAll(" ",""); String emailCode = entity.getEmailCode();// 先检验一下验证码,对不对,邮箱有没有被更改 if(redisUtils.hasKey(RedisTransKey.getEmailKey(username))){ Object o = redisUtils.get(RedisTransKey.getEmailKey(username)); EmailCodeEntity emailCodeEntity = JSON.parseObject(o.toString(), EmailCodeEntity.class); if(username.equals(emailCodeEntity.getUsername())){ if(emailCode.equals(emailCodeEntity.getEmailCode())){ //开始封装用户并进行存储 UserEntity userEntity = new UserEntity(); userEntity.setEmail(entity.getEmail()); userEntity.setNickname(entity.getNickname()); userEntity.setPassword(SecurityUtils.encodePassword( entity.getPassword()).replaceAll(" ","") );// 用户状态,1-正常 2-警告 3-封禁 userEntity.setStatus(1); userEntity.setCreatTime(DateUtils.getCurrentTime()); userEntity.setUsername(username); userEntity.setPhone(entity.getPhone()); userService.save(userEntity); redisUtils.del(RedisTransKey.getEmailKey(username)); }else { return R.error(BizCodeEnum.BAD_EMAILCODE_VERIFY.getCode(),BizCodeEnum.BAD_EMAILCODE_VERIFY.getMsg()); } }else { return R.error(BizCodeEnum.BAD_DOING.getCode(),BizCodeEnum.BAD_DOING.getMsg()); } }else { return R.error(BizCodeEnum.OVER_TIME.getCode(),BizCodeEnum.OVER_TIME.getMsg()); } return R.ok(BizCodeEnum.SUCCESSFUL.getMsg()); }
总结
到此的话,一个简单的用户登录注册就做好了,那么接下来的话就是咱们的Auto2.0了,这里咱们要实现的就是这两个功能