仿黑马点评-redis整合【邮件登陆部分】
原创
©著作权归作者所有:来自51CTO博客作者笑霸final的原创作品,请联系作者获取转载授权,否则将追究法律责任
目录
- 🐉发送验证码
- 🐉 邮件登陆(短信登陆)
- 🐉发送验证码
- 🐉 邮件登陆(短信登陆)
- 🐉优化
一、简介
课程介绍

项目地址(资料都在这里):
gitee仓库

数据库相关信息

项目架构

🐉项目短信登陆(修改成邮件登陆)
如何使用邮件登陆 详细请看此仿瑞吉外卖 【手机登陆功能换成邮件登陆】
二、基于session实现
流程

🐉发送验证码
请求

代码步骤
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
- 邮件配置
如何使用邮件登陆 详细请看此仿瑞吉外卖 【手机登陆功能换成邮件登陆】
提前注入对象
@Autowired
private JavaMailSender mailSender;//邮件
@Value("${spring.mail.username}")
private String MyFrom;
SimpleMailMessage 详细说明
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom();//发送人
message.setTo();//谁要接收
message.setSubject("");//邮件标题
message.setText();邮件内容
controller代码
/**
* 发送邮件验证码
*/
@PostMapping("code")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
// 验证邮箱号
if (RegexUtils.isEmailInvalid(phone)) {
return Result.fail("邮箱格式错误");
}
//验证码生成器
String code = RandomUtil.randomNumbers(6);
// TODO 发送短信验证码并保存验证码
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(MyFrom);//发送人
message.setTo(phone);//谁要接收
message.setSubject("验证码");//邮件标题
message.setText("您的验证码是 \n" +code);//邮件内容
try {
//发生验证码
mailSender.send(message);
//需要保存一下验证码,后面用来验证
session.setAttribute(phone,code);
System.out.println("==========");
log.info(code);
System.out.println("==========");
} catch (MailException e) {
e.printStackTrace();
return Result.fail("邮箱发生失败");
}
return Result.ok();
}
🐉 邮件登陆(短信登陆)
前端请求图


同时还有密码登陆所以我们是单独用个tdo
但是这里我们先不管密码登陆

请求 URL http://localhost:8080/api/user/login
代码
/**
* 登录功能
* @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
*/
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
// TODO 实现登录功能
//1 先验证邮箱
String phone = loginForm.getPhone();
if (RegexUtils.isEmailInvalid(phone)) {
return Result.fail("邮箱格式错误");
}
//2 在验证验证码
Object cacheCode = session.getAttribute(phone);
String code = loginForm.getCode();
//注意String类型的 不能用==、!=来判断是否相等
if( cacheCode==null || !cacheCode.toString().equals(code)){
return Result.fail("验证码错误");
}
//3.查数据库存在此手机号?
User user = userService.findByPone(phone);
// 3.1 不存在 创建新用户
if(user==null){
//存入数据库
user=userService.creatUser(phone);
}
// 3.2 存入session
session.setAttribute("user",user);
return Result.ok();
}
三、基于redis

🐉发送验证码
发生验证码:

/**
* 发送邮件验证码
*/
@PostMapping("code")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
// 验证邮箱号
if (RegexUtils.isEmailInvalid(phone)) {
return Result.fail("邮箱格式错误");
}
//验证码生成器
String code = RandomUtil.randomNumbers(6);
// TODO 发送短信验证码并保存验证码
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(MyFrom);//发送人
message.setTo(phone);//谁要接收
message.setSubject("验证码");//邮件标题
message.setText("您的验证码是 \n" +code);//邮件内容
try {
//发生验证码
mailSender.send(message);
//需要保存一下验证码,后面用来验证
//session.setAttribute(phone,code);
//保存到redis
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY+phone,code,2, TimeUnit.MINUTES);
System.out.println("==========");
log.info(code+"======="+session.getAttribute(phone));
System.out.println("==========");
} catch (MailException e) {
e.printStackTrace();
return Result.fail("邮箱发送失败");
}
return Result.ok();
}
🐉 邮件登陆(短信登陆)
验证登陆功能:
login方法会把生成的token返回给前端,浏览器会将其保存到session中。

