黑马点评-02使用Redis代替session,Redis + token机制实现

Redis代替session

session共享问题

每个Tomcat中都有一份属于自己的session,所以多台Tomcat并不共享session存储空间,当请求切换到不同tomcat服务时可能会导致数据丢失

用户第一次访问1号tomcat并把自己的信息存放session域中, 如果第二次访问到了2号tomcat就无法获取到在1号服务器存放的信息,导致登录拦截功能会出问题

session拷贝: 每当任意一台服务器的session修改时,都会同步给其他的Tomcat服务器的session实现session的共享

  • 每台服务器中都有完整的一份session数据导致服务器压力过大
  • session拷贝数据时,可能会出现延迟

使用Redis替换session可以实现服务器共享数据的问题

  • redis的三大特点: 数据共享(所有的服务器都可以在里面查询数据),内存存储(高性能),键值对的存储结构
    在这里插入图片描述

存储用户信息key的结构

如果存入的数据(登陆用户的信息)比较简单,可以考虑使用Redis的String或hash结构存储数据

  • String结构: 使用JSON字符串来保存登录的用户信息,信息比较直观但不易修改数据, 并且会额外的存储一些字符占用内存

在这里插入图片描述

  • Hash结构: 将对象中的每个属性独立存储,既可以针对单个字段做CRUD, 内存占用少只会存储数据本身不用保存序列化对象信息或者JSON的一些额外字符串

在这里插入图片描述

基于Redis实现短信登录

Redis的key是共享的,key要具有唯一性避免其他服务器在存储数据的时候出现key重复value覆盖的问题,用户发起请求时key还要方便携带

在这里插入图片描述

第一步: 修改sendCode方法,验证码不再保存到session中而是保存到Redis中(手机号作为key),验证码需要设置一个有效期节省Redsi内存

 public static final String LOGIN_CODE_KEY = "login:code:";public static final Long LOGIN_CODE_TTL = 2L;
// 自动注入StringRedisTemplate客户端操作Redis
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public Result sendCode(String phone, HttpSession session) throws MessagingException {// 验证邮箱的格式if (RegexUtils.isEmailInvalid(phone)) {return Result.fail("邮箱格式不正确");}// 生成验证码并保存到Redis,执行set key value ex 120String code = MailUtils.achieveCode();stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);log.info("发送登录验证码:{}", code);// 发送验证码,注意子类继承的方法不能比父类抛出更多的异常MailUtils.sendTestMail(phone, code);return Result.ok();
}

第二步: 修改login方法,将验证码和用户信息都存储到Redis中

  • 存储验证码时不再存储到session中,而是将手机号作为key存储到Redis的String结构中,校验验证码时从Redis中获取验证码
  • 存储用户信息时不再是存储到session中,而是随机生成一个token(登录令牌)作为key,将用户信息转换为HashMap对象存储到Redis的Hash结构中
  • 使用StringRedisTemplate向Redis中存数据时要求key和value都是String类型,所以将对象的每个属性转换成Map集合的元素时要求key和value都是String类型
public static final String LOGIN_USER_KEY = "login:token:";
public static final Long LOGIN_USER_TTL = 30L;
@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);// 7.2.将UserDTO对象的属性转为HashMap集合中的元素,这样一次可以存储多个键值对UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);// 手动将UserDTO的每个属性及其值都转化为String类型并存储到HashMap集合HashMap<String, String > userMap = new HashMap<>();userMap.put("icon", userDTO.getIcon());userMap.put("id", String.valueOf(userDTO.getId()));userMap.put("nickName", userDTO.getNickName());// 使用万能工具类将userDTO对象的属性转化为HashMap集合中的元素,创建CopyOptions用来自定义key和value的类型    Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));// 7.3.调用putAll方法将HashMap集合存储到Redis当中String tokenKey = LOGIN_USER_KEY + token;stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);// 7.4.设置tokenKey有效期为30分钟stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);// 7.5 登陆成功则删除验证码信息stringRedisTemplate.delete(LOGIN_CODE_KEY + phone);// 8.返回tokenreturn Result.ok(token);
}

第三步:前端将后端返回的token保存到浏览器当中

