0


SpringBoot轻轻松松搞定用户邮箱登录注册

文章目录

前言

ok,我又来水博文了,今天的内容很简单,就是咱们的这个用户登录注册,加上邮箱验证,非常简单,为我们接下来的Auto2.0做好前置工作。因为这个没做好,那个也做不好。本文的内容可能比较多,但是都很简单。

效果演示

注册效果

ok,多说无益,我们先来看看完成后的效果咋样。
注册首页:
在这里插入图片描述

在这里插入图片描述
这块的话,咱们这边发送邮箱验证码之后,前端这边的验证码还会重新刷新一次,反正是在10分钟内完成操作。
在这里插入图片描述

登录

在这里插入图片描述

环境准备

邮箱准备

首先我们需要使用到邮箱服务,所以的话我们需要去申请到这个邮箱发送的权限,这个也简单,我们以QQ邮箱为例子,打开这个QQ邮箱的管理页面,然后开启下面的服务就好了,之后的话,会显示密钥,这个请记下来。
在这里插入图片描述

相关工具类

为了提高开发效率,我这里准备了几个工具类。用于方便后续的操作。
在这里插入图片描述

验证码工具类

这个工具类就是单纯用来产生验证码的。

publicclassCodeUtils{publicstaticvoidmain(String[] args){String s =creatCode(4);System.out.println("随机验证码为:"+ s);}//定义一个方法返回一个随机验证码publicstaticStringcreatCode(int n){String code ="";Random r =newRandom();//2.在方法内部使用for循环生成指定位数的随机字符,并连接起来for(int i =0; i <= n; i++){//生成一个随机字符:大写 ,小写 ,数字(0  1  2)int type = r.nextInt(3);switch(type){case0:char ch =(char)(r.nextInt(26)+65);
                    code += ch;break;case1:char ch1 =(char)(r.nextInt(26)+97);
                    code += ch1;break;case2:
                    code +=  r.nextInt(10);break;}}return code;}}

日期工具类

这个主要是对日期进行处理的,可以很方便得到YY-MM-DD HH:MM:SS 的日期。

