​Redis实战——短信登录(一)

原创 L1296686146 冗谪 2023-06-29 14:03 发表于陕西

收录于合集#redis7个

项目搭建

  • 前期准备

    • 导入SQL

      CREATE TABLE `tb_user` (
        `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
        `phone` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '手机号码',
        `password` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '密码,加密存储',
        `nick_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '昵称,默认是用户id',
        `icon` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '人物头像',
        `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
        `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
        PRIMARY KEY (`id`) USING BTREE,
        UNIQUE KEY `uniqe_key_phone` (`phone`) USING BTREE
      ) ENGINE=InnoDB AUTO_INCREMENT=1011 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=COMPACT;
      

    • 创建项目

      • 导入依赖

        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
           <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
                <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-boot-starter</artifactId>
                <version>3.4.3</version>
            </dependency>
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <scope>runtime</scope>
                <version>8.0.33</version>
            </dependency>
            
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <optional>true</optional>
            </dependency>
        
            <!--hutool-->
            <dependency>
                <groupId>cn.hutool</groupId>
                <artifactId>hutool-all</artifactId>
                <version>5.7.17</version>
            </dependency>
        </dependencies>
        
      • 编写启动类

        @MapperScan("com.liang.mapper")
        @SpringBootApplication
        public class HmDianPingApplication {
        
            public static void main(String[] args) {
                SpringApplication.run(HmDianPingApplication.class, args);
            }
        
        }
        
      • 编写配置文件

        server:
          port: 8081
        spring:
          datasource:
            driver-class-name: com.mysql.cj.jdbc.Driver
            url: #配置自己的数据库url
            username: #配置自己的数据库用户名
            password: #配置自己的密码
        
      • 编写实体类

        /**
         * 登录信息
         */
        @Data
        public class LoginFormDTO {
            private String phone;
            private String code;
        
        }
        
        /**
         * 统一结果返回
         */
        @Data
        @NoArgsConstructor
        @AllArgsConstructor
        public class Result {
            private Boolean success;
            private String errorMsg;
            private Object data;
            private Long total;
        
            public static Result ok(){
                return new Result(true, null, null, null);
            }
            public static Result ok(Object data){
                return new Result(true, null, data, null);
            }
            public static Result ok(List<?> data, Long total){
                return new Result(true, null, data, total);
            }
            public static Result fail(String errorMsg){
                return new Result(false, errorMsg, null, null);
            }
        }
        
        /**
         * User实体类 对应数据库表tb_user
         */
        @Data
        @TableName("tb_user")
        public class User implements Serializable {
        
            private static final long serialVersionUID = 1L;
        
            /**
             * 主键
             */
            @TableId(value = "id", type = IdType.AUTO)
            private Long id;
        
            /**
             * 手机号码
             */
            private String phone;
        
            /**
             * 密码,加密存储
             */
            private String password;
        
            /**
             * 昵称,默认是随机字符
             */
            private String nickName;
        
            /**
             * 用户头像
             */
            private String icon = "";
        
            /**
             * 创建时间
             */
            private LocalDateTime createTime;
        
            /**
             * 更新时间
             */
            private LocalDateTime updateTime;
        }
        
        /**
         * 存储用户非敏感信息
         */
        @Data
        public class UserDTO {
            private Long id;
            private String nickName;
            private String icon;
        }
        
      • 编写controller层

        /**
         * User对象前端控制器
         */
        @Slf4j
        @RestController
        @RequestMapping("/user")
        public class UserController {
        
            @Resource
            private IUserService userService;
        
            /**
             * 发送手机验证码
             * @param phone 手机号
             * @param session
             * @return
             */
            @PostMapping("code")
            public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
                return userService.sendCode(phone, session)?Result.ok():Result.fail("手机号码不合规");
            }
        
            /**
             *  登录功能
             * @param loginForm
             * @param session
             * @return
             */
            @PostMapping("/login")
            public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
                return userService.login(loginForm, session) ? Result.ok() : Result.fail("手机号或验证码错误");
            }
        
      • 编写service层

        public interface IUserService extends IService<User> {
        
            boolean sendCode(String phone, HttpSession session);
        
            boolean login(LoginFormDTO loginForm, HttpSession session);
        }
        
        @Service
        public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
        
            @Override
            public boolean sendCode(String phone, HttpSession session) {
             
                return true;
            }
        
            @Override
            public boolean login(LoginFormDTO loginForm, HttpSession session) {
           
                return true;
            }
        }

Session实现登录

