简历_使用 Redis 解决集群模式下的 Session 共享问题,使用拦截器实现用户的登录,校验和权限刷新以及对单位时间内请求频繁的用户IP地址进行限流。

系列博客目录


文章目录

  • 系列博客目录
  • 1.使用 Redis 解决集群模式下的 Session 共享问题
    • 集群的session共享问题
    • 总结
  • 2.使用拦截器实现用户的登录,校验和权限刷新
  • 3.对单位时间内请求频繁的用户IP地址进行限流。
    • 实现思路
    • 步骤:
      • 1. 添加 Redis 依赖
      • 2. 配置 Redis
      • 3. 创建限流服务类
      • 4. 创建拦截器
      • 5. 配置拦截器
      • 6. 测试
    • 额外优化:


1.使用 Redis 解决集群模式下的 Session 共享问题

集群的session共享问题

session共享问题:多台Tomcat并不共享session存储空间,当请求切换到不同tomcat服务时导致数据丢失的问题。

session的替代方案应该满足:

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

在这里插入图片描述
如下图所示,以前是保存到session现在是Redis。
在这里插入图片描述
如下图所示,之前是请求并携带cookie,从session中获取用户。
在这里插入图片描述
在这里插入图片描述

@PostMapping("code")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {// 发送短信验证码并保存验证码return userService.sendCode(phone,session);}@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){// 实现登录功能return userService.login(loginForm, session);
}
@Override
public Result sendCode(String phone, HttpSession session) {// 1.校验手机号if (!RegexUtils.isCodeInvalid(phone)) {// 2.如果不符合,返回错误信息。return Result.fail("手机号格式错误");}// 3.符合,生成验证码String code = RandomUtil.randomNumbers(6);// 4.保存验证码到redisstringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);// session.setAttribute("code", code);// 5.发送验证码,这里不调用第三方了,不是讲解的重点    log.debug("发送短信验证码成功,验证码{}", code);// 6.返回okreturn Result.ok();
}@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);// Object cacheCode = session.getAttribute("code");String code = loginForm.getCode();if(cacheCode == null || !cacheCode.equals(code)){// 3.不一致return Result.fail("验证码错误");}// 4.一致User user = query().eq("phone",phone).one();// 5.判断用户是否存在if(user == null){// 6.不存在,创建新用户并保存user = createUserWithPhone(phone);}// 7.保存用户信息到redis中// 7.1.随机生成TokenString token = UUID.randomUUID().toString(true);// 7.2.将User对象转为HashMap存储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);stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);// 8.返回tokenreturn Result.ok(token);
}

总结

Redis代替session需要考虑的问题:

  • 选择合适的数据结构
  • 选择合适的key
  • 选择合适的存储粒度

2.使用拦截器实现用户的登录,校验和权限刷新

下图是之前的拦截器,由于需要刷新token有效期,因为用户在浏览非登录页面的时候,不应该突然掉登录。
在这里插入图片描述
下图是更新后的逻辑。
在这里插入图片描述

package com.hmdp.config;import com.hmdp.utils.LoginInterceptor;
import com.hmdp.utils.RefreshTokenInterceptor;
import org.springframework.context.annotation.Configuration;
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;/*** ClassName: MvcConfig* Package: com.hmdp.config* Description:** @Author 醒了就刷牙* @Create 2025/1/3 15:00* @Version 1.0*/@Configuration
public class MvcConfig implements WebMvcConfigurer {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new LoginInterceptor()).excludePathPatterns("/shop/**","/voucher/**","/upload/**","/shop-type/**","/user/code","/blog/hot","/user/login").order(1);registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);}
}
package com.hmdp.utils;import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.hmdp.dto.UserDTO;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.concurrent.TimeUnit;/*** ClassName: LoginInterceptor* Package: com.hmdp.utils* Description:** @Author 醒了就刷牙* @Create 2025/1/3 14:51* @Version 1.0*/
public class RefreshTokenInterceptor implements HandlerInterceptor {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 = RedisConstants.LOGIN_USER_KEY + token;Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);// 3.判断用户是否存在if ( userMap.isEmpty()){return true;}// 5.将查询到的Hash数据转为UserDTO对象UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);// 6.存在,保存用户信息到ThreadLocalUserHolder.saveUser(userDTO);// 7.刷新token有效期stringRedisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.SECONDS);// 8.放行return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {UserHolder.removeUser();}
}
package com.hmdp.utils;import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.hmdp.dto.UserDTO;
import com.hmdp.entity.User;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.Map;
import java.util.concurrent.TimeUnit;/*** ClassName: LoginInterceptor* Package: com.hmdp.utils* Description:** @Author 醒了就刷牙* @Create 2025/1/3 14:51* @Version 1.0*/
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 1.判断是否需要拦截(ThreadLocal中是否又用户)if(UserHolder.getUser() == null){// 未登录时跳转到登录页面 ,将请求重定向到登录页面//response.sendRedirect("/user/login");  response.setStatus(401);return false;}return true;}
}

