后端实现分布式登录注册接口,细节拉满~~


前言

恰逢1024程序员节,兴致一来,决定写一个最常用最经典也是最能考验一个程序员水平的接口,登录注册接口。


大佬勿喷,如果有写的不好的地方欢迎在评论区提出来,大家的评论是我创作最大的动力,欢迎指教。

一、项前工作

博主是一名应用平台开发,所以本文主要针对后端来做实现,是一个前后端分离的项目,具有一定的参考意义。

涉及技术栈

  • SpringBoot
  • MyBatis
  • MySql
  • Maven
  • Redis
  • 短信服务(第三方服务)
  • Hibernate(只是在参数方面做一个简单的校验)

准备工作

  1. 项目搭建的话这里就不在多说了,就是基本的Springboot+SpringMVC,需要Maven参与构建项目,这里可以使用一些自己的代码生成器直接生成对应的文件。

这里我只提供需要引用的jar包,具体版本号需要自己去定义。

    <dependencies>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>compile</scope>
        </dependency>
        
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-spring-ui</artifactId>
            <version>3.0.3</version>
        </dependency>
        
      <!-- 第三方云厂商相关的依赖 -->
        <dependency>
            <groupId>com.tencentcloudapi</groupId>
            <artifactId>tencentcloud-sdk-java</artifactId>
            <version>3.1.270</version>
        </dependency>

        <!-- 引入SpringBoot 依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        
        <!-- lombok工具 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <!-- jackson -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
        </dependency>
        
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-annotations</artifactId>
        </dependency>
        
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        
        <!-- apache 工具类 -->
        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>
        
        <dependency>
            <groupId>commons-fileupload</groupId>
            <artifactId>commons-fileupload</artifactId>
        </dependency>
        
        <!-- google 工具类 -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
        </dependency>
        
        <!-- joda-time 时间工具 -->
        <dependency>
            <groupId>joda-time</groupId>
            <artifactId>joda-time</artifactId>
        </dependency>

        <!-- 引入 redis 依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

        <!-- mybatis -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>

        <!-- 通用mapper逆向工具 -->
        <dependency>
            <groupId>tk.mybatis</groupId>
            <artifactId>mapper-spring-boot-starter</artifactId>
        </dependency>

    </dependencies>
  1. 需要去阿里云或者腾讯去申请一个短信验证的签名以及模板。发送短信需要用到的配置参数如下,申请好之后就将参数带到模板当中即可发送代码。

在密钥管理中找到下面这三个参数

  • secretId:
  • secretKey:
  • 签名:

在短信模板中找到下面两个参数

  • 模板ID:
  • SDKAppId:
  1. 数据库的话就简单创建一个用户表即可,里面需要包含最基础的用户名和密码,特殊情况下可以根据自己的业务加一些其他的字段。
  2. Redis连接的话,我在之前的博客中也有写到,可以点击参考 Redis整合SpringBoot。

流程讲解

登录流程图
如上图,为本次业务的流程图,大部分网站中注册也使用了这种流程。
那么废话不多说,下面我就带大家进入代码环节。


二、代码编辑

2.1 拦截器使用

定义拦截器

定义拦截器需要继承 HandlerInterceptor ,实现他的三个方法,下面我就详细介绍一下它的三个方法

继承拦截器实现三个方法分别为

  1. preHandle(……)方法,当某个URL以及匹配到对应的Controller中的某个方法,且在这个方法执行之前。来通过返回值判断是否放行
  2. postHandle(……)方法:该方法的执行时机是,当某个URL已经匹配到对应的Controler中的某个方法,且在执行完了该方法,但是在返回给视图渲染之前。所以这个方法中有个ModelAndView参数可以做一些修改动作。
  3. afterCompleton(……)方法:整个请求完成处理,这时做一些资源的清理工作

下面我就使用preHandle方法来拦截判断Redis中存储的验证码是否过期,判断结果为true或者是

public class passportInterceptor implements HandlerInterceptor{

