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


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


    1、登录业务

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

    image-20240620202741568

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

    前置条件

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

    • 配置短信服务

      • 开通短信服务

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

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

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

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

      • 创建AccessKey

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

        image-20240620203300251

    查看接口

    image-20240620203128198

    代码开发

    • 配置所需依赖

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

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

      <dependency>
          <groupId>com.aliyungroupId>
          <artifactId>dysmsapi20170525artifactId>
      dependency>
      
    • 配置发送短信客户端

      • application.yml中增加如下内容

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

        注意

        上述access-key-idaccess-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、登录和注册接口

    查看接口

    image-20240620203512210

    登录注册校验逻辑

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

    代码开发

    • 接口实现

      • 编写Controller层逻辑

        LoginController中增加如下内容

        @PostMapping("login")
        @Operation(summary = "登录")
        public Result<String> 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<UserInfo> 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、查询登录用户的个人信息

    查看接口

    image-20240620203705812

    代码开发

    • 查看响应数据结构

      查看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<UserInfoVo> 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());
        }
        
  • 相关阅读:
    Azure Neural TTS 持续上新,助力企业开拓小语种市场
    使用open3d可视化3d人脸
    1336. 每次访问的交易次数
    简单对比一下 C 与 Go 两种语言
    【Qt】Qt界面美化指南:深入理解QSS样式表的应用与实践
    如何在Win系统搭建Oracle数据库并实现远程访问【内网穿透】
    Hadoop集群搭建--时间同步
    Web安全—Web漏扫工具Nikto安装与使用
    leetcode刷题(第四十五天)70. 爬楼梯 (进阶); 322. 零钱兑换 ; 279.完全平方数
    HTTP/1.1协议的状态码
  • 原文地址:https://blog.csdn.net/weixin_53961667/article/details/139842260