Redis - 登录场景分析

java代码中使用Redis

环境准备

这里我们需要导入一些所需的依赖 使用的是Spring集成的框架SpringDataRedis

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

配置连接

通过yaml文件配置 对应的redis连接参数

  redis:
    host: ip地址
    port: 端口号
    auth: 密码
    lettuce:
      pool:
        max-active: 最大连接数
        max-idle: 最大空闲连接
        min-idle: 最小空闲连接
        time-between-eviction-runs: 连接等待时间

使用

package com.caidan.redis;

import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;

@SpringBootTest
public class TestDemo {
    @Resource
    @Qualifier("redisTemplate")
    private RedisTemplate<String, Object> redisTemplate;

    @Test
    public void demo() {
        if(redisTemplate == null ){
            System.out.println("注入失败");
            return;
        }
//      设置字符串类型的键值对 name 张三
        redisTemplate.opsForValue().set("name","张三");
//      获取字符串类型键位name的值 并输出
        Object o = redisTemplate.opsForValue().get("name");
        System.out.println("o = " + o);
    }
}

序列化

我们使用Redis的客户端查看刚刚 设置的缓存,发现存入的数据的键和值编码格式都不是按照我们想要的预期存入的。
在这里插入图片描述
主要是默认底层使用的是OutPutStream字节流进行的转换,他会把这个字符串对象给序列化之后 存入Redis。此时就出现这种情况,发现出现一些非必要的内容。

进行自定义序列化器

这个使用自定义的内容可以使用json转换会更加合格便于阅读和减少内存,这边我直接贴黑马的图了,就不手写了。在这里插入图片描述
但是说使用这个工具的话也有一个问题,我们使用json转换的时候怎么保证他转换的目标对象是谁呢?你会发现这个JSON序列化器 存入的时候会把这个类的信息给存入到json结果中,这样用来反序列化时辨认读取,但是这样会造成额外内容开销。
在这里插入图片描述

解决

Spring支持手动序列化 和反序列化的接口,他默认提供了一个StringRedisTemplate类,它的key和value的序列化方式默认是String方式,省去了我们字定义RedisTemplate的过程

使用
package com.caidan.redis;

import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;

@SpringBootTest
public class TestDemo {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Test
    public void demo() {
        if(stringRedisTemplate == null ){
            System.out.println("注入失败");
            return;
        }
//      设置字符串类型的键值对 name 张三
        stringRedisTemplate.opsForValue().set("age","21");
//      获取字符串类型键位name的值 并输出
        String o = stringRedisTemplate.opsForValue().get("age");
        System.out.println("o = " + o);
    }
}

在这里插入图片描述
这个时候我们发现,呈递的效果符合预期。

登录业务

这边介绍一下业务逻辑,由于代码太多只展示一些核心逻辑代码。要是想要看代码分析的话,我这边贴上gitee代码,这个也是我再做这个黑马点评时思考过的一些问题,就以注释的形式展示在那边了。

https://gitee.com/xiaocaidan1/redis-learning.git

登录业务实现

这里的登录实现的是,手机验证码登录。
在这里插入图片描述
这个位置我们来思考一下,发送验证码这个逻辑肯定是服务器执行的操作。
当大批用户同时发送的话,那么验证码存储面临一个问题。
存储在哪里?
当存储在数据库的时候,你就面临大量的读写操作,这样对服务器性能影响较大,并且验证码需要有效期来避免重复使用,那么过期设置的话,数据库明显实现不了。
此时我们可以使用Redis来解决这个场景,Redis中键可以设置有效期,并且读取效率高。
储存标识
123 和 213 手机号校验的时候我们怎么知道,手机号验证码是正确呢?怎么知道发送的验证码是对应的那个手机号呢?
这个时候我们就需要使用一个标识来储存验证码,由于Redis中有分层结构我们为了便于便利 可以对验证码这个位置加一个统一前缀 login:code: 跟上 phone 这样标识还有一个好处,就是当一个人刷新页面一直发送验证码的话,对这个手机号而言生效的只是最后一个。 然后设置过期时间 2分钟 就非常完美了。

stringRedisTemplate.opsForValue().set(
	    RedisConstants.LOGIN_CODE_KEY + phone,
	    c, RedisConstants.LOGIN_CODE_TTL, TimeUnit.MINUTES);
登录
校验验证码
String code = (String) stringRedisTemplate.opsForValue()
				.get(RedisConstants.LOGIN_CODE_KEY + loginForm.getPhone());
