- 一:前言
- 二:登录流程
- 三:代码实现
做项目时候经常会使用到登录模块,登录模块可以有多种验证方式,可以使用密码验证,还可以使用验证码验证,其中验证码验证还可以分为短信验证码和邮箱验证码,由于短信验证码服务需要收费,我学生党为了省吃俭用,就选用了邮箱验证功能,这个是免费的。至于如何开启邮箱发送验证码功能可以自己在CSDN上面搜索一下,我这里仅提供登录功能的逻辑部分。
二:登录流程登录流程可以看上面的流程图,这里不再赘述。
三:代码实现- 前端部分
馅饼研究所
登录
{{ verifyCode }}
邮箱格式输入不正确,请重新输入
登录
前端部分实现并不难,一开始我使用的是手机号作为登录验证,后来才改回的邮箱,所以变量名才是phone,我这里就不再改了,不影响使用。为了交互友好性,我这里引入了一个简单的倒计时功能,让用户在60秒之后才能第二次获取验证码,以防止用户恶意点击造成验证码频繁发送。
- 后端部分
2.1:工具类
package com.my.reggie.util;
import org.apache.commons.mail.HtmlEmail;
public class SendEmailUtils {
public static void sendAuthCodeEmail(String email,String code) {
try {
HtmlEmail mail = new HtmlEmail();
mail.setHostName("smtp.qq.com");
mail.setCharset("UTF-8");
mail.setAuthentication("666@qq.com", "rtyhuaujrjzshbaeg");
mail.setFrom("666@qq.com", "馅饼研究所");
mail.setSSLOnConnect(true);
mail.addTo(email);
mail.setSubject("登录验证码");
mail.setMsg("尊敬的用户:你好! 登录验证码为:" + code + "(有效期为一分钟)");
mail.send();//发送
} catch (Exception e) {
e.printStackTrace();
}
}
}
2.2:UserController
package com.my.reggie.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.my.reggie.common.R;
import com.my.reggie.pojo.User;
import com.my.reggie.service.UserService;
import com.my.reggie.util.ValidateCodeUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@Resource
private StringRedisTemplate stringRedisTemplate;
@PostMapping("/sendMsg")
public R sendMsg(@RequestBody User user, HttpSession session) {
// 获取邮箱号
String phone = user.getPhone();
if (phone == null) {
return R.error("邮箱号为空!");
}
// 随机生成4位验证码
String code = String.valueOf(ValidateCodeUtils.generateValidateCode(4));
log.info("验证码为:{}", code);
//发送验证码到邮箱
// SendEmailUtils.sendAuthCodeEmail(phone,code);
// 将验证码保存到session
// session.setAttribute("code", code);
//将验证码缓存到Redis(有效时间为1分钟)
stringRedisTemplate.opsForValue().set("code",code,1, TimeUnit.MINUTES);
//将手机号保存到session
session.setAttribute("phone",phone);
return R.success("验证码发送成功!");
}
@PostMapping("/login")
public R login(@RequestBody Map map, HttpSession session) {
// 获取手机号
String phone = map.get("phone");
// 获取验证码
String code = map.get("code");
// 从session中获取验证码
// String codeInSession = (String) session.getAttribute("code");
// 从缓存中获取验证码
String codeInRedis = stringRedisTemplate.opsForValue().get("code");
//从session中获取请求验证码的手机号
String phoneInSession = (String) session.getAttribute("phone");
// 进行验证码比对
if (codeInRedis == null || phoneInSession == null || !codeInRedis.equals(code) || !phoneInSession.equals(phone)) {
return R.error("验证码错误");
}
// 判断该用户是否注册
LambdaQueryWrapper lqw = new LambdaQueryWrapper<>();
lqw.eq(User::getPhone, phone);
User user = userService.getOne(lqw);
if (user == null) {
// 用户还未注册,自动注册
user = new User();
user.setPhone(phone);
userService.save(user);
}
//设置session
session.setAttribute("user", user.getId());
session.setMaxInactiveInterval(6*60*60);
//删除验证码缓存
stringRedisTemplate.delete("code");
return R.success(user);
}
@PostMapping("/loginout")
public R logout(HttpServletRequest request){
//清除session
request.getSession().removeAttribute("user");
request.getSession().removeAttribute("phone");
// request.getSession().removeAttribute("code");
return R.success("退出登录成功");
}
}
后端的实现也不复杂,主要是如何保存和获取数据的问题,我这里保存验证码一开始是保存在session中,后开改进方案是将它保存到redis中,因为这样就可以实现验证码自动过期功能。这里由一个小细节需要注意,当用户登录成功之后需要删除redis中的验证码,因为假如不删除的话当用户登录成功之后立马点击退出登录,这时候用户再使用上次的验证码还是可以登录成功,因为这时候redis中的验证码还未过期。对于这个细节你们可以尝试一下,找一个app用验证码登录,登录成功之后点击退出登录,然后再次输入上一次获取到的验证码,这时候你会发现验证码是失效的。我也是通过这样的尝试才知道这一细节,虽然这不影响使用,因为很少会有用户闲着没事干去测试这个,只不过是为了严谨一点才加入这个。当然,我们也可以通过当用户点击退出登录时删除redis缓存来实现,只是我感觉上者比较好。除此之外,当用户退出登录之后也要删除session中的缓存。补充说明一下,session中的"user"是用户检测用户是否登录的,因为项目中设置了过滤器功能用于检测用户是否登录以防止用户非法访问,当然,也可以使用拦截器来实现,用拦截器来实现代码量会少一点,但是过滤器的话会更明了,各有利弊,这里就留给你们去实现了。
补充:
3.过滤器代码
package com.my.reggie.filter;
import com.alibaba.fastjson.JSON;
import com.my.reggie.common.BaseContext;
import com.my.reggie.common.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.AntPathMatcher;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Slf4j
@WebFilter(filterName = "loginFilter", urlPatterns = "
public boolean check(String[] urls, String URI) {
for (String url : urls) {
boolean match = PATH_MATCHER.match(url, URI);
if (match) {
return true;
}
}
return false;
}
}
4.随机验证码生成器
package com.my.reggie.util;
import java.util.Random;
public class ValidateCodeUtils {
public static Integer generateValidateCode(int length){
Integer code =null;
if(length == 4){
code = new Random().nextInt(9999);//生成随机数,最大为9999
if(code < 1000){
code = code + 1000;//保证随机数为4位数字
}
}else if(length == 6){
code = new Random().nextInt(999999);//生成随机数,最大为999999
if(code < 100000){
code = code + 100000;//保证随机数为6位数字
}
}else{
throw new RuntimeException("只能生成4位或6位数字验证码");
}
return code;
}
public static String generateValidateCode4String(int length){
Random rdm = new Random();
String hash1 = Integer.toHexString(rdm.nextInt());
String capstr = hash1.substring(0, length);
return capstr;
}
}



