【黑马点评】基于session实现短信登录

一、登录流程

1、发送验证码

用户在提交手机号后,会校验手机号是否合法,如果不合法,则要求用户重新输入手机号

如果手机号合法,后台此时生成对应的验证码,同时将验证码进行保存,然后再通过短信的方式将验证码发送给用户

2、短信验证码登录、注册

用户将验证码和手机号进行输入,

后台从session中拿到当前验证码,然后和用户输入的验证码进行校验,

如果不一致,则无法通过校验,

如果一致,则后台根据手机号查询用户,

如果用户不存在,则为用户创建账号信息,保存到数据库。

无论是否存在,都会将用户信息保存到session中,方便后续获得当前登录信息

3、校验登录状态

用户在请求时候,会从cookie中携带者JsessionId到后台,后台通过JsessionId从session中拿到用户信息,如果没有session信息,则进行拦截,如果有session信息,则将用户信息保存到threadLocal中,并且放行,

校验手机号是否合法

二、发送短信验证码

页面流程:

 

1、controller层

UserController

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

 

2、格式校验工具类

1️⃣正则表达式

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

}

2️⃣校验手机号格式

使用了hutool工具库

官网:简介 | Hutool📚简介

import cn.hutool.core.util.StrUtil;
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);
    }
}

3、service层

1️⃣实现sendCode方法

//service层接口
public interface IUserService extends IService<User> {
    Result sendCode(String phone, HttpSession session);
}

实现短信验证功能

校验手机号:需要用到正则表达式,这里已经提供好了一个工具类RegexPatternsRegexUtils

校验成功后,使用工具类RandomUtils随机生成一个验证码,

这里涉及到的发送手机验证码的步骤,并未讲到,可转springboot开发实战篇——阿里云手机短信验证

保存验证码

发送验证码

2️⃣serviceImpl实现类

UserServiceImpl类

@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);
        //6、返回ok
        return Result.ok();
    }

输入手机号发送验证码,后端随机生成验证码进行校验

优化:阿里云短信手机验证(可以看看我的上一篇文章《手机短信验证登录》)

三、短信验证码登录、注册

1、controller层

/**
     * 登录功能
     * @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
     */
    @PostMapping("/login")
    public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
        // 实现登录功能
        return userService.login(loginForm,session);
    }

2、service层

IUserService接口实现login方法

Result login(LoginFormDTO loginForm, HttpSession session);

UserServiceImpl实现类

短信验证登录注册逻辑

  • 校验手机号

  • 校验验证码

    • 取出session中保存的验证码与表单中的输入的验证码吗进行比较

  • 不一致:报错

  • 一致:根据手机号查询用户

  • 判断用户是否存在

  • 不存在,根据手机号创建新用户并保存

    • 用户是凭空创建的,所以密码可以没有,

    • 用户呢称和头像都是随机默认的

  • 保存用户信息到session中

 @Override
     public Result login(LoginFormDTO loginForm, HttpSession session) {
         //1、校验手机号
         String phone= loginForm.getPhone();
         if (RegexUtils.isPhoneInvalid(phone)) {
             //2、如果不符合,返回错误信息
             return Result.fail("手机号格式错误!!!");
         }
         //2、校验验证码
         //取出session中保存的验证码与表单中的输入的验证码吗进行比较
         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",user);
         return null;
     }

实现创建新用户方法

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

后端控制台

前端浏览器

 

 

这里登录成功了,但是还未做登录校验,所以它又返回了原来的页面

这里会出现页面一闪而过

四、实现登录校验拦截器

1、简介

访问`Controller之前都要先校验用户的登录状态,用户的请求直接访问controller实现这一段逻辑,

那随着业务的增加,每个controller都来执行这段逻辑太过麻烦,所以,这里定义了一个拦截器,

这样用户请求就不是直接访问controller,所有的请求都必须先经过这个拦截器,再由拦截器放行,将请求发送给controller,拦截到的信息需要受到保护,然后发送给controller

 

我们可以得知 每个用户其实对应都是去找tomcat线程池中的一个线程来完成工作的, 使用完成后再进行回收,既然每个请求都是独立的,所以在每个用户去访问我们的工程时,我们可以使用threadlocal来做到线程隔离,每个线程操作自己的一份数据

温馨小贴士:关于threadlocal

如果小伙伴们看过threadLocal的源码,你会发现在threadLocal中,无论是他的put方法和他的get方法, 都是先从获得当前用户的线程,然后从线程中取出线程的成员变量map,只要线程不一样,map就不一样,所以可以通过这种方式来做到线程隔离

2、实现登录校验拦截器

拦截器的作用是将拦截的页面进行校验,校验通过才可放行

这里实现两个方法

  • 前置拦截器preHandle:用户登陆之前需要做校验,根据返回值表示是否拦截,true是放行,false表示拦截

  • afterCompletion:用户登录视图渲染执行完毕需要销毁,避免内存泄漏

1️⃣ 新建登录拦截器

//方法快捷键:ctrl+i
public class LoginInterceptor implements HandlerInterceptor {
    /**
     * 前置拦截器
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @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((UserDTO) user);
        //6、放行
        return true;
    }

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

实现拦截器还需要做一些配置,使其拦截生效

2️⃣ 新建SpringMVC配置类

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

3️⃣ controller层

UserController

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

4️⃣ 修改前端login页面

前端页面中设置登陆后是跳转到个人首页,这里需要进行修改

 

五、用户信息脱敏

上面用户登录校验信息返回的信息太多了,一些敏感信息也被返回了,这是不安全的,而这里只需要返回用户基本信息——id、呢陈、头像

1、定义一个信息返回类DTO

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

2、修改userserviceimpl实现类

调用的是包cn.hutool.core.bean下hutool方法中的工具类BeanUtils

//修改login方法
/**
* copyProperties:属性拷贝——把user中的属性字动拷贝到UserDTO中
* BeanUtils:使用的是包cn.hutool.core.bean下的工具类
*/
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
session.setAttribute("user", userDTO);

3、修改user

将user都修改成userDTO

登录拦截器——LoginInterceptor

修改userHolder

 /**
     * 线程隔离
     */
    private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();

    /**
     * 保存用户
     * @param user
     */
    public static void saveUser(UserDTO user){
        tl.set(user);
    }

    /**
     * 获取用户
     * @return
     */
    public static UserDTO getUser(){
        return tl.get();
    }

userController中的me方法

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

登录成功 

这样也就实现了用户信息的隐藏,只返回了设定的用户信息

如果用户已存在,前端控制台是不会将信息返回的

六、集群的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实现短信登录

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值