Redis实现短信登录的示例代码
一、基于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 { @Override public 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(); } @Override public 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 { @Override public 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; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { UserHolder.removeUser(); } } ---------------------------------------------------添加拦截器并指定拦截的请求和不拦截的请求 @Configuration public class MvcConfig implements WebMvcConfigurer { @Override public 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 { @Resource private StringRedisTemplate stringRedisTemplate; @Override public 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(); } @Override public 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 { @Resource private StringRedisTemplate stringRedisTemplate; @Override public 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 { @Override public 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; @Override public 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; } @Override public 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就会过期。问题解决如下图。
到此这篇关于Redis实现短信登录的示例代码的文章就介绍到这了,更多相关Redis 短信登录内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
相关文章
Spring redis使用报错Read timed out排查及解决过程
项目使用spring集成redis,偶尔会出现read timed out的情况,刚开始以为是网络不稳定引起的,后面发现影响业务测试的准确性,这篇文章主要给大家介绍了关于Spring redis使用报错Read timed out排查及解决过程的相关资料,需要的朋友可以参考下2024-02-02Springboot/Springcloud项目集成redis进行存取的过程解析
大家都知道Redis支持五种数据类型:string(字符串),hash(哈希),list(列表),set(集合),zset(sorted set:有序集合),本文重点给大家介绍Springboot/Springcloud项目集成redis进行存取的过程,需要的朋友参考下吧2021-12-12
最新评论