Spring Boot应用集成Actuator端点自定义Filter解决未授权访问的漏洞

news2024/11/25 13:42:03

一、前言

我们知道想要实时监控我们的应用程序的运行状态,比如实时显示一些指标数据,观察每时每刻访问的流量,或者是我们数据库的访问状态等等,需要使用到Actuator组件,但是Actuator有一个访问未授权问题,简单说就是其他人可以通过Actuator组件暴露的URL进行端点信息访问,甚至shutdown应用。那么我们有没有什么解决方法呢?

二、解决方案(Actuator端口与应用端口一致)

我们创建一个spring Boot项目进行演示说明。思路就是对spring Boot actuator暴露的URL访问时,增加携带用户名、密码,同时增加一个Filter进行拦截,为了防止密码泄露,需要对密码进行加密配置,由于后端需要进行对比密码,所以我们需要采用对称加密,这里我们采用SM4加密算法,可以参考博文:

使用SM4国密加密算法对Spring Boot项目数据库连接信息以及yaml文件配置属性进行加密配置(读取时自动解密)

为什么不采用主流的集成Spring Security组件呢,出于两方面考虑:

  • 集成Spring Security相对Filter来说比较重量级
  • 集成Spring Security进行Actuator端点认证可能会与原有业务安全认证冲突

2.1 创建spring Boot项目,导入相关依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcprov-jdk15to18</artifactId>
    <version>1.76</version>
</dependency>

<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.25</version>
</dependency>

2.2 增加相关配置

management:
  endpoints:
    web:
      exposure:
        include: health

2.3 启动验证

在这里插入图片描述

2.4 授权改造

2.4.1 增加自定义配置
management:
  endpoints:
    web:
      exposure:
        include: health
        whiteUrl:   # 白名单,配置白名单的URL请求时不需要验证,多个可以用英文逗号分隔
    user: admin # 认证用户
    password: '@SM4@-HUYu9S6osKi65pZr7YQO9w=='  # 认证用户密码 SM4Utils.encryptStr("password")
2.4.2 自定义授权过滤器逻辑
package com.learn.filter;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.learn.SM4Utils;
import org.springframework.http.MediaType;
import org.springframework.util.StringUtils;
import sun.misc.BASE64Decoder;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;

public class ActuatorFilter implements Filter {

    private String[] whiteUrl;

    private String user;

    private String password;

    public ActuatorFilter(String whiteUrl, String user, String password) {
        this.whiteUrl = whiteUrl == null ? null : whiteUrl.split(",");
        this.user = user;
        this.password = password;
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        boolean flag = false;
        HttpServletRequest httpServletRequest = ((HttpServletRequest) servletRequest);
        String url = httpServletRequest.getRequestURI();
        // 判断是否是白名单
        if (whiteUrl != null && whiteUrl.length != 0) {
            for (String str : whiteUrl) {
                if (url.equals(str)) {
                    flag = true;
                    break;
                }
            }
        }
        if (flag) { // 如果是白名单则直接放行
            filterChain.doFilter(servletRequest, servletResponse);
        } else {
            if (StringUtils.hasText(user) && StringUtils.hasText(password)) { // 如果配置用户名密码
                String authorization = httpServletRequest.getHeader("authorization");
                if (StringUtils.hasText(authorization)) { // Basic YWRtaW46YWRtaW4=
                    user = SM4Utils.decryptStr(user);
                    password = SM4Utils.decryptStr(password); // @Value注入可以自动解密,这里手动解密
                    String auth = authorization.replace("Basic ", "");
                    auth = new String(new BASE64Decoder().decodeBuffer(auth), StandardCharsets.UTF_8);
                    String[] authArr = auth.split(":");
                    if (user.equals(authArr[0]) && password.equals(authArr[1])) { //如果用户名密码都可以匹配上则进行放行
                        filterChain.doFilter(servletRequest, servletResponse);
                    } else {
                        errorHandler((HttpServletResponse) servletResponse, "user or password info error!");
                        return;
                    }
                } else {
                    errorHandler((HttpServletResponse) servletResponse, "Authorization info must be not null");
                    return;
                }
            } else { // 如果没有配置用户名密码则直接放行
                filterChain.doFilter(servletRequest, servletResponse);
            }

        }
    }

    /**
     * 异常处理
     *
     * @param response
     * @param msg
     * @throws IOException
     */
    private void errorHandler(HttpServletResponse response, String msg) throws IOException {
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);

        ObjectMapper objectMapper = new ObjectMapper();

        PrintWriter writer = response.getWriter();
        writer.write(objectMapper.writeValueAsString(msg));
        writer.close();

    }
}

