在 AWS Lambda Spring 引导中加载自定义 ApplicationContextInitializer

Loading a custom ApplicationContextInitializer in AWS Lambda Spring boot

如何在 spring 启动 AWS Lambda 时加载自定义 ApplicationContextInitializer? 我有一个使用 spring 启动的 aws lambda 应用程序,我想编写一个 ApplicationContextInitializer 来解密数据库密码。我有以下代码在 运行 将其作为 spring 本地启动应用程序时有效,但是当我将其作为 lambda 部署到 AWS 控制台时它不起作用。

这是我的代码 1. applications.properties

spring.datasource.url=url
spring.datasource.username=testuser
CIPHER.spring.datasource.password=encryptedpassword

以下代码为ApplicationContextInitializer,假设密码为Base64编码,仅供测试(实际情况下会使用AWM KMS加密)。这里的想法是,如果密钥以 'CIPHER.' 开头(如 CIPHER.spring.datasource.password),我假设它的值需要被解密,并且另一个具有实际密钥的密钥值对(此处为 spring.datasource.password)和它的解密值将在上下文初始化时添加。

会像spring.datasource.password=decrypted password

@Component
public class DecryptedPropertyContextInitializer 
        implements ApplicationContextInitializer<ConfigurableApplicationContext> {

  private static final String CIPHER = "CIPHER.";

  @Override
  public void initialize(ConfigurableApplicationContext applicationContext) {
      ConfigurableEnvironment environment = applicationContext.getEnvironment();
      for (PropertySource<?> propertySource : environment.getPropertySources()) {
          Map<String, Object> propertyOverrides = new LinkedHashMap<>();
          decodePasswords(propertySource, propertyOverrides);
          if (!propertyOverrides.isEmpty()) {
              PropertySource<?> decodedProperties = new MapPropertySource("decoded "+ propertySource.getName(), propertyOverrides);
              environment.getPropertySources().addBefore(propertySource.getName(), decodedProperties);
          }
      }
  }

  private void decodePasswords(PropertySource<?> source, Map<String, Object> propertyOverrides) {
    if (source instanceof EnumerablePropertySource) {
        EnumerablePropertySource<?> enumerablePropertySource = (EnumerablePropertySource<?>) source;
        for (String key : enumerablePropertySource.getPropertyNames()) {
            Object rawValue = source.getProperty(key);
            if (rawValue instanceof String && key.startsWith(CIPHER)) {
                String cipherRemovedKey =  key.substring(CIPHER.length());
                String decodedValue = decode((String) rawValue);
                propertyOverrides.put(cipherRemovedKey, decodedValue);
            }
        }
    }
}

  public String decode(String encodedString) {
    byte[] valueDecoded = org.apache.commons.codec.binary.Base64.decodeBase64(encodedString);
    return new String(valueDecoded);
  }

这是 Spring 引导初始化程序

@SpringBootApplication
@ComponentScan(basePackages = "com.amazonaws.serverless.sample.springboot.controller")
public class Application extends SpringBootServletInitializer {

    @Bean
    public HandlerMapping handlerMapping() {
        return new RequestMappingHandlerMapping();
    }

    @Bean
    public HandlerAdapter handlerAdapter() {
        return new RequestMappingHandlerAdapter();
    }

    @Bean
    public HandlerExceptionResolver handlerExceptionResolver() {
        return new HandlerExceptionResolver() {
            @Override
            public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
                return null;
            }
        };
    }

   //loading the initializer here
   public static void main(String[] args) {
     SpringApplication application=new SpringApplication(Application.class);
     application.addInitializers(new DecryptedPropertyContextInitializer());
     application.run(args);
    }

这在 运行 作为 spring 启动应用程序时有效,但是当它作为 lambda 部署到 AWS 时,我的 SpringBootServletInitializer 中的 main() 方法将永远不会被调用通过拉姆达。这是我的 Lambda 处理程序。

public class StreamLambdaHandler implements RequestStreamHandler {
  private static Logger LOGGER = LoggerFactory.getLogger(StreamLambdaHandler.class); 

    private static SpringBootLambdaContainerHandler<AwsProxyRequest, AwsProxyResponse> handler;
    static {
        try {
            handler = SpringBootLambdaContainerHandler.getAwsProxyHandler(Application.class);
            handler.onStartup(servletContext -> {
                FilterRegistration.Dynamic registration = servletContext.addFilter("CognitoIdentityFilter", CognitoIdentityFilter.class);
                registration.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/*");
            });
        } catch (ContainerInitializationException e) {
            e.printStackTrace();
            throw new RuntimeException("Could not initialize Spring Boot application", e);
        }
    }

    @Override
    public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context)
            throws IOException {
        handler.proxyStream(inputStream, outputStream, context);
        outputStream.close();
    }
}

Lambda加载ApplicationContextInitializer的代码需要做哪些改动?任何帮助将不胜感激。

我可以通过以下方式解决它。

先把属性值改成带前缀的占位符,前缀表示需要解密的值,例如

spring.datasource.password=${MY_PREFIX_placeHolder}

aws lambda 环境变量名称应与占位符匹配

('MY_PREFIX_placeHolder') 并且它的值使用 AWS KMS 加密(此示例是 base64 解码)。

创建一个 ApplicationContextInitializer,它将解密 属性 值

public class DecryptedPropertyContextInitializer 
        implements ApplicationContextInitializer<ConfigurableApplicationContext> {

  private static final String CIPHER = "MY_PREFIX_";

  @Override
  public void initialize(ConfigurableApplicationContext applicationContext) {
      ConfigurableEnvironment environment = applicationContext.getEnvironment();
      for (PropertySource<?> propertySource : environment.getPropertySources()) {
          Map<String, Object> propertyOverrides = new LinkedHashMap<>();
          decodePasswords(propertySource, propertyOverrides);
          if (!propertyOverrides.isEmpty()) {
              PropertySource<?> decodedProperties = new MapPropertySource("decoded "+ propertySource.getName(), propertyOverrides);
              environment.getPropertySources().addBefore(propertySource.getName(), decodedProperties);
          }
      }
  }

  private void decodePasswords(PropertySource<?> source, Map<String, Object> propertyOverrides) {
    if (source instanceof EnumerablePropertySource) {
        EnumerablePropertySource<?> enumerablePropertySource = (EnumerablePropertySource<?>) source;
        for (String key : enumerablePropertySource.getPropertyNames()) {
            Object rawValue = source.getProperty(key);
            if (rawValue instanceof String && key.startsWith(CIPHER)) {
                String decodedValue = decode((String) rawValue);
                propertyOverrides.put(key, decodedValue);
            }
        }
    }
}

  public String decode(String encodedString) {
    byte[] valueDecoded = org.apache.commons.codec.binary.Base64.decodeBase64(encodedString);
    return new String(valueDecoded);
  }
}

以上代码将解密前缀为 MY_PREFIX_ 的所有值,并将它们添加到 属性 源的顶部。

由于 spring boot 被部署到 aws lambda 中,lambda 不会调用 main() 函数,因此如果 ApplicationContextInitializer 在 main() 中初始化,它将无法工作。为了让它工作需要覆盖 SpringBootServletInitializer 的 createSpringApplicationBuilder() 方法,所以 SpringBootServletInitializer 会像

@SpringBootApplication
@ComponentScan(basePackages = "com.amazonaws.serverless.sample.springboot.controller")
public class Application extends SpringBootServletInitializer {

    @Bean
    public HandlerMapping handlerMapping() {
        return new RequestMappingHandlerMapping();
    }

    @Bean
    public HandlerAdapter handlerAdapter() {
        return new RequestMappingHandlerAdapter();
    }

    @Bean
    public HandlerExceptionResolver handlerExceptionResolver() {
        return new HandlerExceptionResolver() {
            @Override
            public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
                return null;
            }
        };
    }


@Override
protected SpringApplicationBuilder createSpringApplicationBuilder() {
  SpringApplicationBuilder builder = new SpringApplicationBuilder();
  builder.initializers(new DecryptedPropertyContextInitializer());
  return builder;
}

   public static void main(String[] args) {
      SpringApplication.run(Application.class, args);
    }
}

无需对 lambdahandler 进行任何更改。