springboot中endpoint安全控制

阅读更多

     springboot中endpoint的安全访问,需要谨慎设计,endpoint可以暴露很多有用的信息,这对外部数据采集器来说非常便捷,但是潜在也引入了安全问题,稍有不慎可能被非法访问。本文基于springboot2,基于spring-security来认证endpoint的访问授权。

 

    在springboot2之后,health和info接口将是公开的,其他endpoint将默认在security的控制之下,不允许被访问,如果需要访问,则必须接入security。本文描述,如果将health和info接口继续保持公开(当然也可以安全控制)、其他endpoint需要授权才能访问,此外web项目的其他接口则可以正常访问。

 

    1)将spring-security(或者其starter)加入classpath。

    2)基于security实现httpBasic授权认证,保护其他endpoint。

    

    springboot2默认实现,有些不妥之处,一旦引入security,不仅endpoint被防护,web项目的其他接口也被拦截,这一点需要重新设计。我们通过实现自定义的security配置,并覆盖原有安全配置。

 

一、pom.xml


    org.springframework.boot
    spring-boot-starter-actuator


    org.springframework.boot
    spring-boot-starter-web


    org.springframework.boot
    spring-boot-autoconfigure-processor
    true


    org.springframework.boot
    spring-boot-starter-security

 

二、Environment设置

import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.MutablePropertySources;

import java.util.Map;
import java.util.TreeMap;

/**
 * @author liuguanqing
 * created 2018/11/2 3:32 PM
 * 调整优化环境变量,对于框架会默认覆盖一些环境变量,此时我们需要在processor中执行
 * 我们不再需要使用单独的yml文件来解决此问题。原则:
 * 1)所有设置为系统属性的,初衷为"对系统管理员可见"、"对外部接入组件可见"(比如starter或者日志组件等)
 * 2)对设置为lastSource,表示"当用户没有通过yml"配置选项时的默认值--担保策略。
 **/
public class MeteorEnvironmentPostProcessor implements EnvironmentPostProcessor {

    //可以被application覆盖的配置,此处作为默认担保
    private static final Map lastSource = new TreeMap<>();

    //框架限定,不能被覆盖或者外部指定无效的配置
    private static final Map firstSource = new TreeMap<>();

    private static final String NAME_FIRST = "CUSTOMIZE_FIRST";

    private static final String NAME_LAST = "CUSTOMIZE_LAST";


    @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
        MutablePropertySources propertySources = environment.getPropertySources();
        //已装载,则中断
        if (propertySources.contains(NAME_FIRST) || propertySources.contains(NAME_LAST)) {
            return;
        }

        resolveSecurity(environment);
        resolveManagement(environment);

        MapPropertySource lastPropertySource = new MapPropertySource(CONTEXT_NAME_LAST, lastSource);
        propertySources.addLast(lastPropertySource);

        MapPropertySource firstPropertySource = new MapPropertySource(CONTEXT_NAME_FIRST, firstSource);
        propertySources.addFirst(firstPropertySource);
    }


    private void resolveSecurity(final ConfigurableEnvironment environment) {
        String password = environment.getProperty(PASSWORD);
        if (password == null) {
            password = environment.getProperty("spring.security.user.password");
        }

        if (password == null) {
            password = RandomStringUtils.random(16,true,true);
        }
        firstSource.put("spring.security.user.password",password);
        //可被系统管理员查看
        System.setProperty(WEB_SECURITY_PASSWORD_SP_KEY,password);

        //覆盖
        firstSource.put("spring.security.user.name","application");
        lastSource.put("spring.security.user.roles","application");
    }

    private void resolveManagement(final ConfigurableEnvironment environment) {
        //有关Endpoints安全问题
        String excludes = environment.getProperty("spring.autoconfigure.exclude");
        StringBuilder sb = new StringBuilder();
        if(excludes != null) {
            sb.append(",");
        }
        //关闭spring-boot默认的security配置
        sb.append(ManagementWebSecurityAutoConfiguration.class.getName());
        firstSource.put("spring.autoconfigure.exclude",sb.toString());

        //默认全部关闭
        firstSource.put("management.endpoints.enabled-by-default",false);
        firstSource.put("management.endpoints.jmx.domain",ContextConstants.JMX_DOMAIN);
        firstSource.put("management.endpoints.jmx.exposure.include","*");

        firstSource.put("management.endpoints.web.exposure.include","*");
        firstSource.put("management.endpoints.web.exposure.exclude","shutdown,threaddump,heapdump");
        firstSource.put("management.endpoints.web.base-path","/actuator");

        firstSource.put("management.endpoint.health.enabled",true);
        lastSource.put("management.endpoint.health.show-details","never");

        firstSource.put("management.endpoint.info.enabled",true);
        firstSource.put("management.endpoint.mappings.enabled",true);
        firstSource.put("management.endpoint.metrics.enabled",true);
        firstSource.put("management.endpoint.env.enabled",true);
        firstSource.put("management.endpoint.configprops.enabled",true);
        firstSource.put("management.endpoint.beans.enabled",true);
        firstSource.put("management.endpoint.httptrace.enabled",true);
    }
}

 

    有时候,我们不希望用户干扰有关security和endpoint的参数配置,比如用户好奇的打开或者关闭了一些“不安全的”endpoint,将可能导致一些严重的事故,我们需要在项目框架中集成有关Environment的处理器,强制覆盖有关配置。

    比如本文,有关endpoint的开关配置是用户无法配置的(不生效),我们也强制为项目设定security的用户名和密码。

    需要注意,这个EnvironmentPostProcessor会被执行两次,分别为spring环境和servlet环境,此时Environment类型不同,所以如果你在Porcessor中处理有关“字符串拼接”、“累加”等操作时需要慎重。

 

