当前位置: 首页 > 工具软件 > Sa-Token > 使用案例 >

Sa-Token中接口的限流

祁永嘉
2023-12-01

❤❣❤

❤️Sa-Token❤️中对接口进行限流设置

❤❣❤

一、添加依赖
        <!-- Sa-Token 权限认证, 在线文档:http://sa-token.dev33.cn/ -->
        <dependency>
            <groupId>cn.dev33</groupId>
            <artifactId>sa-token-spring-boot-starter</artifactId>
            <version>1.29.0</version>
        </dependency>

  		<!-- Sa-Token整合SpringAOP实现注解鉴权、拦截等 -->
        <dependency>
            <groupId>cn.dev33</groupId>
            <artifactId>sa-token-spring-aop</artifactId>
            <version>1.30.0</version>
        </dependency>

        <!-- Sa-Token 整合 Redis (使用jackson序列化方式) -->
        <dependency>
            <groupId>cn.dev33</groupId>
            <artifactId>sa-token-dao-redis-jackson</artifactId>
            <version>1.29.0</version>
        </dependency>
        <!-- 提供Redis连接池 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        
        <!-- Sa-Token 整合 jwt -->
        <dependency>
            <groupId>cn.dev33</groupId>
            <artifactId>sa-token-jwt</artifactId>
            <version>1.29.0</version>
        </dependency>
        
        <!--IP管理jar-->
        <dependency>
            <groupId>eu.bitwalker</groupId>
            <artifactId>UserAgentUtils</artifactId>
            <version>1.21</version>
        </dependency>
        <!--IP解析-->
        <dependency>
            <groupId>org.lionsoul</groupId>
            <artifactId>ip2region</artifactId>
            <version>1.7.2</version>
        </dependency>
二、实现
1、下载ip2region.db文件

下载地址:链接:https://pan.baidu.com/s/1ID6NUbDVqLPkVrejD4jnYA 提取码:xwlo。

将ip2region.db放在resources目录下

2、实现代码

application.yml配置

server:
  # 端口
  port: 7710
  servlet:
    context-path: /api
spring:
#==============================redis配置==============================
  redis:
    # Redis数据库索引(默认为0)
    database: 1
    # Redis服务器地址
    host: 127.0.0.1
    # Redis服务器连接端口
    port: 6379
    # Redis服务器连接密码(默认为空)
    password:
    # 连接超时时间
    timeout: 10s
    lettuce:
      pool:
        # 连接池最大连接数
        max-active: 200
        # 连接池最大阻塞等待时间(使用负值表示没有限制)
        max-wait: -1ms
        # 连接池中的最大空闲连接
        max-idle: 10
        # 连接池中的最小空闲连接
        min-idle: 0
       
#==============================Sa-Token配置==============================
sa-token:
  #是否关掉每次启动时的字符画打印
  isPrint: false
  # token名称 (同时也是cookie名称)
  token-name: authorization
  # token有效期,单位s 默认30天, -1代表永不过期
  timeout: 3600
  # token临时有效期 (指定时间内无操作就视为token过期) 单位: 秒
  activity-timeout: -1
  # 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录)
  is-concurrent: false
  # 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token)
  is-share: true
  # 是否输出操作日志
  is-log: true
  # 是否允许操作cookie
  isReadCookie: true
  # token前缀
  token-prefix: Bearer
  # token风格
  token-style: jwt
  # jwt秘钥
  jwt-secret-key: *****
  
#==============================IP限流次数和时间配置==============================
IpLimit:
  #请求次数
  count: 5
  #请求时间
  time: 10

IpUtil工具类

import eu.bitwalker.useragentutils.UserAgent;
import com.alibaba.fastjson.JSONObject;
import com.leaves.common.constant.PublicConstant;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.lionsoul.ip2region.DataBlock;
import org.lionsoul.ip2region.DbConfig;
import org.lionsoul.ip2region.DbMakerConfigException;
import org.lionsoul.ip2region.DbSearcher;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.InputStreamReader;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.net.URL;
import java.net.URLConnection;
import java.net.UnknownHostException;
import java.util.Map;
import java.util.Objects;

/**
 * @Author: LEAVES
 * @Version 1.0
 * @Date: 2022年04月01日  11时03分11秒
 * @Description: IP工具类
 */
public class IpUtil {

    private static final Logger logger = LoggerFactory.getLogger(IpUtil.class);

    /**
     * 在腾讯位置服务(https://lbs.qq.com)上申请 key
     */
    private final static String FORMAT_URL = "https://apis.map.qq.com/ws/location/v1/ip?ip={}&key=S4FBZ-6QQLX-2QE4S-ZAMQJ-M3IC5-JPBV2";

    private static final String dbPath;
    private static DbSearcher searcher;
    private static DbConfig config;

    private final static String localIp = "127.0.0.1";

