一、短信登录流程

二、开发

2.1 准备

  • HuTool依赖

Hutool是一个Java工具库,提供了丰富的工具方法和简化开发的功能。我们需要使用随机生成数字的功能

        <!--hutool-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.7.17</version>
        </dependency>
  • 校验手机号/邮箱工具类

    • 先定义使用正则表达式定义校验规则

    public abstract class RegexPatterns {
        /**
         * 手机号正则
         */
        public static final String PHONE_REGEX = "^1([38][0-9]|4[579]|5[0-3,5-9]|6[6]|7[0135678]|9[89])\\d{8}$";
        /**
         * 邮箱正则
         */
        public static final String EMAIL_REGEX = "^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$";
        /**
         * 密码正则。4~32位的字母、数字、下划线
         */
        public static final String PASSWORD_REGEX = "^\\w{4,32}$";
        /**
         * 验证码正则, 6位数字或字母
         */
        public static final String VERIFY_CODE_REGEX = "^[a-zA-Z\\d]{6}$";
    
    }
    • 定义校验工具类

public class RegexUtils {
    /**
     * 是否是无效手机格式
     * @param phone 要校验的手机号
     * @return true:符合,false:不符合
     */
    public static boolean isPhoneInvalid(String phone){
        return mismatch(phone, RegexPatterns.PHONE_REGEX);
    }
    /**
     * 是否是无效邮箱格式
     * @param email 要校验的邮箱
     * @return true:符合,false:不符合
     */
    public static boolean isEmailInvalid(String email){
        return mismatch(email, RegexPatterns.EMAIL_REGEX);
    }

    /**
     * 是否是无效验证码格式
     * @param code 要校验的验证码
     * @return true:符合,false:不符合
     */
    public static boolean isCodeInvalid(String code){
        return mismatch(code, RegexPatterns.VERIFY_CODE_REGEX);
    }

    // 校验是否不符合正则格式
    private static boolean mismatch(String str, String regex){
        if (StrUtil.isBlank(str)) {
            return true;
        }
        return !str.matches(regex);
    }
}

2.2 发送验证码

  1. controller

    /**
     * 发送手机验证码
     */
    @PostMapping("code")
    public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
        //发送短信验证码并保存验证码
        return userService.sendCode(phone, session);
    }
  1. service

    /**
     * 发送验证码
     * @param phone
     * @param session
     */
    Result sendCode(String phone, HttpSession session);
  1. serviceImpl

    @Override
    public Result   sendCode(String phone, HttpSession session) {
        // 1.校验手机号
        if (RegexUtils.isPhoneInvalid(phone)) {
            // 2.如果不符合,返回错误信息
            return Result.fail("手机号格式错误!");
        }
        // 3.符合,生成验证码
        String code = RandomUtil.randomNumbers(6);

        // 4.保存验证码到 session
        session.setAttribute("code",code);
        // 5.模拟发送验证码
        log.debug("发送短信验证码成功,验证码:{}", code);
        // 返回ok
        return Result.ok();
    }

2.3登录 & 注册

controller 和 service 省略....

serviceImpl

里面的query()方法和 save() 方法都是mybatis-plus提供的,不用我们自己定义

因为Session

public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {   
 @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        // 1.校验手机号
        String phone = loginForm.getPhone();
        if (RegexUtils.isPhoneInvalid(phone)) {
            // 2.如果不符合,返回错误信息
            return Result.fail("手机号格式错误!");
        }
        // 3.校验验证码
        Object cacheCode = session.getAttribute("code");
        String code = loginForm.getCode();
        if(cacheCode == null || !cacheCode.toString().equals(code)){
             //3.不一致,报错
            return Result.fail("验证码错误");
        }
        //一致,根据手机号查询用户
        User user = query().eq("phone", phone).one();

        //5.判断用户是否存在
        if(user == null){
            //不存在,则创建
            user =  createUserWithPhone(phone);
        }
        //7.保存用户信息到session中
        session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));

        return Result.ok();
    }

    private User createUserWithPhone(String phone) {
        //1.创建用户
        User user = new User();
        user.setPhone(phone);
        user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
        //2.保存用户
        save(user);
        return user;
    }
}

因为我们是通过Session来实现登录的,所以不需要返回任何登录凭证。只需要将用户信息保存在Session中就可以了。

Tomcat服务器负责处理客户端的请求、创建和管理Session对象,并将Session ID发送给客户端

后端的代码服务器负责验证用户的身份、创建和管理Session对象中的数据,并根据请求处理逻辑生成响应

流程:

  1. 客户端发送请求的时候,tomcat服务器都会创建一个Session对象,并分配一个SessionID

  2. 后端负责把用户信息存入Sessiond对象中,返回响应的时候,tomcat服务器都会将Session ID发送给客户端,客户端拿到Session ID后就可以保存在Cookie中

  3. 客户端每次发送请求就会自动Cookie中的Session ID 发送给tomcat服务器,tomcat服务器就能通过Session ID找到对应的Session对象

所以说不用把登录凭证返回,因为信息都放在了Session里,而且客户端请求的时候也会自动把SessionID传入

所以说基于Session的验证 是比较消耗服务器性能,的,对服务器的影响比较大,因为Session对象是存在tomcat服务器中的。

2.4登录拦截校验

  • 编写拦截器

public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
       //1.获取session
        HttpSession session = request.getSession();
        //2.获取session中的用户
        Object user = session.getAttribute("user");
        //3.判断用户是否存在
        if(user == null){
              //4.不存在,拦截,返回401状态码
              response.setStatus(401);
              return false;
        }
        //5.存在,保存用户信息到Threadlocal
        UserHolder.saveUser((User)user);
        //6.放行
        return true;
    }
}
  • 注册拦截器让其生效

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 登录拦截器
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(
                        "/shop/**",
                        "/voucher/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/blog/hot",
                        "/user/code",
                        "/user/login"
                ).order(1);
        // token刷新的拦截器
        registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
    }
}

三、Session存在的问题

每个tomcat中都有一份属于自己的session,假设用户第一次访问第一台tomcat,并且把自己的信息存放到第一台服务器的session中,但是第二次这个用户访问到了第二台tomcat,那么在第二台服务器上,肯定没有第一台服务器存放的session,所以此时 整个登录拦截功能就会出现问题,我们能如何解决这个问题呢?早期的方案是session拷贝,就是说虽然每个tomcat上都有不同的session,但是每当任意一台服务器的session修改时,都会同步给其他的Tomcat服务器的session,这样的话,就可以实现session的共享了

但是这种方案具有两个大问题

1、每台服务器中都有完整的一份session数据,服务器压力过大。

2、session拷贝数据时,可能会出现延迟

所以咱们后来采用的方案都是基于redis来完成,我们把session换成redis,redis数据本身就是共享的,就可以避免session共享的问题了

四、使用Redis代替Session

基于Redis实现短信登录功能 - 落叶知秋 (preke.top)

文章作者: 落叶知秋
本文链接:
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 落叶知秋
学习笔记 后端 学习 Java SpringBoot
喜欢就支持一下吧