if (code == null || !code.equals(loginForm.getCode())) {
   return Result.fail("验证失败!");
}
校验用户是否存在
1.用户存在

查询出用户信息

2.用户不存在

再数据库中创建用户,然后返回用户信息。

场景分析

我们来想一下我登录之后,请求一些资源的时候需不需要身份认证?发送评论需不需要身份认证?显然是需要的。那么我们怎么来储存这个标识呢?
你也可以使用jwt,但是说jwt的缺点还是很明显的,他储存在客户端,服务端只是校验他格式是否正确,时间是否到期,也就是说,当这个用户在网站发表不良言论的时候,我此时封禁他的账号,但是之前的jwt并没有过期,校验的时候还是可以使用,这就比较尴尬了。
再这个位置我们将用户信息储存在redis中,便于后续校验或者是使用 ,设置有效期用来避免存储浪费,代码返回前端一个Redis键用来后续校验。如果用户在网站发表了不良言论等违规行为,可以直接在 Redis 中删除该用户的身份信息,或者将其有效期设置为0,使其立即失效。这样就可以防止用户继续访问需要身份认证的资源,保护网站的安全性和用户体验。

String token = UUID.randomUUID().toString().replace("-", "");
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
Map<String, Object> stringObjectMap = BeanUtil.beanToMap(userDTO, new HashMap<>(), CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
String tokenKey = RedisConstants.LOGIN_USER_KEY + token;
	/*
	*   储存以及设置有效期
	* */
stringRedisTemplate.opsForHash().putAll(tokenKey, stringObjectMap);
stringRedisTemplate.expire(tokenKey, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
return Result.ok(token);

登录逻辑书写完成之后我们需要说,书写拦截器,来做到校验和读取分析。

拦截器

我们在登录中将用户的信息储存在Redis中,然后将这个键返回给前端,那么我们在请求中需要来获取这个内容,然后去redis中读取,查看是否有这个用户。
这边有一个想法确实很优秀,就是说,用户此时登录之后 假设我们设置的有效期是30分钟,但是说他此时要使用这个网站1小时1分钟,那么难道他要登录两次?所以这个位置我们就需要满足一个场景,更新有效期,设置有效期从最后一次登录开始记时。

校验拦截器
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 {
        String authorization = request.getHeader("authorization");
        if (authorization == null || authorization.isEmpty()) {
            return true;
        }
        String key = RedisConstants.LOGIN_USER_KEY + authorization;
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
//        userMap==null 这个不需要写的原因 userMap.isEmpty() 时会抛出 NullPointerException。所以你可以简化为:
        if (userMap.isEmpty()) {
            response.setStatus(401);
            return false;
        }
        // 解析以及储存
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        UserHolder.saveUser(userDTO);
        /*
         *   刷新token的有效期
         *   为什么要刷新呢?
         *      这个位置主要是说 当我设置有效期为30min 但是这个用户准备完40分钟 不能说我在他玩的时候给他退了
         *      所以 按照用户最后一次上线之后的30分钟为 有效期是体验感最好的
         * */
        stringRedisTemplate.expire(key,RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
        return true;
    }

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

这个位置的流程也就是说,拦截所有(再SpringMvc中配置) 判断请求中是否携带authorization,这个是返回的那个用户标识,再前端向后端请求所携带的请求头。
如果有的话,证明这个位置需要请求资源,请求资源的话,开始校验一下,由于是hash结构的需要转换和解析

 String key = RedisConstants.LOGIN_USER_KEY + authorization;
 Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
//        userMap==null 这个不需要写的原因 userMap.isEmpty() 时会抛出 NullPointerException。所以你可以简化为:
 if (userMap.isEmpty()) {
     response.setStatus(401);
     return false;
 }
 // 解析以及储存
 UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
 UserHolder.saveUser(userDTO);

更新有效期

stringRedisTemplate.expire(key,RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
return true;

第二层拦截器

这个地方用来校验,不携带authorization 的请求是什么类型的请求,如果是非登录之类的再这个位置获取是否有用户信息,没有的话直接返回错误信息。

package com.hmdp.utils;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (UserHolder.getUser() == null) {
            response.setStatus(401);
            return false;
        }
        // 有用户则进行放行
        return true;
    }

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

拦截器路径配置

package com.hmdp.config;

import com.hmdp.utils.LoginInterceptor;
import com.hmdp.utils.RefreshTokenInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import javax.annotation.Resource;

@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

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

以上就是基本的登录逻辑实现,其中涉及到验证码,拦截器 Redis,字符串,hash结构的使用。如果认为不错的话留个赞再走吧。

  • 39
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值