当前位置: 首页 > java >正文

【Spring拦截器实战】路径拦截与访问控制系统设计

一、前言

二、 GlobalFilter实现路径拦截

三、GlobalFilter实现IP限流

四、HandlerInterceptor实现访问频率控制

五、结语


一、前言

在现代Web应用开发中,安全防护和访问控制是至关重要的环节。本文将详细介绍如何在Spring Boot应用中通过两种不同的技术方案实现常见的防护功能:

路径拦截:限制特定URL的访问权限

IP限流:防止恶意用户通过高频请求攻击系统

访问频率:防止用户多此访问一个接口造成资源浪费

我们将分别通过GlobalFilter网关级全局过滤器和Spring MVC的HandlerInterceptor来实现这些功能。

具体可参考:【Spring 拦截器详解】GlobalFilter与HandlerInterceptor深度解析-CSDN博客

二、 GlobalFilter实现路径拦截

由于系统中的路由路径基本都要通过Gateway网关进行访问,因此路径拦截应该使用GlobalFilter网关级全局过滤器进行实现。

excludeUrlPath.properties

resources目录下的excludeUrlPath.properties排除不拦截的路径

exclude.urls[0]=/passport/getSMSCode      // 获取验证码
exclude.urls[1]=/passport/regist          // 注册
exclude.urls[2]=/passport/login           // 登录
exclude.urls[3]=/passport/registOrLogin   // 登录或注册exclude.fileStart=/static/**              // 静态资源

 ExcludeUrlProperties配置文件类

@Component
@PropertySource("classpath:excludeUrlPath.properties")
@ConfigurationProperties(prefix = "exclude")
public class ExcludeUrlProperties {private List<String> urls;  // 不拦截的路径列表private String fileStart;   // 静态资源路径}

静态资源可以对外暴露出去,比如服务器中的文件夹下的图片文件 

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration
public class StaticResourceConfig extends WebMvcConfigurationSupport {/*** 添加静态资源映射路径,图片、视频、音频等都房子classpath下的static中* @param registry*/@Overrideprotected void addResourceHandlers(ResourceHandlerRegistry registry) {/*** addResourceHandler: 指的是对外暴露的访问路径映射* addResourceLocations: 指的本地文件所在的目录*/registry.addResourceHandler("/static/**").addResourceLocations("file:/Volumes/lee/workspaces/images/");// http://127.0.0.1:8000/static/1.pngsuper.addResourceHandlers(registry);}
}

 SecurityFilterToken拦截器类

import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.itzixi.base.BaseInfoProperties;
import org.itzixi.grace.result.ResponseStatusEnum;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.List;@Component
@Slf4j
public class SecurityFilterToken implements GlobalFilter, Ordered {@Resourceprivate ExcludeUrlProperties excludeUrlProperties;// 路径匹配规则器private AntPathMatcher antPathMatcher = new AntPathMatcher();@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {// 1. 获得当前用户请求的路径urlString url = exchange.getRequest().getURI().getPath();log.info("SecurityFilterToken url = {}", url);// 2. 获得所有的需要排除校验的url listList<String> excludeList = excludeUrlProperties.getUrls();// 3.1 校验并且排除excludeListif (excludeList != null && !excludeList.isEmpty()) {for (String excludeUrl : excludeList) {if (antPathMatcher.matchStart(excludeUrl, url)) {// 如果匹配到,则直接放行,表示当前的url是不需要被拦截校验的return chain.filter(exchange);}}}// 3.1 排除静态资源服务staticString fileStart = excludeUrlProperties.getFileStart();if (StringUtils.isNotBlank(fileStart)) {boolean matchFileStart = antPathMatcher.matchStart(fileStart, url);if (matchFileStart) return chain.filter(exchange);}// 4. 代码到达此处,表示请求被拦截,需要进行校验log.info("当前请求的路径[{}]被拦截...", url);// 5. 从header中获得用户的id以及tokenHttpHeaders headers = exchange.getRequest().getHeaders();String userId = headers.getFirst(HEADER_USER_ID);String userToken = headers.getFirst(HEADER_USER_TOKEN);log.info("userId = {}", userId);log.info("userToken = {}", userToken);// 6. 判断header中是否有token,对用户请求进行判断拦截if (StringUtils.isNotBlank(userId) && StringUtils.isNotBlank(userToken)) {// 拿到redis中的token并且进行对比String userIdRedis = redis.get(REDIS_USER_TOKEN + ":" + userToken);if (userIdRedis.equals(userId)) {// 匹配则放行return chain.filter(exchange);}}// 默认不放行return RenderErrorUtils.display(exchange, ResponseStatusEnum.UN_LOGIN);}// 过滤器的顺序,数字越小则优先级越大@Overridepublic int getOrder() {return 0;}
}