    @Autowired
    public RedisOperator redis;
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    //这里为通过request获取Ip,就不用代码演示了
        String userIp = IPUtil.getRequestIp(request);
	//这里就是调用了redis的查看redis存在的API
        boolean keyIsExist = redis.keyIsExist("mobile:smscode" + ":" + userIp);

        if (keyIsExist){
            //可以抛出异常,用于提示前端用户
            log.info("短信发送评率过大");
            return false;
        }
        return true;
    }
}

注册拦截器

注册拦截器用于指定拦截的接口需要继承 WebMvcConfigurer

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {

	//注入上面定义的拦截器
    @Autowired
    public PassportInterceptor passportInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(passportInterceptor)
        //与下文中的路径对应
                .addPathPatterns("/passport/getSMSCode");
    }
}

2.2 短信服务调用

工具类

这里根据自己的调用的服务商来进行对应的方法调用,这里我以腾讯云为例

  • 必要步骤:
    实例化一个认证对象,入参需要传入腾讯云账户密钥对secretId,secretKey。
    这里采用的是从环境变量读取的方式,需要在环境变量中先设置这两个值。
    你也可以直接在代码中写死密钥对,但是小心不要将代码复制、上传或者分享给他人,
    以免泄露密钥对危及你的财产安全。
    CAM密匙查询获取: https://console.cloud.tencent.com/cam/capi

下面进入代码展示(两个参数分别为下面对应的模板参数):

  public void sendSMS(String phone, String code,String time) throws Exception {
        try {
            Credential cred = new Credential(secretId,secretKey);

            // 实例化一个http选项,可选的,没有特殊需求可以跳过
            HttpProfile httpProfile = new HttpProfile();

//            httpProfile.setReqMethod("POST"); // 默认使用POST

            /* SDK会自动指定域名。通常是不需要特地指定域名的,但是如果你访问的是金融区的服务
             * 则必须手动指定域名,例如sms的上海金融区域名: sms.ap-shanghai-fsi.tencentcloudapi.com */
            httpProfile.setEndpoint("sms.tencentcloudapi.com");

            // 实例化一个client选项
            ClientProfile clientProfile = new ClientProfile();
            clientProfile.setHttpProfile(httpProfile);
            // 实例化要请求产品的client对象,clientProfile是可选的
            SmsClient client = new SmsClient(cred, "ap-nanjing", clientProfile);

            // 实例化一个请求对象,每个接口都会对应一个request对象
            SendSmsRequest req = new SendSmsRequest();
            //电话号码
            String[] phoneNumberSet1 = {"+86" + phone};
            req.setPhoneNumberSet(phoneNumberSet1);
            // 短信应用ID: 短信SdkAppId在 [短信控制台] 添加应用后生成的实际SdkAppId
            req.setSmsSdkAppId("对应的SDKAppId");
            // 签名
            req.setSignName("对应模板签名");
            // 模板id:必须填写已审核通过的模板 ID。模板ID可登录 [短信控制台] 查看
            req.setTemplateId("模板Id");

            /* 模板参数(自定义占位变量): 若无模板参数,则设置为空 */
            String[] templateParamSet1 = {code,time};
            req.setTemplateParamSet(templateParamSet1);

            // 返回的resp是一个SendSmsResponse的实例,与请求对象对应
            SendSmsResponse resp = client.SendSms(req);
            // 输出json格式的字符串回包
//            System.out.println(SendSmsResponse.toJsonString(resp));
        } catch (TencentCloudSDKException e) {
            System.out.println(e.toString());
        }
    }

短信接口

上面定义好短信业务工具类之后,就通过下面的短信接口供给前端调用。

  1. 注入短信服务

