利用session进行短信登录的关键思路

396 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 1 天,点击查看活动详情

1.内容概述

最近在学习黑马的一些redis课程,其中有一个内容是利用redis来进行短信登录。在引入这部分内容之前,先介绍了如何用session实现短信登录,以及使用session的不足之处。再此对这部分知识进行梳理。

2.实现思路

2.1发送验证码

用手机号码进行登录或者注册,那么我们便需要验证用户是否拥有这个手机号,所以我们需要准备一个接口来生成验证码和发送验证码。具体流程如下所示:

image.png

伪代码如下:

说明:

1.校验手机验证码用的正则表达式工具类可以自己写,如果觉得麻烦可以参考其他同学的代码; 2.生成验证码用的也是工具类,这也可以在网上找到许多参考代码;
3.因为我们将验证码存放在session对象里面,所以接口应该有两个参数,一个接收手机号码,一个是HttpSession对象

@PostMapping("code")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
    // TODO 发送短信验证码并保存验证码
    return userService.sendCode(phone,session);
}
//发送验证码
@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.2短信验证码登录或注册

在用户用手机号码和验证码进行登录的时候,我们可以判断用户是否已经注册,如果没有注册,我们便帮用户注册,如果用户已经注册,那么我们将用户信息存放到session对象里面,具体流程如下:

image.png

伪代码

说明:

  1. 校验验证码的方式是根据用户提交的验证码和生成验证码时保存在session里的验证码进行比对
  2. 如果用户未注册的话,我们可以帮用户进行注册,注册的时候只需保存用户手机号和一个随机的昵称即可。用户密码之类的内容可以让用户随后自己设置
  3. 用户注册成功或者在数据库查询得到,我们便将这个用户的信息保存到session对象中。原因是:以后判断用户是否登录,我们是从session中能否取出用户来进行判断的。有些同学可能发现了,保存到session对象的用户信息我们进行了一个转化,这里把用户信息转化到了UserDTO对象里面,原因是如果直接保存用户的所有信息可能有安全隐患,所以我们用hutool-all提供的工具类,将user对象的一些信息封装到UserDTO对象里面,并将UserDTO对象保存到session对象里面。
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
    // 实现登录功能
    return userService.login(loginForm,session);
}
//登录
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
    //1.校验手机号
    String phone = loginForm.getPhone();
    if (RegexUtils.isPhoneInvalid(phone)){
        //2.如果不符合,返回错误信息
        return Result.fail("手机号格式错误");
    }

    //2.校验验证码
    Object cacheCode = session.getAttribute("code");
    String code = loginForm.getCode();
    if (cacheCode == null | !cacheCode.toString().equals(code)) {
        // 3.不一致,报错
        return Result.fail("验证码错误");
    }

    //4.一致,根据手机号查询用户 select * from tb_user where phone = ?
    User user = query().eq("phone", phone).one();

    //5.判断用户是否存在
    if (user == null){
        //6.不存在,创建用户并保存
        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;
}

2.3 拦截器

如果需要使用一些登录之后才能进行操作的功能时,我们总是需要再写一遍获取session对象来判断用户是否登录,那么会很麻烦。为此,我们创建一个拦截器,将登录等少数无需登录即可使用对应功能的接口放通,其他接口在拦截器中拦截下来,统一在拦截器里用session来判断用户是否已经登录。

image.png

伪代码:

说明:

  1. 拦截器的功能很简单,就是当用户操作的时候,根据拦截器的配置,将某些接口拦截下来并判断是否用户已经登录,如果登录,那边放行,否则拦截
  2. 有同学可能看到拦截器里有一个UserHolder类。这个类其实封装了ThreadLocal类,目的是保存用户信息,以后如果有接口需要获取用户信息,那么便可以通过UserHolder获取用户信息。
@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new Logininterceptor())
                .excludePathPatterns(
                        "/shop/**",
                        "/voucher/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/blog/hot",
                        "/user/code",
                        "/user/login"
                );
    }
}
@Slf4j
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.不存在,拦截
            response.setStatus(401);
            return false;
        }
        //5.存在,保存用户信息到ThreadLocal
        UserHolder.saveUser((UserDTO) user);
        //6.放行
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 移除用户
        UserHolder.removeUser();
    }
}

2.4获取用户信息

说明:在拦截器中,如果用户已经登录,我们便将用户信息保存到ThreadLocal里面,所以获取用户信息,直接通过ThreadLocal获取即可。在本例中,UserHolder封装了ThreadLocal。

@GetMapping("/me")
public Result me(){
    // TODO 获取当前登录的用户并返回
    UserDTO user = UserHolder.getUser();
    return Result.ok(user);
}

3.不足之处和解决办法

如果采用了Session来进行用户信息,短信验证码的保存,并以此来判断用户是否登录,在非集群的模式下是可以的。如果是集群模式的话,那么便产生了新的问题,问题在于如何进行session的共享。其实为了解决这个问题,我们可以用redis来代替session,这样子的话,无论你是否是集群的布置项目,你只需将用户信息,短信验证码等保存在redis。需要这些信息时,在redis里获取即可。