Redis实现短信登录

article/2024/3/2 11:48:58

文章目录

  • 一、基于Session实现登录
  • 二、基于Redis实现共享Session实现登录

一、基于Session实现登录

在这里插入图片描述

---------------------------------------------------Controller
@PostMapping("code")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {//发送短信验证码并保存验证码return userService.sendCode(phone,session);
}
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){//实现登录功能  loginForm 登录参数,包含手机号、验证码;或者手机号、密码return userService.login(loginForm,session);
}
@GetMapping("/me")
public Result me(){// 获取当前登录的用户并返回UserDTO user = UserHolder.getUser();return Result.ok(user);
}
---------------------------------------------------Service
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {@Overridepublic Result sendCode(String phone, HttpSession session) {if(RegexUtils.isPhoneInvalid(phone)){return Result.fail("手机号格式错误");}String code = RandomUtil.randomNumbers(6);session.setAttribute("code",code);log.debug("短信验证码" + code);return Result.ok();}@Overridepublic Result login(LoginFormDTO loginForm, HttpSession session) {if(RegexUtils.isPhoneInvalid(loginForm.getPhone())){return Result.fail("手机号格式错误");}Object catchCode = session.getAttribute("code");String code = loginForm.getCode();if(catchCode == null || !catchCode.equals(code)){Result.fail("验证码错误");}User user = query().eq("phone", loginForm.getPhone()).one();if (user == null) {user = createUserWithPhone(loginForm.getPhone());}session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));return Result.ok();}private User createUserWithPhone(String phone) {User user = new User();user.setPhone(phone);user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomNumbers(3));save(user);return user;}
}
---------------------------------------------------自定义拦截器
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {HttpSession session = request.getSession();Object user = session.getAttribute("user");if (user == null) {response.setStatus(401);return false;}UserHolder.saveUser((UserDTO) user);return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {UserHolder.removeUser();}
}
---------------------------------------------------添加拦截器并指定拦截的请求和不拦截的请求
@Configuration
public class MvcConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {//实现WebMvcConfigurer接口中的addInterceptors方法把自定义的拦截器类添加进来即可registry.addInterceptor(new LoginInterceptor()).excludePathPatterns("/user/code","/user/login","/blog/hot","/shop/**","/shop-type/**","/voucher/**");}
}

总结: 短信验证码用随机工具生成6位数,保存了session中,在用户使用手机号登录时,获取session中的验证码和请求参数中的验证码比对,一致则去库里查该手机号的用户是否存在,不存在则新建用户,并把该用户对象存在在session中。校验登录状态是使用HandlerInterceptor拦截器实现的,在此之前需要配置拦截哪些请求,不拦截哪些请求,从客户端的请求中获取session信息,为空返回401状态,不为空则把用户信息存储在ThreadLocal中,在请求处理完之后销毁用户信息。
问题: 集群的session共享问题。多台Tomcat并不共享session存储空间,当请求切换到不同Tomcat服务时导致数据丢失的问题。早期的Tomcat提供session拷贝功能,但是并不能解决问题,问题有1.多台Tomcat保存相同的数据信息,内存空间浪费;2.拷贝需要时间,在延迟之内有用户访问,多台Tomcat依然存在数据不一致。

二、基于Redis实现共享Session实现登录

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

-------------------只记录有变化的--------------------------------Service
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic Result sendCode(String phone, HttpSession session) {if (RegexUtils.isPhoneInvalid(phone)) {return Result.fail("手机号格式错误");}String code = RandomUtil.randomNumbers(6);stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);log.debug("短信验证码" + code);return Result.ok();}@Overridepublic Result login(LoginFormDTO loginForm, HttpSession session) {String phone = loginForm.getPhone();if (RegexUtils.isPhoneInvalid(phone)) {return Result.fail("手机号格式错误");}String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);String code = loginForm.getCode();if (cacheCode == null || !cacheCode.equals(code)) {return Result.fail("验证码错误");}User user = query().eq("phone", phone).one();if (user == null) {user = createUserWithPhone(phone);}String token = UUID.randomUUID().toString(true);UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));stringRedisTemplate.opsForHash().putAll(LOGIN_USER_KEY + token, userMap);stringRedisTemplate.expire(LOGIN_USER_KEY + token, LOGIN_USER_TTL, TimeUnit.MINUTES);return Result.ok(token);}
}
---------------------------------------------------添加拦截器并指定拦截的请求和不拦截的请求
@Configuration
public class MvcConfig implements WebMvcConfigurer {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new LoginInterceptor()).excludePathPatterns("/user/code","/user/login","/blog/hot","/shop/**","/shop-type/**","/voucher/**").order(1);registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);}
}
---------------------------------------------------自定义拦截器
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//判断ThreadLocal中是否有用户if(UserHolder.getUser() == null){response.setStatus(401);return false;}return true;}
}
public class RefreshTokenInterceptor implements HandlerInterceptor {private StringRedisTemplate stringRedisTemplate;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String token = request.getHeader("authorization");if (StrUtil.isBlank(token)) {return true;}Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(RedisConstants.LOGIN_USER_KEY + token);if (userMap.isEmpty()) {return true;}UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);//保存在ThreadLocal中UserHolder.saveUser(userDTO);//刷新token有效期stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token,RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {UserHolder.removeUser();}public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}
}

