【尚庭公寓springboot + vue的项目实战】- 移动端登录模块的实现

【尚庭公寓springboot + vue的项目实战】- 移动端登录模块的实现

技术选型与核心组件

1. JWT(JSON Web Token)认证方案

什么是Token?
  • 令牌本质:代表用户身份和权限的安全字符串,用于客户端与服务端间的身份验证

  • JWT结构

    text 复制代码
    Header.Payload.Signature
    • Header

      Base64Url编码的JSON对象,声明令牌类型和签名算法:

      json 复制代码
      { 
          "alg": "HS256",
          "typ": "JWT" 
      }
    • Payload

      存储用户声明信息,支持标准字段与自定义字段:

      json 复制代码
      { "sub":
       "userInfo", "name":
       "John Doe", 
       "iat": 1516239022 
      }
    • Signature

      使用Header指定算法对前两部分签名,确保数据完整性

2. 手机验证码实现

  • 技术工具:阿里云短信接口
  • 存储方案:Redis单节点存储验证码

3. 登录状态管理

  • ThreadLocal:保存Token解析后的用户上下文信息
  • Redis:存储验证码及登录态关键数据

核心功能实现详解

一、 登录流程

移动端的具体登录流程如下图所示

二、短信验证码获取

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

  • 配置短信服务

    • 开通短信服务

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

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

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

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

  • 创建AccessKey

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

1. 依赖配置

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

xml 复制代码
<dependency>
    <groupId>com.aliyun</groupId>
    <artifactId>dysmsapi20170525</artifactId>
</dependency>
2.属性配置

配置发送短信客户端

  • application.yml中增加如下内容

    yaml 复制代码
    aliyun:
      sms:
        access-key-id: <access-key-id>
        access-key-secret: <access-key-secret>
        endpoint: dysmsapi.aliyuncs.com

注意 :上述access-key-idaccess-key-secret需根据实际情况进行修改

  • 添加属性配置类 :在com.atguigu.lease.common.sms 创建AliyunSMSProperties类,内容如下
