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

本文介绍了如何使用SpringBoot搭建项目,包括数据库表结构设计、引入依赖、编写启动类和配置文件,以及实现登录功能,特别关注了基于session的登录验证和拦截器的使用。同时,讨论了session在集群环境下的问题及解决方案,如使用Redis进行session共享。
摘要由CSDN通过智能技术生成

项目搭建

  • 前期准备

    • 导入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) {
                //获取手机号,验证手机号是否合规
                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;
            }
        
            @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实现登录

  • 基于session实现登录流程

    • 发送验证码

      • 校验手机号是否合法

        • 合法,生成验证码,并保存到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中,主要是方便后序获取当前登录信息

        验证码登录注册

      @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;
       }     
      
    • 校验登录状态

      • 用户发送请求时,会从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集群,以提供更好地服务。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值