2.4.3 注册Filter生效
import com.learn.filter.ActuatorFilter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ActuatorConfig {

    @Value("${management.endpoints.web.exposure.whiteUrl:}")
    private String whiteUrl;

    @Value("${management.endpoints.user:}")
    private String user;

    @Value("${management.endpoints.password:}")
    private String password;

    @Bean
    public FilterRegistrationBean filterRegistrationBean() {
        FilterRegistrationBean<ActuatorFilter> filterRegistrationBean = new FilterRegistrationBean<>();
        filterRegistrationBean.setFilter(new ActuatorFilter(whiteUrl, user, password));
        filterRegistrationBean.addUrlPatterns("/actuator/*");
        filterRegistrationBean.setName("ActuatorFilter");
        return filterRegistrationBean;
    }

}

2.5 功能测试

  • 成功
    注意:URL格式: http://明文user:明文password@ip:port/actuator/health
    前端访问时需要携带密码,后端配置密码进行了加密,防止密码泄露。
    在这里插入图片描述

  • 失败

在这里插入图片描述

此时访问actuator端点都需要携带用户名密码了。

三、解决方案(Actuator端口与应用端口不一致)

众所周知,spring Boot Actuator组件的端口是可以自定义配置的,如果是自定义配置端口,那么上面的Filter不会生效,那么要怎么处理呢。

测试密码错误,此时还是会正常返回:

在这里插入图片描述

3.1 增加自定义端口配置

server:
  port: 8080
  
management:
  server:
    port: 9999
  endpoints:
    web:
      exposure:
        include: health
        whiteUrl:   # 白名单,配置白名单的URL请求时不需要验证,多个可以用英文逗号分隔
    user: admin # 认证用户
    password: '@SM4@-HUYu9S6osKi65pZr7YQO9w=='  # 认证用户密码 SM4Utils.encryptStr("password")

3.2 修改Filter逻辑,增加Actuator端口判断

import com.fasterxml.jackson.databind.ObjectMapper;
import com.learn.SM4Utils;
import org.springframework.http.MediaType;
import org.springframework.util.StringUtils;
import sun.misc.BASE64Decoder;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;

public class ActuatorFilter implements Filter {

    private String[] whiteUrl;

    private String user;

    private String password;

    /**
     * actuator端口
     */
    private Integer actuatorPort;

    public ActuatorFilter(String whiteUrl, String user, String password, Integer actuatorPort) {
        this.whiteUrl = whiteUrl == null ? null : whiteUrl.split(",");
        this.user = user;
        this.password = password;
        this.actuatorPort = actuatorPort;
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = ((HttpServletRequest) servletRequest);

        int serverPort = httpServletRequest.getServerPort();
        if (actuatorPort != null && serverPort == actuatorPort) { // 判断是否是actuator端口
            boolean flag = false;
            String url = httpServletRequest.getRequestURI();
            // 判断是否是白名单
            if (whiteUrl != null && whiteUrl.length != 0) {
                for (String str : whiteUrl) {
                    if (url.equals(str)) {
                        flag = true;
                        break;
                    }
                }
            }
            if (flag) { // 如果是白名单则直接放行
                filterChain.doFilter(servletRequest, servletResponse);
            } else {
                if (StringUtils.hasText(user) && StringUtils.hasText(password)) { // 如果配置用户名密码
                    String authorization = httpServletRequest.getHeader("authorization");
                    if (StringUtils.hasText(authorization)) { // Basic YWRtaW46YWRtaW4=
                        user = SM4Utils.decryptStr(user);
                        password = SM4Utils.decryptStr(password); // @Value注入可以自动解密,这里手动解密
                        String auth = authorization.replace("Basic ", "");
                        auth = new String(new BASE64Decoder().decodeBuffer(auth), StandardCharsets.UTF_8);
                        String[] authArr = auth.split(":");
                        if (user.equals(authArr[0]) && password.equals(authArr[1])) { //如果用户名密码都可以匹配上则进行放行
                            filterChain.doFilter(servletRequest, servletResponse);
                        } else {
                            errorHandler((HttpServletResponse) servletResponse, "user or password info error!");
                            return;
                        }
                    } else {
                        errorHandler((HttpServletResponse) servletResponse, "Authorization info must be not null");
                        return;
                    }
                } else { // 如果没有配置用户名密码则直接放行
                    filterChain.doFilter(servletRequest, servletResponse);
                }

            }
        } else {
            filterChain.doFilter(servletRequest, servletResponse);
        }

    }

    /**
     * 异常处理
     *
     * @param response
     * @param msg
     * @throws IOException
     */
    private void errorHandler(HttpServletResponse response, String msg) throws IOException {
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);

