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 MaplastSource = 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错误