3.对单位时间内请求频繁的用户IP地址进行限流。

在 Spring Boot 中实现基于 IP 地址的请求限流,通常使用 Redis 结合时间窗口(例如:一分钟)来进行限制。可以通过在每次请求时,记录用户 IP 地址的请求次数并设置过期时间,来实现请求频率控制。

实现思路

  1. 每次请求时,获取请求的 IP 地址。
  2. 使用 Redis 存储 IP 地址和对应的请求次数,并设置过期时间(时间窗口)。
  3. 每次请求时,判断该 IP 在当前时间窗口内的请求次数是否超过了预设的限制。
  4. 如果请求次数超过限制,拒绝请求或返回错误提示。
  5. 如果没有超过限制,允许请求继续执行。

步骤:

1. 添加 Redis 依赖

首先,确保在 pom.xml 中添加了 Redis 相关的依赖:

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

2. 配置 Redis

application.propertiesapplication.yml 中配置 Redis 连接信息:

spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=yourpassword

3. 创建限流服务类

接下来,创建一个 RateLimiterService 类,用于处理基于 Redis 的限流逻辑。

package com.example.demo.service;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;@Service
public class RateLimiterService {@Autowiredprivate StringRedisTemplate redisTemplate;// 时间窗口,单位为秒private static final long TIME_WINDOW = 60;// 最大请求次数private static final int MAX_REQUESTS = 100;/*** 判断用户 IP 是否超过限流* * @param ipAddress 请求的 IP 地址* @return 是否超过限流*/public boolean isRateLimited(String ipAddress) {String key = "rate_limit:" + ipAddress;// 使用 Redis 的 INCR 命令递增请求次数Long requestCount = redisTemplate.opsForValue().increment(key, 1);if (requestCount == 1) {// 如果是第一次请求,设置过期时间为时间窗口(单位:秒)redisTemplate.expire(key, TIME_WINDOW, TimeUnit.SECONDS);}// 判断请求次数是否超过最大请求次数return requestCount != null && requestCount > MAX_REQUESTS;}
}
  • TIME_WINDOW:设定限流的时间窗口(比如60秒内)。
  • MAX_REQUESTS:设定在这个时间窗口内允许的最大请求次数。
  • StringRedisTemplate:用于操作 Redis 数据,increment 命令用于递增请求次数,expire 设置过期时间。

4. 创建拦截器

然后,我们可以在 Spring 的拦截器中使用 RateLimiterService,拦截请求并根据请求 IP 判断是否超过限流。

package com.example.demo.interceptor;import com.example.demo.service.RateLimiterService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;public class RateLimiterInterceptor implements HandlerInterceptor {@Autowiredprivate RateLimiterService rateLimiterService;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 获取客户端 IP 地址String ipAddress = request.getRemoteAddr();// 检查是否超过限流if (rateLimiterService.isRateLimited(ipAddress)) {response.setStatus(HttpServletResponse.SC_TOO_MANY_REQUESTS);  // HTTP 429 状态码response.getWriter().write("Too many requests. Please try again later.");return false;}return true; // 继续处理请求}
}

5. 配置拦截器

MvcConfig 中注册限流拦截器,并指定需要限流的请求路径。

