ok,我又来水博文了,今天的内容很简单,就是咱们的这个用户登录注册,加上邮箱验证,非常简单,为我们接下来的Auto2.0做好前置工作。因为这个没做好,那个也做不好。本文的内容可能比较多,但是都很简单。
这块的话,咱们这边发送邮箱验证码之后,前端这边的验证码还会重新刷新一次,反正是在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());
}
}
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 ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
redisTemplate.opsForValue().set(key, value);
return true;
}
/**
* 普通缓存放入并设置时间
* @param key 键
* @param value 值
* @param time time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
this.set(key, value);
}
return true;
}
/**
* 普通缓存放入并设置时间
* @param key 键
* @param value 值
* @param time time 时间类型自定义设定
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time,TimeUnit tiemtype) {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, tiemtype);
} else {
this.set(key, value);
}
return true;
}
/**
* 递增
* @param key 键
* @param delta 要增加几(大于0)
* @return
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
* @param key
* @param delta 要减少几(大于0)
* @return
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
// ================================Map=================================
/**
* HashGet
* @param key 键
* @param item 项 不能为null
* @return
*/
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取hashKey对应的所有键值
* @param key 键
* @return 对应的多个键值
*/
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet
* @param key 键
* @param map 对应多个键值
* @return true 成功 false 失败
*/
public boolean hmset(String key, Map<String, Object> map) {
redisTemplate.opsForHash().putAll(key, map);
return true;
}
/**
* HashSet 并设置时间
* @param key 键
* @param map 对应多个键值
* @param time 时间(秒)
* @return true成功 false失败
*/
public boolean hmset(String key, Map<String, Object> map, long time) {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time);
}
return true;
}
/**
* 向一张hash表中放入数据,如果不存在将创建
* @param key 键
* @param item 项
* @param value 值
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value) {
redisTemplate.opsForHash().put(key, item, value);
return true;
}
/**
* 向一张hash表中放入数据,如果不存在将创建
* @param key 键
* @param item 项
* @param value 值
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value, long time) {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time);
}
return true;
}
/**
* 删除hash表中的值
* @param key 键 不能为null
* @param item 项 可以使多个 不能为null
*/
public void hdel(String key, Object... item) {
redisTemplate.opsForHash().delete(key, item);
}
/**
* 判断hash表中是否有该项的值
* @param key 键 不能为null
* @param item 项 不能为null
* @return true 存在 false不存在
*/
public boolean hHasKey(String key, String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}
/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
* @param key 键
* @param item 项
* @param by 要增加几(大于0)
* @return
*/
public double hincr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash递减
* @param key 键
* @param item 项
* @param by 要减少记(小于0)
* @return
*/
public double hdecr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}
// ============================set=============================
/**
* 根据key获取Set中的所有值
* @param key 键
* @return
*/
public Set<Object> sGet(String key) {
return redisTemplate.opsForSet().members(key);
}
/**
* 根据value从一个set中查询,是否存在
* @param key 键
* @param value 值
* @return true 存在 false不存在
*/
public boolean sHasKey(String key, Object value) {
return redisTemplate.opsForSet().isMember(key, value);
}
/**
* 将数据放入set缓存
* @param key 键
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSet(String key, Object... values) {
return redisTemplate.opsForSet().add(key, values);
}
/**
* 将set数据放入缓存
* @param key 键
* @param time 时间(秒)
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSetAndTime(String key, long time, Object... values) {
final Long count = redisTemplate.opsForSet().add(key, values);
if (time > 0)
expire(key, time);
return count;
}
/**
* 获取set缓存的长度
* @param key 键
* @return
*/
public long sGetSetSize(String key) {
return redisTemplate.opsForSet().size(key);
}
/**
* 移除值为value的
* @param key 键
* @param values 值 可以是多个
* @return 移除的个数
*/
public long setRemove(String key, Object... values) {
final Long count = redisTemplate.opsForSet().remove(key, values);
return count;
}
// ===============================list=================================
/**
* 获取list缓存的内容
* @param key 键
* @param start 开始
* @param end 结束 0 到 -1代表所有值
* @return
*/
public List<Object> lGet(String key, long start, long end) {
return redisTemplate.opsForList().range(key, start, end);
}
/**
* 获取list缓存的长度
* @param key 键
* @return
*/
public long lGetListSize(String key) {
return redisTemplate.opsForList().size(key);
}
/**
* 通过索引 获取list中的值
* @param key 键
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
* @return
*/
public Object lGetIndex(String key, long index) {
return redisTemplate.opsForList().index(key, index);
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, Object value) {
redisTemplate.opsForList().rightPush(key, value);
return true;
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, Object value, long time) {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0) {
expire(key, time);
}
return true;
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
* @return
*/
public boolean lSetList(String key, List<Object> value) {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
* @return
*/
public boolean lSetList(String key, List<Object> value, long time) {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0) {
expire(key, time);
}
return true;
}
/**
* 根据索引修改list中的某条数据
* @param key 键
* @param index 索引
* @param value 值
* @return
*/
public boolean lUpdateIndex(String key, long index, Object value) {
redisTemplate.opsForList().set(key, index, value);
return true;
}
}
这个主要是对key做一个分组,一是美化,而是方便管理。
public class RedisTransKey {
public static final String RedisNameSpace="user";
public static final String RedisTokenName="token";
public static final String RedisLoginName="login";
public static final String RedisEmailCodeName="emailCode";
public static String setEmailKey(String key){
return RedisNameSpace+":"+RedisEmailCodeName+":"+key;
}
public static String setRootKey(String key){
return RedisNameSpace+":"+key+":";
}
public static String setTokenKey(String key){
return RedisNameSpace+':'+RedisTokenName+":"+key;
}
public static String setLoginKey(String key){
return RedisNameSpace+':'+RedisLoginName+":"+key;
}
public static String getEmailKey(String key){return setEmailKey(key);}
public static String getRootKey(String key){return setRootKey(key);}
public static String getTokenKey(String key){return setTokenKey(key);}
public static String getLoginKey(String key){return setLoginKey(key);}
}
这个主要是为了校验,产生token用的,值得一提的是,我们的token并不是放在Mysql里面的,而是放在了redis里面,主要是为了提高查询速度,减轻数据库压力,毕竟token这玩意是有时间限制的,而且对于校验修改的速度要求还挺高。
public class JwtTokenUtil {
private static String secret;
private static Long expiration;
private static Map<String, Object> header;
static {
secret="你的SRET";
expiration = 7*24*60*60*1000L;
header=new HashMap<>();
header.put("typ", "jwt");
}
/**
* 生成token令牌
* @return 令token牌
*/
public static String generateToken(UserEntity user) {
Map<String, Object> claims = new HashMap<>();
claims.put("username", user.getUsername());
claims.put("userid",user.getUserid());
claims.put("created", new Date());
return generateToken(claims);
}
/**
* @param token 令牌
* @return 用户名
*/
public static String GetUserNameFromToken(String token) {
String username;
try {
Claims claims = getClaimsFromToken(token);
username = (String) claims.get("username");
} catch (Exception e) {
username = null;
}
return username;
}
public static String GetUserIDFromToken(String token) {
String username;
try {
Claims claims = getClaimsFromToken(token);
username = (String) claims.get("userid");
} catch (Exception e) {
username = null;
}
return username;
}
/**
* 判断令牌是否过期
* @param token 令牌
* @return 是否过期
*/
public static Boolean isTokenExpired(String token) {
try {
Claims claims = getClaimsFromToken(token);
Date expiration = claims.getExpiration();
return expiration.before(new Date());
} catch (Exception e) {
return false;
}
}
/**
* 刷新令牌
*
* @param token 原令牌
* @return 新令牌
*/
public String refreshToken(String token) {
String refreshedToken;
try {
Claims claims = getClaimsFromToken(token);
claims.put("created", new Date());
refreshedToken = generateToken(claims);
} catch (Exception e) {
refreshedToken = null;
}
return refreshedToken;
}
/**
* 验证令牌
* @return 是否有效
*/
public static Boolean validateToken(String token, UserEntity user) {
String username = GetUserNameFromToken(token);
return (username.equals(user.getUsername()) && !isTokenExpired(token));
}
/**
* 从claims生成令牌,如果看不懂就看谁调用它
*
* @param claims 数据声明
* @return 令牌
*/
private static String generateToken(Map<String, Object> claims) {
Date expirationDate = new Date(System.currentTimeMillis() + expiration);
return Jwts.builder()
.setHeader(header)
.setClaims(claims)
.setExpiration(expirationDate)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
/**
* 从令牌中获取数据声明,如果看不懂就看谁调用它
*
* @param token 令牌
* @return 数据声明
*/
public static Claims getClaimsFromToken(String token) {
Claims claims;
try {
claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
} catch (Exception e) {
claims = null;
}
return claims;
}
}
这个主要是用来加密和对比密码的,这里密码是解不了密的,因为用的md5压根不是“加密”算法,而是“特征”算法,计算你的密码的特征,既然只是拿到了“特征”那么显然不可能通过“特征”完整地还原一个东西,因为信息存在损失。
我们这块使用的是Spring提供的方案。
public class SecurityUtils {
/**
* 生成BCryptPasswordEncoder密码
* @param password 密码
* @return 加密字符串
*/
public static BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
public static String encodePassword(String password)
{
return passwordEncoder.encode(password);
}
/**
* 判断密码是否相同
* @param rawPassword 真实密码
* @param encodedPassword 加密后字符
* @return 结果
*/
public static boolean matchesPassword(String rawPassword, String encodedPassword)
{
return passwordEncoder.matches(rawPassword, encodedPassword);
}
}
之后是我们的接口和实体类,这个很重要,我们目前的这个玩意其实就三个接口。
register
login
emailCode
之后是我们前后端分类的实体类了。专门再提取出单独的Entity而不是直接使用数据库的原因很简单,首先提交的一些信息和那些实体类有些对不上,其次,我的实体类是按照数据库的字段来的,如果直接这样搞的话,很容易猜到我数据库的字段。所以我单独搞了一个专门和前端交互的Entity,同时使用JSR303校验美滋滋。
这些实体我是按照功能来划分的。
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginEntity {
private String username;
@NotEmpty(message = "用户密码不能为空")
@Length(min = 6,max = 18,message="密码必须是6-18位")
private String password;
}
这个EmailCode主要是后面对Email的验证码进行处理的,这个是存在Redis里面的将来。
@Data
@NoArgsConstructor
@AllArgsConstructor
public class EmailCodeEntity {
private String emailCode;
private String username;
private String email;
private int times;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public 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
@NoArgsConstructor
public 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.bootgroupId>
<artifactId>spring-boot-starter-mailartifactId>
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());
}