publicclassDateUtils{/**
     * 获得当前日期 yyyy-MM-dd HH:mm:ss
     *
     */publicstaticStringgetCurrentTime(){// 小写的hh取得12小时,大写的HH取的是24小时SimpleDateFormat df =newSimpleDateFormat("yyyy-MM-dd HH:mm:ss");Date date =newDate();return df.format(date);}/**
     * 获取系统当前时间戳
     *
     */publicstaticStringgetSystemTime(){String current =String.valueOf(System.currentTimeMillis());return current;}/**
     * 获取当前日期 yy-MM-dd
     */publicstaticStringgetDateByString(){Date date =newDate();SimpleDateFormat sdf =newSimpleDateFormat("yyyy-MM-dd");return sdf.format(date);}/**
     * 得到两个时间差  格式yyyy-MM-dd HH:mm:ss
     
     */publicstaticlongdateSubtraction(String start,String end){SimpleDateFormat df =newSimpleDateFormat("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();return0;}}/**
     * 得到两个时间差
     *
     * @param start 开始时间
     * @param end 结束时间
     * @return
     */publicstaticlongdateTogether(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
     */publicstaticStringtransferLongToDate(String millSec){SimpleDateFormat sdf =newSimpleDateFormat("yyyy-MM-dd  HH:mm:ss.SSS");Date date =newDate(Long.parseLong(millSec));return sdf.format(date);}/**
     * 获得当前日期 yyyy-MM-dd HH:mm:ss
     *
     * @return
     */publicstaticStringgetOkDate(String date){try{if(StringUtils.isEmpty(date)){returnnull;}Date date1 =newSimpleDateFormat("EEE MMM dd HH:mm:ss Z yyyy",Locale.ENGLISH).parse(date);//格式化SimpleDateFormat sdf =newSimpleDateFormat("yyyy-MM-dd HH:mm:ss");return sdf.format(date1);}catch(Exception e){
            e.printStackTrace();}returnnull;}/**
     * 获取当前日期是一个星期的第几天
    
     */publicstaticintgetDayOfWeek(){Calendar cal =Calendar.getInstance();
        cal.setTime(newDate());return cal.get(Calendar.DAY_OF_WEEK)-1;}/**
     * 判断当前时间是否在[startTime, endTime]区间,注意时间格式要一致
     * @param nowTime     当前时间
     * @param dateSection 时间区间   yy-mm-dd,yy-mm-dd
     */publicstaticbooleanisEffectiveDate(Date nowTime,String dateSection){try{String[] times = dateSection.split(",");String format ="yyyy-MM-dd";Date startTime =newSimpleDateFormat(format).parse(times[0]);Date endTime =newSimpleDateFormat(format).parse(times[1]);if(nowTime.getTime()== startTime.getTime()|| nowTime.getTime()== endTime.getTime()){returntrue;}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)){returntrue;}if(date.after(begin)&& date.before(end)){returntrue;}else{returnfalse;}}catch(Exception e){
            e.printStackTrace();returnfalse;}}publicstaticbooleanisSameDay(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{thrownewIllegalArgumentException("The date must not be null");}}publicstaticlonggetTimeByDate(String time){SimpleDateFormat format =newSimpleDateFormat("yyyy-MM-dd HH:mm:ss");try{Date date = format.parse(time);//日期转时间戳(毫秒)return date.getTime();}catch(Exception e){
            e.printStackTrace();return0;}}/**
     * 获取当前小时 :2020-10-3 17
    
     */publicstaticStringgetCurrentHour(){GregorianCalendar calendar =newGregorianCalendar();int hour = calendar.get(Calendar.HOUR_OF_DAY);if(hour <10){returnDateUtils.getCurrentTime()+" 0"+ hour;}returnDateUtils.getDateByString()+" "+ hour;}/**
     * 获取当前时间一个小时前
     */publicstaticStringgetCurrentHourBefore(){GregorianCalendar calendar =newGregorianCalendar();int hour = calendar.get(Calendar.HOUR_OF_DAY);if(hour >0){
            hour = calendar.get(Calendar.HOUR_OF_DAY)-1;if(hour <10){returnDateUtils.getDateByString()+" 0"+ hour;}returnDateUtils.getDateByString()+" "+ hour;}//获取当前日期前一天returnDateUtils.getBeforeDay()+" "+23;}/**
     * 获取当前日期前一天

     */publicstaticStringgetBeforeDay(){SimpleDateFormat sdf =newSimpleDateFormat("yyyy-MM-dd");Date date =newDate();Calendar calendar =Calendar.getInstance();
        calendar.setTime(date);
        calendar.add(Calendar.DAY_OF_MONTH,-1);
        date = calendar.getTime();return sdf.format(date);}/**
     * 获取最近七天

     */publicstaticStringgetServen(){SimpleDateFormat sdf =newSimpleDateFormat("yyyy-MM-dd");Calendar c =Calendar.getInstance();

        c.add(Calendar.DATE,-7);Date monday = c.getTime();String preMonday = sdf.format(monday);return preMonday;}/**
     * 获取最近一个月

     */publicstaticStringgetOneMonth(){SimpleDateFormat sdf =newSimpleDateFormat("yyyy-MM-dd");Calendar c =Calendar.getInstance();

        c.add(Calendar.MONTH,-1);Date monday = c.getTime();String preMonday = sdf.format(monday);return preMonday;}/**
     * 获取最近三个月

     */publicstaticStringgetThreeMonth(){SimpleDateFormat sdf =newSimpleDateFormat("yyyy-MM-dd");Calendar c =Calendar.getInstance();

        c.add(Calendar.MONTH,-3);Date monday = c.getTime();String preMonday = sdf.format(monday);return preMonday;}/**
     * 获取最近一年

     */publicstaticStringgetOneYear(){SimpleDateFormat sdf =newSimpleDateFormat("yyyy-MM-dd");Calendar c =Calendar.getInstance();
        c.add(Calendar.YEAR,-1);Date start = c.getTime();String startDay = sdf.format(start);return startDay;}privatestaticint month =Calendar.getInstance().get(Calendar.MONTH)+1;/**
     * 获取今年月份数据
     * 说明 有的需求前端需要根据月份查询每月数据,此时后台给前端返回今年共有多少月份
     *
     * @return [1, 2, 3, 4, 5, 6, 7, 8]
     */publicstaticListgetMonthList(){List list =newArrayList();for(int i =1; i <= month; i++){
            list.add(i);}return list;}/**
     * 返回当前年度季度list
     * 本年度截止目前共三个季度,然后根据1,2,3分别查询相关起止时间
     * @return [1, 2, 3]
     */publicstaticListgetQuartList(){int quart = month /3+1;List list =newArrayList();for(int i =1; i <= quart; i++){
            list.add(i);}return list;}publicstaticvoidmain(String[] args){System.out.println(DateUtils.getQuartList());}}

Redis 工具类

publicclassRedisUtils{@AutowiredprivateRedisTemplate<String,Object> redisTemplate;/**
     *  指定缓存失效时间
     * @param key 键
     * @param time 时间(秒)
     * @return
     */publicbooleanexpire(String key,long time){if(time >0){
            redisTemplate.expire(key, time,TimeUnit.SECONDS);returntrue;}else{thrownewRuntimeException("超时时间小于0");}}/**
     * 根据key 获取过期时间
     * @param key 键 不能为null
     * @return 时间(秒) 返回0代表为永久有效
     */publiclonggetExpire(String key){return redisTemplate.getExpire(key,TimeUnit.SECONDS);}/**
     * 根据key 获取过期时间
     * @param key 键 不能为null
     * @param tiemtype 时间类型
     * @return 时间(秒) 返回0代表为永久有效
     */publiclonggetExpire(String key,TimeUnit tiemtype){return redisTemplate.getExpire(key, tiemtype);}/**
     *  判断key是否存在
     * @param key 键
     * @return true 存在 false不存在
     */publicbooleanhasKey(String key){returnBoolean.TRUE.equals(redisTemplate.hasKey(key));}/**
     *  删除缓存
     * @param key 可以传一个值 或多个
     */@SuppressWarnings("unchecked")publicvoiddel(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 值
     */publicObjectget(String key){return key ==null?null: redisTemplate.opsForValue().get(key);}/**
     * 普通缓存放入
     * @param key 键
     * @param value  值
     * @return true成功 false失败
     */publicbooleanset(String key,Object value){
        redisTemplate.opsForValue().set(key, value);returntrue;}/**
     *  普通缓存放入并设置时间
     * @param key 键
     * @param value 值
     * @param time time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
     * @return true成功 false 失败
     */publicbooleanset(String key,Object value,long time){if(time >0){
            redisTemplate.opsForValue().set(key, value, time,TimeUnit.SECONDS);}else{this.set(key, value);}returntrue;}/**
     *  普通缓存放入并设置时间
     * @param key 键
     * @param value 值
     * @param time time 时间类型自定义设定
     * @return true成功 false 失败
     */publicbooleanset(String key,Object value,long time,TimeUnit tiemtype){if(time >0){
            redisTemplate.opsForValue().set(key, value, time, tiemtype);}else{this.set(key, value);}returntrue;}/**
     *  递增
     * @param key 键
     * @param delta 要增加几(大于0)
     * @return
     */publiclongincr(String key,long delta){if(delta <0){thrownewRuntimeException("递增因子必须大于0");}return redisTemplate.opsForValue().increment(key, delta);}/**
     *  递减
     * @param key
     * @param delta 要减少几(大于0)
     * @return
     */publiclongdecr(String key,long delta){if(delta <0){thrownewRuntimeException("递减因子必须大于0");}return redisTemplate.opsForValue().increment(key,-delta);}// ================================Map=================================/**
     *  HashGet
     * @param key 键
     * @param item  项 不能为null
     * @return
     */publicObjecthget(String key,String item){return redisTemplate.opsForHash().get(key, item);}/**
     *  获取hashKey对应的所有键值
     * @param key 键
     * @return 对应的多个键值
     */publicMap<Object,Object>hmget(String key){return redisTemplate.opsForHash().entries(key);}/**
     * HashSet
     * @param key 键
     * @param map 对应多个键值
     * @return true 成功 false 失败
     */publicbooleanhmset(String key,Map<String,Object> map){
        redisTemplate.opsForHash().putAll(key, map);returntrue;}/**
     * HashSet 并设置时间
     * @param key 键
     * @param map 对应多个键值
     * @param time 时间(秒)
     * @return true成功 false失败
     */publicbooleanhmset(String key,Map<String,Object> map,long time){
        redisTemplate.opsForHash().putAll(key, map);if(time >0){expire(key, time);}returntrue;}/**
     * 向一张hash表中放入数据,如果不存在将创建
     * @param key 键
     * @param item 项
     * @param value 值
     * @return true 成功 false失败
     */publicbooleanhset(String key,String item,Object value){
        redisTemplate.opsForHash().put(key, item, value);returntrue;}/**
     * 向一张hash表中放入数据,如果不存在将创建
     * @param key 键
     * @param item 项
     * @param value 值
     * @param time 时间(秒)  注意:如果已存在的hash表有时间,这里将会替换原有的时间
     * @return true 成功 false失败
     */publicbooleanhset(String key,String item,Object value,long time){
        redisTemplate.opsForHash().put(key, item, value);if(time >0){expire(key, time);}returntrue;}/**
     * 删除hash表中的值
     * @param key 键 不能为null
     * @param item 项 可以使多个 不能为null
     */publicvoidhdel(String key,Object... item){
        redisTemplate.opsForHash().delete(key, item);}/**
     * 判断hash表中是否有该项的值
     * @param key 键 不能为null
     * @param item 项 不能为null
     * @return true 存在 false不存在
     */publicbooleanhHasKey(String key,String item){return redisTemplate.opsForHash().hasKey(key, item);}/**
     * hash递增 如果不存在,就会创建一个 并把新增后的值返回
     * @param key 键
     * @param item 项
     * @param by 要增加几(大于0)
     * @return
     */publicdoublehincr(String key,String item,double by){return redisTemplate.opsForHash().increment(key, item, by);}/**
     * hash递减
     * @param key 键
     * @param item 项
     * @param by 要减少记(小于0)
     * @return
     */publicdoublehdecr(String key,String item,double by){return redisTemplate.opsForHash().increment(key, item,-by);}// ============================set=============================/**
     * 根据key获取Set中的所有值
     * @param key 键
     * @return
     */publicSet<Object>sGet(String key){return redisTemplate.opsForSet().members(key);}/**
     * 根据value从一个set中查询,是否存在
     * @param key 键
     * @param value 值
     * @return true 存在 false不存在
     */publicbooleansHasKey(String key,Object value){return redisTemplate.opsForSet().isMember(key, value);}/**
     * 将数据放入set缓存
     * @param key 键
     * @param values 值 可以是多个
     * @return 成功个数
     */publiclongsSet(String key,Object... values){return redisTemplate.opsForSet().add(key, values);}/**
     * 将set数据放入缓存
     * @param key 键
     * @param time 时间(秒)
     * @param values 值 可以是多个
     * @return 成功个数
     */publiclongsSetAndTime(String key,long time,Object... values){finalLong count = redisTemplate.opsForSet().add(key, values);if(time >0)expire(key, time);return count;}/**
     * 获取set缓存的长度
     * @param key 键
     * @return
     */publiclongsGetSetSize(String key){return redisTemplate.opsForSet().size(key);}/**
     * 移除值为value的
     * @param key 键
     * @param values 值 可以是多个
     * @return 移除的个数
     */publiclongsetRemove(String key,Object... values){finalLong count = redisTemplate.opsForSet().remove(key, values);return count;}// ===============================list=================================/**
     * 获取list缓存的内容
     * @param key 键
     * @param start 开始
     * @param end 结束  0 到 -1代表所有值
     * @return
     */publicList<Object>lGet(String key,long start,long end){return redisTemplate.opsForList().range(key, start, end);}/**
     * 获取list缓存的长度
     * @param key 键
     * @return
     */publiclonglGetListSize(String key){return redisTemplate.opsForList().size(key);}/**
     * 通过索引 获取list中的值
     * @param key 键
     * @param index 索引  index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
     * @return
     */publicObjectlGetIndex(String key,long index){return redisTemplate.opsForList().index(key, index);}/**
     * 将list放入缓存
     * @param key 键
     * @param value 值
     * @return
     */publicbooleanlSet(String key,Object value){
        redisTemplate.opsForList().rightPush(key, value);returntrue;}/**
     * 将list放入缓存
     * @param key 键
     * @param value 值
     * @param time 时间(秒)
     * @return
     */publicbooleanlSet(String key,Object value,long time){
        redisTemplate.opsForList().rightPush(key, value);if(time >0){expire(key, time);}returntrue;}/**
     * 将list放入缓存
     * @param key 键
     * @param value 值
     * @return
     */publicbooleanlSetList(String key,List<Object> value){
        redisTemplate.opsForList().rightPushAll(key, value);returntrue;}/**
     * 将list放入缓存
     * @param key 键
     * @param value 值
     * @return
     */publicbooleanlSetList(String key,List<Object> value,long time){
        redisTemplate.opsForList().rightPushAll(key, value);if(time >0){expire(key, time);}returntrue;}/**
     * 根据索引修改list中的某条数据
     * @param key 键
     * @param index 索引
     * @param value 值
     * @return
     */publicbooleanlUpdateIndex(String key,long index,Object value){
        redisTemplate.opsForList().set(key, index, value);returntrue;}}

专门用来管理key的类

这个主要是对key做一个分组,一是美化,而是方便管理。

publicclassRedisTransKey{publicstaticfinalStringRedisNameSpace="user";publicstaticfinalStringRedisTokenName="token";publicstaticfinalStringRedisLoginName="login";publicstaticfinalStringRedisEmailCodeName="emailCode";publicstaticStringsetEmailKey(String key){returnRedisNameSpace+":"+RedisEmailCodeName+":"+key;}publicstaticStringsetRootKey(String key){returnRedisNameSpace+":"+key+":";}publicstaticStringsetTokenKey(String key){returnRedisNameSpace+':'+RedisTokenName+":"+key;}publicstaticStringsetLoginKey(String key){returnRedisNameSpace+':'+RedisLoginName+":"+key;}publicstaticStringgetEmailKey(String key){returnsetEmailKey(key);}publicstaticStringgetRootKey(String key){returnsetRootKey(key);}publicstaticStringgetTokenKey(String key){returnsetTokenKey(key);}publicstaticStringgetLoginKey(String key){returnsetLoginKey(key);}}

JWT的封装

这个主要是为了校验,产生token用的,值得一提的是,我们的token并不是放在Mysql里面的,而是放在了redis里面,主要是为了提高查询速度,减轻数据库压力,毕竟token这玩意是有时间限制的,而且对于校验修改的速度要求还挺高。

publicclassJwtTokenUtil{privatestaticString secret;privatestaticLong expiration;privatestaticMap<String,Object> header;static{
        secret="你的SRET";
        expiration =7*24*60*60*1000L;
        header=newHashMap<>();
        header.put("typ","jwt");}/**
     * 生成token令牌
     * @return 令token牌
     */publicstaticStringgenerateToken(UserEntity user){Map<String,Object> claims =newHashMap<>();
        claims.put("username", user.getUsername());
        claims.put("userid",user.getUserid());
        claims.put("created",newDate());returngenerateToken(claims);}/**
     * @param token 令牌
     * @return 用户名
     */publicstaticStringGetUserNameFromToken(String token){String username;try{Claims claims =getClaimsFromToken(token);
            username =(String) claims.get("username");}catch(Exception e){
            username =null;}return username;}publicstaticStringGetUserIDFromToken(String token){String username;try{Claims claims =getClaimsFromToken(token);
            username =(String) claims.get("userid");}catch(Exception e){
            username =null;}return username;}/**
     * 判断令牌是否过期
     * @param token 令牌
     * @return 是否过期
     */publicstaticBooleanisTokenExpired(String token){try{Claims claims =getClaimsFromToken(token);Date expiration = claims.getExpiration();return expiration.before(newDate());}catch(Exception e){returnfalse;}}/**
     * 刷新令牌
     *
     * @param token 原令牌
     * @return 新令牌
     */publicStringrefreshToken(String token){String refreshedToken;try{Claims claims =getClaimsFromToken(token);
            claims.put("created",newDate());
            refreshedToken =generateToken(claims);}catch(Exception e){
            refreshedToken =null;}return refreshedToken;}/**
     * 验证令牌
     * @return 是否有效
     */publicstaticBooleanvalidateToken(String token,UserEntity user){String username =GetUserNameFromToken(token);return(username.equals(user.getUsername())&&!isTokenExpired(token));}/**
     * 从claims生成令牌,如果看不懂就看谁调用它
     *
     * @param claims 数据声明
     * @return 令牌
     */privatestaticStringgenerateToken(Map<String,Object> claims){Date expirationDate =newDate(System.currentTimeMillis()+ expiration);returnJwts.builder().setHeader(header).setClaims(claims).setExpiration(expirationDate).signWith(SignatureAlgorithm.HS512, secret).compact();}/**
     * 从令牌中获取数据声明,如果看不懂就看谁调用它
     *
     * @param token 令牌
     * @return 数据声明
     */publicstaticClaimsgetClaimsFromToken(String token){Claims claims;try{
            claims =Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();}catch(Exception e){
            claims =null;}return claims;}}

密码加密比对

这个主要是用来加密和对比密码的,这里密码是解不了密的,因为用的md5压根不是“加密”算法,而是“特征”算法,计算你的密码的特征,既然只是拿到了“特征”那么显然不可能通过“特征”完整地还原一个东西,因为信息存在损失。

我们这块使用的是Spring提供的方案。

publicclassSecurityUtils{/**
     * 生成BCryptPasswordEncoder密码
     * @param password 密码
     * @return 加密字符串
     */publicstaticBCryptPasswordEncoder passwordEncoder =newBCryptPasswordEncoder();publicstaticStringencodePassword(String password){return passwordEncoder.encode(password);}/**
     * 判断密码是否相同
     * @param rawPassword 真实密码
     * @param encodedPassword 加密后字符
     * @return 结果
     */publicstaticbooleanmatchesPassword(String rawPassword,String encodedPassword){return passwordEncoder.matches(rawPassword, encodedPassword);}}

接口与实体

接口

之后是我们的接口和实体类,这个很重要,我们目前的这个玩意其实就三个接口。

register
login
emailCode

在这里插入图片描述
在这里插入图片描述

实体

之后是我们前后端分类的实体类了。专门再提取出单独的Entity而不是直接使用数据库的原因很简单,首先提交的一些信息和那些实体类有些对不上,其次,我的实体类是按照数据库的字段来的,如果直接这样搞的话,很容易猜到我数据库的字段。所以我单独搞了一个专门和前端交互的Entity,同时使用JSR303校验美滋滋。
在这里插入图片描述
这些实体我是按照功能来划分的。

@Data@AllArgsConstructor@NoArgsConstructorpublicclassLoginEntity{privateString username;@NotEmpty(message ="用户密码不能为空")@Length(min =6,max =18,message="密码必须是6-18位")privateString password;}

这个EmailCode主要是后面对Email的验证码进行处理的,这个是存在Redis里面的将来。

@Data@NoArgsConstructor@AllArgsConstructorpublicclassEmailCodeEntity{privateString emailCode;privateString username;privateString email;privateint times;}
@Data@AllArgsConstructor@NoArgsConstructorpublicclassGetEmailCodeEntity{@NotNull(message ="用户邮箱不能为空")@Email(message ="邮箱格式错位")privateString email;@NotNull(message ="用户账号不能为空")privateString username;@NotNull(message ="用户密码不能为空")@Size(min=6, max=15,message="密码长度必须在 6 ~ 15 字符之间!")@Pattern(regexp="^[a-zA-Z0-9|_]+$",message="密码必须由字母、数字、下划线组成!")privateString password;@NotNull(message ="用户昵称不能为空")privateString nickname;}
@Data@AllArgsConstructor@NoArgsConstructorpublicclassRegisterEntity{//    @Pattern(regexp = "^[1]([3-9])[0-9]{9}$",message = "手机号格式不正确")privateString phone;@NotEmpty(message ="用户昵称不能为空")privateString nickname;@NotEmpty(message ="用户账号不能为空")privateString username;@NotEmpty(message ="用户密码不能为空")@Length(min =6,max =18,message="密码必须是6-18位")privateString password;@NotEmpty(message ="用户邮箱不能为空")@Email(message ="邮箱格式错位")privateString email;@NotEmpty(message ="邮箱验证码不能为空")privateString emailCode;}

信息枚举类

同时的话,为了统一方便管理,这里专门做了一个枚举类。

publicenumBizCodeEnum{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");privateint code;privateString msg;BizCodeEnum(int code,String msg){this.code = code;this.msg = msg;}publicintgetCode(){return code;}publicStringgetMsg(){return msg;}}

这个是用来定义一些异常操作的。
当然还有我们还有R返回类。

publicclassRextendsHashMap<String,Object>{privatestaticfinallong serialVersionUID =1L;publicR(){put("code",0);put("msg","success");}publicstaticRerror(){returnerror(HttpStatus.SC_INTERNAL_SERVER_ERROR,"未知异常,请联系管理员");}publicstaticRerror(String msg){returnerror(HttpStatus.SC_INTERNAL_SERVER_ERROR, msg);}publicstaticRerror(int code,String msg){R r =newR();
        r.put("code", code);
        r.put("msg", msg);return r;}publicstaticRok(String msg){R r =newR();
        r.put("msg", msg);return r;}publicstaticRok(Map<String,Object> map){R r =newR();
        r.putAll(map);return r;}publicstaticRok(){returnnewR();}publicRput(String key,Object value){super.put(key, value);returnthis;}}

统一异常处理

之后是我们的异常处理了,这个直接交给切片去做。
这里的话复杂管理整个的Controller的异常

@Slf4j@RestControllerAdvice(basePackages ="com.huterox.whitehole.whiteholeuser.controller")publicclassUserExceptionControllerAdvice{@ExceptionHandler(value=MethodArgumentNotValidException.class)publicRhandleVaildException(MethodArgumentNotValidException e){
        log.error("数据校验出现问题{},异常类型:{}",e.getMessage(),e.getClass());BindingResult bindingResult = e.getBindingResult();Map<String,String> errorMap =newHashMap<>();
        bindingResult.getFieldErrors().forEach((fieldError)->{
            errorMap.put(fieldError.getField(),fieldError.getDefaultMessage());});returnR.error(BizCodeEnum.VAILD_EXCEPTION.getCode(),BizCodeEnum.VAILD_EXCEPTION.getMsg()).put("data",errorMap);}@ExceptionHandler(value =Throwable.class)publicRhandleException(Throwable throwable){

        log.error("错误:",throwable);returnR.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("请将对应信息填写完整!")}elseif(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))){returnR.error(BizCodeEnum.OVER_REQUESTS.getCode(),BizCodeEnum.OVER_REQUESTS.getMsg());}

20s的话,可能太久了可以适当减少一点,但是如果是密码输错了的话可能很快就修改好了。当然这样做还是有漏洞的,我们只是根据这个username来的,实际上脚本换一个username就好了,只要随机生成username我们就一样不行,那么这个时候的话就要锁IP了,这个也有个问题,那就是有些地方是公共IP,也就是很多人共用一个IP,那就尴尬了,而且还有就是这个要做的话应该在网关去做,这样会更好一点,或者是拦截器去做。所以这里我就不做了,原理是一样的。

完整代码

publicRLogin(LoginEntity entity){String username = entity.getUsername();String password = entity.getPassword();
        password=password.replaceAll(" ","");if(redisUtils.hasKey(RedisTransKey.getLoginKey(username))){returnR.error(BizCodeEnum.OVER_REQUESTS.getCode(),BizCodeEnum.OVER_REQUESTS.getMsg());}
        redisUtils.set(RedisTransKey.setLoginKey(username),1,20);UserEntityUser= userService.getOne(newQueryWrapper<UserEntity>().eq("username", username));if(User!=null){if(SecurityUtils.matchesPassword(password,User.getPassword())){//登录成功,签发tokenString token =JwtTokenUtil.generateToken(User);
                redisUtils.set(RedisTransKey.setTokenKey(username),token,7,TimeUnit.DAYS);returnR.ok(BizCodeEnum.SUCCESSFUL.getMsg()).put("loginToken",token);}else{returnR.error(BizCodeEnum.BAD_PUTDATA.getCode(),BizCodeEnum.BAD_PUTDATA.getMsg());}}else{returnR.error(BizCodeEnum.NO_SUCHUSER.getCode(),BizCodeEnum.NO_SUCHUSER.getMsg());}}

注册流程

前端

咱们这个前端也没啥,就是两个。

  getEmailCode (){const that =thisif(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;}elseif(!this.formRegister.username){this.$message.error('请填写账号');this.refreshCode();
          flag=false;}elseif(!this.formRegister.password){this.$message.error('请填写密码');this.refreshCode();
          flag=false;}elseif(!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("请将对应信息填写完整!")}elseif(res.code===0){alert("邮箱验证码发送成功,请及时查看,10分钟有效")}else{this.$message.error(res.msg);}});//倒计时if(!this.timer){this.show =falsethis.timer =setInterval(()=>{if(this.count >0&&this.count <=this.TIME_COUNT){this.count--}else{this.show =trueclearInterval(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;}elseif(!this.formRegister.emailCode){this.$message.error('请填写邮箱验证码');this.refreshCode();
        flag=false;}elseif(!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("请将对应信息填写完整!")}elseif(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: [email protected]
    #配置密码,注意不是真正的密码,而是刚刚申请到的授权码
    password: vmtwmkq6564651asd
    #端口号465或587
    port:587
    #默认的邮件编码为UTF-8default-encoding: UTF-8
    #其他参数
    properties:
      mail:
        #配置SSL 加密工厂
        smtp:
          ssl:
            #本地测试,先放开ssl
            enable:false
            required:false
          #开启debug模式,这样邮件发送过程的日志会在控制台打印出来,方便排查错误
        debug:true
        socketFactory:class:javax.net.ssl.SSLSocketFactory

之后就是咱们的服务了

publicclassMaliServiceImplimplementsMailService{/**
     * 注入邮件工具类
     */@AutowiredprivateJavaMailSenderImpl javaMailSender;@Value("${spring.mail.username}")privateString sendMailer;/**
     * 检测邮件信息类
     * @param to
     * @param subject
     * @param text
     */privatevoidcheckMail(Stringto,String subject,String text){if(StringUtils.isEmpty(to)){thrownewRuntimeException("邮件收信人不能为空");}if(StringUtils.isEmpty(subject)){thrownewRuntimeException("邮件主题不能为空");}if(StringUtils.isEmpty(text)){thrownewRuntimeException("邮件内容不能为空");}}/**
     * 发送纯文本邮件
     * @param to
     * @param subject
     * @param text
     */@OverridepublicvoidsendTextMailMessage(Stringto,String subject,String text){try{//true 代表支持复杂的类型MimeMessageHelper mimeMessageHelper =newMimeMessageHelper(javaMailSender.createMimeMessage(),true);//邮件发信人
            mimeMessageHelper.setFrom(sendMailer);//邮件收信人  1或多个
            mimeMessageHelper.setTo(to.split(","));//邮件主题
            mimeMessageHelper.setSubject(subject);//邮件内容
            mimeMessageHelper.setText(text);//邮件发送时间
            mimeMessageHelper.setSentDate(newDate());//发送邮件
            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
     */@OverridepublicvoidsendHtmlMailMessage(Stringto,String subject,String content){

        content="<!DOCTYPE html>\n"+"<html>\n"+"<head>\n"+"<meta charset=\"utf-8\">\n"+"<title>邮件</title>\n"+"</head>\n"+"<body>\n"+"\t<h3>这是一封HTML邮件!</h3>\n"+"</body>\n"+"</html>";try{//true 代表支持复杂的类型MimeMessageHelper mimeMessageHelper =newMimeMessageHelper(javaMailSender.createMimeMessage(),true);//邮件发信人
            mimeMessageHelper.setFrom(sendMailer);//邮件收信人  1或多个
            mimeMessageHelper.setTo(to.split(","));//邮件主题
            mimeMessageHelper.setSubject(subject);//邮件内容   true 代表支持html
            mimeMessageHelper.setText(content,true);//邮件发送时间
            mimeMessageHelper.setSentDate(newDate());//发送邮件
            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 附件路径
     */@OverridepublicvoidsendAttachmentMailMessage(Stringto,String subject,String content,String filePath){try{//true 代表支持复杂的类型MimeMessageHelper mimeMessageHelper =newMimeMessageHelper(javaMailSender.createMimeMessage(),true);//邮件发信人
            mimeMessageHelper.setFrom(sendMailer);//邮件收信人  1或多个
            mimeMessageHelper.setTo(to.split(","));//邮件主题
            mimeMessageHelper.setSubject(subject);//邮件内容   true 代表支持html
            mimeMessageHelper.setText(content,true);//邮件发送时间
            mimeMessageHelper.setSentDate(newDate());//添加邮件附件FileSystemResource file =newFileSystemResource(newFile(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
     */@OverridepublicvoidsendCodeMailMessage(Stringto,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)))

成功后
在这里插入图片描述

这里多了点东西,主要是还需要计数。

验证码

这个我得说一下的就是,那个验证码的话,我是在前端做的,每次还是来骗骗不太“懂”的用户的。这里不是后端做,主要是因为,第一有了邮箱验证不需要再鉴别一次,我们在接口层就做了硬性指标只能访问多少次,不太再需要验证码防脚本了,之后也是降低服务端请求次数。

完整代码

publicRemailCode(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){returnR.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{UserEntityUser= userService.getOne(newQueryWrapper<UserEntity>().eq("username", username));if(User!=null){returnR.error(BizCodeEnum.HAS_USERNAME.getCode(),BizCodeEnum.HAS_USERNAME.getMsg());}else{String emailCode =CodeUtils.creatCode(6);//            我们这里做一件事情,那就是最多允许用户在10分钟内发送2次的邮箱验证//            60s倒计时后用户可以再发送验证码,但是间隔在10分钟内只能再发送1次EmailCodeEntity emailCodeEntity =newEmailCodeEntity(
                        emailCode, username,email,1);
                redisUtils.set(RedisTransKey.setEmailKey(username), emailCodeEntity,
                        limitTime,TimeUnit.MINUTES
                );
                mailService.sendCodeMailMessage(email, emailCodeEntity.getEmailCode());}}returnR.ok(BizCodeEnum.SUCCESSFUL.getMsg());}

注册

这个也是类似的,我们拿到这个验证码后,去校验就好了。

那么注册这里的话就不需要做防刷了,或者说已经做好了因为有邮箱验证码间接做好了。因为如果没有验证码,直接校验过不去,如果有验证码在Redis当中的对不到一样没法后序操作。其实这里的防刷都是消耗了服务器资源的,只是消耗了多少的问题,因为咱们这边都是拿Redis先顶住的。

publicRregister(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 =newUserEntity();
                    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{returnR.error(BizCodeEnum.BAD_EMAILCODE_VERIFY.getCode(),BizCodeEnum.BAD_EMAILCODE_VERIFY.getMsg());}}else{returnR.error(BizCodeEnum.BAD_DOING.getCode(),BizCodeEnum.BAD_DOING.getMsg());}}else{returnR.error(BizCodeEnum.OVER_TIME.getCode(),BizCodeEnum.OVER_TIME.getMsg());}returnR.ok(BizCodeEnum.SUCCESSFUL.getMsg());}

总结

到此的话,一个简单的用户登录注册就做好了,那么接下来的话就是咱们的Auto2.0了,这里咱们要实现的就是这两个功能
在这里插入图片描述


本文转载自: https://blog.csdn.net/FUTEROX/article/details/127232757
版权归原作者 Huterox 所有, 如有侵权,请联系我们删除。

“SpringBoot轻轻松松搞定用户邮箱登录注册”的评论:

还没有评论