Gitee仓库
https://gitee.com/Lin_DH/system
介绍
常用的 API 安全措施包括:防火墙、验证码、鉴权、IP限制、数据加密、限流、监控、网关等,以确保接口的安全性。
常见措施
1)防火墙
防火墙是网络安全中最基本的安全设备之一,主要用于防止未经授权的网络访问和攻击。
防火墙主要用于过滤和控制网络流量,以保护网络安全。
防火墙可以防止的攻击行为包括:
- 无效数据包:防火墙可以识别和过滤无效的数据包,如错误的 IP 地址、伪造的数据包、无法识别的协议等。
- DOS 和 DDOS 攻击:防火墙可以使用不同的技术来检测和阻止 DOS 和 DDOS 攻击,如阻止大量 TCP / UDP 连接、IP 地址过滤、流量限制等。
- 病毒和蠕虫攻击:防火墙可以使用特定的病毒和蠕虫检测技术,如签名检测、行为检测、模式识别等,来防止这些恶意软件的传播。
- 网络钓鱼和欺骗攻击:防火墙可以检测、防止网络钓鱼、欺骗攻击,如防止虚假登录页面、欺骗的网站等。
- 恶意流量攻击:防火墙可以检测和防止恶意流量攻击,如过滤带有恶意载荷的数据包和防止被黑客利用的端口。
- 网络侦察攻击:防火墙可以使用一些技术来防止网络侦察攻击,如防止扫描、端口扫描、漏洞利用等。
2)验证码
在特定接口上,要求用户在访问前先进行验证码验证,以确保发送该请求的为真实用户。
3)鉴权
要求用户在访问 API 时,进行身份认证,并根据用户的权限进行授权,只允许有权限的用户访问特定的接口。
4)IP限制
仅限特定 IP 范围对 API 的访问,例如允许内网或者加入 IP 白名单的能够访问特定 API 。
5)数据加密
对敏感数据进行加密传输,使用 HTTPS 协议保证数据传输的安全性。
以往很多接口都是使用 HTTP 协议(Hyper Text Transport Protocol,超文本传输协议),用于传输客户端和服务器端的数据。
HTTP 协议使用虽然简单方便,但也存在着问题:
- 使用明文通讯,传输内容容易被窃听
- 不验证通讯方的身份,容易遭到伪装
- 无法证明报文的完整性,报文容易被篡改
为了解决 HTTP 协议的一系列问题,出现了 HTTPS 协议。HTTPS 协议是在 HTTP 协议上添加了加密机制。
SSL(Secure Socket Layer,安全套接层)
TLS(Transport Layer Security,传输层安全)
HTTPS = HTTP + 加密 + 认证 + 完整性保护
为了安全性考虑,接口的协议需要使用 HTTPS 协议。
6)限流
设置访问频率限制,例如每分钟、每小时、每天只允许请求访问一定次数,超出限制则返回错误信息或者封禁 IP。
7)监控
监控 API 的访问日志,统计用户对接口的调用情况,对流量激增、某个IP频繁请求同一接口,则自动发送邮件等通知,及时采取相应的安全措施。
8)网关
在 API 和客户端之间引入 API 网关,对请求进行过滤、鉴权、限流等操作,保护后端 API 的安全。
IP限制方式(拦截器)
代码实现
第一步:定义 IP 限制拦截器
IPInterceptor.java
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.List;
/**
* IP拦截器
* @author DUHAOLIN
* @date 2024/11/13
*/
@Component
public class IPInterceptor implements HandlerInterceptor {
//IP白名单
private static final List<String> ALLOWED_IPS = Arrays.asList("127.0.0.1", "0:0:0:0:0:0:0:1");
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String ipAddress = request.getRemoteAddr();
//不允许访问的IP返回"Access denied"错误信息,并且设置响应的状态码为403(Forbidden)
if (!ALLOWED_IPS.contains(ipAddress)) {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.getWriter().write("Access denied");
return false;
}
return true;
}
}
效果图
限制次数方式(Redis + 拦截器)
Windows安装Redis
Redis 下载链接:https://pan.baidu.com/s/1BMt4cIxjKTtyL3T0_iSC2w 密码:rkne
打开 CMD 命令窗口,在 Redis 安装目录执行如下命令:redis-server.exe redis.windows.conf
依赖
pom.xml
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 分布式锁工具 -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>${redission.version}</version>
</dependency>
配置文件
application.yml
spring:
redis:
host: localhost
port: 6379
timeout: 10
代码实现
第一步:添加解析 Redis Key 前缀的接口
KeyPrefix.java
package com.lm.system.redis;
/**
* @author DUHAOLIN
* @date 2024/11/13
*/
public interface KeyPrefix {
int expireSeconds();
String getPrefix();
}
第二步:添加解析 Redis 基础前缀的抽象类
BasePrefix.java
package com.lm.system.redis;
/**
* @author DUHAOLIN
* @date 2024/11/13
*/
public abstract class BasePrefix implements KeyPrefix {
public BasePrefix() {}
public BasePrefix(String prefix) {
this(0, prefix);
}
private int expireSeconds;
private String prefix;
public BasePrefix(int expireSeconds, String prefix) {
this.expireSeconds = expireSeconds;
this.prefix = prefix;
}
@Override
public int expireSeconds() {
return 0; //默认永不过期
}
@Override
public String getPrefix() {
String simpleName = this.getClass().getSimpleName();
return simpleName + ":" + prefix;
}
}
第三步:添加解析用户Key的实现类
AccessKey.java
package com.lm.system.common;
import com.lm.system.redis.BasePrefix;
/**
* @author DUHAOLIN
* @date 2024/11/13
*/
public class AccessKey extends BasePrefix {
public AccessKey() {}
public AccessKey(String prefix) {
super(0, prefix);
}
public AccessKey(int expireSeconds, String prefix) {
super(expireSeconds, prefix);
}
public static AccessKey withExpire(int expireSeconds) {
return new AccessKey(expireSeconds, "prefix");
}
@Override
public int expireSeconds() {
return super.expireSeconds();
}
@Override
public String getPrefix() {
return super.getPrefix();
}
}
第四步:在需要限流的接口上,添加 @AccessLimit 注解。
注:
- 其他 User 实体类等可以查看 Gitee 仓库(https://gitee.com/Lin_DH/system)。
- Redis 不能和 cache 缓存一起使用,ServiceImpl中 users 方法使用需要了,需要注释掉 @Cacheable 注解。
UserController.java
@GetMapping("users")
@ApiOperation("获取所有用户信息")
@AccessLimit(seconds = 10, maxCount = 3)
public String users() {
List<User> users = userService.queryAllUser();
return ResultBody
.build(HttpStatus.OK)
.setData(users)
.setCount(users.size())
.getReturn();
}
第五步:启动类添加 @EnableCaching 注解
SystemApplication.java
package com.lm.system;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
import org.springframework.cache.annotation.EnableCaching;
@EnableCaching
@SpringBootApplication
@MapperScan("com.lm.system.mapper")
public class SystemApplication extends SpringBootServletInitializer {
public static void main(String[] args) {
SpringApplication.run(SystemApplication.class, args);
}
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(SystemApplication.class);
}
}
第六步:添加 Redis 操作接口
RedisService.java
package com.lm.system.redis;
import org.redisson.api.RReadWriteLock;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* @author DUHAOLIN
* @date 2024/11/13
*/
public interface RedisService {
/**
* 保存属性
*/
void set(String key, Object value, long time);
/**
* 保存属性
*/
void set(String key, Object value);
/**
* 获取属性
*/
Object get(String key);
/**
* 删除属性
*/
Boolean del(String key);
/**
* 批量删除属性
*/
Long del(List<String> keys);
/**
* 设置过期时间
*/
Boolean expire(String key, long time);
/**
* 获取过期时间
*/
Long getExpire(String key);
/**
* 判断是否有该属性
*/
Boolean hasKey(String key);
/**
* 按delta递增
*/
Long incr(String key, long delta);
/**
* 按delta递减
*/
Long decr(String key, long delta);
/**
* 获取Hash结构中的属性
*/
Object hGet(String key, String hashKey);
/**
* 向Hash结构中放入一个属性
*/
Boolean hSet(String key, String hashKey, Object value, long time);
/**
* 向Hash结构中放入一个属性
*/
void hSet(String key, String hashKey, Object value);
/**
* 直接获取整个Hash结构
*/
Map<Object, Object> hGetAll(String key);
/**
* 直接设置整个Hash结构
*/
Boolean hSetAll(String key, Map<String, Object> map, long time);
/**
* 直接设置整个Hash结构
*/
void hSetAll(String key, Map<String, ?> map);
/**
* 删除Hash结构中的属性
*/
void hDel(String key, Object... hashKey);
/**
* 判断Hash结构中是否有该属性
*/
Boolean hHasKey(String key, String hashKey);
/**
* Hash结构中属性递增
*/
Long hIncr(String key, String hashKey, Long delta);
/**
* Hash结构中属性递减
*/
Long hDecr(String key, String hashKey, Long delta);
/**
* 获取Set结构
*/
Set<Object> sMembers(String key);
/**
* 向Set结构中添加属性
*/
Long sAdd(String key, Object... values);
/**
* 向Set结构中添加属性
*/
Long sAdd(String key, long time, Object... values);
/**
* 是否为Set中的属性
*/
Boolean sIsMember(String key, Object value);
/**
* 获取Set结构的长度
*/
Long sSize(String key);
/**
* 删除Set结构中的属性
*/
Long sRemove(String key, Object... values);
/**
* 获取List结构中的属性
*/
List<Object> lRange(String key, long start, long end);
/**
* 获取List结构的长度
*/
Long lSize(String key);
/**
* 根据索引获取List中的属性
*/
Object lIndex(String key, long index);
/**
* 向List结构中添加属性
*/
Long lPush(String key, Object value);
/**
* 向List结构中添加属性
*/
Long lPush(String key, Object value, long time);
/**
* 向List结构中批量添加属性
*/
Long lPushAll(String key, Object... values);
/**
* 向List结构中批量添加属性
*/
Long lPushAll(String key, Long time, Object... values);
/**
* 从List结构中移除属性
*/
Long lRemove(String key, long count, Object value);
/**
* 尝试获取分布式锁
* @param key
* @param timeOut
* @param expireTime
* @return
* @throws InterruptedException
*/
boolean tryLock(String key, long timeOut, long expireTime) throws InterruptedException;
/**
* 解锁
* @param key
* @return
*/
void unLock(String key);
/**
* 获取分布式读写锁对象
* @param lockKey
* @return
*/
RReadWriteLock getReadWriteLock(String lockKey);
}
第七步:添加 Redis 操作实现类
RedisServiceImpl.java
package com.lm.system.redis;
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* @author DUHAOLIN
* @date 2024/11/13
*/
@Service
public class RedisServiceImpl implements RedisService {
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private RedissonClient redissonClient;
@Override
public void set(String key, Object value, long time) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
}
@Override
public void set(String key, Object value) {
redisTemplate.opsForValue().set(key, value);
}
@Override
public Object get(String key) {
return redisTemplate.opsForValue().get(key);
}
@Override
public Boolean del(String key) {
return redisTemplate.delete(key);
}
@Override
public Long del(List<String> keys) {
return redisTemplate.delete(keys);
}
@Override
public Boolean expire(String key, long time) {
return redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
@Override
public Long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
@Override
public Boolean hasKey(String key) {
return redisTemplate.hasKey(key);
}
@Override
public Long incr(String key, long delta) {
return redisTemplate.opsForValue().increment(key, delta);
}
@Override
public Long decr(String key, long delta) {
return redisTemplate.opsForValue().increment(key, -delta);
}
@Override
public Object hGet(String key, String hashKey) {
return redisTemplate.opsForHash().get(key, hashKey);
}
@Override
public Boolean hSet(String key, String hashKey, Object value, long time) {
redisTemplate.opsForHash().put(key, hashKey, value);
return expire(key, time);
}
@Override
public void hSet(String key, String hashKey, Object value) {
redisTemplate.opsForHash().put(key, hashKey, value);
}
@Override
public Map<Object, Object> hGetAll(String key) {
return redisTemplate.opsForHash().entries(key);
}
@Override
public Boolean hSetAll(String key, Map<String, Object> map, long time) {
redisTemplate.opsForHash().putAll(key, map);
return expire(key, time);
}
@Override
public void hSetAll(String key, Map<String, ?> map) {
redisTemplate.opsForHash().putAll(key, map);
}
@Override
public void hDel(String key, Object... hashKey) {
redisTemplate.opsForHash().delete(key, hashKey);
}
@Override
public Boolean hHasKey(String key, String hashKey) {
return redisTemplate.opsForHash().hasKey(key, hashKey);
}
@Override
public Long hIncr(String key, String hashKey, Long delta) {
return redisTemplate.opsForHash().increment(key, hashKey, delta);
}
@Override
public Long hDecr(String key, String hashKey, Long delta) {
return redisTemplate.opsForHash().increment(key, hashKey, -delta);
}
@Override
public Set<Object> sMembers(String key) {
return redisTemplate.opsForSet().members(key);
}
@Override
public Long sAdd(String key, Object... values) {
return redisTemplate.opsForSet().add(key, values);
}
@Override
public Long sAdd(String key, long time, Object... values) {
Long count = redisTemplate.opsForSet().add(key, values);
expire(key, time);
return count;
}
@Override
public Boolean sIsMember(String key, Object value) {
return redisTemplate.opsForSet().isMember(key, value);
}
@Override
public Long sSize(String key) {
return redisTemplate.opsForSet().size(key);
}
@Override
public Long sRemove(String key, Object... values) {
return redisTemplate.opsForSet().remove(key, values);
}
@Override
public List<Object> lRange(String key, long start, long end) {
return redisTemplate.opsForList().range(key, start, end);
}
@Override
public Long lSize(String key) {
return redisTemplate.opsForList().size(key);
}
@Override
public Object lIndex(String key, long index) {
return redisTemplate.opsForList().index(key, index);
}
@Override
public Long lPush(String key, Object value) {
return redisTemplate.opsForList().rightPush(key, value);
}
@Override
public Long lPush(String key, Object value, long time) {
Long index = redisTemplate.opsForList().rightPush(key, value);
expire(key, time);
return index;
}
@Override
public Long lPushAll(String key, Object... values) {
return redisTemplate.opsForList().rightPushAll(key, values);
}
@Override
public Long lPushAll(String key, Long time, Object... values) {
Long count = redisTemplate.opsForList().rightPushAll(key, values);
expire(key, time);
return count;
}
@Override
public Long lRemove(String key, long count, Object value) {
return redisTemplate.opsForList().remove(key, count, value);
}
@Override
public boolean tryLock(String key, long timeOut, long expireTime) throws InterruptedException {
RLock lock = redissonClient.getLock(key);
return lock.tryLock(timeOut, expireTime, TimeUnit.SECONDS);
}
/**
* 解锁
* @param key
* @return
*/
@Override
public void unLock(String key){
RLock lock = redissonClient.getLock(key);
lock.unlock();
}
@Override
public RReadWriteLock getReadWriteLock(String lockKey) {
return redissonClient.getReadWriteLock(lockKey);
}
}
第八步:添加访问频率限制拦截器
AntiBrushInterceptor.java
package com.lm.system.interceptor;
import com.lm.system.annotation.AccessLimit;
import com.lm.system.common.AccessKey;
import com.lm.system.redis.RedisService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* 访问频率限制拦截器
* @author DUHAOLIN
* @date 2024/11/13
*/
@Slf4j
public class AntiBrushInterceptor implements HandlerInterceptor {
private final RedisService redisService;
public AntiBrushInterceptor(RedisService redisService) {
this.redisService = redisService;
}
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//判断请求是否属于方法请求
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
//获取方法中的注解,判断是否有该注解
AccessLimit accessLimit = handlerMethod.getMethodAnnotation(AccessLimit.class);
if (accessLimit == null) {
return true;
}
int seconds = accessLimit.seconds();
int maxCount = accessLimit.maxCount();
boolean needLogin = accessLimit.needLogin();
String key = request.getRequestURI();
if (needLogin) {
//判断是否登录
key += "_userId001"; //已登录,获取userId
}
//从redis中获取用户的访问次数
AccessKey accessKey = AccessKey.withExpire(seconds);
String realKey = accessKey.getPrefix() + key;
Integer count = (Integer) redisService.get(realKey);
//访问次数处理
if (count == null) {
//首次访问
redisService.set(realKey, 1, 60);
}
else if (count < maxCount) {
//加1
redisService.incr(realKey, 1);
}
else {
//超出访问次数
log.info("进入服务降级,时间{}", LocalDateTime.now().format(FORMATTER));
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
response.getWriter().write("Too Many Requests. Number of visits: " + maxCount);
return false;
}
}
return true;
}
}
效果图
前三次访问正常,第四次开始返回错误信息。
项目结构图
参考链接
如何防范API经常被人频繁调用【https://baijiahao.baidu.com/s?id=1791472081681790682&wfr=spider&for=pc】
API接口防刷的9种方案【https://baijiahao.baidu.com/s?id=1802852256970678261&wfr=spider&for=pc】
Spring Boot 项目的 API 接口防刷【https://www.iocoder.cn/Fight/Spring-Boot-project-API-anti-brush-interface/?self】