        ObjectMapper objectMapper = new ObjectMapper();

        PrintWriter writer = response.getWriter();
        writer.write(objectMapper.writeValueAsString(msg));
        writer.close();

    }
}

3.3 修改Filter注册逻辑

import com.learn.filter.ActuatorFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;

@Configuration
public class ActuatorConfig {

    @Bean
    public ActuatorFilter actuatorFilter(Environment environment) {
        String whiteUrl = environment.getProperty("management.endpoints.web.exposure.whiteUrl");
        String user = environment.getProperty("management.endpoints.user");
        String password = environment.getProperty("management.endpoints.password");
        String portStr = environment.getProperty("management.server.port");
        Integer port = portStr == null ? null : Integer.parseInt(portStr);

        return new ActuatorFilter(whiteUrl, user, password, port);
    }

}

3.4 注册ManagementContextConfiguration

这是最重要的一步,需要配置ManagementContextConfiguration,不然Filter依旧不会生效:

resource目录下新建META-INF目录,新建spring.factories文件

在这里插入图片描述

增加内容:

org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration=\
  com.learn.config.ActuatorConfig

3.5 测试

在这里插入图片描述

此时密码错误情况下就进行拦截了

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1465293.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2023全新UI千月影视APP源码 | 前后端完美匹配、后端基于ThinkPHP框架

应用介绍 本文来自&#xff1a;2023全新UI千月影视APP源码 | 前后端完美匹配、后端基于ThinkPHP框架 - 源码1688 简介&#xff1a; 2023全新UI千月影视APP源码 | 前后端完美匹配、后端基于thinkphp框架 图片&#xff1a;

索引大战:探秘InnoDB数据库中B树和Hash索引的优劣

欢迎来到我的博客&#xff0c;代码的世界里&#xff0c;每一行都是一个故事 索引大战&#xff1a;探秘InnoDB数据库中B树和Hash索引的优劣 前言B树索引的深度解析Hash索引的奥秘揭晓性能对比分析 前言 在当今软件开发的世界中&#xff0c;数据库扮演着至关重要的角色。而InnoD…

怎么用sora赚第一桶金?

&#x1f31f;解锁文字变视频的强大功能&#xff01;&#x1f31f; ✨欢迎来到 Sora Cand&#xff0c;一个革命性的网站&#xff0c;利用 OpenAI 的 Sora 模型帮你把文字变成酷炫的视频&#xff01;✨ 想象一下&#xff0c;你的文字从纸上跳出来&#xff0c;变成引人入胜的视觉…

如何用IP地址找到实际位置?

在互联网世界中&#xff0c;每个设备都有一个独特的标识&#xff0c;那就是IP地址。它不仅是设备在网络中的“身份证”&#xff0c;还承载着设备在网络中的位置信息。那么&#xff0c;我们是否可以通过IP地址来找到设备的实际位置呢&#xff1f;本文将深入探讨这一问题。 一、I…

#11vue3中使用el-dialog展示与关闭交由父组件控制的写法

目录 1、法一&#xff1a;通过defineEmits调用父组件方法 1.1、父组件 1.2、子组件&#xff08;CONTENT&#xff09; 2、法二&#xff1a;通过difineExpose暴露子组件属性 2.1、父组件 2.2、子组件&#xff08;Child&#xff09; 1、法一&#xff1a;通过defineEmits调用…

【C语言】注释

&#x1f388;个人主页&#xff1a;豌豆射手^ &#x1f389;欢迎 &#x1f44d;点赞✍评论⭐收藏 &#x1f917;收录专栏&#xff1a;C语言 &#x1f91d;希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出指正&#xff0c;让我们共同学习、交流进步&…

vue3 toRefs之后的变量修改方法

上效果 修改值需要带上解构之前的对象名obj&#xff0c; changeName:()>{ // toRefs 解决后变量修改值方法&#xff1a; 解构前变量.字段新值 obj.name FEIFEI; } } 案例源码 <!DOCTYPE html> <html> <head><me…

算法——模拟

1. 什么是模拟算法&#xff1f; 官方一点来说 模拟算法&#xff08;Simulation Algorithm&#xff09;是一种通过模拟现实或抽象系统的运行过程来研究、分析或解决问题的方法。它通常涉及创建一个模型&#xff0c;模拟系统中的各种事件和过程&#xff0c;以便观察系统的行为&a…

从单体服务到微服务:多模式 Web 应用开发记录<一>背景全局变量优化

背景 最近在做一个事&#xff1a;下线一个超级大单体服务。单一统计代码行数其实不够全面&#xff0c;反正项目 git clone 下来文件就有这么大&#xff1a; 这是一个已经存在了十年以上的服务&#xff0c;随着业务的发展&#xff0c;这个服务已经无法满足我们的需求。 我们统…