三、自定义Security配置

/**
 * created 2018/11/5 2:34 PM
 * 用于保护endpoint不被非法访问
 **/
@Order(99)
public class MeteorWebSecurityConfigurer extends WebSecurityConfigurerAdapter {
    @Override
    public void configure(WebSecurity web) throws Exception {
        //普通web资源
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .requestMatchers(
                        EndpointRequest.to(MetricsEndpoint.class,
                                EnvironmentEndpoint.class,
                                HttpTraceEndpoint.class,
                                ConfigurationPropertiesReportEndpoint.class,
                                BeansEndpoint.class,
                                MappingsEndpoint.class)).hasRole("application").anyRequest()
                .permitAll().and()
                .httpBasic();

    }
}

 

    安全控制策略需要特别注意,如果上下文中有多个WebSecurityConfigurerAdapter,必须为每个实现声明@Order和指定顺序且order不能重复,此外@Order只能声明在类上,不能声明在@Bean。

    对于springboot2而言,默认已经提供了一个有关endpoint的安全控制类:ManagementWebSecurityAutoConfiguration,这个类的加载条件也“如果上下文中没有提供“WebSecurityConfigurerAdapter”实现,ManagementWebSecurityAutoConfiguration主要是为了将“health和info”可被公开访问、其他接口需要认证授权,为了避免不必要的设计问题,我们强制关闭此类的自动装配(或者开发自定义的WebSecurityConfigurerAdapter,且优先级高于100)。

    1)通过@SpringBootApplication(exclude = {ManagementWebSecurityAutoConfiguration.class})。

    2)在yml中显式声明:

spring.autoconfigure.exclude=org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration

 

    3)你可以像本文描述,通过上述EnvironmentPostProcessor,强制覆盖“spring.autofigure.exclude”的参数,并追加ManagementWebSecurityAutoConfiguration。

 

    如果你的上下文中,有多个自定义的WebSecurityConfigurerAdapter实现,那么他们的Order必须唯一,否则将会抛出:

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration': Injection of autowired dependencies failed; nested exception is java.lang.IllegalStateException: @Order on WebSecurityConfigurers must be unique. Order of 100 was already used on com.example.demo.Application$TestSecurity@54c60202, so it cannot be used on org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityConfigurerAdapter$$EnhancerBySpringCGLIB$$1d8564e5@7889b4b9 too.

 

三、初始化WebSecurityConfigurerAdapter

   @Bean
    public WebSecurityConfigurer meteorWebSecurityConfigurer() {
        return new MeteorWebSecurityConfigurer();
    }

    此代码,可以放在任何被@Configuration修饰的类中,当然也可以为你自定义的AutoConfiguration类中。

 

四、配置Environment处理器(META-INF/spring.factories)

org.springframework.boot.env.EnvironmentPostProcessor=\
com.test.springboot.environment.MeteorEnvironmentPostProcessor

 

五、yml配置

spring:
  security:
    user:
      name: application
      password: ${password:test123}

 

    你可以通过启动时使用“-Dpassword=”来传入密码,这样必要安全,如果未传入,则使用“test123”。

 

六、访问与验证

    此后,我们也已通过postman、浏览器、curl等方式验证安全是否生效:

> curl http://127.0.0.1:8080/actuator/metrics -u application:test123
或者
> curl http://application:[email protected]:8080/actuator/metrics
#可以访问
> curl http://127.0.0.1:8080/actuator/health
##无认证可以访问
> curl http://127.0.0.1:8080/index
##无认证可以访问
> curl http://127.0.0.1:8080/actuator/metrics
##401错误

  

你可能感兴趣的:(springboot中endpoint安全控制)