【尚庭公寓SpringBoot + Vue 项目实战】移动端登录管理(二十)

【尚庭公寓SpringBoot + Vue 项目实战】移动端登录管理(二十)


文章目录

      • 【尚庭公寓SpringBoot + Vue 项目实战】移动端登录管理(二十)
        • 1、登录业务
        • 2、接口开发
          • 2.1、获取短信验证码
          • 2.2、登录和注册接口
          • 2.3、查询登录用户的个人信息
            1、登录业务

            登录管理共需三个接口,分别是获取短信验证码、登录、查询登录用户的个人信息。除此之外,同样需要编写HandlerInterceptor来为所有受保护的接口增加验证JWT的逻辑。移动端的具体登录流程如下图所示

            2、接口开发
            2.1、获取短信验证码

            前置条件

            该接口需向登录手机号码发送短信验证码,各大云服务厂商都提供短信服务,本项目使用阿里云完成短信验证码功能,下面介绍具体配置。

            • 配置短信服务

              • 开通短信服务

                • 在阿里云官网,注册阿里云账号,并按照指引,完成实名认证(不认证,无法购买服务)

                • 找到短信服务,选择免费开通

                • 进入短信服务控制台,选择快速学习和测试

                • 找到发送测试下的API发送测试,绑定测试用的手机号(只有绑定的手机号码才能收到测试短信),然后配置短信签名和短信模版,这里选择**[专用]测试签名/模版**。

                • 创建AccessKey

                  云账号 AccessKey 是访问阿里云 API 的密钥,没有AccessKey无法调用短信服务。点击页面右上角的头像,选择AccessKey管理,然后创建AccessKey。

                  查看接口

                  代码开发

                  • 配置所需依赖

                    如需调用阿里云的短信服务,需使用其提供的SDK,具体可参考官方文档。

                    在common模块的pom.xml文件中增加如下内容

                     com.aliyun dysmsapi20170525
                  • 配置发送短信客户端

                    • 在application.yml中增加如下内容

                      aliyun:
                        sms:
                          access-key-id: -key-id> access-key-secret: -key-secret> endpoint: dysmsapi.aliyuncs.com
                      

                      注意:

                      上述access-key-id、access-key-secret需根据实际情况进行修改。

                    • 在common模块中创建com.atguigu.lease.common.sms.AliyunSMSProperties类,内容如下

                      @Data
                      @ConfigurationProperties(prefix = "aliyun.sms")
                      public class AliyunSMSProperties { private String accessKeyId;
                          private String accessKeySecret;
                          private String endpoint;
                      }
                      
                    • 在common模块中创建com.atguigu.lease.common.sms.AliyunSmsConfiguration类,内容如下

                      @Configuration
                      @EnableConfigurationProperties(AliyunSMSProperties.class)
                      @ConditionalOnProperty(name = "aliyun.sms.endpoint")
                      public class AliyunSMSConfiguration { @Autowired
                          private AliyunSMSProperties properties;
                          @Bean
                          public Client smsClient() { Config config = new Config();
                              config.setAccessKeyId(properties.getAccessKeyId());
                              config.setAccessKeySecret(properties.getAccessKeySecret());
                              config.setEndpoint(properties.getEndpoint());
                              try { return new Client(config);
                              } catch (Exception e) { throw new RuntimeException(e);
                              }
                          }
                      }
                      
                    • 配置Redis连接参数

                      spring: 
                        data:
                          redis:
                            host: 192.168.10.101
                            port: 6379
                            database: 0
                      
                    • 编写Controller层逻辑

                      在LoginController中增加如下内容

                      @GetMapping("login/getCode")
                      @Operation(summary = "获取短信验证码")
                      public Result getCode(@RequestParam String phone) { service.getSMSCode(phone);
                          return Result.ok();
                      }
                      
                    • 编写Service层逻辑

                      • 编写发送短信逻辑

                        • 在SmsService中增加如下内容

                          void sendCode(String phone, String verifyCode);
                          
                        • 在SmsServiceImpl中增加如下内容

                          @Override
                          public void sendCode(String phone, String code) { SendSmsRequest smsRequest = new SendSmsRequest();
                              smsRequest.setPhoneNumbers(phone);
                              smsRequest.setSignName("阿里云短信测试");
                              smsRequest.setTemplateCode("SMS_154950909");
                              smsRequest.setTemplateParam("{\"code\":\"" + code + "\"}");
                              try { client.sendSms(smsRequest);
                              } catch (Exception e) { throw new RuntimeException(e);
                              }
                          }
                          
                        • 编写生成随机验证码逻辑

                          在common模块中创建com.atguigu.lease.common.utils.VerifyCodeUtil类,内容如下

                          public class VerifyCodeUtil { public static String getVerifyCode(int length) { StringBuilder builder = new StringBuilder();
                                  Random random = new Random();
                                  for (int i = 0; i < length; i++) { builder.append(random.nextInt(10));
                                  }
                                  return builder.toString();
                              }
                          }
                          
                        • 编写获取短信验证码逻辑

                          • 在LoginServcie中增加如下内容

                            void getSMSCode(String phone);
                            
                          • 在LoginServiceImpl中增加如下内容

                            @Override
                            public void getSMSCode(String phone) { //1. 检查手机号码是否为空
                                if (!StringUtils.hasText(phone)) { throw new LeaseException(ResultCodeEnum.APP_LOGIN_PHONE_EMPTY);
                                }
                                //2. 检查Redis中是否已经存在该手机号码的key
                                String key = RedisConstant.APP_LOGIN_PREFIX + phone;
                                boolean hasKey = redisTemplate.hasKey(key);
                                if (hasKey) { //若存在,则检查其存在的时间
                                    Long expire = redisTemplate.getExpire(key, TimeUnit.SECONDS);
                                    if (RedisConstant.APP_LOGIN_CODE_TTL_SEC - expire < RedisConstant.APP_LOGIN_CODE_RESEND_TIME_SEC) { //若存在时间不足一分钟,响应发送过于频繁
                                        throw new LeaseException(ResultCodeEnum.APP_SEND_SMS_TOO_OFTEN);
                                    }
                                }
                                //3.发送短信,并将验证码存入Redis
                                String verifyCode = VerifyCodeUtil.getVerifyCode(6);
                                smsService.sendCode(phone, verifyCode);
                                redisTemplate.opsForValue().set(key, verifyCode, RedisConstant.APP_LOGIN_CODE_TTL_SEC, TimeUnit.SECONDS);
                            }
                            

                            注意:需要注意防止频繁发送短信。

                            2.2、登录和注册接口

                            查看接口

                            登录注册校验逻辑

                            • 前端发送手机号码phone和接收到的短信验证码code到后端。
                            • 首先校验phone和code是否为空,若为空,直接响应手机号码为空或者验证码为空,若不为空则进入下步判断。
                            • 根据phone从Redis中查询之前保存的验证码,若查询结果为空,则直接响应验证码已过期 ,若不为空则进入下一步判断。
                            • 比较前端发送的验证码和从Redis中查询出的验证码,若不同,则直接响应验证码错误,若相同则进入下一步判断。
                            • 使用phone从数据库中查询用户信息,若查询结果为空,则创建新用户,并将用户保存至数据库,然后进入下一步判断。
                            • 判断用户是否被禁用,若被禁,则直接响应账号被禁用,否则进入下一步。
                            • 创建JWT并响应给前端。

                              代码开发

                              • 接口实现

                                • 编写Controller层逻辑

                                  在LoginController中增加如下内容

                                  @PostMapping("login")
                                  @Operation(summary = "登录")
                                  public Result login(LoginVo loginVo) { String token = service.login(loginVo);
                                      return Result.ok(token);
                                  }
                                  
                                • 编写Service层逻辑

                                  • 在LoginService中增加如下内容

                                    String login(LoginVo loginVo);
                                    
                                  • 在LoginServiceImpl总增加如下内容

                                    @Override
                                    public String login(LoginVo loginVo) { //1.判断手机号码和验证码是否为空
                                        if (!StringUtils.hasText(loginVo.getPhone())) { throw new LeaseException(ResultCodeEnum.APP_LOGIN_PHONE_EMPTY);
                                        }
                                        if (!StringUtils.hasText(loginVo.getCode())) { throw new LeaseException(ResultCodeEnum.APP_LOGIN_CODE_EMPTY);
                                        }
                                        //2.校验验证码
                                        String key = RedisConstant.APP_LOGIN_PREFIX + loginVo.getPhone();
                                        String code = redisTemplate.opsForValue().get(key);
                                        if (code == null) { throw new LeaseException(ResultCodeEnum.APP_LOGIN_CODE_EXPIRED);
                                        }
                                        if (!code.equals(loginVo.getCode())) { throw new LeaseException(ResultCodeEnum.APP_LOGIN_CODE_ERROR);
                                        }
                                        //3.判断用户是否存在,不存在则注册(创建用户)
                                        LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
                                        queryWrapper.eq(UserInfo::getPhone, loginVo.getPhone());
                                        UserInfo userInfo = userInfoService.getOne(queryWrapper);
                                        if (userInfo == null) { userInfo = new UserInfo();
                                            userInfo.setPhone(loginVo.getPhone());
                                            userInfo.setStatus(BaseStatus.ENABLE);
                                            userInfo.setNickname("用户-"+loginVo.getPhone().substring(6));
                                            userInfoService.save(userInfo);
                                        }
                                        //4.判断用户是否被禁
                                        if (userInfo.getStatus().equals(BaseStatus.DISABLE)) { throw new LeaseException(ResultCodeEnum.APP_ACCOUNT_DISABLED_ERROR);
                                        }
                                        //5.创建并返回TOKEN
                                        return JwtUtil.createToken(userInfo.getId(), loginVo.getPhone());
                                    }
                                    
                                  • 编写HandlerInterceptor

                                    • 编写AuthenticationInterceptor

                                      在web-app模块创建com.atguigu.lease.web.app.custom.interceptor.AuthenticationInterceptor,内容如下

                                      @Component
                                      public class AuthenticationInterceptor implements HandlerInterceptor { @Override
                                          public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token = request.getHeader("access-token");
                                              Claims claims = JwtUtil.parseToken(token);
                                              Long userId = claims.get("userId", Long.class);
                                              String username = claims.get("username", String.class);
                                              LoginUserHolder.setLoginUser(new LoginUser(userId, username));
                                              return true;
                                          }
                                          @Override
                                          public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { LoginUserHolder.clear();
                                          }
                                      }
                                      
                                    • 注册AuthenticationInterceptor

                                      在web-app模块创建com.atguigu.lease.web.app.custom.config.WebMvcConfiguration,内容如下

                                      @Configuration
                                      public class WebMvcConfiguration implements WebMvcConfigurer { @Autowired
                                          private AuthenticationInterceptor authenticationInterceptor;
                                          @Override
                                          public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(this.authenticationInterceptor).addPathPatterns("/app/**").excludePathPatterns("/app/login/**");
                                          }
                                      }
                                      
                                    • Knife4j增加认证相关配置

                                      在增加上述拦截器后,为方便继续调试其他接口,可以获取一个长期有效的Token,将其配置到Knife4j的全局参数中。

                                      2.3、查询登录用户的个人信息

                                      查看接口

                                      代码开发

                                      • 查看响应数据结构

                                        查看web-app模块下的com.atguigu.lease.web.app.vo.user.UserInfoVo,内容如下

                                        @Schema(description = "用户基本信息")
                                        @Data
                                        @AllArgsConstructor
                                        public class UserInfoVo { @Schema(description = "用户昵称")
                                            private String nickname;
                                            @Schema(description = "用户头像")
                                            private String avatarUrl;
                                        }
                                        
                                      • 编写Controller层逻辑

                                        在LoginController中增加如下内容

                                        @GetMapping("info")
                                        @Operation(summary = "获取登录用户信息")
                                        public Result info() { UserInfoVo info = service.getUserInfoById(LoginUserHolder.getLoginUser().getUserId());
                                            return Result.ok(info);
                                        }
                                        
                                      • 编写Service层逻辑

                                        • 在LoginService中增加如下内容

                                          UserInfoVo getUserInfoId(Long id);
                                          
                                        • 在LoginServiceImpl中增加如下内容

                                          @Override
                                          public UserInfoVo getUserInfoId(Long id) { UserInfo userInfo = userInfoService.getById(id);
                                              return new UserInfoVo(userInfo.getNickname(), userInfo.getAvatarUrl());
                                          }