package com.example.demo.config;import com.example.demo.interceptor.RateLimiterInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration
public class MvcConfig implements WebMvcConfigurer {@Beanpublic RateLimiterInterceptor rateLimiterInterceptor() {return new RateLimiterInterceptor();}@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 将 RateLimiterInterceptor 应用到所有请求路径registry.addInterceptor(rateLimiterInterceptor()).addPathPatterns("/**").order(1);}
}

6. 测试

  • 当用户 IP 地址在单位时间内超过最大请求次数(如100次)时,返回 HTTP 429 状态码,并提示 "Too many requests. Please try again later."
  • 如果请求次数未超过限制,允许请求继续执行。

额外优化:

  1. 滑动窗口:如果需要更加精细的控制,可以实现滑动时间窗口(Sliding Window)算法,而不是使用固定时间窗口。
  2. 令牌桶/漏斗算法:对于更复杂的限流需求,可以使用令牌桶(Token Bucket)或漏斗算法(Leaky Bucket)来实现更加动态的限流。

通过这种方式,你可以轻松实现对单位时间内频繁请求的 IP 地址进行限流,确保系统的稳定性和响应能力。

response 在这种情况下会直接返回给浏览器,而不会经过 Controller 层。这是因为你在拦截器中已经通过 response.setStatus(HttpServletResponse.SC_TOO_MANY_REQUESTS) 设置了 HTTP 响应状态,并通过 response.getWriter().write(...) 写入了响应内容。

具体来说,拦截器的 preHandle 方法执行时,如果判断出请求频繁(即超过了限流),你通过调用 response.setStatus 设置了 HTTP 状态码(HTTP 429:Too Many Requests),并且用 response.getWriter().write 写入了错误消息 "Too many requests. Please try again later."

因为拦截器的 preHandle 返回 false 时,Spring MVC 会阻止请求继续传递到后续的处理器(即 Controller)。这样就可以拦截到请求,避免它进一步执行,直接返回给客户端响应错误信息。

具体执行流程:

  1. 客户端发起请求。
  2. RateLimiterInterceptor 拦截器的 preHandle 方法被调用。
  3. 如果请求超过限流限制:
    • 设置响应状态码为 429(表示过多请求)。
    • 返回提示消息 "Too many requests. Please try again later."
  4. preHandle 返回 false,请求被拦截,不会继续传递到 Controller 层
  5. 客户端收到 HTTP 429 响应,显示限流错误信息。

如果 preHandle 返回 true

  • 请求会继续传递到 Controller 层,最终由相应的 Controller 方法处理。

为什么不经过 Controller?
当你在拦截器中返回 false 时,Spring MVC 就会认为这个请求已经处理完毕(因为请求已经被拦截且不再传递给后续的处理器)。因此,这个请求不会到达 Controller 层。preHandle 方法的作用是拦截请求,决定是否允许它继续传递。

如果希望请求到达 Controller 层并且再做其他操作(如返回一个 JSON 格式的错误消息),你可以在 Controller 层进行处理,而不直接在拦截器中返回响应。

如果想要处理后续的 Controller 返回:
如果你希望请求被允许到达 Controller 层,但同时在 Controller 层进行限流处理,你可以在 preHandle 中返回 true,并让 Controller 层根据请求是否超限来返回适当的响应。

这可以通过如下方式实现:

  1. 拦截器允许请求继续。
  2. 在 Controller 层进行限流判断。
  3. 如果超限,返回适当的 HTTP 状态码和消息。

比如:

@GetMapping("/some-api")
public ResponseEntity<String> someApi() {if (rateLimiterService.isRateLimited(ipAddress)) {return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body("Too many requests. Please try again later.");}// 正常处理逻辑return ResponseEntity.ok("Request successful!");
}

通过这种方式,你可以让 Controller 层控制返回的内容,同时在拦截器中不直接返回响应。

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

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

相关文章

构建安全防线:基于视频AI的煤矿管理系统架构创新成果展示

