IT俱乐部 Redis Redis实现短信登录的示例代码

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 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 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 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 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 短信登录内容请搜索IT俱乐部以前的文章或继续浏览下面的相关文章希望大家以后多多支持IT俱乐部!

本文收集自网络,不代表IT俱乐部立场,转载请注明出处。https://www.2it.club/database/redis/9009.html
上一篇
下一篇
联系我们

联系我们

在线咨询: QQ交谈

邮箱: 1120393934@qq.com

工作时间:周一至周五,9:00-17:30,节假日休息

关注微信
微信扫一扫关注我们

微信扫一扫关注我们

返回顶部