上面拦截器以经将短信超过一定时间的短信拦截了,所以这里业务我们直接考虑发送短信

  1. 根据用户Ip为Key存储Redis中
  2. 返回成功信息
    @PostMapping("/getSMSCode")
    public Object sms(@RequestParam String mobile,
                      HttpServletRequest request) throws Exception {
        if (StringUtils.isBlank(mobile)) {
            return GraceJSONResult.ok();
        }

//        根据ip进行限制,限制在60s之内只能获取一次验证码(通过工具类获取用户Ip)
        String userIp = IPUtil.getRequestIp(request);
        redis.setnx60s(MOBILE_SMSCODE + ":" + userIp, userIp);

        Random random = new Random();
        String code = String.valueOf(random.nextInt(9999));
        smsUtil.sendSMS(mobile, code, "5");
        log.info(code);

//        将验证码存储到redis中
        redis.set(MOBILE_SMSCODE + ":" + mobile, code, 30 * 60);

        return "OK";
    }

2.3 数据库操作

对数据库操作这里只做查询与添加新用户。我这里使用的是MyBatis当然也可以使用其他的框架。

Service接口层

    /**
     * 判断用户是否存在,存在返回用户信息
     * @param mobile 手机号
     * @return 用户信息
     */
     Users queryMobileIsExist(String mobile);

    /**
     * 创建用户信息
     * @param mobile 手机号
     * @return 用户
     */
     Users createUsers(String mobile);

ServiceImpl接口层实现类

    @Override
    @Transactional
    public Users queryMobileIsExist(String mobile) {
        Users users = usersMapper.getUsers(mobile);
        return users;
    }

    @Override
    @Transactional
    public Users createUsers(String mobile) {
//        获取全局唯一主键(这里可以使用UUID)
        String userId = sid.nextShort();

        Users user = new Users();
        user.setId(userId);
        user.setMobile(mobile);
        user.setNickname("用户:" + mobile);
        user.setBirthday(DateUtil.stringToDate("1900-01-01"));
        user.setCreatedTime(new Date());
        user.setUpdatedTime(new Date());
        usersMapper.insertUser(user);
        return user;
    }

三、登录实现

前面的工具类的定义实现,那么下来我们就进行最后一步编写接口

首先定义一个接受前端的请求的BO类,简单说一下这个BO类

这里我才用了 Hibernate 中的参数校验,变量名上的注解校验参数是否符合要求,在这里定义之后需要在接口层去接收是否校验异常,在进行相应的处理。

    @NotBlank(message = "手机号不能为空")
    @Size(min = 11,max = 11,message = "手机号长度不够")
    private String mobile;

    @NotBlank(message = "验证码不能为空")
    private String code;

终于到了登录环节了

 @PostMapping("/login")
    public Object login(@Valid @RequestBody LoginBo loginBo,
//                        BindingResult result,  
                        HttpServletRequest request) throws Exception {
        String mobile = loginBo.getMobile();
        String code = loginBo.getCode();

//          1. 从redis获取验证码
        String redisCode = redis.get(MOBILE_SMSCODE+":"+mobile);

//        1.1从redis中获得验证码进行校验匹配
        if (code.equals(redisCode)){
            return "验证码错误";
        }

//        2. 判断用户是否存在
        Users userLogin = userService.queryMobileIsExist(loginBo.getMobile());

//        2.2 如果用户为空,表示没有注册过,则为null,需要注册入库
        if (userLogin == null){
             userLogin = userService.createUsers(loginBo.getMobile());
        }

//        3. 如果不为空就继续下方的业务,可以保存用户的信息和会话信息(token)
        String token = UUID.randomUUID().toString();
        redis.set(REDIS_SUER_TOKEN+":"+loginBo.getMobile(),token);

//        4. 用户登录注册成功之后,删除redis中的短信验证码
        redis.del(MOBILE_SMSCODE+":"+mobile);

//        5. 返回用户信息,包含token令牌
        UsersVO usersVO = new UsersVO();
        BeanUtils.copyProperties(userLogin,usersVO);
        usersVO.setUserToken(token);

        return userVO;
    }

总结

关于注册登录就总结到这里了,感兴趣的朋友记得一键三连哦。

  • 2
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

远方的雁

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值