前言 本文我将介绍一款AI产品的成果展示——“基于视频AI识别技术的煤矿安全生产管理系统”。这款产品是目前我在创业阶段和几位矿业大学的博士共同从架构设计、开发到交付的全过程中首次在博客频道发布, 我之前一直想写但没有机会来整理这套系统的架构, 因此我也特别感谢CSDN平…

浅谈计算机网络04 | 现代网络需求与技术支撑

现代网络需求与技术支撑 一、网络和因特网流量的类型剖析1.1 弹性流量的自适应特征1.2 非弹性流量的刚性特征1.3 实时流量特性 二、特定领域的网络需求解析2.1 大数据环境下的网络需求分析2.2 云计算环境下的网络需求分析2.3 移动数据环境下的网络需求分析 三、QoS和QoE&#x…

麒麟操作系统服务架构保姆级教程(十一)https配置

如果你想拥有你从未拥有过的东西&#xff0c;那么你必须去做你从未做过的事情 在运维工作中&#xff0c;加密和安全的作用是十分重要的&#xff0c;如果仅仅用http协议来对外展示我们的网站&#xff0c;过一段时间就会发现网站首页被人奇奇怪怪的篡改了&#xff0c;本来好好的博…

考研计算机组成原理——零基础学习的笔记

第一章 研究计算机硬件的学科。 1.计算机系统概述 计算机系统硬件软件&#xff08;系统软件&#xff1a;比如操作系统、数据库管理系统、标准程序库等&#xff0c;应用软件&#xff1a;QQ等&#xff09; 1.2计算机的层次结构 1.2.1计算机硬件的基本组成 冯诺伊曼计算机&a…

利用 LNMP 实现 WordPress 站点搭建

部署MySQL数据库 在主机192.168.138.139主机部署数据库服务 包安装数据库 apt-get install mysql-server 创建wordpress数据库和用户并授权 mysql> create database wordpress;#MySQL8.0要求指定插件 mysql> create user wordpress192.168.138.% identified with mys…

通过idea创建的springmvc工程需要的配置

在创建的spring mvc工程中&#xff0c;使用idea开发之前需要配置文件包括porm.xml、web.xml、springmvc.xml 1、porm.xml 工程以来的spring库&#xff0c;主要包括spring-aop、spring-web、spring-webmvc&#xff0c;示例配置如下&#xff1a; <project xmlns"http:/…

ASP.NET Core - 配置系统之自定义配置提供程序

ASP.NET Core - 配置系统之自定义配置提供程序 4. 自定义配置提供程序IConfigurationSourceIConfigurationProvider 4. 自定义配置提供程序 在 .NET Core 配置系统中封装一个配置提供程序关键在于提供相应的 IconfigurationSource 实现和 IConfigurationProvider 接口实现&…

gitlab runner正常连接 提示 作业挂起中,等待进入队列 解决办法

方案1 作业挂起中,等待进入队列 重启gitlab-runner gitlab-runner stop gitlab-runner start gitlab-runner run方案2 启动 gitlab-runner 服务 gitlab-runner start成功启动如下 [rootdocserver home]# gitlab-runner start Runtime platform …

python爬虫报错日记

python爬虫报错日记 类未定义 原因&#xff1a;代码检查没有问题**&#xff0c;位置错了**&#xff0c;测试代码包含在类里…… UnicodedecodeError错误 原因&#xff1a;字符没有自动转换成utf-8格式 KeyError&#xff1a;“href” 原因&#xff1a;前面运行正常&#x…

简历_基于 Cache Aside 模式解决数据库与缓存一致性问题。

系列博客目录 文章目录 系列博客目录缓存更新策略总结案例&#xff1a;给查询商铺的缓存添加超时剔除和主动更新的策略 说到解决数据库与缓存一致性的问题&#xff0c;其实就是要解决缓存更新的问题。 缓存更新策略 业务场景: 低一致性需求:使用内存淘汰机制。例如店铺类型的…

UllnnovationHub,一个开源的WPF控件库

目录 UllnnovationHub1.项目概述2.开发环境3.使用方法4.项目简介1.WPF原生控件1.Button2.GroupBox3.TabControl4.RadioButton5.SwitchButton6.TextBox7.PasswordBox8.CheckBox9.DateTimePicker10.Expander11.Card12.ListBox13.Treeview14.Combox15.Separator16.ListView17.Data…

