diff --git a/src/main/java/com/eactive/eai/adapter/http/dynamic/filter/custom/JsonToStdConverterFilter.java b/src/main/java/com/eactive/eai/adapter/http/dynamic/filter/custom/JsonToStdConverterFilter.java new file mode 100644 index 0000000..1df1583 --- /dev/null +++ b/src/main/java/com/eactive/eai/adapter/http/dynamic/filter/custom/JsonToStdConverterFilter.java @@ -0,0 +1,110 @@ +package com.eactive.eai.adapter.http.dynamic.filter.custom; + +import java.io.UnsupportedEncodingException; +import java.util.Properties; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; +import org.apache.commons.lang3.StringUtils; + +import com.eactive.eai.adapter.AdapterManager; +import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceKey; +import com.eactive.eai.adapter.http.dynamic.filter.HttpAdapterFilter; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; + +public class JsonToStdConverterFilter implements HttpAdapterFilter { + private ObjectMapper mapper = new ObjectMapper(); + + @Override + public Object doPreFilter(String adptGrpName, String adptName, Object message, Properties prop, + HttpServletRequest request, HttpServletResponse response) throws Exception { + JsonNode rootNode = parseJson(adptGrpName, message); + + if (rootNode.has("header_part")) { + JsonNode headerPart = rootNode.path("header_part"); + if (!headerPart.has("eaiIntfId") || headerPart.get("eaiIntfId").asText().isEmpty()) { + // /api/v1/public/getUserInfo.svc + String inboundUri = prop.getProperty(HttpAdapterServiceKey.INBOUND_EXTURI); + String apiPath = prop.getProperty(HttpAdapterServiceKey.API_PATH) + "/"; + + // getUserInfo.svc + String eaiIntfId = org.apache.commons.lang.StringUtils.removeStart(inboundUri, apiPath); + + // header_part 아래 eaiIntfId가 추가 또는 수정 + if (headerPart instanceof ObjectNode) { + ((ObjectNode) headerPart).put("eaiIntfId", eaiIntfId); + // 요청 응답 구분코드 강제 설정 -> API명 찾아갈때 필요 + if (!headerPart.has("reqRspnsDscd")) { + ((ObjectNode) headerPart).put("reqRspnsDscd", "S"); + } + + // GUID 강제 설정 + String orgnlTx = ""; + if (headerPart.has("orgnlTx") && !headerPart.get("orgnlTx").asText().isEmpty()) { + orgnlTx = headerPart.get("orgnlTx").asText(); + } + if (!headerPart.has("tlgrWrtnDt")) { + ((ObjectNode) headerPart).put("tlgrWrtnDt", orgnlTx); + } + if (!headerPart.has("tlgrCrtnSysNm")) { + ((ObjectNode) headerPart).put("tlgrCrtnSysNm", ""); + } + if (!headerPart.has("tlgrSrlNo")) { + ((ObjectNode) headerPart).put("tlgrSrlNo", ""); + } + } + } + } + else { + return message; + } + return rootNode.toString(); + } + + @Override + public Object doPostFilter(String adptGrpName, String adptName, Object resultMessage, Properties prop, + HttpServletRequest request, HttpServletResponse response) throws Exception { + JsonNode rootNode = parseJson(adptGrpName, resultMessage); + + if (rootNode.has("header_part")) { + JsonNode headerPart = rootNode.path("header_part"); + if (!headerPart.has("eaiIntfId") || headerPart.get("eaiIntfId").asText().isEmpty()) { + // /api/v1/public/getUserInfo.svc + String inboundUri = prop.getProperty(HttpAdapterServiceKey.INBOUND_EXTURI); + String apiPath = prop.getProperty(HttpAdapterServiceKey.API_PATH) + "/"; + + // getUserInfo.svc + String eaiIntfId = org.apache.commons.lang.StringUtils.removeStart(inboundUri, apiPath); + + // header_part 아래 eaiIntfId가 추가 또는 수정 + if (headerPart instanceof ObjectNode) { + ((ObjectNode) headerPart).put("eaiIntfId", eaiIntfId); + // 요청 응답 구분코드 강제 설정 -> API명 찾아갈때 필요 + ((ObjectNode) headerPart).put("reqRspnsDscd", "S"); + } + } + } + else { + return resultMessage; + } + return rootNode.toString(); + } + + public JsonNode parseJson(String adptGrpName, Object message) throws UnsupportedEncodingException, JsonMappingException, JsonProcessingException { + String charset = StringUtils.defaultIfBlank(AdapterManager.getInstance().getAdapterGroupVO(adptGrpName).getMessageEncode(), "UTF-8"); + String orgMessageString; + if(message instanceof byte[]) { + orgMessageString = new String((byte[])message, charset); + } else { + orgMessageString = (String) message; + } + + return mapper.readTree(orgMessageString); + } + +} diff --git a/src/main/java/com/eactive/eai/adapter/http/dynamic/filter/custom/KbankEaiJsonParseFilter.java b/src/main/java/com/eactive/eai/adapter/http/dynamic/filter/custom/KbankEaiJsonParseFilter.java new file mode 100644 index 0000000..d0d6fc0 --- /dev/null +++ b/src/main/java/com/eactive/eai/adapter/http/dynamic/filter/custom/KbankEaiJsonParseFilter.java @@ -0,0 +1,68 @@ +package com.eactive.eai.adapter.http.dynamic.filter.custom; + +import java.io.UnsupportedEncodingException; +import java.util.Iterator; +import java.util.Properties; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.lang3.StringUtils; + +import com.eactive.eai.adapter.AdapterManager; +import com.eactive.eai.adapter.http.dynamic.filter.HttpAdapterFilter; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; + +public class KbankEaiJsonParseFilter implements HttpAdapterFilter { + + private ObjectMapper mapper = new ObjectMapper(); + + @Override + public Object doPreFilter(String adptGrpName, String adptName, Object message, Properties prop, + HttpServletRequest request, HttpServletResponse response) throws Exception { + JsonNode rootNode = parseJson(adptGrpName, message); + + ObjectNode replacedJson = mapper.createObjectNode(); + for(Iterator it = rootNode.fieldNames(); it.hasNext();) { + String fieldName = it.next(); + String value = rootNode.get(fieldName).asText(); + JsonNode jsonNode = mapper.readTree(value); + replacedJson.set(fieldName, jsonNode); + } + + return replacedJson.toString(); + } + + @Override + public Object doPostFilter(String adptGrpName, String adptName, Object resultMessage, Properties prop, + HttpServletRequest request, HttpServletResponse response) throws Exception { + JsonNode rootNode = parseJson(adptGrpName, resultMessage); + + ObjectNode replacedJson = mapper.createObjectNode(); + for(Iterator it = rootNode.fieldNames(); it.hasNext();) { + String fieldName = it.next(); + JsonNode childNode = rootNode.get(fieldName); + String childString = mapper.writeValueAsString(childNode); + replacedJson.put(fieldName, childString); + } + + return replacedJson.toString(); + } + + public JsonNode parseJson(String adptGrpName, Object message) throws UnsupportedEncodingException, JsonMappingException, JsonProcessingException { + String charset = StringUtils.defaultIfBlank(AdapterManager.getInstance().getAdapterGroupVO(adptGrpName).getMessageEncode(), "UTF-8"); + String orgMessageString; + if(message instanceof byte[]) { + orgMessageString = new String((byte[])message, charset); + } else { + orgMessageString = (String) message; + } + + return mapper.readTree(orgMessageString); + } + +} diff --git a/src/main/java/com/eactive/eai/adapter/http/dynamic/filter/custom/KbankHmacSha256VerifyFilter.java b/src/main/java/com/eactive/eai/adapter/http/dynamic/filter/custom/KbankHmacSha256VerifyFilter.java new file mode 100644 index 0000000..e68cb90 --- /dev/null +++ b/src/main/java/com/eactive/eai/adapter/http/dynamic/filter/custom/KbankHmacSha256VerifyFilter.java @@ -0,0 +1,215 @@ +package com.eactive.eai.adapter.http.dynamic.filter.custom; + +import java.io.UnsupportedEncodingException; +import java.util.Properties; +import java.util.Date; +import java.util.Enumeration; +import java.util.Iterator; +import java.text.SimpleDateFormat; + +import javax.crypto.Cipher; +import javax.crypto.spec.SecretKeySpec; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.Mac; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import com.eactive.eai.adapter.AdapterManager; +import com.eactive.eai.common.property.PropManager; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import org.apache.commons.net.util.Base64; +import org.springframework.http.HttpStatus; +import org.apache.commons.lang3.StringUtils; + +import com.eactive.eai.adapter.http.dynamic.filter.HttpAdapterFilter; +import com.eactive.eai.adapter.http.dynamic.filter.JwtAuthException; +import com.eactive.eai.common.server.Keys; +import com.eactive.eai.common.util.Logger; + +import com.fasterxml.jackson.databind.ObjectMapper; + +public class KbankHmacSha256VerifyFilter implements HttpAdapterFilter { + + static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER); + + private static final String SERVICE_CODE_UNKNOWN = "00"; + private static final int TIMESTAMP_EXPIRATION_SECONDS = 60*10*1000; + private static final String GROUP_NAME = "HMAC_INFO"; + + private ObjectMapper mapper = new ObjectMapper(); + + @Override + public Object doPreFilter(String adptGrpName, String adptName, Object message, Properties prop, + HttpServletRequest request, HttpServletResponse response) throws Exception { + + try { + + String hmacSignature = getHeaderCaseInsensitive(request, "hmac_signature"); + String hmacTimestamp = getHeaderCaseInsensitive(request, "hmac_timestamp"); + + if(StringUtils.isBlank(hmacSignature) || StringUtils.isBlank(hmacTimestamp)) { + throw new JwtAuthException(String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"), "Can not find hmac info"); + } + + long inputTime = new SimpleDateFormat("yyyyMMddHHmmss").parse(hmacTimestamp).getTime(); + long currentTime = new Date().getTime(); + String timeStamp = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()); + + logger.debug("### inputTime :"+inputTime); + logger.debug("### currentTime :"+currentTime); + logger.debug("### timeStamp :"+timeStamp); + + String systemMode = System.getProperty(Keys.EAI_SYSTEMMODE); + + if(currentTime - inputTime > TIMESTAMP_EXPIRATION_SECONDS) { + + logger.debug("### systemMode:" + systemMode); + logger.debug("### currentTime - inputTime:" + (currentTime - inputTime)); + + if("P".equals(systemMode)) { + throw new JwtAuthException( + String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN,"00"), "Signature verifying failed"); + } + } + + String sMessage = (String)message; + + sMessage = sMessage.replace(" ",""); + sMessage = sMessage.replace("\n",""); + sMessage = sMessage.replace("\r",""); + + sMessage = sMessage + hmacTimestamp; + + logger.info("### sMessage:"+sMessage); + + String alCoId = "AL2021012901001"; + + JsonNode rootNode = parseJson(adptGrpName, message); + + for(Iterator it = rootNode.fieldNames(); it.hasNext();){ + String fieldName = it.next(); + if(fieldName.equals("ALCO_ID")) alCoId = rootNode.get(fieldName).asText(); + } + + logger.info("### alCoId:"+alCoId); + + Properties hmacProp = PropManager.getInstance().getProperties(GROUP_NAME); + + String hmacKey = hmacProp.getProperty(alCoId+"-hmack-key",""); + String hmacKek = hmacProp.getProperty(alCoId+"-hmack-kek",""); + String hmacIvForKey = hmacProp.getProperty(alCoId+"-hmack-iv-for-key",""); + + logger.debug("### hmacKey:"+hmacKey); + logger.debug("### hmacIvForKey:"+hmacIvForKey); + logger.debug("### hmacKek:"+hmacKek); + + //kbank.payment.hmac-key + int hmacKeyLen = hmacKey.length(); + byte[] hmacKeyData = new byte[hmacKeyLen/2]; + for(int i=0;i headerNames = request.getHeaderNames(); + while (headerNames.hasMoreElements()) { + String header = headerNames.nextElement(); + if (header != null && header.equalsIgnoreCase(headerName)) { + return request.getHeader(header); + } + } + return null; + } + + + public JsonNode parseJson(String adptGrpName, Object message) throws UnsupportedEncodingException, JsonMappingException,JsonProcessingException { + String charset = StringUtils.defaultIfBlank(AdapterManager.getInstance().getAdapterGroupVO(adptGrpName).getMessageEncode(), "UTF-8"); + String orgMessageString; + + if(message instanceof byte[]) { + orgMessageString = new String((byte[])message, charset); + } else { + orgMessageString = (String) message; + } + + return mapper.readTree(orgMessageString); + } + + @Override + public Object doPostFilter(String adptGrpName, String adptName, Object resultMessage, Properties prop, + HttpServletRequest request, HttpServletResponse response) throws Exception { + return resultMessage; + } + +} diff --git a/src/main/java/com/eactive/eai/adapter/http/dynamic/filter/custom/SnapHmacSha512VerifyFilter.java b/src/main/java/com/eactive/eai/adapter/http/dynamic/filter/custom/SnapHmacSha512VerifyFilter.java new file mode 100644 index 0000000..dbd9c89 --- /dev/null +++ b/src/main/java/com/eactive/eai/adapter/http/dynamic/filter/custom/SnapHmacSha512VerifyFilter.java @@ -0,0 +1,205 @@ +package com.eactive.eai.adapter.http.dynamic.filter.custom; + +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Properties; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.codec.digest.HmacAlgorithms; +import org.apache.commons.codec.digest.HmacUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.http.HttpStatus; + +import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceSupport; +import com.eactive.eai.adapter.http.dynamic.filter.HttpAdapterFilter; +import com.eactive.eai.adapter.http.dynamic.filter.JwtAuthException; +import com.eactive.eai.authserver.service.OAuth2Manager; +import com.eactive.eai.authserver.vo.ClientVO; +import com.eactive.eai.common.util.Logger; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +public class SnapHmacSha512VerifyFilter implements HttpAdapterFilter { + static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER); + + private static final String SERVICE_CODE_UNKNOWN = "00"; + private static final int X_TIMESTAMP_EXPIRATION_SECONDS = 60 * 5; + + @Override + public Object doPreFilter(String adptGrpName, String adptName, Object message, Properties prop, + HttpServletRequest request, HttpServletResponse response) throws Exception { + // @formatter:off + /* + * HMAC_SHA512 (clientSecret, stringToSign) + * + * stringToSign = HTTPMethod:EndpointUrl:AccessToken + * :Lowercase(HexEncode(SHA-256(minify(RequestBody)))):TimeStamp + */ + try { + String xSignature = request.getHeader("X-SIGNATURE"); + if (StringUtils.isBlank(xSignature)) { + throw new JwtAuthException(String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"), "Can not find X-SIGNATURE"); + } + + String clientSecret = assignClientSecret(prop, request); + + StringBuilder sb = new StringBuilder(); + sb.append(request.getMethod()) + .append(":") + .append(getEndpointUrl(request)) + .append(":") + .append(SnapSimpleOauth2Filter.extractToken(request)) + .append(":") + .append(getRequestBody((String)message)) + .append(":") + .append(getTimeStamp(request)); + // @formatter:on + + if (logger.isDebug()) { + logger.debug(sb.toString()); + } + + String hmac = new HmacUtils(HmacAlgorithms.HMAC_SHA_512, clientSecret).hmacHex(sb.toString()); + + if (!StringUtils.equals(xSignature, hmac)) { + throw new JwtAuthException( + String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"), + "Signature verifying failed"); + } + } catch (JwtAuthException e) { + logger.debug(e.getMessage()); + throw e; + } catch (Exception e) { + logger.error(e.getMessage()); + throw new JwtAuthException( + String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"), + "Signature verifying failed(unkown)"); + } + + return message; + } + + private String assignClientSecret(Properties prop, HttpServletRequest request) throws Exception { + String clientId = assignClientId(prop, request); + if (StringUtils.isBlank(clientId)) { + throw new JwtAuthException( + String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"), + "Can not find clientId info"); + } + + ClientVO client = (ClientVO) OAuth2Manager.getInstance().getClientDeatilsStore().get(clientId); + if (client == null) { + throw new JwtAuthException( + String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"), + "Can not find client info"); + } + + return client.getClientSecret(); + } + + private String assignClientId(Properties prop, HttpServletRequest request) { + String clientId = null; + try { + // 이전 filter에서 셋팅한 clientId 확보 + clientId = prop.getProperty(HttpAdapterServiceSupport.PROPERTIES_NAME_CLIENT_ID); + + // 이전 filter에서 셋팅한 값이 없다면 header 정보에서 확보 + if (StringUtils.isBlank(clientId)) { + clientId = request.getHeader(HttpAdapterServiceSupport.HEADER_NAME_CLIENT_ID); + } + } catch (Exception e) { + logger.error(e.getMessage()); + } + + return clientId; + } + + private String getEndpointUrl(HttpServletRequest request) { + String servletPath = request.getServletPath(); + String pathInfo = request.getPathInfo(); + String queryString = request.getQueryString(); + + StringBuilder url = new StringBuilder(); + url.append(servletPath); + + if (pathInfo != null) { + url.append(pathInfo); + } + + if (queryString != null) { + url.append("?").append(queryString); + } + + return url.toString(); + } + + private String getRequestBody(String message) throws Exception { + if (StringUtils.isEmpty(message)) { + return ""; + } + + // Lowercase(HexEncode(SHA-256(minify(RequestBody)))) + String body = minifyJson(message); + try { + body = DigestUtils.sha256Hex(body); + } catch (Exception e) { + throw new JwtAuthException( + String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"), + "SHA-256 digest error"); + } + return body.toLowerCase(); + } + + private String minifyJson(String json) { + try { + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode jsonNode = objectMapper.readValue(json, JsonNode.class); + return jsonNode.toString(); + } catch (Exception e) { + // json이 아닐경우 + return json; + } + } + + private String getTimeStamp(HttpServletRequest request) throws Exception { + String timeStamp = request.getHeader("X-TIMESTAMP"); + if (StringUtils.isBlank(timeStamp)) { + throw new JwtAuthException( + String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"), + "Can not find X-TIMESTAMP"); + } + + // 2020-12-23T09:10:11+07:00(현재시간이랑 비교로직 추가-보안) + ZonedDateTime xTimeStamp = ZonedDateTime.parse(timeStamp, DateTimeFormatter.ISO_DATE_TIME); + xTimeStamp = xTimeStamp.plusSeconds(X_TIMESTAMP_EXPIRATION_SECONDS); + ZonedDateTime now = ZonedDateTime.now(); + if (xTimeStamp.isBefore(now.withZoneSameInstant(xTimeStamp.getZone()))) { + throw new JwtAuthException( + String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"), + "Too old X-TIMESTAMP"); + } + + return timeStamp; + } + + @Override + public Object doPostFilter(String adptGrpName, String adptName, Object resultMessage, Properties prop, + HttpServletRequest request, HttpServletResponse response) throws Exception { + return resultMessage; + } + + public static void main(String[] args) { + String str = "POST:/api/test/aaa/type02/555:" + + "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6WyJzbmFwIl0sImV4cCI6MTY3NjUyNDI1MSwianRpIjoiNzI0MWMyZGUtZjJhNi00NTFmLWExMjQtZGM3NmI0ZGY1OThjIiwiY2xpZW50X2lkIjoic1NHSkR3bEpzZWw1V2VYb2R6ZDNKM01RMjBLWUNacEwifQ.Obe8t8IwuQ3P7i4yCSASYze-9DLmVwXe0g_gBYlwv_Jzc7V8-ooTwp6SVnaNsM6pp1GV-9lxMpAzQO_CRHQq_hdMeiPI_CXHh3gETvjQThn57QGEGdCNp_lUEVYtMPUxi5u3X6Of07o0_OB83WI7rA1zD0DaP8V67pgfq-jMRe_3IXztOYu_nwMgREbGvs9tqcsjxppRKkEHcK1S8Eq4HzaLwjwyHJAhIlTba27yd4T8ELC7IpFphJBfEPF_t0ZpYW15lOrg5pHPjy1xpTV0Kgipog5wycTZlFUeCWWjfxTrpVmt_1XlEOlZH9X6xAefWrH93_fpbt3OcxwP4u9KhA" + + ":424058b889b9d860d13b79d90df52ddcbefba2fc9d27607e999c7c3c4d61d9c8:" + "2023-02-16T09:47:07+07:00"; + + String hmac = new HmacUtils(HmacAlgorithms.HMAC_SHA_512, + "o3Hre3voTrHbLueDrHC0LYM5effHQZILI8t23htbD12TKD5QJUMONqFqv4494iQS46bzHS0xW1fBC5LHo3D0SzhZvQcPUaJZw0iQ79NnzYFUCTrEsoFJRL300dp3Ql4y") + .hmacHex(str); + + // System.out.println(hmac); + } +} diff --git a/src/main/java/com/eactive/eai/adapter/http/dynamic/filter/custom/SnapJwtAuthFilter.java b/src/main/java/com/eactive/eai/adapter/http/dynamic/filter/custom/SnapJwtAuthFilter.java new file mode 100644 index 0000000..77dc261 --- /dev/null +++ b/src/main/java/com/eactive/eai/adapter/http/dynamic/filter/custom/SnapJwtAuthFilter.java @@ -0,0 +1,237 @@ +package com.eactive.eai.adapter.http.dynamic.filter.custom; + +import java.io.IOException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; +import java.text.ParseException; +import java.util.Base64; +import java.util.Date; +import java.util.HashSet; +import java.util.Properties; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpStatus; + +import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceSupport; +import com.eactive.eai.adapter.http.dynamic.filter.HttpAdapterFilter; +import com.eactive.eai.adapter.http.dynamic.filter.JwtAuthException; +import com.eactive.eai.adapter.http.dynamic.filter.JwtAuthFilter; +import com.eactive.eai.authserver.service.OAuth2Manager; +import com.eactive.eai.common.property.PropGroupVO; +import com.eactive.eai.common.property.PropManager; +import com.eactive.eai.common.util.Logger; +import com.eactive.eai.inbound.action.ActionFactory; +import com.eactive.eai.inbound.action.RequestAction; +import com.eactive.eai.inbound.processor.Processor; +import com.eactive.eai.message.StandardMessageUtil; +import com.nimbusds.jose.JWSVerifier; +import com.nimbusds.jose.crypto.RSASSAVerifier; +import com.nimbusds.jose.util.IOUtils; +import com.nimbusds.jwt.SignedJWT; + +public class SnapJwtAuthFilter implements HttpAdapterFilter { + static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER); + + public static final String PROP_GROUP_AUTH_SERVER = "OAuthServer"; + public static final String PROP_KEYSTORE_PATH = "certification.publicKeyPath"; + + private static final String SERVICE_CODE_UNKNOWN = "00"; + + public static final String PAYLOAD_PARAM_NAME_SCOPE = "scope"; + public static final String PAYLOAD_PARAM_NAME_CLIENT_ID = "client_id"; + + private JWSVerifier jwsVerifier; + + public SnapJwtAuthFilter() { + super(); + + PropGroupVO vo = PropManager.getInstance().getPropGroupVO(PROP_GROUP_AUTH_SERVER); + String publicKeyPath = "/certificate/elink-oauth-dev.pub"; + if (vo != null) { + publicKeyPath = vo.getProperty(PROP_KEYSTORE_PATH); + } else { + logger.warn("The properties has not been set.[" + PROP_GROUP_AUTH_SERVER + "]"); + } + + Resource resource = new ClassPathResource(publicKeyPath); + String publicKey = null; + try { + publicKey = IOUtils.readInputStreamToString(resource.getInputStream()); + publicKey = StringUtils.replace(publicKey, "-----BEGIN PUBLIC KEY-----", ""); + publicKey = StringUtils.replace(publicKey, "-----END PUBLIC KEY-----", ""); + publicKey = StringUtils.remove(publicKey, "\r"); + publicKey = StringUtils.remove(publicKey, "\n"); + + X509EncodedKeySpec keySpec = new X509EncodedKeySpec(Base64.getDecoder().decode(publicKey)); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + jwsVerifier = new RSASSAVerifier((RSAPublicKey) keyFactory.generatePublic(keySpec)); + } catch (final IOException | NoSuchAlgorithmException | InvalidKeySpecException e) { + throw new RuntimeException(e); + } + } + + @Override + public Object doPreFilter(String adptGrpName, String adptName, Object message, Properties prop, + HttpServletRequest request, HttpServletResponse response) throws Exception { + String apiId = assignApiId(adptGrpName, adptName, message, prop, request); + if (StringUtils.isBlank(apiId)) { + throw new JwtAuthException( + String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"), + "Can not find apiId(apiCode) info"); + } + + try { + String token = JwtAuthFilter.extractJWTToken(request); + SignedJWT signedJWT = SignedJWT.parse(token); + + if (new Date().after(signedJWT.getJWTClaimsSet().getExpirationTime())) { + throw new JwtAuthException( + String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "01"), + "Invalid Token - expired (B2B)"); + } + + if (!signedJWT.verify(jwsVerifier)) { + throw new JwtAuthException( + String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "01"), + "Invalid Token (B2B)"); + } + + OAuth2Manager manager = OAuth2Manager.getInstance(); + HashSet scopeSet = manager.getApiScopeMap().get(apiId); + if (!verifyScope(scopeSet, signedJWT)) { + throw new JwtAuthException( + String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "01"), + String.format("Insufficient [Scope](Allowed Scope=%s)", scopeSet.toString())); + } + + // header 값으로 전송된 clientId와 토큰 소유 clientId check + if (!verifyClientId(prop, signedJWT)) { + throw new JwtAuthException( + String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"), + "client_id not matched(header/token)"); + } + } catch (JwtAuthException e) { + logger.debug(e.getMessage()); + throw e; + } catch (Exception e) { + logger.error(e.getMessage()); + throw new JwtAuthException( + String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "01"), + "Invalid Token (B2B)"); + } + + return message; + } + + private boolean verifyClientId(Properties prop, SignedJWT signedJWT) { + try { + // 이전 filter에서 셋팅한 clientId 확보 + String headerClientId = prop.getProperty(HttpAdapterServiceSupport.PROPERTIES_NAME_CLIENT_ID); + String tokenClientId = (String) signedJWT.getJWTClaimsSet().getClaim(PAYLOAD_PARAM_NAME_CLIENT_ID); + + // 이전 filter에서 셋팅한 값이 없다면 token 정보에서 확보 + if (StringUtils.isBlank(headerClientId)) { + if (StringUtils.isNotBlank(tokenClientId)) { + prop.put(HttpAdapterServiceSupport.PROPERTIES_NAME_CLIENT_ID, tokenClientId); + } + } else { // header로 전달된 clientId가 있을때만 비교 + if (!headerClientId.equals(tokenClientId)) { + return false; + } + } + } catch (Exception e) { + logger.error(e.getMessage()); + } + + return true; + } + + private String assignApiId(String adptGrpName, String adptName, Object message, Properties prop, + HttpServletRequest request) { + String apiId = null; + try { + String actionName = prop.getProperty(Processor.REQUEST_ACTION); + RequestAction action = ActionFactory.createAction(actionName); + action.setAdapterInfo(adptGrpName, adptName, prop); + String[] keys = action.perform(message); + apiId = keys[0]; + + // PathVariable 지원 추가 + apiId = StandardMessageUtil.getMatchedKey(apiId, actionName); + } catch (Exception e) { + logger.error(e.getMessage()); + } + +// // header 에서 확보 +// String apiId = request.getHeader(HEADER_NAME_API_CODE); +// if (StringUtils.isBlank(apiId)) { +// // url에서 확보 +// // /ONLWeb/api/v1/public/getUserInfo.svc/ +// apiId = request.getRequestURI(); +// // /ONLWeb/api/v1/public/getUserInfo.svc +// apiId = StringUtils.removeEnd(apiId, "/"); +// // getUserInfo.svc +// apiId = StringUtils.substringAfterLast(apiId, "/"); +// } + + return apiId; + } + + @Override + public Object doPostFilter(String adptGrpName, String adptName, Object resultMessage, Properties prop, + HttpServletRequest request, HttpServletResponse response) throws Exception { + return resultMessage; + } + + private boolean verifyScope(HashSet scopeSet, SignedJWT signedJWT) throws ParseException { + if (scopeSet == null || scopeSet.isEmpty()) { + return true; // If no scope is specified in the api, all are allowed + } + + String[] scopeArr = signedJWT.getJWTClaimsSet().getStringArrayClaim(PAYLOAD_PARAM_NAME_SCOPE); + if (scopeArr == null || scopeArr.length == 0) { + return false; + } + + for (String scope : scopeArr) { + if (scopeSet.contains(scope)) { + return true; + } + } + + return false; + } + + public static String extractJWTToken(HttpServletRequest request) throws JwtAuthException { + String authorization = request.getHeader("Authorization"); + if (StringUtils.isBlank(authorization)) { + throw new JwtAuthException( + String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"), + "No have header info: Authorization"); + } + + String[] components = authorization.split("\\s"); + + if (components.length != 2) { + throw new JwtAuthException( + String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"), + "Malformat [Authorization] content"); + } + + if (!StringUtils.equalsIgnoreCase(components[0], "Bearer")) { + throw new JwtAuthException( + String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"), + "[Bearer] is needed"); + } + + return components[1].trim(); + } +} \ No newline at end of file diff --git a/src/main/java/com/eactive/eai/adapter/http/dynamic/filter/custom/SnapSimpleOauth2Filter.java b/src/main/java/com/eactive/eai/adapter/http/dynamic/filter/custom/SnapSimpleOauth2Filter.java new file mode 100644 index 0000000..9148b32 --- /dev/null +++ b/src/main/java/com/eactive/eai/adapter/http/dynamic/filter/custom/SnapSimpleOauth2Filter.java @@ -0,0 +1,205 @@ +package com.eactive.eai.adapter.http.dynamic.filter.custom; + +import java.util.HashSet; +import java.util.Properties; +import java.util.Set; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.common.OAuth2AccessToken; +import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.security.oauth2.provider.token.TokenStore; + +import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceSupport; +import com.eactive.eai.adapter.http.dynamic.filter.HttpAdapterFilter; +import com.eactive.eai.adapter.http.dynamic.filter.JwtAuthException; +import com.eactive.eai.authserver.service.OAuth2Manager; +import com.eactive.eai.authserver.util.BeanUtils; +import com.eactive.eai.common.util.Logger; +import com.eactive.eai.inbound.action.ActionFactory; +import com.eactive.eai.inbound.action.RequestAction; +import com.eactive.eai.inbound.processor.Processor; +import com.eactive.eai.message.StandardMessageUtil; + +public class SnapSimpleOauth2Filter implements HttpAdapterFilter { + static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER); + + private static final String SERVICE_CODE_UNKNOWN = "00"; + + private TokenStore tokenStore; + + public SnapSimpleOauth2Filter() { + super(); + tokenStore = BeanUtils.getBean("tokenStore", TokenStore.class); + } + + @Override + public Object doPreFilter(String adptGrpName, String adptName, Object message, Properties prop, + HttpServletRequest request, HttpServletResponse response) throws Exception { + try { + String apiId = assignApiId(adptGrpName, adptName, message, prop, request); + if (StringUtils.isBlank(apiId)) { + throw new JwtAuthException( + String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"), + "Can not find apiId(apiCode) info"); + } + + String tokenStr = extractToken(request); + + OAuth2AccessToken token = tokenStore.readAccessToken(tokenStr); + if (token == null) { + throw new JwtAuthException( + String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "03"), + "Token Not Found (B2B)"); + } + + if (token.isExpired()) { + throw new JwtAuthException( + String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "01"), + "Invalid Token - expired (B2B)"); + } + + OAuth2Authentication authentication = tokenStore.readAuthentication(token); + if (!authentication.isAuthenticated()) { + throw new JwtAuthException( + String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "01"), + "Invalid Token (B2B)"); + } + + // check scope + OAuth2Manager manager = OAuth2Manager.getInstance(); + HashSet scopeSet = manager.getApiScopeMap().get(apiId); + if (!verifyScope(scopeSet, authentication)) { + throw new JwtAuthException( + String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "01"), + String.format("Insufficient [Scope](Allowed Scope=%s)", scopeSet.toString())); + } + + // header 값으로 전송된 clientId와 토큰 소유 clientId check + if (!verifyClientId(prop, authentication)) { + throw new JwtAuthException( + String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"), + "client_id not matched(header/token)"); + } + } catch (JwtAuthException e) { + logger.debug(e.getMessage()); + throw e; + } catch (Exception e) { + logger.error(e.getMessage()); + throw new JwtAuthException( + String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "01"), + "Invalid Token (B2B)"); + } + + return message; + } + + private boolean verifyScope(HashSet scopeSet, OAuth2Authentication auth) { + if (scopeSet == null || scopeSet.isEmpty()) { + return true; // If no scope is specified in the api, all are allowed + } + + Set scopesInToken = auth.getOAuth2Request().getScope(); + + if (scopesInToken == null || scopesInToken.isEmpty()) { + return false; + } + + for (String scope : scopesInToken) { + if (scopeSet.contains(scope)) { + return true; + } + } + + return false; + } + + private boolean verifyClientId(Properties prop, Authentication authentication) { + try { + // 이전 filter에서 셋팅한 clientId 확보 + String headerClientId = prop.getProperty(HttpAdapterServiceSupport.PROPERTIES_NAME_CLIENT_ID); + String tokenClientId = ((OAuth2Authentication) authentication).getOAuth2Request().getClientId(); + + // 이전 filter에서 셋팅한 값이 없다면 token 정보에서 확보 + if (StringUtils.isBlank(headerClientId)) { + if (StringUtils.isNotBlank(tokenClientId)) { + prop.put(HttpAdapterServiceSupport.PROPERTIES_NAME_CLIENT_ID, tokenClientId); + } + } else { // header로 전달된 clientId가 있을때만 비교 + if (!headerClientId.equals(tokenClientId)) { + return false; + } + } + } catch (Exception e) { + logger.error(e.getMessage()); + } + + return true; + } + + private String assignApiId(String adptGrpName, String adptName, Object message, Properties prop, + HttpServletRequest request) { + String apiId = null; + try { + String actionName = prop.getProperty(Processor.REQUEST_ACTION); + RequestAction action = ActionFactory.createAction(actionName); + action.setAdapterInfo(adptGrpName, adptName, prop); + String[] keys = action.perform(message); + apiId = keys[0]; + + // PathVariable 지원 추가 + apiId = StandardMessageUtil.getMatchedKey(apiId, actionName); + } catch (Exception e) { + logger.error(e.getMessage()); + } + +// // header 에서 확보 +// String apiId = request.getHeader(HEADER_NAME_API_CODE); +// if (StringUtils.isBlank(apiId)) { +// // url에서 확보 +// // /ONLWeb/api/v1/public/getUserInfo.svc/ +// apiId = request.getRequestURI(); +// // /ONLWeb/api/v1/public/getUserInfo.svc +// apiId = StringUtils.removeEnd(apiId, "/"); +// // getUserInfo.svc +// apiId = StringUtils.substringAfterLast(apiId, "/"); +// } + + return apiId; + } + + public static String extractToken(HttpServletRequest request) throws JwtAuthException { + String authorization = request.getHeader("Authorization"); + if (StringUtils.isBlank(authorization)) { + throw new JwtAuthException( + String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"), + "No have header info: Authorization"); + } + + String[] components = authorization.split("\\s"); + + if (components.length != 2) { + throw new JwtAuthException( + String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"), + "Malformat [Authorization] content"); + } + + if (!StringUtils.equalsIgnoreCase(components[0], "Bearer")) { + throw new JwtAuthException( + String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"), + "[Bearer] is needed"); + } + + return components[1].trim(); + } + + @Override + public Object doPostFilter(String adptGrpName, String adptName, Object resultMessage, Properties prop, + HttpServletRequest request, HttpServletResponse response) throws Exception { + return resultMessage; + } +} \ No newline at end of file