 RenderErrorUtils封装返回消息

import com.google.gson.Gson;
import lombok.extern.slf4j.Slf4j;
import org.itzixi.base.BaseInfoProperties;
import org.itzixi.grace.result.GraceJSONResult;
import org.itzixi.grace.result.ResponseStatusEnum;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.MimeTypeUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;@Component
@Slf4j
@RefreshScope
public class RenderErrorUtils {/*** 重新包装并且返回错误信息* @param exchange* @param statusEnum* @return*/public static Mono<Void> display(ServerWebExchange exchange,ResponseStatusEnum statusEnum) {// 1. 获得相应responseServerHttpResponse response = exchange.getResponse();// 2. 构建jsonResult// statusEnum是一些错误信息与状态码,自行实现ResultResult jsonResult = Result.exception(statusEnum);// 3. 设置header类型if (!response.getHeaders().containsKey("Content-Type")) {response.getHeaders().add("Content-Type",MimeTypeUtils.APPLICATION_JSON_VALUE);}// 4. 修改response的状态码code为500response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);// 5. 转换json并且像response中写入数据String resultJson = new Gson().toJson(jsonResult);DataBuffer buffer = response.bufferFactory().wrap(resultJson.getBytes(StandardCharsets.UTF_8));return response.writeWith(Mono.just(buffer));}
}

IPUtils工具类

import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Optional;/*** 用户获得用户ip的工具类*/
public class IPUtil {/*** 获取请求IP:* 用户的真实IP不能使用request.getRemoteAddr()* 这是因为可能会使用一些代理软件,这样ip获取就不准确了* 此外我们如果使用了多级(LVS/Nginx)反向代理的话,ip需要从X-Forwarded-For中获得第一个非unknown的IP才是用户的有效ip。* @param request* @return*/public static String getRequestIp(HttpServletRequest request) {String ip = request.getHeader("x-forwarded-for");if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {ip = request.getHeader("Proxy-Client-IP");}if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {ip = request.getHeader("WL-Proxy-Client-IP");}if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {ip = request.getHeader("HTTP_CLIENT_IP");}if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {ip = request.getHeader("HTTP_X_FORWARDED_FOR");}if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {ip = request.getRemoteAddr();}return ip;}private static final String IP_UNKNOWN = "unknown";private static final String IP_LOCAL = "127.0.0.1";private static final int IP_LEN = 15;/*** 获取客户端真实ip* @param request request* @return 返回ip*/public static String getIP(ServerHttpRequest request) {HttpHeaders headers = request.getHeaders();String ipAddress = headers.getFirst("x-forwarded-for");if (ipAddress == null || ipAddress.length() == 0 || IP_UNKNOWN.equalsIgnoreCase(ipAddress)) {ipAddress = headers.getFirst("Proxy-Client-IP");}if (ipAddress == null || ipAddress.length() == 0 || IP_UNKNOWN.equalsIgnoreCase(ipAddress)) {ipAddress = headers.getFirst("WL-Proxy-Client-IP");}if (ipAddress == null || ipAddress.length() == 0 || IP_UNKNOWN.equalsIgnoreCase(ipAddress)) {ipAddress = Optional.ofNullable(request.getRemoteAddress()).map(address -> address.getAddress().getHostAddress()).orElse("");if (IP_LOCAL.equals(ipAddress)) {// 根据网卡取本机配置的IPtry {InetAddress inet = InetAddress.getLocalHost();ipAddress = inet.getHostAddress();} catch (UnknownHostException e) {// ignore}}}// 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割if (ipAddress != null && ipAddress.length() > IP_LEN) {int index = ipAddress.indexOf(",");if (index > 0) {ipAddress = ipAddress.substring(0, index);}}return ipAddress;}}

当然只使用这个类不能真正的获得用户的IP地址,使用时还需要配置Nginx。(不配置Nginx的话所有请求都会显示为127.0.0.1或Nginx内网IP)

server {# ...其他配置...location / {# 核心配置:传递真实客户端IPproxy_set_header X-Real-IP $remote_addr;proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;# 保持原有配置proxy_set_header Host $http_host;proxy_set_header X-Forwarded-Proto $scheme;# 其他原有配置...}
}

ResponseStatusEnum返回状态枚举

/*** 响应结果枚举,用于提供给Result返回给前端的* 本枚举类中包含了很多的不同的状态码供使用,可以自定义* 便于更优雅的对状态码进行管理,一目了然*/
public enum ResponseStatusEnum {SUCCESS(200, true, "操作成功!"),FAILED(500, false, "操作失败!"),// 50xUN_LOGIN(501,false,"请登录后再继续操作!"),TICKET_INVALID(502,false,"会话失效,请重新登录!"),USER_ALREADY_EXIST_ERROR(5021,false,"该用户已存在,不可重复注册!"),USER_ISNOT_EXIST_ERROR(5023,false,"该用户不存在,请前往注册!"),WECHAT_NUM_ALREADY_MODIFIED_ERROR(5024,false,"微信号已被修改,请等待1年后再修改!"),CAN_NOT_ADD_SELF_FRIEND_ERROR(5025,false,"无法添加自己为好友!"),FRIEND_NOT_EXIST_ERROR(5026,false,"好友不存在!"),NO_AUTH(503,false,"您的权限不足,无法继续操作!"),MOBILE_ERROR(504,false,"短信发送失败,请稍后重试!"),SMS_NEED_WAIT_ERROR(505,false,"短信发送太快啦~请稍后再试!"),SMS_CODE_ERROR(506,false,"验证码过期或不匹配,请稍后再试!"),USER_FROZEN(507,false,"用户已被冻结,请联系管理员!"),USER_UPDATE_ERROR(508,false,"用户信息更新失败,请联系管理员!"),USER_INACTIVE_ERROR(509,false,"请前往[账号设置]修改信息激活后再进行后续操作!"),USER_INFO_UPDATED_ERROR(5091,false,"用户信息修改失败!"),USER_INFO_UPDATED_NICKNAME_EXIST_ERROR(5092,false,"昵称已经存在!"),USER_INFO_UPDATED_IMOOCNUM_EXIST_ERROR(5092,false,"慕课号已经存在!"),USER_INFO_CANT_UPDATED_IMOOCNUM_ERROR(5092,false,"慕课号无法修改!"),FILE_UPLOAD_NULL_ERROR(510,false,"文件不能为空,请选择一个文件再上传!"),FILE_UPLOAD_FAILD(511,false,"文件上传失败!"),FILE_FORMATTER_FAILD(512,false,"文件图片格式不支持!"),FILE_MAX_SIZE_500KB_ERROR(5131,false,"仅支持500kb大小以下的文件上传!"),FILE_MAX_SIZE_2MB_ERROR(5132,false,"仅支持2MB大小以下的文件上传!"),FILE_MAX_SIZE_8MB_ERROR(5133,false,"体验版仅支持8MB以下的文件上传!"),FILE_MAX_SIZE_100MB_ERROR(5134,false,"仅支持100MB大小以下的文件上传!"),FILE_NOT_EXIST_ERROR(514,false,"你所查看的文件不存在!"),USER_STATUS_ERROR(515,false,"用户状态参数出错!"),USER_NOT_EXIST_ERROR(516,false,"用户不存在!"),USER_PARAMS_ERROR(517,false,"用户请求参数出错!"),USER_REGISTER_ERROR(518,false,"用户注册失败,请重试!"),// 自定义系统级别异常 54xSYSTEM_INDEX_OUT_OF_BOUNDS(541, false, "系统错误,数组越界!"),SYSTEM_ARITHMETIC_BY_ZERO(542, false, "系统错误,无法除零!"),SYSTEM_NULL_POINTER(543, false, "系统错误,空指针!"),SYSTEM_NUMBER_FORMAT(544, false, "系统错误,数字转换异常!"),SYSTEM_PARSE(545, false, "系统错误,解析异常!"),SYSTEM_IO(546, false, "系统错误,IO输入输出异常!"),SYSTEM_FILE_NOT_FOUND(547, false, "系统错误,文件未找到!"),SYSTEM_CLASS_CAST(548, false, "系统错误,类型强制转换错误!"),SYSTEM_PARSER_ERROR(549, false, "系统错误,解析出错!"),SYSTEM_DATE_PARSER_ERROR(550, false, "系统错误,日期解析出错!"),SYSTEM_NO_EXPIRE_ERROR(552, false, "系统错误,缺少过期时间!"),HTTP_URL_CONNECT_ERROR(551, false, "目标地址无法请求!"),// admin 管理系统 56xADMIN_USERNAME_NULL_ERROR(561, false, "管理员登录名不能为空!"),ADMIN_USERNAME_EXIST_ERROR(562, false, "管理员账户名已存在!"),ADMIN_NAME_NULL_ERROR(563, false, "管理员负责人不能为空!"),ADMIN_PASSWORD_ERROR(564, false, "密码不能为空或者两次输入不一致!"),ADMIN_CREATE_ERROR(565, false, "添加管理员失败!"),ADMIN_PASSWORD_NULL_ERROR(566, false, "密码不能为空!"),ADMIN_LOGIN_ERROR(567, false, "管理员不存在或密码不正确!"),ADMIN_FACE_NULL_ERROR(568, false, "人脸信息不能为空!"),ADMIN_FACE_LOGIN_ERROR(569, false, "人脸识别失败,请重试!"),ADMIN_DELETE_ERROR(5691, false, "删除管理员失败!"),CATEGORY_EXIST_ERROR(570, false, "文章分类已存在,请换一个分类名!"),// 媒体中心 相关错误 58xARTICLE_COVER_NOT_EXIST_ERROR(580, false, "文章封面不存在,请选择一个!"),ARTICLE_CATEGORY_NOT_EXIST_ERROR(581, false, "请选择正确的文章领域!"),ARTICLE_CREATE_ERROR(582, false, "创建文章失败,请重试或联系管理员!"),ARTICLE_QUERY_PARAMS_ERROR(583, false, "文章列表查询参数错误!"),ARTICLE_DELETE_ERROR(584, false, "文章删除失败!"),ARTICLE_WITHDRAW_ERROR(585, false, "文章撤回失败!"),ARTICLE_REVIEW_ERROR(585, false, "文章审核出错!"),ARTICLE_ALREADY_READ_ERROR(586, false, "文章重复阅读!"),COMPANY_INFO_UPDATED_ERROR(5151,false,"企业信息修改失败!"),COMPANY_INFO_UPDATED_NO_AUTH_ERROR(5151,false,"当前用户无权修改企业信息!"),COMPANY_IS_NOT_VIP_ERROR(5152,false,"企业非VIP或VIP特权已过期,请至企业后台充值续费!"),// 人脸识别错误代码FACE_VERIFY_TYPE_ERROR(600, false, "人脸比对验证类型不正确!"),FACE_VERIFY_LOGIN_ERROR(601, false, "人脸登录失败!"),// 系统错误,未预期的错误 555SYSTEM_ERROR(555, false, "系统繁忙,请稍后再试!"),SYSTEM_OPERATION_ERROR(556, false, "操作失败,请重试或联系管理员"),SYSTEM_RESPONSE_NO_INFO(557, false, ""),SYSTEM_ERROR_GLOBAL(558, false, "全局降级:系统繁忙,请稍后再试!"),SYSTEM_ERROR_FEIGN(559, false, "客户端Feign降级:系统繁忙,请稍后再试!"),SYSTEM_ERROR_ZUUL(560, false, "请求系统过于繁忙,请稍后再试!"),SYSTEM_PARAMS_SETTINGS_ERROR(5611, false, "参数设置不规范!"),ZOOKEEPER_BAD_VERSION_ERROR(5612, false, "数据过时,请刷新页面重试!"),SYSTEM_ERROR_BLACK_IP(5621, false, "请求过于频繁,请稍后重试!"),SYSTEM_SMS_FALLBACK_ERROR(5587, false, "短信业务繁忙,请稍后再试!"),SYS_DATA_ERROR(5588, false, "系统参数为空,请检查系统参数表sys_params!"),SYSTEM_ERROR_NOT_BLANK(5599, false, "系统错误,参数不能为空!"),DATA_DICT_EXIST_ERROR(5631, false, "数据字典已存在,不可重复添加或修改!"),DATA_DICT_DELETE_ERROR(5632, false, "删除数据字典失败!"),REPORT_RECORD_EXIST_ERROR(5721, false, "请不要重复举报噢~!"),RESUME_MAX_LIMIT_ERROR(5711, false, "本日简历刷新次数已达上限!"),JWT_SIGNATURE_ERROR(5555, false, "用户校验失败,请重新登录!"),JWT_EXPIRE_ERROR(5556, false, "登录有效期已过,请重新登录!"),SENTINEL_BLOCK_FLOW_LIMIT_ERROR(5801, false, "系统访问繁忙,请稍后再试!"),// 支付错误相关代码PAYMENT_USER_INFO_ERROR(5901, false, "用户id或密码不正确!"),PAYMENT_ACCOUT_EXPIRE_ERROR(5902, false, "该账户授权访问日期已失效!"),PAYMENT_HEADERS_ERROR(5903, false, "请在header中携带支付中心所需的用户id以及密码!"),PAYMENT_ORDER_CREATE_ERROR(5904, false, "支付中心订单创建失败,请联系管理员!"),// admin 相关错误代码ADMIN_NOT_EXIST(5101, false, "管理员不存在!");// 响应业务状态private Integer status;// 调用是否成功private Boolean success;// 响应消息,可以为成功或者失败的消息private String msg;ResponseStatusEnum(Integer status, Boolean success, String msg) {this.status = status;this.success = success;this.msg = msg;}public Integer status() {return status;}public Boolean success() {return success;}public String msg() {return msg;}
}

三、GlobalFilter实现IP限流

由于系统中http访问接口基本都要通过Gateway网关进行访问,因此IP限流应该使用GlobalFilter网关级全局过滤器进行实现。

application.ymal配置

blackIp:continueCounts: 10   # ip连续请求的次数timeInterval: 60    # ip判断的时间间隔,单位:秒limitTimes: 60      # 黑名单ip限制的时间,单位:秒

IPLimitFilter拦截器类

import com.google.gson.Gson;
import lombok.extern.slf4j.Slf4j;
import org.itzixi.base.BaseInfoProperties;
import org.itzixi.grace.result.GraceJSONResult;
import org.itzixi.grace.result.ResponseStatusEnum;
import org.itzixi.utils.IPUtil;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;@Component
@Slf4j
@RefreshScope
public class IPLimitFilter implements GlobalFilter, Ordered {/*** 需求:* 判断某个请求的ip在60秒内的请求次数是否超过10次* 如果超过10次,则限制访问60秒* 等待60秒静默后,才能够继续恢复访问*/@Value("${blackIp.continueCounts}")private Integer continueCounts;@Value("${blackIp.timeInterval}")private Integer timeInterval;@Value("${blackIp.limitTimes}")private Integer limitTimes;@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {log.info("continueCounts: {}", continueCounts);log.info("timeInterval: {}", timeInterval);log.info("limitTimes: {}", limitTimes);return doLimit(exchange, chain);// 默认放行请求到后续的路由(服务)// return chain.filter(exchange);}/*** 限制ip请求次数的判断* @param exchange* @param chain* @return*/public Mono<Void> doLimit(ServerWebExchange exchange,GatewayFilterChain chain) {// 根据request获得请求ipServerHttpRequest request = exchange.getRequest();String ip = IPUtil.getIP(request);// 正常的ip定义final String ipRedisKey = "gateway-ip:" + ip;// 被拦截的黑名单ip,如果在redis中存在,则表示目前被关小黑屋final String ipRedisLimitKey = "gateway-ip:limit:" + ip;// 获得当前的ip并且查询还剩下多少时间,如果时间存在(大于0),则表示当前仍然处在黑名单中long limitLeftTimes = redis.ttl(ipRedisLimitKey);if (limitLeftTimes > 0) {// 终止请求,返回错误return renderErrorMsg(exchange, ResponseStatusEnum.SYSTEM_ERROR_BLACK_IP);}// 在redis中获得ip的累加次数long requestCounts = redis.increment(ipRedisKey, 1);/*** 判断如果是第一次进来,也就是从0开始计数,则初期访问就是1,* 需要设置间隔的时间,也就是连续请求的次数的间隔时间*/if (requestCounts == 1) {redis.expire(ipRedisKey, timeInterval);}/*** 如果还能获得请求的正常次数,说明用户的连续请求落在限定的[timeInterval]之内* 一旦请求次数超过限定的连续访问次数[continueCounts],则需要限制当前的ip*/if (requestCounts > continueCounts) {// 限制ip访问的时间[limitTimes]redis.set(ipRedisLimitKey, ipRedisLimitKey, limitTimes);// 终止请求,返回错误return renderErrorMsg(exchange, ResponseStatusEnum.SYSTEM_ERROR_BLACK_IP);}return chain.filter(exchange);}/*** 重新包装并且返回错误信息* @param exchange* @param statusEnum* @return*/public Mono<Void> renderErrorMsg(ServerWebExchange exchange,ResponseStatusEnum statusEnum) {// 1. 获得相应responseServerHttpResponse response = exchange.getResponse();// 2. 构建jsonResultResult jsonResult = Result.exception(statusEnum);// 3. 设置header类型if (!response.getHeaders().containsKey("Content-Type")) {response.getHeaders().add("Content-Type",MimeTypeUtils.APPLICATION_JSON_VALUE);}// 4. 修改response的状态码code为500response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);// 5. 转换json并且像response中写入数据String resultJson = new Gson().toJson(jsonResult);DataBuffer buffer = response.bufferFactory().wrap(resultJson.getBytes(StandardCharsets.UTF_8));return response.writeWith(Mono.just(buffer));}// 过滤器的顺序,数字越小则优先级越大@Overridepublic int getOrder() {return 1;}
}

四、HandlerInterceptor实现访问频率控制

访问频率控制由于是对特定功能的需求,如发送验证码功能(十分钟内有效),所以应该用Spring MVC的HandlerInterceptor来进行实现(当然也可以通过GlobalFilter拿到访问的接口地址进行实现),此处以发送验证码功能为例。 

前提:在发送信息业务后面将验证码存到Redis中

@PostMapping("getSMSCode")
public GraceJSONResult getSMSCode(String mobile,HttpServletRequest request) throws Exception {if (StringUtils.isBlank(mobile)) {return Result.error();}// 获得用户的手机号/ipString  userIp = IPUtil.getRequestIp(request);// 限制该用户的手机号/ip在60秒内只能获得一次验证码redis.setnx60s(MOBILE_SMSCODE + ":" + userIp, mobile);String code = String.valueOf((int)((Math.random() * 9 + 1) * 100000));// 把验证码存入到redis中,用于后续的注册/登录的校验redis.set(MOBILE_SMSCODE + ":" + mobile, code, 10 * 60);return Result.ok();
}

