seesion/redis 验证码登录

短信登录

基于session的短信登录

  • 发送验证码

    • 校验手机号码合法性(正则表达式)

    • 保存验证码在session中

      • session.setAttribute("code",code)
        
  • 登录,验证码校验 ,注册

    • 获取验证码session,一致则根据电话号码查询数据库,无,则新建用户

    • 已登录用户信息,存在用户session中

      • session.setAttribute("user" , BeanUtil.copyProperties(user,UserDTO.class)) // user复制到UserDTO中
        
  • 登录状态校验

    • 用户请求时,从cookie中携带JsessionId到后台,通过JsessionId从session中获取用户信息
    • 没有session信息,则拦截
    • 有session信息,则将用户信息保存在ThreadLocal中
public class UserHolder {
    private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();

    public static void saveUser(UserDTO user){
        tl.set(user);
    }

    public static UserDTO getUser(){
        return tl.get();
    }

    public static void removeUser(){
        tl.remove();
    }
}
  1. 定义:
    • ThreadLocal 是 Java 中的一个类,用于实现线程的局部变量。
  2. 作用:
    • ThreadLocal 提供了一种在多线程环境下实现线程私有数据的机制。
    • 每个线程都可以独立地访问和修改自己的局部变量,互不干扰。
  3. 原理:
    • 每个 ThreadLocal 对象维护一个独立的变量副本,存储在各个线程的 ThreadLocalMap 中。
    • 线程通过 ThreadLocal 对象的 get() 方法获取自己的局部变量值,通过 set() 方法设置局部变量值。
  4. 内存泄漏:
    • ThreadLocal 使用弱引用来引用 ThreadLocal 对象,但是值仍然使用强引用。
    • 如果没有及时清理 ThreadLocal 变量,即调用 remove() 方法,可能导致内存泄漏。
    • 内存泄漏发生在 ThreadLocalMap 中,当 ThreadLocal 对象被回收时,对应的值仍然存在于 ThreadLocalMap 中。

发送验证码

@Override
public Result sendCode(String phone, HttpSession session) {
    // 1.校验手机号
    if (RegexUtils.isPhoneInvalid(phone)) {
        // 2.如果不符合,返回错误信息
        return Result.fail("手机号格式错误!");
    }
    // 3.符合,生成验证码  
    String code = RandomUtil.randomNumbers(6); //生成验证码,这里使用的是hutool工具类的随机数生成方法,参数表示生成6位数验证码

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

String code = RandomUtil.randomNumbers(6); //生成验证码,这里使用的是hutool工具类的随机数生成方法,参数表示生成6位数验证码

登录

@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",user);
    return Result.ok();
}
   //创建新用户
    private User createUserWithPhone(String phone) {
        User user = new User();
        user.setPhone(phone);
        user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX+RandomUtil.randomNumbers(10)); // 设置常量user_
        save(user)
        return user;
    }

登录验证功能,登录拦截器

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

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

自定义拦截器

  • 实现HandlerInterceptor 接口
  • 重新 preHandle(在请求处理之前) postHandle(请求处理之后的操作) afterCompletion(请求完成后的操作)
  • 拦截器注册到Spring Boot应用中
    • @Configuration
    • 实现WebMvcConfigurer接口
    • register注册
    • excludePathPatterns
    • oder优先级

让拦截器生效

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

隐藏用户敏感信息

在UserHolder处:将user对象换成UserDTO

public class UserHolder {
    private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();

    public static void saveUser(UserDTO user){
        tl.set(user);
    }

    public static UserDTO getUser(){
        return tl.get();
    }

    public static void removeUser(){
        tl.remove();
    }
}

在登录方法处修改

//7.保存用户信息到session中
session.setAttribute("user", BeanUtils.copyProperties(user,UserDTO.class));

BeanUtils.copyProperties(user,UserDTO.class)); // hutool 工具包 工具类 uesr – 》UserDTO.class

在登录拦截器处:

//5.存在,保存用户信息到Threadlocal
UserHolder.saveUser((UserDTO) user);

session共享问题

image-20230211112349115