login(){if(!this.radio){this.$message.error("请先确认阅读用户协议!");return}if(!this.form.phone || !this.form.code){this.$message.error("手机号和验证码不能为空!");return}axios.post("/user/login", this.form).then(({data}) => { // data是后端随机生成的tokenif(data){// 保存taken到浏览器当中sessionStorage.setItem("token", data);}// 跳转到首页,info.html是用户详情页location.href = "/index.html"})
}// request拦截器,每次发请求都会将用户token放入请求头中
let token = sessionStorage.getItem("token");
axios.interceptors.request.use(config => {if(token) config.headers['authorization'] = tokenreturn config},
)

登录状态刷新问题

我们保存到Redis中的tokenKey的有效期为30分钟,在这段时间内根据用户有无操作决定是否刷新tokenKey的有效期

  • 如果用户有操作就需要刷新tokenKey的有效期,如果用户没有任何操作30分钟后tokenKey会消失,此时登录校验时就无法获取登录用户的信息,用户需要重新登录

在这里插入图片描述

第一步: 修改登陆拦截器,通过拦截器拦截到的请求来证明用户是否在操作

public class LoginInterceptor implements HandlerInterceptor {//@Autowired,这里不能自动装配,因为LoginInterceptor是我们手动在WebConfig里new出来的并不受容器的管理private StringRedisTemplate stringRedisTemplate;public LoginInterceptor(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//1. 获取请求头中的tokenString token = request.getHeader("authorization");//2. 如果token是空表示未登录需要拦截if (StrUtil.isBlank(token)) {response.setStatus(401);return false;}//3. 基于token作为key获取Redis的Hash结构中保存的用户数据,返回的是一个Map集合String key = RedisConstants.LOGIN_USER_KEY + token;Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);//4. 判断Map集合中有没有元素,没有则拦截if (userMap.isEmpty()) {response.setStatus(401);return false;}//5. 将查询到的Hash结构的数据转化为UserDto对象,fasle表示不忽略转化时的错误 UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);//6. 将用户信息保存到UserHolder类的ThreadLocal中UserHolder.saveUser(userDTO);//7. 刷新token有效期为30分钟stringRedisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);//8. 放行return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {// 移除用户UserHolder.removeUser();}
}

第二步: 在配置类MvcConfig注册拦截器使其生效

@Configuration
public class MvcConfig implements WebMvcConfigurer {@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 注册登录拦截器registry.addInterceptor(new LoginInterceptor(stringRedisTemplate)).excludePathPatterns(// 排除不需要拦截的路径"/user/code","/user/login","/blog/hot","/shop/**","/shop-type/**","/upload/**","/voucher/**");}
}

登陆状态刷新优化

LoginInterceptor拦截器只能拦截需要登陆校验的路径,若当前用户访问了一些不会被拦截的路径,此时登录拦截器就不会生效,那么令牌刷新动作也就不会执行

在这里插入图片描述

第一步: 编写RefreshTokenInterceptor拦截器拦截所有路径: 负责基于token获取用户信息,然后保存用户的信息到ThreadLocal当中,同时刷新令牌的有效期

public class RefreshTokenInterceptor implements HandlerInterceptor {// 这里并不是自动装配,因为RefreshTokenInterceptor是我们手动在WebConfig里new出来的private StringRedisTemplate stringRedisTemplate;// 构造方法public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 1.获取请求头中的tokenString token = request.getHeader("authorization");if (StrUtil.isBlank(token)) {return true;}// 2.基于token获取redis中的用户信息String key  = LOGIN_USER_KEY + token;Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);// 3.判断用户是否存在if (userMap.isEmpty()) {// 用户不存在也放行交给LoginInterceptor处理return true;}// 5.将从Redis中查询到的Hash结构的数据转为UserDTO对象,fasle表示不忽略转化时的错误UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);// 6.将用户信息保存到UserHolder类的ThreadLocal中UserHolder.saveUser(userDTO);// 7.刷新token有效期为30分钟stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);// 8.放行return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {// 移除用户UserHolder.removeUser();}
}

第二步: 修改LoginInterceptor登陆拦截器只负责登录校验,即只需要判断UserHolder类的ThreadLocal中是否存在用户信息,存在放行不存在则拦截

public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 判断UserHolder类的ThreadLocal中是否有用户if (UserHolder.getUser() == null) {// 用户信息不存在则拦截并设置状态码response.setStatus(401);return false;}// 用户信息存在则放行return true;}
}

第二步: 在配置类MvcConfig注册两个拦截器,并设置它们的执行顺序和拦截路径

  • 拦截器的执行顺序默认按照添加的顺序执行,但也可以由order来指定顺序(数字越小优先级越高),另外如果拦截器未设置拦截路径则默认是拦截所有路径
@Configuration
public class MvcConfig implements WebMvcConfigurer {//自动装配@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 注册登录拦截器registry.addInterceptor(new LoginInterceptor()).excludePathPatterns("/user/code","/user/login","/blog/hot","/shop/**","/shop-type/**","/upload/**","/voucher/**").order(1);        // 注册刷新token的拦截器,RefreshTokenInterceptor是我们手动new出来的,只能通过构造方法为其注入StringRedisTemplate//registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).order(0);}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/127065.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

3. 无重复字符的最长子串(枚举+滑动窗口)

目录 一、题目 二、代码 一、题目 力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱的技术成长平台 二、代码 class Solution { public:int lengthOfLongestSubstring(string s) {int _MaxLength 0;int left 0, right 0;vector<int>hash(128, 0);//ASCII…

Android子线程可以更新UI

目录 1 传统更新UI的七种方式1.1 new Handler()1.2 new Handler.Callback()1.3 new Handler().post(Runnable r)1.4 new Handler().postDelayed(Runnable r, long delayMillis)1.5 Activity.runOnUiThread(Runnable action)1.6 View.post(Runnable action)1.7 View.postDelayed…

无需公网IP,企业如何访问私有云?

企业的日常办公中已经习惯利用网盘或在线传输服务来进行文件传输和分享&#xff0c;面对一系列层出不穷的数据泄露和黑客行为&#xff0c;企业也越来越担心隐私泄露的问题&#xff0c;并寻找真正存储私有化的解决方案。某企业的总部在北京&#xff0c;在上海还有2处办公室&…

Multisim:JFET混频器设计(含完整程序)

目录 前言实验内容一、先看作业题目要求二、作业正文IntroductionPre-lab work3.13.2 Experiment Work4.1(2)circuit setup4.1(3)add 12V DC4.1(4)set input x1 and x24.1(5)4.1(6)4.1(7)4.2(1)(2)4.2(3)4.2(4)4.3(1)(2)4.3(3) Conclusion 三、资源包内容 前言 花了好大心血完成…

leetCode 674. 最长连续递增序列 动态规划 / 贪心策略

674. 最长连续递增序列 - 力扣&#xff08;LeetCode&#xff09; 给定一个未经排序的整数数组&#xff0c;找到最长且 连续递增的子序列&#xff0c;并返回该序列的长度。 连续递增的子序列 可以由两个下标 l 和 r&#xff08;l < r&#xff09;确定&#xff0c;如果对于每…

【4】c++设计模式——>UML表示类之间的聚合关系

聚合关系表示整体与部分的关系&#xff0c;在聚合关系中&#xff0c;成员对象时整体的一部分&#xff0c;但是成员对象可以脱离整体对象独立存在&#xff0c;当整体被析构销毁的时候&#xff0c;组成整体的这些子对象是不会被销毁的&#xff0c;是可以继续存活&#xff0c;并在…

2023年【广东省安全员C证第四批(专职安全生产管理人员)】报名考试及广东省安全员C证第四批(专职安全生产管理人员)最新解析

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 2023年【广东省安全员C证第四批&#xff08;专职安全生产管理人员&#xff09;】报名考试及广东省安全员C证第四批&#xff08;专职安全生产管理人员&#xff09;最新解析&#xff0c;包含广东省安全员C证第四批&…

后端解决跨域(极速版)

header(Access-Control-Allow-Origin: *); header(Access-Control-Allow-Methods:*); 代表接收全部的请求&#xff0c;"POST,GET"//允许访问的方式 指定域&#xff0c;如http://172.20.0.206//宝塔的域名&#xff0c;注意不是&#xff1a;http://wang.jingyi.icu等…

【Verilog 教程】7.1Verilog 除法器设计

除法器原理&#xff08;定点&#xff09; 和十进制除法类似&#xff0c;计算 27 除以 5 的过程如下所示&#xff1a; 除法运算过程如下&#xff1a; (1) 取被除数的高几位数据&#xff0c;位宽和除数相同&#xff08;实例中是 3bit 数据&#xff09;。 (2) 将被除数高位数据与…

QT基础入门——界面布局和常用控件(四)

前言&#xff1a; 所谓 GUI 界面&#xff0c;归根结底&#xff0c;就是一堆组件的叠加。我们创建一个窗口&#xff0c;把按钮放上面&#xff0c;把图标放上面&#xff0c;这样就成了一个界面。在放置时&#xff0c;组件的位置尤其重要。我们必须要指定组件放在哪里&#xff0c…

手把手教你编写LoadRunner脚本

编写 LoadRunner 脚本需要熟悉脚本语言、业务场景、参数化技术、断言和事务等基础知识。 在实际编写时&#xff0c;可以根据具体测试需求&#xff0c;结合实际情况进行合理的配置和调整。 基本步骤 创建脚本 在 LoadRunner 的 Controller 模块中&#xff0c;创建一个新的测试…

Python之字符串分割替换移除

Python之字符串分割替换移除 分割 split(sepNone, maxsplit-1) -> list of strings 从左至右sep 指定分割字符串&#xff0c;缺省的情况下空白字符串作为分隔符maxsplit 指定分割的次数&#xff0c;-1 表示遍历整个字符串立即返回列表 rsplit(sepNone, maxsplit-1) -> …