总结: 短信验证码修改为以手机号为key验证码为value保存在redis中,在用户使用手机号登录时,获取redis中的验证码和请求参数中的验证码比对,一致则去库里查该手机号的用户是否存在,不存在则新建用户,并把该用户对象存在在redis中。校验登录状态是使用HandlerInterceptor拦截器实现的,在此之前需要配置拦截哪些请求,不拦截哪些请求,从客户端的请求头中获取token信息,并从redis中获取用户信息, 为空返回401状态,不为空则把用户信息存储在ThreadLocal中,还把验证码和用户信息设置有效时间,时间一过,则退出用户登录。在请求处理完之后销毁用户信息。
改造的点:
  发送短信验证码时,key使用手机号来确保唯一存储在redis中,在用户登录时可以根据key来取出验证码校对。
  短信验证码登录时,key使用UUID来确保唯一存储在redis中,为确保将来前端能把token发送过来进行校验,在请求结束前把token返回给客户端。
问题:
1.拦截器能否真正的实现只要用户一直在访问,token就不会过期?
不行,因为拦截器只拦截需要登录的路径,如果用户在有效期内一直访问不需要登录的路径,那么redis中的token就会过期。
问题解决如下图。
在这里插入图片描述


http://www.ngui.cc/article/show-747573.html

相关文章

上线3天,下载4万,ChatGPT 中文版VSCode插件来了

ChatGPT 的 Debug 功能&#xff0c;有人应用化了。 ChatGPT 这几天可谓是风头无两。作为一个问答语言模型&#xff0c;它最大的优点就是可以回答与编程相关的问题&#xff0c;甚至回复一段代码。 尽管有人指出 ChatGPT 生成的代码有错误&#xff0c;但程序员们还是对它写代码、…

SpringCloud-全面详解(学习总结---从入门到深化)

​​​​​​​ 微服务架构进化论 单体应用阶段 (夫妻摊位) 在互联网发展的初期&#xff0c;用户数量少&#xff0c;一般网站的流量也很少&#xff0c;但硬 件成本较高。因此&#xff0c;一般的企业会将所有的功能都集成在一起开发 一个单体应用&#xff0c;然后将该单体应用…

关于前端低代码的一些个人观点

2022&#xff0c;低代码彻底火了&#xff0c;甚至火到没有点相关经验&#xff0c;都不好意思出去面试的程度&#xff0c;堪称lowcode“元年”。在整个互联网大裁员的背景下&#xff0c;无论你是否相信它是降本提效的利器&#xff0c;彷佛都不重要了。因为行业趋势总是这般浩浩荡…

IBRNet:基于IBR的NeRF

IBRNet: Learning Multi-View Image-Based Rendering 针对问题&#xff1a;使NeRF具有泛化能力 如何做&#xff1a;主要还是针对颜色和密度的预测进行改进&#xff08;三维重建部分&#xff09;&#xff0c;和NeRF一样&#xff0c;使用的是volume rendering&#xff08;渲染部…

Python with as 的用法

With语句是什么&#xff1f; 有一些任务&#xff0c;可能事先需要设置&#xff0c;事后做清理工作。对于这种场景&#xff0c;Python的with语句提供了一种非常方便的处理方式。一个很好的例子是文件处理&#xff0c;你需要获取一个文件句柄&#xff0c;从文件中读取数据&#x…

学习在UE中为截屏工具(SceneCapture)添加一种新的源(Source)

SceneCapture 创建一个SceneCapture2D类型的Actor&#xff0c;再新建一个RenderTarget资源交给它。随后&#xff0c;就能看到截屏的数据出现在RenderTarget中。通过修改 Capture Source &#xff0c;可以改变截取的数据源&#xff0c;比如法线、基础色等等。 目标 本篇的目…

【前沿技术RPA】 一文了解 UiPath 状态机 State Machine

&#x1f40b;作者简介&#xff1a;博主是一位.Net开发者&#xff0c;同时也是RPA和低代码平台的践行者。 &#x1f42c;个人主页&#xff1a;会敲键盘的肘子 &#x1f430;系列专栏&#xff1a;UiPath &#x1f980;专栏简介&#xff1a;UiPath在传统的RPA&#xff08;Robotic…

LeetCode Exercise Track—Day 1 | 704. Binary Search | 27. Remove Element |

文章目录Basics of Array TheoryLeetCode 704. Binary SearchLeetCode 27. Remove ElementBasics of Array Theory Array index start from 0Array is a data collection stored in the contiguous memory spaceBecause the array’s memory space is contiguous, we must mod…

前端工程化实践——快速入门treeshaking

treeshaking treeshaking本质是去除多余代码算法。在前端的性能优化中&#xff0c;es6 推出了tree shaking机制&#xff0c;tree shaking就是当我们在项目中引入其他模块时&#xff0c;他会自动将我们用不到的代码&#xff0c;或者永远不会执行的代码摇掉&#xff0c;在…

C++--类型转换--1128

1.C语言中的类型转换 分为隐式类型转化、显示强制类型转化。 隐式类型转化用于意义相近的类型&#xff0c;比如int,double,short都是表示数值的类型 int i1; double di; //编译、结果无问题 这里是隐式类型转换。 显示强制类型转换 显示强制类型用于意义不相近的类型&…