    static {
        dbPath = Objects.requireNonNull(IpUtil.class.getResource("/ip2region.db")).getPath();
        try {
            config = new DbConfig();
        } catch (DbMakerConfigException e) {
            e.printStackTrace();
        }
        try {
            searcher = new DbSearcher(config, dbPath);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }

    /**
     * 获取用户真实IP地址,不使用request.getRemoteAddr()的原因是有可能用户使用了代理软件方式避免真实IP地址,
     * 可是,如果通过了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP值
     *
     * @return ip
     */
    public static String getIpAddress(HttpServletRequest request) {
        String ipAddress;
        try {
            ipAddress = request.getHeader("x-forwarded-for");
            if (ipAddress != null && ipAddress.length() != 0 && !"unknown".equalsIgnoreCase(ipAddress)) {
                // 多次反向代理后会有多个ip值,第一个ip才是真实ip
                if (ipAddress.indexOf(",") != -1) {
                    ipAddress = ipAddress.split(",")[0];
                    logger.info("多次反向代理后 ip: " + ipAddress);
                }
            }
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getHeader("Proxy-Client-IP");
                logger.info("Proxy-Client-IP ip: " + ipAddress);
            }
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getHeader("WL-Proxy-Client-IP");
                logger.info("WL-Proxy-Client-IP ip: " + ipAddress);
            }
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getHeader("HTTP_CLIENT_IP");
                logger.info("HTTP_CLIENT_IP ip: " + ipAddress);
            }
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getHeader("HTTP_X_FORWARDED_FOR");
                logger.info("HTTP_X_FORWARDED_FOR ip: " + ipAddress);
            }
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getHeader("X-Real-IP");
                logger.info("X-Real-IP ip: " + ipAddress);
            }
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getRemoteAddr();
                if (localIp.equals(ipAddress)) {
                    // 根据网卡取本机配置的IP
                    InetAddress inet = null;
                    try {
                        inet = InetAddress.getLocalHost();
                    } catch (UnknownHostException e) {
                        e.printStackTrace();
                    }
                    assert inet != null;
                    ipAddress = inet.getHostAddress();
                    logger.info("根据网卡取本机配置 ip: " + ipAddress);
                }
            }
            // 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
            if (ipAddress != null && ipAddress.length() > 15) {
                // = 15
                if (ipAddress.indexOf(",") > 0) {
                    ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
                    logger.info("多次反向代理后 ip: " + ipAddress);
                }
            }
        } catch (Exception e) {
            ipAddress = "";
        }
        System.out.println("ipAddress = " + ipAddress);
        return "0:0:0:0:0:0:0:1".equals(ipAddress) ? localIp : ipAddress;
    }

    /**
     * 获取访问设备
     *
     * @param request 请求
     * @return {@link UserAgent} 访问设备
     */
    public static UserAgent getUserAgent(HttpServletRequest request) {
        return UserAgent.parseUserAgentString(request.getHeader("User-Agent"));
    }


    /**
     * 解析ip地址 获取IP来源
     *
     * @param ip ip地址
     * @return 解析后的ip地址
     */
    public static String getCityInfo(String ip) {
        //解析ip地址,获取省市区
        String s = analyzeIp(ip);
        Map map = JSONObject.parseObject(s, Map.class);
        Integer status = (Integer) map.get("status");
        String address = PublicConstant.UNKNOWN;
        if (status == 0) {
            Map result = (Map) map.get("result");
            Map addressInfo = (Map) result.get("ad_info");
            String nation = (String) addressInfo.get("nation");
            String province = (String) addressInfo.get("province");
            String city = (String) addressInfo.get("city");
            address = nation + "-" + province + "-" + city;
        }
        return address;
    }

    /**
     * 根据ip2region解析ip地址
     *
     * @param ip ip地址
     * @return 解析后的ip地址
     */
    public static String getIp2region(String ip)  {
        if (StringUtils.isEmpty(dbPath)) {
            logger.error("Error: Invalid ip2region.db file");
            return null;
        }
        if(config == null || searcher == null){
            logger.error("Error: DbSearcher or DbConfig is null");
            return null;
        }

        try {
            //define the method
            Method method = null;
            //B-tree, B树搜索(更快)
            method = searcher.getClass().getMethod("btreeSearch", String.class);

            DataBlock dataBlock;
            dataBlock = (DataBlock) method.invoke(searcher, ip);
            String ipInfo = dataBlock.getRegion();
            if (!StringUtils.isEmpty(ipInfo)) {
                ipInfo = ipInfo.replace("|0", "");
                ipInfo = ipInfo.replace("0|", "");
            }
            return ipInfo;

        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }

    /**
     * 根据在腾讯位置服务上申请的key进行请求解析ip
     *
     * @param ip ip地址
     * @return
     */
    public static String analyzeIp(String ip) {
        StringBuilder result = new StringBuilder();
        BufferedReader in = null;
        try {
            String url = FORMAT_URL.replace("{}", ip);
            URL realUrl = new URL(url);
            // 打开和URL之间的链接
            URLConnection connection = realUrl.openConnection();
            // 设置通用的请求属性
            connection.setRequestProperty("accept", "*/*");
            connection.setRequestProperty("connection", "Keep-Alive");
            connection.setRequestProperty("user-agent",
                    "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
            // 创建实际的链接
            connection.connect();
            // 定义 BufferedReader输入流来读取URL的响应
            in = new BufferedReader(new InputStreamReader(
                    connection.getInputStream()));
            String line;
            while ((line = in.readLine()) != null) {
                result.append(line);
            }
        } catch (Exception e) {
            logger.error("发送GET请求出现异常!异常信息为:{}", e.getMessage());
        }
        // 使用finally块来关闭输入流
        finally {
            try {
                if (in != null) {
                    in.close();
                }
            } catch (Exception e2) {
                e2.printStackTrace();
            }
        }
        return result.toString();
    }

}

AccessLimitIntercept类

import cn.dev33.satoken.SaManager;
import cn.hutool.core.util.ObjectUtil;
import com.leaves.common.constant.RedisConstant;
import com.leaves.common.util.IpUtil;
import com.leaves.exception.MessageCenterException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @Author: LEAVES
 * @Version 1.0
 * @Date: 2022年04月09日  16时15分40秒
 * @Description: 在指定时间内请求次数限制
 */
@Component
public class AccessLimitIntercept implements HandlerInterceptor {

    private static final Logger logger = LoggerFactory.getLogger(AccessLimitIntercept.class);

    @Value("${IpLimit.count}")
    private int count;
    @Value("${IpLimit.time}")
    private int time;

    /**
     * 接口调用前检查对方ip是否频繁调用接口
     *
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // handler是否为 HandleMethod 实例
        if (handler instanceof HandlerMethod) {
            //可以过滤掉无需限流的接口或者自定义注解进行限流
            
            
            // 拼接redis key = IP + Api限流
            String key = RedisConstant.LIMIT + IpUtil.getIpAddress(request) + request.getRequestURI();
            // 获取redis的value
            Integer maxTimes = null;
            String value = SaManager.getSaTokenDao().get(key);
            if (ObjectUtil.isNotNull(value)) {
                maxTimes = Integer.valueOf(value);
            }
            if (maxTimes == null) {
                maxTimes = 1;
                // 如果redis中没有该ip对应的时间则表示第一次调用,保存key到redis
                SaManager.getSaTokenDao().set(key, String.valueOf(maxTimes), time);
            } else if (maxTimes < count) {
                maxTimes = maxTimes + 1;
                // 如果redis中的时间比注解上的时间小则表示可以允许访问,这是修改redis的value时间
                SaManager.getSaTokenDao().set(key, String.valueOf(maxTimes), time);
            } else {
                // 请求过于频繁
                logger.error("API请求限流拦截启动,{} 请求过于频繁", key);
                throw new MessageCenterException("请求过于频繁,请稍后再试");
            }
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}

SaTokenConfigure

import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.filter.SaServletFilter;
import cn.dev33.satoken.interceptor.SaAnnotationInterceptor;
import cn.dev33.satoken.jwt.StpLogicJwtForStyle;
import cn.dev33.satoken.router.SaHttpMethod;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.spring.SpringMVCUtil;
import cn.dev33.satoken.stp.StpLogic;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.json.JSONUtil;
import com.leaves.common.interceptor.AccessLimitIntercept;
import com.leaves.common.util.IpUtil;
import com.leaves.response.ApiResult;
import com.leaves.response.ApiResultConstant;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.List;

/**
 * @Author: LEAVES
 * @Version 1.0
 * @Date: 2022年03月20日  15时15分24秒
 * @Description: SaToken配置
 */
@Configuration
public class SaTokenConfigure  implements WebMvcConfigurer {

    private static final Logger LOGGER = LoggerFactory.getLogger(SaTokenConfigure.class);

   /**
     * Sa-Token 整合 jwt (Style模式)
     *
     * @return
     */
    @Bean
    public StpLogic getStpLogicJwt() {
        return new StpLogicJwtForStyle();
    }

    /**
     * 注册 [Sa-Token全局过滤器] 解决跨域问题
     */
    @Bean
    public SaServletFilter getSaServletFilter() {

        //拦截排除的路径集合
        List<String> list = new ArrayList<>();
        list.add("/**/v1/*");
        list.add("/**/v2/*");
        list.add("/**/v2/*");
        return new SaServletFilter()
                //放行地址
                .addInclude("/**")
            	// 拦截地址
                .setExcludeList(list)
                // 认证函数: 每次请求执行
                .setAuth(obj -> {
                    LOGGER.info("---------- 进入Sa-Token全局认证 -----------");
                    HttpServletRequest req = SpringMVCUtil.getRequest();
                    printRequest(req);
                    // 登录认证 -- 拦截所有路由但不包括已排除的路由
                    SaRouter.match("/**",  () -> StpUtil.checkLogin());
                    // 更多拦截处理方式,请参考“路由拦截式鉴权”章节
                })
            
                // 异常处理函数:每次认证函数发生异常时执行此函数
                .setError(e -> {
                    LOGGER.error("---------- 进入Sa-Token异常处理 -----------");
                    HttpServletResponse response = SpringMVCUtil.getResponse();
                    String message = "";
                    // 设置响应头
                    SaHolder.getResponse().setHeader("Content-Type", "application/json;charset=UTF-8");
                    LOGGER.error("异常信息 : " + e.getMessage());
                    if (e instanceof NotLoginException) {
                        response.setStatus(ApiResultConstant.CODE_NOT_LOGIN);
                        String type = ((NotLoginException) e).getType();
                        LOGGER.info("type = " + type);
                        String loginType = ((NotLoginException) e).getLoginType();
                        LOGGER.info("loginType = " + loginType);
                        if (NotLoginException.NOT_TOKEN.equals(type)) {
                            LOGGER.error("NotLoginException.NOT_TOKEN = " + NotLoginException.NOT_TOKEN);
                            message = "您的令牌为空";
                        } else if (NotLoginException.INVALID_TOKEN.equals(type)) {
                            LOGGER.error("NotLoginException.INVALID_TOKEN = " + NotLoginException.INVALID_TOKEN);
                            message = "您的令牌无效";
                        } else if (NotLoginException.TOKEN_TIMEOUT.equals(type)) {
                            LOGGER.error("NotLoginException.TOKEN_TIMEOUT = " + NotLoginException.TOKEN_TIMEOUT);
                            message = "您的令牌已过期";
                        } else if (NotLoginException.BE_REPLACED.equals(type)) {
                            LOGGER.error("NotLoginException.BE_REPLACED = " + NotLoginException.BE_REPLACED);
                            message = "账号在别处登录,请注意是否为您本人操作";
                        } else if (NotLoginException.KICK_OUT.equals(type)) {
                            LOGGER.error("NotLoginException.KICK_OUT = " + NotLoginException.KICK_OUT);
                            message = "您已被踢下线";
                        } else {
                            message = "当前会话未登录";
                        }
                    } else {
                        message = "系统异常,请稍再试!";
                    }
                    return JSONUtil.toJsonStr(ApiResult.checkLogin(message));
                })

                // 前置函数:在每次认证函数之前执行
                .setBeforeAuth(obj -> {
                    // ---------- 设置一些安全响应头 ----------
                    SaHolder.getResponse()
                            // 服务器名称
                            .setServer("LEAVES")
                            // 是否可以在iframe显示视图: DENY=不可以 | SAMEORIGIN=同域下可以 | ALLOW-FROM uri=指定域名下可以
                            .setHeader("X-Frame-Options", "SAMEORIGIN")
                            // 是否启用浏览器默认XSS防护: 0=禁用 | 1=启用 | 1; mode=block 启用, 并在检查到XSS攻击时,停止渲染页面
                            .setHeader("X-XSS-Protection", "1; mode=block")
                            // 禁用浏览器内容嗅探
                            .setHeader("X-Content-Type-Options", "nosniff")
                    // ---------- 设置跨域响应头 ----------
                            // 允许指定域访问跨域资源
                            .setHeader("Access-Control-Allow-Origin", "*")
                            // 允许所有请求方式
                            .setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE")
                            // 有效时间
                            .setHeader("Access-Control-Max-Age", "3600")
                            // 允许的header参数
                            .setHeader("Access-Control-Allow-Headers", "*");

                    // 如果是预检请求,则立即返回到前端
                    SaRouter.match(SaHttpMethod.OPTIONS)
                            .free(r -> LOGGER.info("--------OPTIONS预检请求,不做处理"))
                            .back();
                });
    }
    
   /**
     * 这里需要先将限流拦截器入住
     * @return
     */
    @Bean
    public AccessLimitIntercept getAccessLimitIntercept() {
        return new AccessLimitIntercept();
    }
    
    /**
     * 注册 Sa-Token 的拦截器,打开注解式鉴权功能
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册注解拦截器,并排除不需要注解鉴权的接口地址 (与登录拦截器无关)
        registry.addInterceptor(new SaAnnotationInterceptor()).addPathPatterns("/**");
        // 注册限流拦截器
        registry.addInterceptor(getAccessLimitIntercept()).addPathPatterns("/**");
    }
}

 类似资料: