001package com.poscoict.app.job;
002
003import com.poscoict.glueframework.util.GlueCipher;
004import org.slf4j.Logger;
005import org.slf4j.LoggerFactory;
006import org.springframework.beans.BeansException;
007import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
008import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
009import org.springframework.core.env.*;
010import org.springframework.core.io.Resource;
011import org.springframework.util.StringUtils;
012
013import javax.crypto.BadPaddingException;
014import javax.crypto.IllegalBlockSizeException;
015import javax.crypto.NoSuchPaddingException;
016import java.io.File;
017import java.io.IOException;
018import java.security.InvalidKeyException;
019import java.security.NoSuchAlgorithmException;
020import java.util.Objects;
021
022/**
023 * GluePropertySourcesPlaceholderConfigurer.
024 * GluePropertySourcesPlaceholderConfigurer 는 PropertySourcesPlaceholderConfigurer 을 확장한 클래스입니다( Spring 5.2.0 ).
025 *
026 * <pre>
027 * <b>Bean Property</b>
028 *
029 * - keyFile : (선택) keyFile 이 존재할 경우, 암호화된 property 는 복호화함.
030 *
031 *
032 * <b>예제</b><xmp>
033 * 사용 예# 1
034 *
035 *     <bean class="com.poscoict.sample.GluePropertySourcesPlaceholderConfigurer">
036 *         <property name="keyFile" value="file:${CIPHER_PATH}/glue.cipher"></property>
037 *         <property name="locations">
038 *             <list>
039 *                 <value>file:${CONFIG_PATH}/glue.properties</value>
040 *             </list>
041 *         </property>
042 *     </bean>
043 * </xmp>
044 * </pre>
045 */
046public class GluePropertySourcesPlaceholderConfigurer extends PropertySourcesPlaceholderConfigurer {
047
048    private final Logger logger = LoggerFactory.getLogger(getClass());
049    private final EncryptionAwareService encryptionService = new EncryptionAwareService();
050    private Environment env;
051
052    public GluePropertySourcesPlaceholderConfigurer() {
053        super();
054    }
055
056    public void setKeyFile(Resource keyFile) {
057        if (keyFile.exists() && keyFile.isFile()) {
058            try {
059                this.encryptionService.keyFile = keyFile.getFile();
060            } catch (IOException e) {
061                this.logger.warn(e.getMessage(), e);
062            }
063        } else {
064            //암호화 사용되지 않는 경우가 많으니 로그 출력 하지 않음.
065            //this.logger.warn("file not exist - " + keyFile);
066        }
067    }
068
069    @Override
070    public void setEnvironment(Environment environment) {
071        this.env = environment;
072    }
073
074    @Override
075    protected void processProperties(ConfigurableListableBeanFactory beanFactoryToProcess, ConfigurablePropertyResolver propertyResolver) throws BeansException {
076        PropertySources propertySources = ((ConfigurableEnvironment) env).getPropertySources();
077
078        // 여기서부터는 super.getAppliedPropertySources() 를 호출할 수 없어서..
079        // PropertySourcesPlaceholderConfigurer의 postProcessBeanFactory()의 코드를 가져왔습니다.
080        try {
081            PropertySource<?> localPropertySource = new PropertiesPropertySource(LOCAL_PROPERTIES_PROPERTY_SOURCE_NAME, super.mergeProperties());
082            if (this.localOverride) {
083                ((MutablePropertySources) propertySources).addFirst(localPropertySource);
084            } else {
085                ((MutablePropertySources) propertySources).addLast(localPropertySource);
086            }
087        } catch (IOException e) {
088            this.logger.error(e.getMessage(), e);
089        }
090
091        // 암호화여부를 체크해서 복호화를 진행합니다.
092        super.processProperties(beanFactoryToProcess, new EncryptionAwarePropertySourcesPropertyResolver(propertySources, encryptionService));
093    }
094
095    /**
096     * 암호화된 문자열(property)인지 확인해서 복호화하는 서비스입니다.
097     */
098    public class EncryptionAwareService {
099        private final Logger logger = LoggerFactory.getLogger(getClass());
100        public static final String ENCRIPT_PREFIX = "enc##";
101        private File keyFile = null;
102
103        public String tryDecrypt(String text) {
104            if (canTxDecrypted(text)) {
105                try {
106                    return GlueCipher.decrypt(text.split(ENCRIPT_PREFIX)[1], keyFile);
107                } catch (BadPaddingException | IllegalBlockSizeException | NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException e) {
108                    this.logger.error(e.getMessage(), e);
109                }
110            }
111            return text;
112        }
113
114        boolean canTxDecrypted(String text) {
115            return keyFile != null && keyFile.isFile() && StringUtils.hasText(ENCRIPT_PREFIX)
116                    && text.startsWith(ENCRIPT_PREFIX) && !text.endsWith(ENCRIPT_PREFIX);
117        }
118    }
119
120    /**
121     * Property의 문자열을 resolver합니다.
122     */
123    public class EncryptionAwarePropertySourcesPropertyResolver extends PropertySourcesPropertyResolver {
124        private final EncryptionAwareService encryptionAwareService;
125
126        public EncryptionAwarePropertySourcesPropertyResolver(PropertySources propertySources, EncryptionAwareService encryptionAwareService) {
127            super(propertySources);
128            this.encryptionAwareService = encryptionAwareService;
129        }
130
131        @Override
132        public String resolvePlaceholders(String text) {
133            String resolvedText = super.resolvePlaceholders(text);
134            if (Objects.isNull(encryptionAwareService))
135                return resolvedText;
136            return encryptionAwareService.tryDecrypt(resolvedText);
137        }
138
139        @Override
140        public String resolveRequiredPlaceholders(String text) throws IllegalArgumentException {
141            String resolvedText = super.resolveRequiredPlaceholders(text);
142            if (Objects.isNull(encryptionAwareService))
143                return resolvedText;
144            return encryptionAwareService.tryDecrypt(resolvedText);
145        }
146    }
147}