Redis实现短信登入功能(一)传统的Session登入

基于Session实现登入流程

分步实现发送短信登入 

(1)发送短信验证码

(2)短信验证码的登入

(3)登入校验

集群Session共享问题(Redis登入的提出)


基于Session实现登入流程

分步实现发送短信登入 

(1)发送短信验证码

UserController控制层 

控制层调用service层的接口 

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

IUserService接口 

接口定义sendCode()方法 

public interface IUserService extends IService<User> {

    Result sendCode(String phone, HttpSession session);
}

UserServiceImpl实现接口方法 

@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService{

    @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();
    }
}

注意:

上述代码中使用到了许多工具类,各个工具类的定义如下:

RegexUtils (校验手机号)

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

RegexUtils 中调用了 RegexPatterns(正则校验) 

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}$";
}

我们重启服务,在前端页面输入手机号,点击获取验证码,在idea中的日志:

说明该功能实现完成!

(2)短信验证码的登入

UserController控制层  

@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
    // 实现登录功能
    return userService.login(loginForm, session);
}

IUserService接口 

接口定义login()方法 

public interface IUserService extends IService<User> {

    Result login(LoginFormDTO loginForm, HttpSession session);
}

UserServiceImpl实现接口方法  

@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {

    //1. 校验手机号
    String phone = loginForm.getPhone();
    if (RegexUtils.isPhoneInvalid(phone)) {
        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.一致,根据手机号查询用户
    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();
}

login()方法中调用createUserWithPhone()方法创建用户 

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;
}

(3)登入校验

拦截器 LoginInterceptor 

此处使用拦截器校验用户是否存在,存在就登入;反之return false 

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();
    }
}

配置拦截器 MvcConfig

使拦截器LoginInterceptor 生效!并定义拦截的对象。

在SpringBoot中可以使用addPathPatterns配置需要拦截的路径、excludePathPatterns配置不要拦截的路径。

此处,表示放行的资源(即无需登入,就可以访问的页面)

@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"
                );
    }
}

注意:

上述的一系列代码,我们还做了安全性考虑,我们使用了UserDTO,并没有直接使用User,一来减少了数据传输的成本,二来避免了User中的敏感数据泄露!

UserDTO

@Data
public class UserDTO {
    private Long id;
    private String nickName;
    private String icon;
}

集群Session共享问题(Redis登入的提出)

再来看看这一张图,当程序到达性能瓶颈的时候,我们需要将原先的一台Tomcat,扩展Tomcat集群,但是每一台Tomcat都会有一个自己的Session空间不同Tomcat的Session是相互独立的)。如果在登入方面我们不做调整就会出现登入失败的问题。

早期Tomcat是有做过Session拷贝的!但是这种方案会导致内存浪费;在拷贝过程中会产生时延,如果在这个延迟之有人进行访问那么一样会出现问题!

所以我们急需一种可以代替Session的东西,此物需要满足:

  • 数据共享
  • 内存存储
  • key、value结构

显然用Redis来实现短信登入功能有着天然的优势。

 

posted @ 2022-06-29 00:59  金鳞踏雨  阅读(33)  评论(0编辑  收藏  举报  来源