胡夏爱意满满,浪漫尽显,心动不止。

♥ 为方便您进行讨论和分享&#xff0c;同时也为能带给您不一样的参与感。请您在阅读本文之前&#xff0c;点击一下“关注”&#xff0c;非常感谢您的支持&#xff01; 文 |猴哥聊娱乐 编 辑|徐 婷 校 对|侯欢庭 全网热议的胡夏暗恋文学&#xff0c;浪漫指数爆表&#xff01…

力扣382.链表随机节点

Problem: 382. 链表随机节点 文章目录 题目描述思路复杂度Code 题目描述 思路 由水塘抽样易得&#xff0c;当遇到i个元素&#xff0c;有 1 / i 1/i 1/i的概率选择该元素&#xff1b;则在实际操作中我们定义一个下标i从1开始遍历每次判断rand() % i 0&#xff08;该操作就是判断…

Verilog刷题笔记35

题目&#xff1a; Create a 1-bit wide, 256-to-1 multiplexer. The 256 inputs are all packed into a single 256-bit input vector. sel0 should select in[0], sel1 selects bits in[1], sel2 selects bits in[2], etc. 解法&#xff1a; module top_module( input [255:…

猜谜“龘”人速来!网安人元宵灯谜有礼了

​​灯笼高挂&#xff0c;星光璀璨&#xff0c;品味糯叽叽的元宵&#xff0c;以灯谜为媒&#xff0c;亚信安全邀你共赴元宵佳节。 热辣滚烫的班先别上了&#xff0c;文末有奖竞猜&#xff0c;快来参与&#xff01; 喜闹元宵 谜面一&#xff1a;一路向上成大业&#xff1b; 谜…

HTML5新婚、年会、各种聚会的现场抽奖活动(附源码)

文章目录 1.抽奖平台设计来源1.1 主界面效果1.2 抽奖效果1.3 中奖效果 2.效果和源码配置2.1 动态效果2.2 人员信息配置2.3 奖品信息配置2.4 抽奖音效配置2.5 源代码 源码下载 作者&#xff1a;xcLeigh 文章地址&#xff1a;https://blog.csdn.net/weixin_43151418/article/deta…

第一节-docker介绍

这里写自定义目录标题 一、什么是docker二、docker和virtual machine三、docker架构 一、什么是docker docker是一种容器引擎&#xff0c;用于构建、部署、运行应用程序和服务。 docker的每个容器通过沙箱机制相互隔离&#xff0c;互不干扰。 docker容器技术相比传统的虚拟机有…

六、回归与聚类算法 - 模型保存与加载

目录 1、API 2、案例 欠拟合与过拟合线性回归的改进 - 岭回归分类算法&#xff1a;逻辑回归模型保存与加载无监督学习&#xff1a;K-means算法 1、API 2、案例

费舍尔FISHER金属探测器探测仪维修F70

美国FISHER LABS费舍尔地下金属探测器&#xff0c;金属探测仪等维修&#xff08;考古探金银铜探宝等仪器&#xff09;。 费舍尔F70视听目标ID金属探测器&#xff0c;Fisher 金属探测器公司成立于1931年&#xff0c;在实验条件很艰苦的情况下&#xff0c;研发出了地下金属探测器…

昨天Google发布了最新的开源模型Gemma,今天我来体验一下

前言 看看以前写的文章&#xff0c;业余搞人工智能还是很早之前的事情了&#xff0c;之前为了高工资&#xff0c;一直想从事人工智能相关的工作都没有实现。现在终于可以安静地系统地学习一下了。也是一边学习一边写博客记录吧。 昨天Google发布了最新的开源模型Gemma&#xf…

常见锁策略,CAS,synchrodized原理讲解

&#x1f3a5; 个人主页&#xff1a;Dikz12&#x1f4d5;格言&#xff1a;那些在暗处执拗生长的花&#xff0c;终有一日会馥郁传香欢迎大家&#x1f44d;点赞✍评论⭐收藏 目录 常见锁策略 乐观锁和悲观锁 轻量级锁和重量级锁 自旋锁和挂起等待锁 读写锁 公平锁和非公平锁…

【ctfshow—web】——信息搜集篇1(web1~20详解)

ctfshow—web题解 web1web2web3web4web5web6web7web8web9web10web11web12web13web14web15web16web17web18web19web20 web1 题目提示 开发注释未及时删除 那就找开发注释咯&#xff0c;可以用F12来查看&#xff0c;也可以CtrlU直接查看源代码呢 就拿到flag了 web2 题目提示 j…