我们登陆信息存入redis的user信息应该用 hash结构存储,原因是:
- 若使用String结构,以JSON字符串来保存,比较直观
- 但Hash结构可以将对象中的每个字段独立存储,可以针对单个字段做CRUD,并且内存占用更少
/**
* 登录功能
* @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
*/
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
// TODO 实现登录功能
//1 先验证邮箱
String phone = loginForm.getPhone();
if (RegexUtils.isEmailInvalid(phone)) {
return Result.fail("邮箱格式错误");
}
//2 在验证验证码
//Object cacheCode = session.getAttribute(phone);
// 这里用redis获取
String cacheCode
= stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
String code = loginForm.getCode();
//注意String类型的 不能用==、!=来判断是否相等
if( cacheCode==null || !cacheCode.equals(code)){
return Result.fail("验证码错误");
}
//3.查数据库存在此手机号(邮箱)?
User user = userService.findByPone(phone);
// 3.1 不存在 创建新用户
if(user==null){
//存入数据库
user=userService.creatUser(phone);
}
//3.1.保存用户信息到redis中
//3.1随机生成token,作为登陆令牌
String token = UUID.randomUUID().toString();
//3.2将User对象转为HashMap存储
UserDTO userDTO = BeanUtil.copyProperties(user,UserDTO.class);
final Map<String, Object> map = BeanUtil.beanToMap(userDTO, new HashMap<>(),
CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName,fieldValue)->{
return fieldValue.toString();
})
);
//3.3存储
stringRedisTemplate.opsForHash().putAll(LOGIN_USER_KEY+token,map);
//3.4设置token有效期
stringRedisTemplate.expire(LOGIN_USER_KEY+token,3000,TimeUnit.MINUTES);
//4.返回token
return Result.ok(token);
}
🐉优化
我们每次登陆(浏览网页)都应该去象session一样去刷新有效时间
- 首先,对于每个请求,我们首先根据token判断用户是否已经登陆(是否已经保存到ThreadLocal中),如果没有登陆,放行交给登陆拦截器去做,如果已经登陆,刷新token的有效期,然后放行。
- 之后来到登陆拦截器,如果
ThreadLocal
没有用户,说明没有登陆,拦截,否则放行。
ThreadLocal的一些说明
Thread
类有一个类型为ThreadLocal.ThreadLocalMap
的实例变量threadLocals,也就是说每个线程有一个自己的ThreadLocalMap。
- ThreadLocalMap有自己的独立实现,可以简单地将它的key视作ThreadLocal,value为代码中放入的值(实际上key并不是ThreadLocal本身,而是它的一个弱引用)。
- 每个线程在往ThreadLocal里放值的时候,都会往自己的ThreadLocalMap里存,读也是以ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。
- ThreadLocalMap有点类似HashMap的结构,只是HashMap是由数组+链表实现的,而ThreadLocalMap中并没有链表结构。
- 我们还要注意Entry, 它的key是ThreadLocal<?> k ,继承自WeakReference, 也就是我们常说的弱引用类型。
内存泄露问题
由于ThreadLocal的key是弱引用,故在gc时,key会被回收掉,但是value是强引用没有被回收,所以在我们拦截器的方法里必须手动remove()。
官方定义了ThreadLocal工具包


设置拦截器(token拦截器)
public class RefreshTokenInterceptor implements HandlerInterceptor {
private StringRedisTemplate stringRedisTemplate;
public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate){
this.stringRedisTemplate=stringRedisTemplate;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// TODO 获取请求头中的token
String token = request.getHeader("authorization");//authorization是前端返回的
if (StrUtil.isBlank(token)) {
return true;
}
//TODO 获取redis中的token
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(RedisConstants.LOGIN_USER_KEY + token);//entries获取所有的
if(userMap.isEmpty()){
return true;
}
//TODO 将查询到的数据转换为UserTdo
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(),false);
//TODO 将用户存储到 ThreadLocal
UserHolder.saveUser(userDTO);
//刷新
stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY+token,30, TimeUnit.MINUTES);
return true;
}
}
登陆拦截器
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//判断是否需要拦截(TheadLocal是否有用户)
if (UserHolder.getUser()==null){
response.setStatus(401);
return false;
}
//有用户
return true;
}
}
配置拦截器
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate))
.addPathPatterns("/**").order(0);//拦截所有
//order(0) 数字越小越先执行
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns(
"/shop/**",
"/voucher/**",
"/shop-type/**",
"/upload/**",
"/blog/hot",
"/user/code",
"/user/login"
).order(1);
}
}
四、一些问题
- 分享一个免费的redis可视化工具
https://gitee.com/qishibo/AnotherRedisDesktopManager/releases点击跳转
-
此外此项目前端部分问题解决方法借鉴于一下博文
点击跳转==>黑马点评项目-短信登录功能
前端有一个/me请求来查询用户是否登陆

@GetMapping("/me")
public Result me(){
// TODO 获取当前登录的用户并返回
// 获取当前登录的用户并返回
UserDTO user = UserHolder.getUser();
return Result.ok(user);
}