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

一、前言

我们知道想要实时监控我们的应用程序的运行状态,比如实时显示一些指标数据,观察每时每刻访问的流量,或者是我们数据库的访问状态等等,需要使用到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 测试

在这里插入图片描述

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

QT基本组件

四、基本组件 Designer 设计师&#xff08;重点&#xff09; Qt包含了一个Designer程序&#xff0c;用于通过可视化界面设计开发界面&#xff0c;保存文件格式为.ui&#xff08;界面文件&#xff09;。界面文件内部使用xml语法的标签式语言。 在Qt Creator中创建文件时&#xf…

蓝桥杯C++竞赛常用库函数介绍

文章目录 前言一、二分查找1. 二分查找的前提2.binary_search函数3.lower_bound函数和upper_bound函数4.蓝桥杯例题 二、最值查找1. min和max函数2.min_element和max_element函数3.nth_element函数4.蓝桥杯例题 三、排序1.sort函数2.sort自定义比较函数,或lambda表达式(匿名函数…

金和OA UploadFileBlock接口任意文件上传漏洞

声明 本文仅用于技术交流&#xff0c;请勿用于非法用途 由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失&#xff0c;均由使用者本人负责&#xff0c;文章作者不为此承担任何责任 1. 产品简介 金和数字化智能办公平台&#xff08;简称JC6&#xff09;是…

运维SRE-06 阶段性复习软件管理体系

那些年运维必会操作-第一弹 操作 文件&#xff1a;增删改查 增&#xff1a;touch,vim,>,>>,cp删除&#xff1a;rm修改&#xff1a;内容&#xff1a;vi/vim,>,>> 文件名&#xff1a;mv查看&#xff1a;内容&#xff1a;cat/vim/less/more/head/tail/sed/awk/…

编写LLVM Pass的一个小问题

在阅读官方文档时发现一个很有趣的细节&#xff0c;官方给出了一个测试用例&#xff0c;此处有一个小问题&#xff08;%无法复制&#xff09;。但是我在使用自己编译的ll文件时&#xff0c;我发现该pass无法正常使用。最后经过测试发现是利用-O0编译产生的ll文件有optnone的fla…

学生成绩管理系统(C语言课设 )

这个学生成绩管理系统使用C语言编写&#xff0c;具有多项功能以方便管理学生信息和成绩。首先从文件中读取数据到系统中&#xff0c;并提供了多种功能&#xff08;增删改查等&#xff09;选项以满足不同的需求。 学生成绩管理系统功能: 显示学生信息增加学生信息删除学生信息…

Spring Security 重点解析

Spring Security 重点解析 文章目录 Spring Security 重点解析1. 简介2. 依赖3. 登录认证3.1 登录校验流程3.2 Spring Security 默认登录的原理3.2.1 Spring Security 完整流程3.2.2 登录逻辑探究 3.3 自定义改动3.3.1 自定义用户密码校验3.3.2 自定义 UserDetails 获取方式 F1…

C++多线程同步(上)

多线程同步 引言总述详情互斥锁示例运行结果分析条件变量示例一实现分析优化运行结果示例二实现代码运行结果示例三实现代码运行结果读写锁示例实现代码注意分析运行结果附言实现运行结果运行结果个人心得引言 项目中使用多线程,会遇到两种问题,一种是对共享资源的访问时需要…

kafka和ZK的关系

zk相当于是kafka的一个基础设施 Kafka是一种高吞吐量、可扩展的分布式发布订阅消息系统&#xff0c;ZooKeeper是一个分布式协调服务&#xff0c;用于管理和协调分布式系统中的各种资源 Zookeeper&#xff1a;管理broker&#xff0c;consumer 创建broker后&#xff0c;向zk注册…

Redis和Mysql如何保证数据一致性

一般情况下&#xff0c;Redis用来实现应用和数据库之间读操作的缓存层&#xff0c;主要目的是减少数据 库IO&#xff0c;还可以提升数据的IO性能。 这是它的整体架构。 当应用程序需要去读取某个数据的时候&#xff0c;首先会先尝试去Redis里面加载&#xff0c;如果命中就 直…

基于Python3的数据结构与算法 - 04 快速排序

一、快速排序思路 快速排序特点&#xff1a;快 步骤&#xff1a; 取一个元素p&#xff08;第一个元素&#xff09;&#xff0c;使元素p归为&#xff1b;列表被p分成两部分&#xff0c;左边都比p小&#xff0c;右边都比p大&#xff1b;递归完成排序。 因此我们可以得到快速排…

kali xrdp

Kali Linux 使用远程桌面连接——xrdp&xfce_kali xfce桌面-CSDN博客 Ubuntu/Debian/Kali xrdp远程桌面黑屏/空屏/无画面解决办法 - 知乎 (zhihu.com) sudo apt-get install xrdp -y sudo apt-get install xfce4 -ysudo systemctl enable xrdp --now systemctl status xrd…

自动化行业文件数据\资料防泄密软件——天锐绿盾|@德人合科技

天锐绿盾是一款自动化行业文件数据防泄密软件&#xff0c;由德人合科技提供。该软件采用动态加解密技术&#xff0c;能够有效防止公司内部数据泄密&#xff0c;同时支持各种文件格式加密&#xff0c;如CAD、OFFICE、PDF、图纸等。 PC端&#xff1a;https://isite.baidu.com/sit…

C语言-数组指针与指针数组

一、简介 对于使用C语言开发的人来说&#xff0c;指针&#xff0c;大家都是非常熟悉的。数组&#xff0c;大家也同样熟悉。但是这两个组合到一起的话&#xff0c;很多人就开始蒙圈了。这篇文章&#xff0c;就详细的介绍一下这两个概念。 指针数组和数组指针&#xff0c;听起来非…

为什么0.1+0.2不等于0.3

一、JS内部的计算是以二进制形式进行的 js里整数和小数转为二进制形式的方法是不一样的&#xff1a; 二、Number类型使用IEEE754标准64位存储 双精度浮点数&#xff08;double类型&#xff09;为每个数分配64位空间&#xff0c;并以科学计数法的方式存储&#xff1a; 那么对于…

如何使用Inno Setup制作Unity构建程序的Windows安装程序

1. 准备 &#xff08;1&#xff09;准备好Unity构建的程序集合 必须包括&#xff1a; Data文件夹&#xff08;xxx_Data&#xff09; Mono文件夹&#xff08;MonoBleedingEdge&#xff09; 打包的应用程序文件&#xff08;xxx.exe&#xff09; Unity播放器dll文件&#xff…

centos7部署nfs+keepalived+drbd

一、项目需求描述 现在使用的架构是nfskeepalivedrsyncsersync&#xff0c;目前这套架构存在主从nfs节点数据同步不一致问题&#xff0c;大概会有 120s左右的数据延长同步时间&#xff0c;需要提供优化的自动化方案。 二、现有方案缺点 1、切换不能保证主从节点数据一致。 2、…

每日面经02

1.用过哪些集合&#xff1f;hashmap扩容&#xff1f;如果<string>如何查找&#xff1f;散列函数用什么散列为什么大小是2的幂次&#xff1f;如果是key 为abc怎么散列&#xff1f;如何知道key不存在&#xff1f;默认大小是否可以修改 &#xff0c;改为30 、32 可以不&…

【MySQL初阶】索引与事务

1. 索引 1.1 索引基本概念 1.1.1 索引介绍 索引(index)&#xff1a;是一种特殊的文件&#xff0c;包含着对数据表里所有记录的引用指针。可以对表中的一列或者多列创建索引&#xff0c;并指定索引的类型&#xff0c;各类索引有各自的数据结构实现。&#xff08;具体细节在My…

蓝桥杯DP算法——区间DP(C++)

根据题意要求的是将石子合并的最小权值&#xff0c;我们可以根据DP思想使用二维数组f[i,j]来存放所有从第i堆石子到第j堆石子合并成一堆石子的合并方式。 然后由第二个图所示&#xff0c;我们可以将i到j区间分成两个区间&#xff0c;因为将i到j合并成一个区间的前一步一定是合…