后端实现分布式登录注册接口
前言
恰逢1024程序员节,兴致一来,决定写一个最常用最经典也是最能考验一个程序员水平的接口,登录注册接口。
大佬勿喷,如果有写的不好的地方欢迎在评论区提出来,大家的评论是我创作最大的动力,欢迎指教。
一、项前工作
博主是一名应用平台开发,所以本文主要针对后端来做实现,是一个前后端分离的项目,具有一定的参考意义。
涉及技术栈
- SpringBoot
- MyBatis
- MySql
- Maven
- Redis
- 短信服务(第三方服务)
- Hibernate(只是在参数方面做一个简单的校验)
准备工作
- 项目搭建的话这里就不在多说了,就是基本的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>
- 需要去阿里云或者腾讯去申请一个短信验证的签名以及模板。发送短信需要用到的配置参数如下,申请好之后就将参数带到模板当中即可发送代码。
在密钥管理中找到下面这三个参数
- secretId:
- secretKey:
- 签名:
在短信模板中找到下面两个参数
- 模板ID:
- SDKAppId:
- 数据库的话就简单创建一个用户表即可,里面需要包含最基础的用户名和密码,特殊情况下可以根据自己的业务加一些其他的字段。
- Redis连接的话,我在之前的博客中也有写到,可以点击参考 Redis整合SpringBoot。
流程讲解
如上图,为本次业务的流程图,大部分网站中注册也使用了这种流程。
那么废话不多说,下面我就带大家进入代码环节。
二、代码编辑
2.1 拦截器使用
定义拦截器
定义拦截器需要继承 HandlerInterceptor ,实现他的三个方法,下面我就详细介绍一下它的三个方法
继承拦截器实现三个方法分别为
- preHandle(……)方法,当某个URL以及匹配到对应的Controller中的某个方法,且在这个方法执行之前。来通过返回值判断是否放行
- postHandle(……)方法:该方法的执行时机是,当某个URL已经匹配到对应的Controler中的某个方法,且在执行完了该方法,但是在返回给视图渲染之前。所以这个方法中有个ModelAndView参数可以做一些修改动作。
- 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());
}
}
短信接口
上面定义好短信业务工具类之后,就通过下面的短信接口供给前端调用。
- 注入短信服务
上面拦截器以经将短信超过一定时间的短信拦截了,所以这里业务我们直接考虑发送短信
- 根据用户Ip为Key存储Redis中
- 返回成功信息
@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;
}
总结
关于注册登录就总结到这里了,感兴趣的朋友记得一键三连哦。