二进制/源码编译安装mysql 8.0

二进制方式&#xff1a; 1.下载或上传安装包至设备&#xff1a; 2.创建组与用户&#xff1a; [rootopenEuler-1 ~]# groupadd mysql [rootopenEuler-1 ~]# useradd -r -g mysql -s /bin/false mysql 3.解压安装包&#xff1a; tar xf mysql-8.0.36-linux-glibc2.12-x86_64.ta…

快速入门:如何注册并使用GPT

文章目录 ProtonMail邮箱步骤 1&#xff1a;访问Proton官网步骤 2&#xff1a;创建ProtonMail账户步骤 3&#xff1a;选择注册免费账户步骤 4&#xff1a;填写邮箱地址和手机号&#xff08;可选&#xff09;步骤 5&#xff1a;邮箱验证&#xff08;必须进行验证&#xff09;步骤…

迅为瑞芯微RK3562开发板/核心板应用于人脸跟踪、身体跟踪、视频监控、自动语音识别(ASR)、图像分类驾驶员辅助系统(ADAS)...

可应用于人脸跟踪、身体跟踪、视频监控、自动语音识别(ASR)、图像分类驾驶员辅助系统(ADAS)、车牌识别、物体识别等。iTOP-3562开发板/核心板采用瑞芯微RK3562处理器&#xff0c;内部集成了四核A53Mali G52架构&#xff0c;主频2GHZ&#xff0c;内置1TOPSNPU算力&#xff0c;RK…

Mybatis Plus 分页实现

目录 前言&#xff1a; 一、分页插件 1、添加配置类 &#xff08;1&#xff09;创建配置类方式: &#xff08;2&#xff09;启动类中配置分页插件方式(推荐): 2、测试 二、XML自定义分页 1、UserMapper中定义接口方法 2、UserMapper.xml中编写SQL ​编辑 3、测试 前…

20250118-读取并显示彩色图像以及提取彩色图像的 R、G、B 分量

读取并显示彩色图像以及提取彩色图像的 R、G、B 分量 import cv2 #彩图R、G、B的提取 import torch from PIL import Image from matplotlib import pyplot as plt import numpy as np读取并显示彩色图像的三种方法&#xff1a; img_path "./data/yndx"1.1 使用 …

Android BitmapShader实现狙击瞄具十字交叉线准星,Kotlin

Android BitmapShader实现狙击瞄具十字交叉线准星&#xff0c;Kotlin <?xml version"1.0" encoding"utf-8"?> <RelativeLayout xmlns:android"http://schemas.android.com/apk/res/android"xmlns:tools"http://schemas.android.…

20250118拿掉荣品pro-rk3566开发板上Android13下在uboot和kernel启动阶段的Rockchip这个LOGO标识

20250118拿掉荣品pro-rk3566开发板上Android13下在uboot和kernel启动阶段的Rockchip这个LOGO标识 2025/1/18 15:12 缘起&#xff1a;做飞凌OK3588-C开发板/核心板【Linux R4】的时候&#xff0c;测试/生产要求没有开机LOGO【飞凌/Rockchip】 要求&#xff1a;黑屏或者中性界面。…

浙江安吉成新照明电器:Acrel-1000DP 分布式光伏监控系统应用探索

安科瑞吕梦怡 18706162527 摘 要&#xff1a;分布式光伏发电站是指将光伏发电组件安装在用户的建筑物屋顶、空地或其他适合的场地上&#xff0c;利用太阳能进行发电的一种可再生能源利用方式&#xff0c;与传统的大型集中式光伏电站相比&#xff0c;分布式光伏发电具有更灵活…

QQ邮箱登录逆向

文章目录 抓包密码p的加密算法1.全局搜索参数2.加密函数 抓包 输入账号和错误的密码,可用看到如下的两个包: check: 里面包含密码加密时需要的参数及发送登录时需要的参数 check的响应: ptui_checkVC(0, !UIO, \x00\x00\x00\x00\x74\xca\x4f\x59, ef787cccd192b4015ef02ef3…