基于Redis实现登录流程

image-20230211112603966

整体访问流程

key值:生成token作为redis的key

1、key要具有唯一性

2、key要方便携带

img

发送验证码

    //发送验证码
    @Override
    public Result sendCode(String phone, HttpSession session) {
        //1.校验手机号
        if (RegexUtils.isPhoneInvalid(phone)) {
            return Result.fail("手机号格式错误");
        }
        //2.生成验证码
        String code = RandomUtil.randomNumbers(6);
        //3.保存验证码道session中
        //   session.setAttribute("code",code);
        //3.以手机号为KEY保存验证码到Redis中 key=login:code:13168320031  value = 
        stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);
        //4.发送验证码
       log.debug("发送短信验证码成功,验证码:{} ",code);
        return Result.ok();
    }

stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone,code,2, TimeUnit.MINUTES); // 设置k v 获取时间

登录实现

@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
    // 1.校验手机号
    String phone = loginForm.getPhone();
    if (RegexUtils.isPhoneInvalid(phone)) {
        // 2.如果不符合,返回错误信息
        return Result.fail("手机号格式错误!");
    }
    // 3.从redis获取验证码并校验
    String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
    String code = loginForm.getCode();
    if (cacheCode == null || !cacheCode.equals(code)) {
        // 不一致,报错
        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.保存用户信息到 redis中
    // 7.1.随机生成token,作为登录令牌
    String token = UUID.randomUUID().toString(true); //true 是无符号
    
    // 7.2.将User对象转为HashMap存储  将 UserDTO 对象中的属性名和属性值转换为 Map 中的键和值。
    UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
    
    Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
            CopyOptions.create()
                    .setIgnoreNullValue(true)
                    .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
    // 7.3.存储
    String tokenKey = LOGIN_USER_KEY + token;
    stringRedisTemplate.opsForHash().putAll(tokenKey, userMap); //putAll()方法用于将一个 HashMap中的键值对批量存储到Redis的Hash结构中。

    // 7.4.设置token有效期
    stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);

    // 8.返回token
    return Result.ok(token);
}

将用户信息存储到 Redis 中:

  • 生成随机的登录令牌(token)作为键。
  • 将 User 对象转换为 UserDTO 对象,然后将 UserDTO 转换为 HashMap 存储用户信息。
  • 使用 Redis 的 Hash 数据结构将用户信息存储到 Redis 中,以 token 作为键。
  • expire设置登录令牌的有效期。

登录拦截器优化

image-20230211113155077

RefreshTokenInterceptor

public class RefreshTokenInterceptor implements HandlerInterceptor {

    private StringRedisTemplate stringRedisTemplate;

    public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.获取请求头中的token  
        String token = request.getHeader("authorization"); // 在登录成功的时候,返回了token值
        if (StrUtil.isBlank(token)) {
            return true;
        }
        // 2.基于TOKEN获取redis中的用户
        String key  = LOGIN_USER_KEY + token;
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key); // entries() 方法用于获取指定键的所有字段和值。
        // 3.判断用户是否存在
        if (userMap.isEmpty()) {
            return true;
        }
        // 5.将查询到的hash数据转为UserDTO
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        // 6.存在,保存用户信息到 ThreadLocal
        UserHolder.saveUser(userDTO);
        // 7.刷新token有效期
        stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);
        // 8.放行
        return true;
    }

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

string token = request.getHeader("authorization") 返回的是 token 是因为在 HTTP 请求的头部中,常用的一种认证方式是将身份凭证(如 token)放置在 Authorization 头部中传递。这是一种标准的 HTTP 头部字段,用于指示请求的身份验证凭证。

LoginInterceptor

public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.判断是否需要拦截(ThreadLocal中是否有用户)
        if (UserHolder.getUser() == null) {
            // 没有,需要拦截,设置状态码
            response.setStatus(401);
            // 拦截
            return false;
        }
        // 有用户,则放行
        return true;
    }
}

MvcConfig

@Configuration
public class MvcConfig implements WebMvcConfigurer {

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

order(0),1 2 3 4 5 是执行顺序

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值