• Redis从理论到实战:如何使用redis实现短信登录与注册?



    加油加油,不要过度焦虑O(∩_∩)O

    一、基于session实现短信登录

    为了方便理解,先来看看不用redis,基于session来实现登录;然后分析存在的问题缺陷,最后用redis解决存在的问题!

    1、实现发送验证码功能

    在这里插入图片描述

    思路分析:

    • 首先验证输入的手机号格式是否正确;不正确则重新输入手机号,正确则模拟发送6位数的验证码;
    • 最后把手机号和验证码保存到session中。

    代码:

        @PostMapping("/code")
        public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
            return userService.generateCode(phone, session);
        }
        @Override
        public Result generateCode(String phone, HttpSession session) {
            if (RegexUtils.isPhoneInvalid(phone)) {
                return Result.fail("手机号格式错误");
            }
            //手机号格式正确,则生成验证码
            String code = RandomUtil.randomNumbers(6);
            //将验证码和手机号保存到session
            session.setAttribute("code", code);
            session.setAttribute("phone", phone);
            log.info("生成的验证码是:{}", code);
            return Result.ok();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    2、实现用户登录和注册功能(小优化)

    思路分析:

    • 从session中获取手机号(避免用户获取验证码后修改手机号),如果手机号跟表单中的手机号不一致(即用户修改了手机号),则让用户重新发送验证码;
    • 接着从session中获取验证码,如果表单中的验证码和发送的验证码不一致,则让用户重新输入验证码;
    • 然后从数据库中查询是否存在该手机号(即该用户是否已经注册过),如果不存在,则把该用户插入到数据库;
    • 最后返回用户信息(为了减少Tomcat服务器的内存消耗以及用户信息的安全,会使用UserDTO类来返回用户的部分信息

    代码:

        @PostMapping("/login")
        public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session) {
            return userService.login(loginForm, session);
        }
        @Override
        public Result login(LoginFormDTO loginForm, HttpSession session) {
            //校验手机号和验证码
            String phone = (String) session.getAttribute("phone");
            if (!phone.equals(loginForm.getPhone())) {
                return Result.fail("手机号改变,请重新获取验证码");
            }
            String code = (String) session.getAttribute("code");
            String cacheCode = loginForm.getCode();
            if (cacheCode == null || !cacheCode.equals(code)) {
                return Result.fail("验证码错误");
            }
            //判断用户是否存在
            LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>()
                    .eq(User::getPhone, phone);
            User user = userMapper.selectOne(wrapper);
            if (user == null) {
                user = createUser(phone);
            }
            //优化:减少Tomcat内存的使用并隐藏用户的敏感信息
            session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));
            return Result.ok();
        }
        /**
         * 创建用户
         *
         * @param phone
         * @return
         */
        private User createUser(String phone) {
            User user = new User();
            user.setPhone(phone);
            user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(8));
            userMapper.insert(user);
            return user;
        }
    	@Data
    	public class UserDTO {
    	    private Long id;
    	    private String nickName;
    	    private String icon;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    3、实现登录校验拦截器功能

    思路分析:

    • 为了避免在多个controller层中实现登录校验功能,可以使用拦截器,在访问controller之前,进行登录校验;
    • 在后续的业务中,因为要用到用户信息,所以要把拦截器中的用户信息传到controller中,并且,为了确保线程安全问题,可以使用ThreadLocal类。我们可以把拦截到的用户信息保存到ThreadLocal对象中。
    • Java中的ThreadLocal详解

    public class UserHolder {
        private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();
        public static void saveUser(UserDTO user){
            tl.set(user);
        }
        public static UserDTO getUser(){
            return tl.get();
        }
        public static void removeUser(){
            tl.remove();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    拦截器配置代码:

    public class LoginInterceptor implements HandlerInterceptor {
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            HttpSession session = request.getSession();
            UserDTO user = (UserDTO) session.getAttribute("user");
            if (user == null) {
                response.setStatus(401);
                return false;
            }
            UserHolder.saveUser(user);
            return true;
        }
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
            UserHolder.removeUser();
        }
    }
    @Configuration
    public class MyConfiguration implements WebMvcConfigurer {
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(new LoginInterceptor())
                    .excludePathPatterns(
                            "/shop/**",
                            "/voucher/**",
                            "/shop-type/**",
                            "/upload/**",
                            "/blog/hot",
                            "/user/code",
                            "/user/login"
                    );
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    controller层代码:

        @GetMapping("/me")
        public Result me() {
            UserDTO user = UserHolder.getUser();
            return Result.ok(user);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    最后来看看基于session登录的完整流程:

    在这里插入图片描述


    二、session共享的问题分析

    为什么使用Redis实现登录功能,而不使用基于Session实现登录功能?考虑到多台Tomcat并不共享session存储空间(虽然多台Tomcat可以对数据进行拷贝,但是不仅会造成内存空间的浪费,而且还会因为存在数据拷贝时间上的延迟,如果在延迟时间内有使用者来访问,依然会出现数据不一致的情况!),当请求切换到不同tomcat服务时会导致数据丢失!所以,我们的解决方案应该满足:数据共享、内存存储、key-value结构。


    三、基于Redis实现短信登录

    1、实现发送验证码功能

    思路分析:

    • 跟基于session发送验证码不同的是:我们把验证码放到redis数据库中,并设置验证码过期时间;
    • 使用字符串数据结构:以手机号为key,验证码为value。

    代码实现:

        @PostMapping("/code")
        public Result sendCode(@RequestParam("phone") String phone) {
            return userService.generateCode(phone);
        }
        @Override
        public Result generateCode(String phone) {
            if (RegexUtils.isPhoneInvalid(phone)) {
                return Result.fail("手机号格式错误");
            }
            String code = RandomUtil.randomNumbers(6);
            //设置验证码过期时间为2minutes
            redisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);
            log.info("生成的验证码是:{}", code);
            return Result.ok();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    2、实现用户登录和注册功能

    思路分析:

    • 跟基于session实现用户登录和注册不同的是:我们把用户对象放到redis数据库中并设置有效期;
    • 对象存储使用哈希结构,随机生成token,作为登录令牌,并以token作为对象存储的key,对象值为value。

    代码实现:

        @Override
        public Result login(LoginFormDTO loginForm) {
            String phone = loginForm.getPhone();
            if (!redisTemplate.hasKey(LOGIN_CODE_KEY + phone)) {
                return Result.fail("手机号改变,请重新获取验证码");
            }
            String cacheCode = redisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
            String code = loginForm.getCode();
            if (code == null || !code.equals(cacheCode)) {
                return Result.fail("验证码错误");
            }
            LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>()
                    .eq(User::getPhone, phone);
            User user = userMapper.selectOne(wrapper);
            if (user == null) {
                user = createUser(phone);
            }
            //保存用户信息到redis中
            String token = UUID.randomUUID().toString(true);
            UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
            HashMap<String, String> hashMap = getHashMap(userDTO);
            token = LOGIN_USER_KEY + token;
            redisTemplate.opsForHash().putAll(token, hashMap);
            //设置有效期为30分钟
            redisTemplate.expire(token, LOGIN_USER_TTL, TimeUnit.SECONDS);
            return Result.ok(token);
        }
    
        /**
         * 用hashmap存储userDTO对象
         * @param userDTO
         * @return
         */
        private HashMap<String, String> getHashMap(UserDTO userDTO) {
            HashMap<String, String> hashMap = new HashMap<>();
            hashMap.put("id", userDTO.getId().toString());
            hashMap.put("nickName", userDTO.getNickName());
            hashMap.put("icon", userDTO.getIcon());
            return hashMap;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    3、用拦截器实现用户有效期更新

    • 我们在service层设置了用户在不做任何操作的情况下,保存用户信息的有效期为30分钟,但是如果用户进行了某种操作,比如点赞、发布博客等等需要登录后才能完成的操作,就需要重新设置用户信息有效期为30分钟。

    代码实现:

    public class LoginInterceptor implements HandlerInterceptor {
        private StringRedisTemplate redisTemplate;
        //构造器注入
        public LoginInterceptor(StringRedisTemplate redisTemplate) {
            this.redisTemplate = redisTemplate;
        }
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            // 获取请求头中的token
            String token = request.getHeader("authorization");
            if (StrUtil.isBlank(token)) {
                response.setStatus(401);
                return false;
            }
            // 基于token获取redis中的用户
            Map<Object, Object> userMap = redisTemplate.opsForHash().entries(token);
            // 判断用户是否存在
            if (userMap.isEmpty()) {
                response.setStatus(401);
                return false;
            }
            UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
            UserHolder.saveUser(userDTO);
            // 刷新token有效期
            redisTemplate.expire(token, LOGIN_USER_TTL, TimeUnit.MINUTES);
            return true;
        }
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
            UserHolder.removeUser();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    4、拦截器功能的优化

    思路分析:

    • 前面设计的拦截器还是存在一些问题:如果用户在30分钟的时间内,操作的是拦截器不拦截的操作(比如浏览商铺、查看他人博客),那么就会导致用户的有效期时间不会刷新;
    • 为了解决这个问题,我们可以再加一个拦截器,把它放在第一个位置,用来拦截所有的请求。

    代码实现:

    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 {
            // 获取请求头中的token
            String token = request.getHeader("authorization");
            if (StrUtil.isBlank(token)) {
                return true;
            }
            // 基于token获取redis中的用户
            Map<Object, Object> userMap = redisTemplate.opsForHash().entries(token);
            // 判断用户是否存在
            if (userMap.isEmpty()) {
                return true;
            }
            UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
            UserHolder.saveUser(userDTO);
            // 刷新token有效期
            redisTemplate.expire(token, LOGIN_USER_TTL, TimeUnit.SECONDS);
            return true;
        }
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
            UserHolder.removeUser();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    public class LoginInterceptor implements HandlerInterceptor {
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            if (UserHolder.getUser() == null) {
                response.setStatus(401);
                return false;
            }
            //有用户则放行
            return true;
        }
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
            UserHolder.removeUser();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    @Configuration
    public class MyConfiguration implements WebMvcConfigurer {
        @Autowired
        private StringRedisTemplate redisTemplate;
    
        @Override
        public 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(redisTemplate))
                    .addPathPatterns("/**")
                    .order(0);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    整体思路如下:

    在这里插入图片描述


    这些都是自己的理解,有什么疑问可以一起交流讨论~

  • 相关阅读:
    Geotrust哪种通配符证书800
    大模型微调发展-学习调研总结
    【无标题】
    微信扫码授权登录手游(你使用的浏览器暂不支持微信登录)
    安卓恶意应用识别(三)(批量反编译与属性值提取)
    Live555库结构及其核心概念
    k8s安全机制
    第九章 泛型编程
    面试系列分布式事务:谈谈3PC的理解
    介绍一款数据准实时复制(CDC)中间件 `Debezium`
  • 原文地址:https://blog.csdn.net/weixin_59654772/article/details/127829219