@Override
 public boolean login(LoginFormDTO loginForm, HttpSession session) {
        //获取手机号
        String phone = loginForm.getPhone();
        //验证手机号是否合理
        boolean mobile = PhoneUtil.isMobile(phone);
        //如果不合理 提示
        if (!mobile){
            //提示用户手机号不合理
            return false;
        }
        //手机号合理 进行验证码验证
        String code = loginForm.getCode();
        String sessionCode = session.getAttribute("code").toString();
        //如果验证码输入的是错误的  提示
        if (!code.equals(sessionCode)){
            return false;
        }
        //如果验证码也正确 那么通过手机号进行查询
        User user = this.getOne(new LambdaQueryWrapper<User>().eq(User::getPhone, phone));
        // 数据库中没查询到用户信息
        if (ObjectUtil.isNull(user)){
            user = new User();
            user.setPhone(phone);
            user.setNickName("user_"+ RandomUtil.randomString(10));
            this.save(user);
        }
        // 将该用户信息存入session中
        // 简化user,只存储必要信息以及不重要的信息
        UserDTO userDTO = BeanUtil.toBean(user, UserDTO.class);
        session.setAttribute("user", userDTO);
        return true;
 }     
  • 基于session实现登录流程

    • 发送验证码

    • 校验手机号是否合法

      图片

      发送验证码流程

      @Override
      public boolean sendCode(String phone, HttpSession session) {
              //获取手机号,验证手机号是否合规
              boolean mobile = PhoneUtil.isMobile(phone);
              //不合规,则提示
              if (!mobile){
                  return false;
              }
              //生成验证码
              String code = RandomUtil.randomNumbers(6);
              //将验证码保存到session中
              session.setAttribute("code",code);
              //发送验证码
              System.out.println("验证码:" + code);
              return true;
      }
      • 合法,生成验证码,并保存到session中、发送验证码给用户

      • 不合法,提示用户手机号不合法

  • 验证码登录、注册

    • 验证手机号是否合法,验证验证码是否正确

      • 手机号不合法或验证码不正确,提示用户

    • 验证成功后,查看该用户信息是否在数据库中

      • 该用户信息在数据库中,则表明该用户是登录

        • 用户信息保存到session中

      • 该用户信息不在数据库中,则表明该用户是注册

        • 在数据库中存储用户信息

        • 用户信息保存到session中

    • 将用户信息存储在session中,主要是方便后序获取当前登录信息

      图片

      验证码登录注册

  • 校验登录状态

    • 用户发送请求时,会从cookie中携带JsessionId到后台,后台通过JsessionId从session中获取用户信息,

      • 没获取到用户信息 则拦截,需要拦截器

      • 获取到用户信息,则将用户信息保存到ThreadLocal中,再放行

图片

校验登录状态

  • 自定义拦截器,实现HandlerInterceptor接口

    public class LoginInterceptor implements HandlerInterceptor {
    
        /**
         * preHandle方法的返回值决定是否放行,该方法在控制层方法执行前执行
         * @param request
         * @param response
         * @param handler
         * @return
         * @throws Exception
         */
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            HttpSession session = request.getSession();
            UserDTO user = (UserDTO) session.getAttribute("user");
            //判断是否在session中获取到了用户
            if (ObjectUtil.isNull(user)){
                return false;
            }
            UserHolder.saveUser(user);
            return true;
        }
    
        /**
         * postHandle方法在控制层方法执行后,视图解析前执行(可以在这里修改控制层返回的视图和模型)
         * @param request
         * @param response
         * @param handler
         * @param modelAndView
         * @throws Exception
         */
        @Override
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
            HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
        }
    
        /**
         * fterCompletion方法在视图解析完成后执行,多用于释放资源
         * @param request
         * @param response
         * @param handler
         * @param ex
         * @throws Exception
         */
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
            HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
        }
    }
    
  • 实现WebMvcConfigurer接口,通过重写addInterceptors方法添加自定义拦截器

    @Configuration
    public class MvcConfig implements WebMvcConfigurer {
    
        /**
         * 添加拦截器
         * @param registry
         */
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            //添加拦截器
            registry.addInterceptor(new LoginInterceptor())
                    //放行资源
                    .excludePathPatterns(
                            "/shop/**",
                            "/voucher/**",
                            "/shop-type/**",
                            "/upload/**",
                            "/blog/hot",
                            "/user/code",
                            "/user/login"
                    )
                    // 设置拦截器优先级
                    .order(1);
        }
    }
  • 注意隐藏用户敏感信息

    • 我们应当在返回用户信息之前,将用户敏感信息进行隐藏,采用的核心思路就是创建UserDTO类,该类没有用户敏感信息,在返回用户信息之前,将有用户敏感新的的User对象转换为没有敏感信息的UserDTO对象,就可以有效的避免用户信息被泄露的问题。

Session存在问题

  • 当单个tomcat服务器时,服务器崩溃,无法提供足够的处理能力时,系统可能不能使用,为了避免这些情况,提高系统的可用性、可伸缩性等,tomcat将会以集群的形式部署,集群部署的主要优势有: 高可用性 , 可伸缩性 , 负载均衡 , 无中断升级 。

  • 集群部署的tomcat又面临新的问题,即session共享问题,由于每个tomcat都有一份属于自己的session,某个用户第一次访问tomcat时,把自己的信息存放到了编号01的tomcat服务器的session中,当第二次访问时,没有访问01服务器,而是访问到了其他tomcat服务器,而其他tomcat服务器没有该用户存放的session,此时整个登录拦截都会出现问题。

    • 解决方式:

      • 早期方案是session拷贝,即每当任意一台服务器的session修改时,都会同步到其他的tomcat服务器的session中,实现session共享。但此方式存在问题: ①session数据拷贝时,可能会出现延时;②每台服务器中都有完整的一份session数据,服务器压力较大 。

      • 现在方案是基于redis来完成,即把session换成redis,redis数据本身就是共享的,可以避免session共享问题。而且redis中数据是 key-value方式存储 和session一样便于操作,且都默认 存储在内存 中,响应速度快。

图片

集群模型

    客户端发送请求,通过nginx负载均衡到下游的tomcat服务器(一台4核8G的tomcat服务器,在优化和处理简单业务的加持下,处理的并发量很有限),经过nginx负载均衡分流后,利用集群支撑整个项目,同时nginx在部署了前端项目后,做到了动静分离,进一步降低tomcat的压力,
    如果让tomcat直接访问mysql,一般16、32核CPU、32/64G内存,并发量在4k~7K左右,在高并发场景下也是容易崩溃,所有一般会使用mysql集群,同时为了进一步降低mysql压力,增加访问性能,一般会加入redis集群,以提供更好地服务。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值