Redis实战——短信登录
创始人
2024-02-06 09:20:34

目录

1 基于Seesion实现短信登录

1.1 发送短信验证码 

 1.2 登录功能

2 使用Redis进行短信验证码校验登录

2.1 Seesion方法存在的问题

2.2 发送短信验证码

2.3  验证码校验及登录功能

 3.拦截器优化


1 基于Seesion实现短信登录

1.1 发送短信验证码 

发送验证码请求路径 /user/code

Controller层

/*** 发送手机验证码*/@PostMapping("code")public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {// TODO 发送短信验证码并保存验证码return userService.sendCode(phone,session);}

Service层及Service实现

public interface IUserService extends IService {Result sendCode(String phone, HttpSession session);}
package com.hmdp.service.impl;import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.RandomUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.dto.LoginFormDTO;
import com.hmdp.dto.Result;
import com.hmdp.dto.UserDTO;
import com.hmdp.entity.User;
import com.hmdp.mapper.UserMapper;
import com.hmdp.service.IUserService;
import com.hmdp.utils.RegexPatterns;
import com.hmdp.utils.RegexUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.manager.util.SessionUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import javax.servlet.http.HttpSession;import static com.hmdp.utils.SystemConstants.USER_NICK_NAME_PREFIX;/*** 

* 服务实现类*

** @author 虎哥* @since 2021-12-22*/ @Slf4j @Service public class UserServiceImpl extends ServiceImpl implements IUserService {@Overridepublic Result sendCode(String phone, HttpSession session) {//1.校验手机号if (RegexUtils.isPhoneInvalid(phone)){//2.如果错误,返回错误信息return Result.fail("手机号码格式错误!");}//3.生成验证码String code = RandomUtil.randomNumbers(6);//4.保存验证码到session中去session.setAttribute("code",code);//5.发送验证码log.debug("手机验证码为:{}",code);return Result.ok();}}

 1.2 登录功能

请求路径:user/login

 

Controller层

/*** 登录功能* @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码*/@PostMapping("/login")public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){// TODO 实现登录功能return userService.login(loginForm,session);}

 Service层及Service实现

public interface IUserService extends IService {Result login(LoginFormDTO loginForm, HttpSession session);}
package com.hmdp.service.impl;import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.RandomUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.dto.LoginFormDTO;
import com.hmdp.dto.Result;
import com.hmdp.dto.UserDTO;
import com.hmdp.entity.User;
import com.hmdp.mapper.UserMapper;
import com.hmdp.service.IUserService;
import com.hmdp.utils.RegexPatterns;
import com.hmdp.utils.RegexUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.manager.util.SessionUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import javax.servlet.http.HttpSession;import static com.hmdp.utils.SystemConstants.USER_NICK_NAME_PREFIX;/*** 

* 服务实现类*

** @author 虎哥* @since 2021-12-22*/ @Slf4j @Service public class UserServiceImpl extends ServiceImpl implements IUserService {@Overridepublic Result login(LoginFormDTO loginForm, HttpSession session) {String phone = loginForm.getPhone();//1.校验手机号if (RegexUtils.isPhoneInvalid(phone)){//2.如果错误,返回错误信息return Result.fail("手机号码格式错误!");}//2.校验验证码String code = loginForm.getCode();String sendCode = (String) session.getAttribute("code");if (code != null || !code.equals(sendCode)){//验证码不一致Result.fail("验证码错误!");}//验证码相同,查询手机号User user = query().eq("phone", loginForm.getPhone()).one();if (user == null) {//用户不存在,创建一个新用户并且保存到数据库中user = createNewUser(phone);}//用户存在,存入Sessionsession.setAttribute("user", BeanUtil.copyProperties(user,UserDTO.class));return Result.ok();}private User createNewUser(String phone) {//1.创建用户User user = new User();user.setPhone(phone);user.setNickName(USER_NICK_NAME_PREFIX+RandomUtil.randomString(10));//2.保存用户save(user);return user;} }

1.3 登录校验功能(配置拦截器)

 

    

编写拦截器类:

package com.hmdp.utils;import com.hmdp.dto.UserDTO;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;/*** @Author 华子* @Date 2022/11/20 17:30* @Version 1.0*/
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//1.获sessionHttpSession session = request.getSession();//2.获取用户信息Object user = session.getAttribute("user");//3.判断用户信息是否存在if (user == null){//4.不存在,拦截response.setStatus(401);return false;}//5.存在,保存用户信息到ThreadLocalUserHolder.saveUser((UserDTO) user);//6.放行return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {//移除用户UserHolder.removeUser();}
}

配置添加拦截器到SpringMvc中(配置拦截路径)

package com.hmdp.config;import com.hmdp.utils.LoginInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;/*** @Author 华子* @Date 2022/11/20 17:38* @Version 1.0*/
@Configuration
public class MvcConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new LoginInterceptor()).excludePathPatterns("/shop/**","/voucher/**","/shop-type/**","/upload/**","/blog/hot","/user/code","/user/login");}
}

最后Controller层返回用户信息

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

2 使用Redis进行短信验证码校验登录

2.1 Seesion方法存在的问题

session共享问题

多台Tomcat并不共享session存储空间,当请求切换到不同tomcat服务时导致数据丢失的问题。 

session的替代方案应该满足:

数据共享

内存存储

key、value结构

无可厚非,就说我们的Reids啦~

2.2 发送短信验证码

这步操作和之前的Session很相同,只是我们这次把验证码保存到Redis当中

 

对于验证码的保存,我们选择String类型操作,key用手机号(唯一性)value就是验证码

 

代码:(其余操作一样,就是验证码本来是存到Session中,改为存到Reids) 

 

package com.hmdp.service.impl;import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.RandomUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.dto.LoginFormDTO;
import com.hmdp.dto.Result;
import com.hmdp.dto.UserDTO;
import com.hmdp.entity.User;
import com.hmdp.mapper.UserMapper;
import com.hmdp.service.IUserService;
import com.hmdp.utils.RegexPatterns;
import com.hmdp.utils.RegexUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.manager.util.SessionUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;import javax.annotation.Resource;
import javax.servlet.http.HttpSession;import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;import static com.hmdp.utils.RedisConstants.*;
import static com.hmdp.utils.SystemConstants.USER_NICK_NAME_PREFIX;/*** 

* 服务实现类*

** @author 虎哥* @since 2021-12-22*/ @Slf4j @Service public class UserServiceImpl extends ServiceImpl implements IUserService {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic Result sendCode(String phone, HttpSession session) {//1.校验手机号if (RegexUtils.isPhoneInvalid(phone)){//2.如果错误,返回错误信息return Result.fail("手机号码格式错误!");}//3.生成验证码String code = RandomUtil.randomNumbers(6);//4.保存验证码到session中去 // session.setAttribute("code",code);//4.保存验证码到redis中去stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY+phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);//5.发送验证码log.debug("手机验证码为:{}",code);return Result.ok();}}

2.3  验证码校验及登录功能

这一步明显要比上一步复杂的多了,我们一步一步来

首先,就是用户每次访问的时候,每次的路径都是不一样的,而Seesion是从Cookie中获取唯一的SessionId来确保唯一性,以此取得用户值,而对于Redis,我们及需要一个唯一的标识符,就是我们的Token(使用UUID生成的随机字符串)

和之前的Session操作不同的地方在于

1. 使用手机号为key去Redis中获取验证码

2. 使用随机生成的字符串Token保存用户数据

3.返回Token数据给前端,由前端保存到请求头的“Authorization”中 

温馨小提示:这里我们使用Map操作来保存用户数据~

具体实现代码如下:

package com.hmdp.service.impl;import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.RandomUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.dto.LoginFormDTO;
import com.hmdp.dto.Result;
import com.hmdp.dto.UserDTO;
import com.hmdp.entity.User;
import com.hmdp.mapper.UserMapper;
import com.hmdp.service.IUserService;
import com.hmdp.utils.RegexPatterns;
import com.hmdp.utils.RegexUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.manager.util.SessionUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;import javax.annotation.Resource;
import javax.servlet.http.HttpSession;import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;import static com.hmdp.utils.RedisConstants.*;
import static com.hmdp.utils.SystemConstants.USER_NICK_NAME_PREFIX;/*** 

* 服务实现类*

** @author 虎哥* @since 2021-12-22*/ @Slf4j @Service public class UserServiceImpl extends ServiceImpl implements IUserService {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic Result login(LoginFormDTO loginForm, HttpSession session) {String phone = loginForm.getPhone();//1.校验手机号if (RegexUtils.isPhoneInvalid(phone)){//2.如果错误,返回错误信息return Result.fail("手机号码格式错误!");}//2.校验验证码//2.1 从redis中获取验证码String sendCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);String code = loginForm.getCode(); // String sendCode = (String) session.getAttribute("code");if (code != null || !code.equals(sendCode)){//验证码不一致Result.fail("验证码错误!");}//验证码相同,查询手机号User user = query().eq("phone", loginForm.getPhone()).one();if (user == null) {//用户不存在,创建一个新用户并且保存到数据库中user = createNewUser(phone);}// 用户存在,将用户数据存入Redis//随机生成一个tokenString token = UUID.randomUUID().toString(true);//获取用户数据并转成Map集合UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);Map userMap = BeanUtil.beanToMap(userDTO,new HashMap<>(),CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName,fieldValue)->fieldValue.toString()));//将用户数据存入redis当中String tokenKey = LOGIN_USER_KEY+token;stringRedisTemplate.opsForHash().putAll(tokenKey,userMap);//设置有效期,防止用户过多,导致内存不足stringRedisTemplate.expire(tokenKey,LOGIN_USER_TTL,TimeUnit.MINUTES);// session.setAttribute("user", BeanUtil.copyProperties(user,UserDTO.class));return Result.ok(token);}private User createNewUser(String phone) {//1.创建用户User user = new User();user.setPhone(phone);user.setNickName(USER_NICK_NAME_PREFIX+RandomUtil.randomString(10));//2.保存用户save(user);return user;} }

还没完,Session操作中,我们用户每次请求,都会重新设置存活时间,以确保用户可以持续,而我们使用Redis时,可以在拦截器中设置每次拦截到,重新设置Redis字段的存活时间 。

和Session使用的拦截器不同点在于

1.我们要先获取请求头中的token,用来获取用户数据

2.保存完用户数据后,我们需要重新设置Redis中key的存活时间

具体代码如下:

package com.hmdp.utils;import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.hmdp.dto.UserDTO;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.concurrent.TimeUnit;/*** @Author 华子* @Date 2022/11/20 17:30* @Version 1.0*/
public class RefreshTokenInterceptor implements HandlerInterceptor {private StringRedisTemplate stringRedisTemplate;public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//1.获取请求头中的tokenString token = request.getHeader("authorization");if (StrUtil.isBlank(token)) {return true;}//2.获取用户信息String key = RedisConstants.LOGIN_USER_KEY + token;Map userMap = stringRedisTemplate.opsForHash().entries(key);//3.判断用户信息是否存在if (userMap.isEmpty()){return true;}//5.存在,保存用户信息到ThreadLocal//将从redis中获取到的userMap转成UserDtoUserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);UserHolder.saveUser(userDTO);//6.刷新存活时间stringRedisTemplate.expire(key,RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);//7.放行return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {//移除用户UserHolder.removeUser();}
}

Mvc配置类(MvcConfig):

package com.hmdp.config;import com.hmdp.utils.LoginInterceptor;
import com.hmdp.utils.RefreshTokenInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;import javax.annotation.Resource;/*** @Author 华子* @Date 2022/11/20 17:38* @Version 1.0*/
@Configuration
public class MvcConfig implements WebMvcConfigurer {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new LoginInterceptor()).excludePathPatterns("/shop/**","/voucher/**","/shop-type/**","/upload/**","/blog/hot","/user/code","/user/login");}
}

这里有个小知识点,我们自己创造的类,Spring无法帮我们自动注入,所以我们在Mvc配置类中进行注入 

 

这样就可以成功的注入StringRedisTemplate了~ 

 3.拦截器优化

因为我们上述的拦截器只拦截了部分请求,还没做到百分百刷新用户的存活时间,所以我们可以这样做:

在最外层在加上一个拦截器,拦截所有用户,用于专门刷新存活时间且放行,而第二个拦截器用于判断ThreadLocal中是否含有用户信息,没有就拦截。

 

具体实现代码:

最外层拦截器:(只做刷新)

package com.hmdp.utils;import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.hmdp.dto.UserDTO;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.concurrent.TimeUnit;/*** @Author 华子* @Date 2022/11/20 17:30* @Version 1.0*/
public class RefreshTokenInterceptor implements HandlerInterceptor {private StringRedisTemplate stringRedisTemplate;public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//1.获取请求头中的tokenString token = request.getHeader("authorization");if (StrUtil.isBlank(token)) {return true;}//2.获取用户信息String key = RedisConstants.LOGIN_USER_KEY + token;Map userMap = stringRedisTemplate.opsForHash().entries(key);//3.判断用户信息是否存在if (userMap.isEmpty()){return true;}//5.存在,保存用户信息到ThreadLocal//将从redis中获取到的userMap转成UserDtoUserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);UserHolder.saveUser(userDTO);//6.刷新存活时间stringRedisTemplate.expire(key,RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);//7.放行return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {//移除用户UserHolder.removeUser();}
}

第二次拦截器:(只做拦截)

package com.hmdp.utils;import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.hmdp.dto.UserDTO;
import io.netty.util.internal.StringUtil;
import org.springframework.beans.BeanUtils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.Map;
import java.util.concurrent.TimeUnit;/*** @Author 华子* @Date 2022/11/20 17:30* @Version 1.0*/
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//1.判断ThreadLocal中是否存在用户信息if (UserHolder.getUser() == null){//没有,拦截response.setStatus(401);return false;}//2. 有,放行return true;}}

Mvc配置类:

这里面主要设置了添加拦截器,设置第二个拦截器需要拦截的路径 和两个拦截器的优先级。

package com.hmdp.config;import com.hmdp.utils.LoginInterceptor;
import com.hmdp.utils.RefreshTokenInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;import javax.annotation.Resource;/*** @Author 华子* @Date 2022/11/20 17:38* @Version 1.0*/
@Configuration
public class MvcConfig implements WebMvcConfigurer {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new LoginInterceptor()).excludePathPatterns("/shop/**","/voucher/**","/shop-type/**","/upload/**","/blog/hot","/user/code","/user/login").order(1);registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).order(0);}
}

 

 

 

相关内容

热门资讯

埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...