 SMSInterceptor拦截器类

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.itzixi.base.BaseInfoProperties;
import org.itzixi.exceptions.GraceException;
import org.itzixi.grace.result.ResponseStatusEnum;
import org.itzixi.utils.IPUtil;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;@Slf4j
public class SMSInterceptor implements HandlerInterceptor {/*** 拦截请求,在controller调用方法之前* @param request* @param response* @param handler* @return* @throws Exception*/@Overridepublic boolean preHandle(HttpServletRequest request,HttpServletResponse response,Object handler) throws Exception {// 获得用户的ipString  userIp = IPUtil.getRequestIp(request);// 获得用于判断是否存在的booleanboolean isExist = redis.keyIsExist(MOBILE_SMSCODE + ":" + userIp);if (isExist) {log.error("短信发送频率太高了~~!!!");// TODO:抛出异常让全局异常处理器拦截,然后封装返回给前端GraceException.display(ResponseStatusEnum.SMS_NEED_WAIT_ERROR);return false;}/*** false: 请求被拦截* true: 请求放心,正常通过,验证通过*/return true;}/*** 请求controller之后,渲染视图之前* @param request* @param response* @param handler* @param modelAndView* @throws Exception*/@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);}/*** 请求controller之后,渲染视图之后* @param request* @param response* @param handler* @param ex* @throws Exception*/@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {HandlerInterceptor.super.afterCompletion(request, response, handler, ex);}
}

注册绑定拦截器

@Configuration
public class InterceptorConifg implements WebMvcConfigurer {/*** 在Springboot容器中放入拦截器* @return*/@Beanpublic SMSInterceptor smsInterceptor() {return new SMSInterceptor();}/*** 注册拦截器,并且拦截指定的路由,否则不生效* @param registry*/@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(smsInterceptor()).addPathPatterns("/getSMSCode");}
}

五、结语

本文详细介绍了在Spring生态中实现路径拦截、访问控制功能的主要方案。无论选择GlobalFilter还是HandlerInterceptor,都能有效提升应用的安全性。在实际项目中,建议根据具体技术栈和性能需求选择合适的方案,并结合动态配置、分布式缓存等高级特性构建更加健壮的安全防护体系。

最佳实践提示

对于生产环境,建议结合日志监控和告警系统

重要的安全规则应支持热更新,无需重启应用

考虑实现白名单机制作为黑名单的补充

限流策略应根据API的重要性和资源消耗动态调整

http://www.xdnf.cn/news/16049.html

相关文章:

  • 期货配资软件开发注意事项?
  • Linux文件——文件系统Ext2(1)_理解硬件
  • Java (Spring AI) 实现MCP server实现数据库的智能问答
  • 2️⃣tuple(元组)速查表
  • 从“点状用例”到“质量生态”:现代软件测试的演进、困局与破局
  • vscode不识别vsix结尾的插件怎么解决?
  • 应用层攻防启示录:HTTP/HTTPS攻击的精准拦截之道
  • Datawhale AI 夏令营-心理健康Agent开发学习-Task1
  • MongoDB频繁掉线频繁断开服务的核心原因以及解决方案-卓伊凡|贝贝|莉莉|糖果
  • 【OpenCV篇】OpenCV——01day.图像基础
  • 漫画版:细说金仓数据库
  • 2025年COR SCI2区,基于多种配送模式的无人机自主配送车辆路径问题,深度解析+性能实测
  • 面试高频题 力扣 LCR 130.衣柜整理 洪水灌溉(FloodFill) 深度优先遍历(dfs) 暴力搜索 C++解题思路 每日一题
  • PACKET_HOST等宏定义介绍
  • 目标检测系列(六)labelstudio实现自动化标注
  • YOLO-实例分割头
  • 使用vue-pdf-embed发现某些文件不显示内容
  • 能协调控制器的硬件与软件组成及解决方案
  • 16.多生成树MSTP
  • 图论的整合
  • 前端--bom、JQuery
  • 大数据量查询计算引发数据库CPU告警问题复盘
  • WAF 防护与漏洞扫描联动:让安全防御更精准高效
  • python办自动化--读取邮箱中特定的邮件,并下载特定的附件
  • 数据库—修改某字段默认值
  • importlib.import_module() 的用法与实战案例
  • Java值传递和构造函数
  • Java 并发性深度解析
  • C# 基于halcon的视觉工作流-章21-点查找
  • 【前端】ikun-pptx编辑器前瞻问题一: pptx的xml样式, 使用html能100%还原么