❤❣❤
在❤️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>
下载地址:链接:https://pan.baidu.com/s/1ID6NUbDVqLPkVrejD4jnYA 提取码:xwlo。
将ip2region.db放在resources目录下
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("/**");
}
}