【Redis应用】基于Redis实现短信登录

本章目的

  • [ 掌握session实现登录的流程 ]

  • [ 认识使用session实现登录的缺点 ]

  • [ 掌握引入redis实现登录的流程 ]


前置知识(基于session实现短信登录)

在学习redis实现登录功能前,我先学习一下基于session实现的登录功能,然后再来思考一下基于session来实现的登录功能会有什么缺点,为什么要使用redis来实现我们这个短信登录呢?

在解决整个问题之前我先学习了session是如何实现的短信登录。 那么我们根据上面的流程去实现一下这个后端的登录功能

controller层

  • 发送验证码
less 复制代码
@PostMapping("code")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
    // 发送短信验证码并保存验证码
    return userInfoService.sendCode(phone,session);
}
  • 登录(进行验证码的校验)
less 复制代码
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
    // 实现登录功能
    return userService.login(loginForm,session);
}
  • 校验
java 复制代码
@GetMapping("/me")
public Result me(){
    // 获取当前登录的用户并返回
    UserDTO user = UserHolder.getUser();
    return Result.ok(user);
}

Service层

  • 发送验证码
arduino 复制代码
Result sendCode(String phone, HttpSession session);

impl层

typescript 复制代码
@Override
public Result sendCode(String phone, HttpSession session) {
    // 判断手机号码是否正确
    if (RegexUtils.isPhoneInvalid(phone)) {
        // 如果手机号不合法
        return Result.fail(SystemConstants.PHONE_INVALID);
    }
    // 如果手机号码正确,生成验证码
    String code = RandomUtil.randomNumbers(6);
    // 将验证码保存至session
    session.setAttribute("code",code);
    // 发送验证码(短信的发送需要调用第三方平台,这里就不写了)
    log.debug("发送短信验证码成功,验证码:{}",code);
    return Result.ok();
}
  • 登录(进行验证码的校验)
scss 复制代码
Result login(LoginFormDTO loginForm, HttpSession session);

impl层

scss 复制代码
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
    /**
     * 短信校验登录注册
     */
    // 校验手机号
    if (loginForm.getPhone() == null || RegexUtils.isPhoneInvalid(loginForm.getPhone())){
        return Result.fail(SystemConstants.PHONE_INVALID);
    }
    // 判断验证码是否正确
    Object code = session.getAttribute(SystemConstants.LOGIN_CODE);
    if (loginForm.getCode() == null || !code.toString().equals(loginForm.getCode())) {
        return Result.fail(SystemConstants.LOGIN_CODE_WRONG);
    }
    // 通过手机号获取用户信息
    User userInfo = lambdaQuery().eq(User::getPhone, loginForm.getPhone()).one();
    // 判断是否存在该用户
    if(userInfo == null){
        // 创建新用户,并将用户信息存入数据库
        userInfo = createUserWithPhone(loginForm.getPhone());
    }
    UserDTO userDTO = new UserDTO();
    BeanUtils.copyProperties(userInfo,userDTO);
    // 保存用户到session
    session.setAttribute(SystemConstants.USER_INFO,userDTO);
    return Result.ok();
}

private User createUserWithPhone(String phone) {
    User user = new User();
    user.setPhone(phone);
    user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
    save(user);
    return user;
}

注意:这里说一下为什么使用UserDTO,我们的session是存放在浏览器的(基于内存),那么我们从数据库里查询出来的是完整的用户信息,如果我们将其存放在session的缺点:

  • 不安全,敏感信息直接显示在session(浏览器)
  • 占用内存 所以我们会自定义一个DTO,去隐藏我们的敏感信息。

拦截器实现校验功能

我们的校验是针对整个项目而言的,那我们的项目中存在很多的controller,这样我们每次发送请求的时候就都需要去重复写一个校验请求,如何更加简洁呢?其实只需要引入一个拦截器,用户发送请求,先经过拦截器进行判断,再将请求发送给controller。实现如下:

  1. 拦截器的实现流程
java 复制代码
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.获取session
        HttpSession session = request.getSession();
        // 2.获取session中的用户信息
        Object userInfo = session.getAttribute(SystemConstants.USER_INFO);
        // 3.判断用户信息是否存在
        if (Objects.isNull(userInfo)) {
            // 3.2 不存在放行
            response.setStatus(401);
            return false;
        }
        // 3.1 存在保存到threadLocal
        BeanUtils.copyProperties(userInfo,userInfo);
        UserHolder.saveUser((UserDTO) userInfo);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserHolder.removeUser();
    }
}
  1. 配置拦截器
typescript 复制代码
@Configuration
public class MvcConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(
                        "/user/code",
                        "/user/login",
                        "/blog/hot",
                        "/shop/**",
                        "/shop-type/**",
                        "voucher/**"
                );
    }
}

session的缺点(集群情况)

session共享问题:多台Tomcat并不共享session存储空间,当请求切换到不同的tomcat服务时导致数据丢失的问题 解决的思想:让session可以共享(数据拷贝) --》 问题:时间延迟,数据丢失、 session的替代方案:

  • 数据共享
  • 内存存储
  • key,value结构 redis中满足上面的解决思想要求

基于Redis实现共享session登录

基于redis实现登录业务更改

scss 复制代码
@Autowired
private RedisTemplate redisTemplate;

@Override
public Result sendCode(String phone, HttpSession session) {
    // 判断手机号码是否正确
    if (RegexUtils.isPhoneInvalid(phone)) {
        // 如果手机号不合法
        return Result.fail(SystemConstants.PHONE_INVALID);
    }
    // 如果手机号码正确,生成验证码
    String code = RandomUtil.randomNumbers(6);
    // 将验证码保存至session
    // session.setAttribute(SystemConstants.LOGIN_CODE,code);

    // 将验证码保存至redis
    redisTemplate.opsForValue().set(RedisConstants.LOGIN_CODE_KEY+phone,code,RedisConstants.LOGIN_CODE_TTL, TimeUnit.MINUTES);
    // 发送验证码
    log.debug("发送短信验证码成功,验证码:{}",code);
    return Result.ok();
}

@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
    /**
     * 短信校验登录注册
     */
    // 校验手机号
    if (loginForm.getPhone() == null || RegexUtils.isPhoneInvalid(loginForm.getPhone())){
        return Result.fail(SystemConstants.PHONE_INVALID);
    }
    // 判断验证码是否正确
    // Object code = session.getAttribute(SystemConstants.LOGIN_CODE);

    // 获取redis中的验证码进行校验
    Object code = stringRedisTemplate.opsForValue().get(RedisConstants.LOGIN_CODE_KEY+loginForm.getPhone());

    if (loginForm.getCode() == null || !code.toString().equals(loginForm.getCode())) {
        return Result.fail(SystemConstants.LOGIN_CODE_WRONG);
    }
    // 通过手机号获取用户信息
    User userInfo = lambdaQuery().eq(User::getPhone, loginForm.getPhone()).one();
    // 判断是否存在该用户
    if(userInfo == null){
        // 创建新用户,并将用户信息存入数据库
        userInfo = createUserWithPhone(loginForm.getPhone());
    }

    UserDTO userDTO = new UserDTO();
    BeanUtils.copyProperties(userInfo,userDTO);

    // 保存用户到session
    // session.setAttribute(SystemConstants.USER_INFO,userDTO);

    // 7.保存用户信息到redis
    // 7.1 生成一个随机token,作为登录令牌
    String token = UUID.randomUUID().toString();

    // 7.2 将userInfo对象转为hash存储
    Map<String, Object> map = BeanUtil.beanToMap(userDTO,new HashMap<>(),
            CopyOptions.create().setIgnoreNullValue(true)
                    .setFieldValueEditor((fieldName,fieldValue)-> fieldValue.toString()));
    // 7.3 保存数据到redis
    String key = RedisConstants.LOGIN_USER_KEY + token;
    stringRedisTemplate.opsForHash().putAll(key,map);
    stringRedisTemplate.expire(token,RedisConstants.LOGIN_USER_TTL,TimeUnit.MINUTES);
    // 返回token
    return Result.ok(token);
}

private User createUserWithPhone(String phone) {
    User user = new User();
    user.setPhone(phone);
    user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
    save(user);
    return user;
}

拦截器:

java 复制代码
public class LoginInterceptor implements HandlerInterceptor {
    
    private RedisTemplate redisTemplate;

    public LoginInterceptor(RedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.获取请求头中的token
        String token = request.getHeader("authorization");
        if (StrUtil.isBlank(token)) {
            response.setStatus(401);
            return false;
        }
        // 2.获取redis中的用户信息
        String key = RedisConstants.LOGIN_USER_KEY + token;
        Map<Object,Object> userMap = redisTemplate.opsForHash().entries(key);
        // 3.判断用户信息是否存在
        if (userMap.isEmpty()) {
            // 3.2 不存在放行
            response.setStatus(401);
            return false;
        }
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        // 3.1 存在保存到threadLocal
        UserHolder.saveUser(userDTO);
        // 刷新redis过期时间
        redisTemplate.expire(key,RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserHolder.removeUser();
    }
}

优化拦截器

上面就成功实现了我们的一个基于redis实现登录功能,但是上面有一个bug,我们知道,我们的拦截器是针对我们的一个需要登录的路径做拦截,顾名思义就是针对需要登录的路径才做redis对token过期时间的刷新,那么这就会造成用户如果长期访问的是无需登录的页面,也会造成token过期 ,这是不友好的。怎么办呢?我们可以多加一个拦截器,进行拦截器的分工

RefreshTokenInterceptor拦截器

用于拦截一切请求

java 复制代码
public class RefreshTokenInterceptor implements HandlerInterceptor {

    private StringRedisTemplate redisTemplate;

    public RefreshTokenInterceptor(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.获取请求头中的token
        String token = request.getHeader("authorization");
        if (StrUtil.isBlank(token)) {
            return true;
        }
        // 2.获取redis中的用户信息
        String key = RedisConstants.LOGIN_USER_KEY + token;
        Map<Object,Object> userMap = redisTemplate.opsForHash().entries(key);
        // 3.判断用户信息是否存在
        if (userMap.isEmpty()) {
            return true;
        }
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        // 3.1 存在保存到threadLocal
        UserHolder.saveUser(userDTO);
        // 刷新redis过期时间
        redisTemplate.expire(key,RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserHolder.removeUser();
    }
}

LoginInterceptor拦截器

用于拦截需要登录的路径

java 复制代码
public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 判断是否需要做拦截
        UserDTO user = UserHolder.getUser();
        if (user == null) {
            response.setStatus(401);
            return false;
        }

        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserHolder.removeUser();
    }
}
typescript 复制代码
@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @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(redisTemplate))
                .addPathPatterns("/**").order(0);
    }
}

这里有多个拦截器,那么我们可以使用order来指定拦截器的一个执行顺序,order值越小,优先级越高

好啦这样我们的一个基于redis实现的短信登录就实现啦!!!

相关推荐
A_cot8 小时前
Redis 的三个并发问题及解决方案(面试题)
java·开发语言·数据库·redis·mybatis
芊言芊语9 小时前
分布式缓存服务Redis版解析与配置方式
redis·分布式·缓存
攻城狮的梦10 小时前
redis集群模式连接
数据库·redis·缓存
Amagi.13 小时前
Redis的内存淘汰策略
数据库·redis·mybatis
无休居士14 小时前
【实践】应用访问Redis突然超时怎么处理?
数据库·redis·缓存
.Net Core 爱好者14 小时前
Redis实践之缓存:设置缓存过期策略
java·redis·缓存·c#·.net
码爸17 小时前
flink 批量压缩redis集群 sink
大数据·redis·flink
微刻时光18 小时前
Redis集群知识及实战
数据库·redis·笔记·学习·程序人生·缓存
丁总学Java19 小时前
如何使用 maxwell 同步到 redis?
数据库·redis·缓存
蘑菇蘑菇不会开花~19 小时前
分布式Redis(14)哈希槽
redis·分布式·哈希算法