java 复制代码
    @Component
    @Data
    @ConfigurationProperties(prefix = "aliyun.sms")
    public class AliyunSMSProperties {
        private String accessKeyId;
        private String accessKeySecret;
        private String endpoint;

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

    java 复制代码
    @Configuration
    @EnableConfigurationProperties(AliyunSMSProperties.class)
    @ConditionalOnProperty(prefix = "aliyun.sms", name = "endpoint")
    public class AliyunSMSConfiguration {
    
        @Autowired
        private AliyunSMSProperties aliyunSMSProperties;
    
        @Bean
        public Client client(){
            try {
                Config config = new Config();
                config.setAccessKeyId(aliyunSMSProperties.getAccessKeyId());
                config.setAccessKeySecret(aliyunSMSProperties.getAccessKeySecret());
                config.setEndpoint(aliyunSMSProperties.getEndpoint());
                return new Client(config);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }
    
    }
3.编写逻辑代码
  • 编写Controller层逻辑

    LoginController中增加如下内容

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

    • 编写发送代码逻辑

      • SmsService中增加如下内容

        java 复制代码
        void sendSms(String phone, String code);
      • SmsServiceImpl中增加如下内容

        java 复制代码
        @Service
        public class SmsServiceImpl implements SmsService {
            @Autowired
            private Client client;
        
            @Override
            public void sendSms(String phone, String code) {
                try {
                    SendSmsRequest sendSmsRequest = new SendSmsRequest()
                            .setSignName("阿里云短信测试")
                            .setTemplateCode("SMS_154950909")
                            .setPhoneNumbers(phone)
                            .setTemplateParam("{\"code\":\""+code+"\"}");
                    client.sendSms(sendSmsRequest);
                } catch (Exception e) {
                    e.printStackTrace();
                }
        
            }
        }
  • 编写生成随机验证码逻辑

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

    java 复制代码
    package com.atguigu.lease.common.utils;
    
    import java.util.Random;
    
    /**
     * ClassName: VerifyCodeUtil
     * Description: 生成验证码的工具类
     *
     * @Author linz
     * @Creat 2025/2/11 23:12
     * @Version 1.00
     */
    public class VerifyCodeUtil {
        public static String generateVerifyCode(int len){
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < len; i++) {
                sb.append(new Random().nextInt(10));
            }
            return sb.toString();
        }
    }
    • 编写获取短信验证码逻辑

      • LoginServcie中增加如下内容

        java 复制代码
        void getSMSCode(String phone);
      • LoginServiceImpl中增加如下内容

        java 复制代码
         @Override
            public void getCode(String phone) {
                if(StringUtils.isEmpty(phone)){
                    throw new LeaseException(ResultCodeEnum.APP_LOGIN_PHONE_EMPTY);
                }
                //验证码的key
                String key = RedisConstant.APP_LOGIN_PREFIX+phone;
                //redis中是否已经包含手机的key
                Boolean result = redisTemplate.hasKey(key);
                if(result){ //该手机号发送过短信验证码且在redis中尚未过期
                    //继续判断验证码是否是一分钟内发送的
        
                    //获取验证码还有多长时间过期
                    Long expire = redisTemplate.getExpire(key, TimeUnit.SECONDS);
                    //验证码过期时间(总的时间)
                    Long totalTime=RedisConstant.APP_LOGIN_CODE_TTL_SEC * 60 * 60L;
        
                    if(totalTime-expire<RedisConstant.APP_LOGIN_CODE_RESEND_TIME_SEC){
                        throw new LeaseException(ResultCodeEnum.APP_SEND_SMS_TOO_OFTEN);
                    }
                }
        
                //生成随机6位的验证码
                String code = VerifyCodeUtil.generateVerifyCode(6);
                //发送手机验证码
                smsService.sendSms(phone,code);
                //将验证码存储到redis中(存储过期时间为60 * 10小时,不用重复扣费)
                redisTemplate.opsForValue().set(key,code, RedisConstant.APP_LOGIN_CODE_TTL_SEC, TimeUnit.HOURS);
            }

三、登录和注册接口

1.登录注册校验逻辑
  • 登录注册校验逻辑

    • 前端发送手机号码phone和接收到的短信验证码code到后端。

    • 首先校验phonecode是否为空,若为空,直接响应手机号码为空或者验证码为空,若不为空则进入下步判断。

    • 根据phone从Redis中查询之前保存的验证码,若查询结果为空,则直接响应验证码已过期 ,若不为空则进入下一步判断。

    • 比较前端发送的验证码和从Redis中查询出的验证码,若不同,则直接响应验证码错误,若相同则进入下一步判断。

    • 使用phone从数据库中查询用户信息,若查询结果为空,则创建新用户,并将用户保存至数据库,然后进入下一步判断。

    • 判断用户是否被禁用,若被禁,则直接响应账号被禁用,否则进入下一步。

    • 创建JWT并响应给前端。

2.编写逻辑代码
  • 接口实现

    • 编写Controller层逻辑

      LoginController中增加如下内容

      java 复制代码
          @PostMapping("login")
          @Operation(summary = "登录")
          public Result<String> login(@RequestBody LoginVo loginVo) {
              String token = loginService.login(loginVo);
              return Result.ok(token);
          }
    • 编写Service层逻辑

      LoginService中增加如下内容

      java 复制代码
      String login(LoginVo loginVo);

      LoginServiceImpl总增加如下内容

      java 复制代码
       @Override
          public String login(LoginVo loginVo) {
              String phone = loginVo.getPhone();
              //判断手机号码是否为空
              if(!StringUtils.hasText(phone)){
                  throw new LeaseException(ResultCodeEnum.APP_LOGIN_PHONE_EMPTY);
              }
      
      
              String code = loginVo.getCode();
              //判断验证码是否为空
              if(!StringUtils.hasText(code)){
                  throw new LeaseException(ResultCodeEnum.APP_LOGIN_CODE_EMPTY);
              }
              //判断验证码是否正确
              //获取redis中的验证码
              String key = RedisConstant.APP_LOGIN_PREFIX+phone;
              String redisCode = redisTemplate.opsForValue().get(key);
      
              if(redisCode==null){
                  throw new LeaseException(ResultCodeEnum.APP_LOGIN_CODE_EXPIRED);
              }
              if(!redisCode.equals(code)){
                  throw new LeaseException(ResultCodeEnum.APP_LOGIN_CODE_ERROR);
              }
      
              //查询手机号的用户是否注册为会员
              UserInfo userInfo = userInfoMapper.selectOne(new LambdaQueryWrapper<UserInfo>().eq(UserInfo::getPhone, phone));
              if(userInfo==null){
                  userInfo=new UserInfo();
                  userInfo.setPhone(phone);
                  userInfo.setStatus(BaseStatus.ENABLE);
                  userInfo.setNickname("用户-"+phone.substring(5));
                  userInfoMapper.insert(userInfo);
              }
      
              if(userInfo.getStatus()==BaseStatus.DISABLE){
                  throw new LeaseException(ResultCodeEnum.APP_ACCOUNT_DISABLED_ERROR);
              }
      
      
              //登录成功,创建token
              String token = JwtUtil.createToken(phone, userInfo.getId());
              return token;
          }
3.实现简化登录
  • 添加拦截器

    • om.atguigu.lease.web.app.custom.interceptor中创建AuthenticationInterceptor

      添加以下内容

      java 复制代码
      @Component
      public class AuthenticationInterceptor implements HandlerInterceptor {
          @Override
          public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
      
              String token = request.getHeader("access_token");
              if(token==null){
                  throw new LeaseException(ResultCodeEnum.APP_LOGIN_AUTH);
              }else{
                  //解析token
                  Claims claims = JwtUtil.parseToken(token);
                  String phone = claims.get("userName", String.class);
                  Long userId = claims.get("userId", Long.class);
      
                  //将负载信息保存到ThreadLocal中
                  LoginUser loginUser = new LoginUser(userId,phone);
                  LoginUserContext.set(loginUser);
              }
      
              return true;
          }
      
          @Override
          public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
              //将保存到ThreadLocal中的token的负载信息删除
              LoginUserContext.remove();
          }
      }
  • 注册AuthenticationInterceptor(前期可以先不添加拦截器,不方便测试)

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

    java 复制代码
    @Configuration
    public class WebMvcConfiguration implements WebMvcConfigurer {
        @Autowired
        AuthenticationInterceptor authenticationInterceptor;
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(authenticationInterceptor).addPathPatterns("/app/**").excludePathPatterns("/app/login/**");
        }
    }
相关推荐
老华带你飞4 分钟前
工会管理|基于springboot 工会管理系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·spring
Echo flower12 分钟前
Spring Boot WebFlux 实现流式数据传输与断点续传
java·spring boot·后端
小徐Chao努力34 分钟前
Go语言核心知识点底层原理教程【变量、类型与常量】
开发语言·后端·golang
锥锋骚年35 分钟前
go语言异常处理方案
开发语言·后端·golang
北城以北888843 分钟前
SpringBoot--Redis基础知识
java·spring boot·redis·后端·intellij-idea
superman超哥1 小时前
仓颉语言中并发集合的实现深度剖析与高性能实践
开发语言·后端·python·c#·仓颉
superman超哥1 小时前
仓颉语言中原子操作的封装深度剖析与无锁编程实践
c语言·开发语言·后端·python·仓颉
⑩-1 小时前
SpringCloud-Feign客户端实战
后端·spring·spring cloud
阿杰AJie1 小时前
Docker 容器启动的全方位方法汇总
后端
sdguy1 小时前
在 Windows 上正确安装 OpenAI Codex CLI:一次完整的 pnpm 全局环境修复实录
后端·openai