目录
最近搞到一个关于接口保护的题目,主要是对开放接口进行保护,拦截恶意流量,不能影响正常用户的使用。
总体的思路:
于是就搭建了一个springcloud的项目,来实现对接口的保护。项目结构如下:
模块介绍
父模块
这里使用了maven多模块管理的能力,parent模块为父模块,删除了src目录,将pom文件中的打包方式改为pom。
主要功能是为子模块提供统一的依赖
下图是pom文件的内容:
entity模块和utils模块
提供统一的实体类(User、Order等)和工具类(TokenUtil等)。把项目所需的实体类都写在这里,其它需要实体类的模块依赖这个模块。
eureka模块
eureka是注册中心,没什么好说的。
feign模块
feign模块主要是一些接口,对应了项目的远程接口,mysql接口,redis接口。
mysql模块和redis模块
是远程提供者,提供了项目所需的mysql能力和redis能力。
User模块和Order模块
提供用户和订单相关的业务接口。通过feign远程调用mysql和redis接口。
GateWay模块
zuul网关,对外提供项目的统一入口。
security模块
安全模块,实现了token验证和黑名单功能。是接口保护的主要实现模块。
接口安全的实现
接口安全的功能都是通过注解实现的。
token验证
1.创建了一个RequireLogin注解。被这个注解修饰的方法需要通过登录验证后才能被调用。 在这个接口中可以配置所需的权限和角色,以便做接口角色控制和权限控制。
Symbol是一个枚举类,为了实现多个权限之间、多个角色之间,角色和权限之间是使用“&”还是“|”连接创建的。
2.使用HandlerInterceptorAdapter来拦截请求
/**
* 登录拦截
* @author Mr.Wan
* @date 2022/10/12 - 20:17
*/
public class LoginFilter extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HandlerMethod handlerMethod = (HandlerMethod)handler;
RequireLogin requireLgoinAnnotion = handlerMethod.getMethodAnnotation(RequireLogin.class);
//不需要登录
if (null == requireLgoinAnnotion){
return true;
}else{
String token = JWTUtil.getToken(request);
if (null == token){
System.out.println("未登录");
//没有登录
return false;
}
if (!JWTUtil.verify(token)){
//登录失效
System.out.println("登录失效");
return false;
}
ResultInfo<User> resultInfo = RedisUtil.getUser(token);
if (!resultInfo.isFlag()){
//redis中的信息失效
System.out.println("登录失效");
return false;
}
User user = resultInfo.getData();
//获取角色、权限、连接符
String[] authorities = requireLgoinAnnotion.authorities();
String[] roles = requireLgoinAnnotion.roles();
Symbol authoritiesRolesConnection = requireLgoinAnnotion.authoritiesRolesConnection();
Symbol authoritiesConnection = requireLgoinAnnotion.authoritiesConnection();
Symbol rolesConnection = requireLgoinAnnotion.rolesConnection();
boolean authoritiesMeet = isMeet(user.getAuthorities(),authorities,authoritiesConnection);
boolean rolesMeet = isMeet(user.getRoles(),roles,rolesConnection);
//返回权限验证结果
if (authoritiesRolesConnection.isAnd()){
return authoritiesMeet & rolesMeet;
}else{
return authoritiesMeet | rolesMeet;
}
}
}
/**
* 检查角色或权限是否满足
* @param src 用户的权限或角色
* @param target 需求的权限或角色
* @param symbol 连接符
* @return 如果满足要求返回true,反之返回false
*/
private boolean isMeet(List<String> src, String[] target, Symbol symbol){
//没有要求直接返回true
if (null == target || 0 == target.length){
return true;
}
if (symbol.isAnd() && target.length > src.size()){
return false;
}
for (String t : target){
//找到这个字符,要求为or,返回true
if (src.contains(t)){
if (!symbol.isAnd()){
return true;
}
}else{
//没找到字符,要求全部满足,返回false;
if (symbol.isAnd()){
return false;
}
}
}
//要求是and,到现在还没返回说明全部找到了,要求是or反之
return symbol.isAnd();
}
}
黑名单校验
使用redis进行黑名单校验,当有请求被拦截时,先去获取到ip,并在redis中对ip的访问次数加1,当ip访问次数达到被认为是注水时,就将这个ip加入黑名单,加入黑名单的ip无法访问项目中的接口。
1.实现一个RestrictCalls注解。注解中的count表示一秒内到达count次访问就将这个ip加入黑名单。其实可以的话,能做的更加精细,对时间、访问失败次数等都可以做限制。
/**
* @author Mr.Wan
* @date 2022/10/12 - 21:23
*/
public class RestrictFilter extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String ip = getIp(request);
if (!RedisUtil.isInBlack(ip)) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
RestrictCalls restrictCallsAnnotion = handlerMethod.getMethodAnnotation(RestrictCalls.class);
if (null != restrictCallsAnnotion) {
return addRequsetCount(ip, restrictCallsAnnotion);
}
return true;
}
//打日志
System.out.println(ip + "在黑名单中");
//抛异常也拦截
return false;
}
/**
* 添加访问记录
*
* @param ip ip地址
* @param restrictCallsAnnotion 限制注解
* @return 是否被加入黑名单
*/
public boolean addRequsetCount(String ip, RestrictCalls restrictCallsAnnotion) {
long max = restrictCallsAnnotion.count();
long count = RedisUtil.blackCount(ip);
if (max == count){
RedisUtil.blackAdd(ip);
return false;
}
return true;
}
/**
* 获取ip
*
* @param request 请求对象
* @return 返回ip地址
*/
private String getIp(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (null == ip || 0 == ip.length() || "unknown".equals(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (null == ip || 0 == ip.length() || "unknown".equals(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (null == ip || 0 == ip.length() || "unknown".equals(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
}
拦截器注册
为了方便设置为全部路径都要拦截
注解使用
user模块和order模块对security模块依赖。将注解添加到需要被保护的方法。(这边提一嘴,为了使用的方便,可以将注解修改成可以修饰类的,使用HandlerMethod的getBean()和getBeanType()方法可以得到被调用方法所在的类和类的类对象,从而实现对整个类中的方法都进行保护,简化注解的使用)。
这里我在Order模块里对获取订单的方法进行了保护,为了方便测试,设置为一秒访问3次就认为是注水,需要登录后才能访问。设置需要的权限为order-get和order-delete,需要的角色为normal。并且权限至少满足一个并且角色至少满足一个的情况下才允许访问。
接口保护测试
试一下看看好不好使。启动需要的服务:
postman没有携带登录信息,直接去访问订单获取接口(被拦截)
postman携带登录信息,用户没有权限访问接口,去访问订单获取接口(被拦截)
用户ls只有nromal角色,没有任何权限,不满足接口权限要求。
postman携带登录信息,用户满足权限和角色要求,成功调用接口
用户zs角色为nromal,权限有order-get和order-delete,满足((order-get | order-delete) & nromal)的要求。
当快速发送请求后,ip被加入黑名单,查看控制台打印信息(代码中使用了控制台打印代替日志打印)
示例代码
以上就是我对接口保护做的全部工作。代码上传到了gitee仓库,仓库地址:https://gitee.com/syyrjx/open-interface-protection.git