listVo = null;
+
+ if(selectedKey!=null) {
+ listVo = manager.getEAIMessage(selectedKey).getSvcMsgs();
+ }
+%>
+
+
+
+
+
+ EAI ServiceMessage Manager
+
+
+
+
+
+ EAI ServiceMessage Configuration
+
+
+
+
\ No newline at end of file
diff --git a/WebContent/mgr/svcmonitor.jsp b/WebContent/mgr/svcmonitor.jsp
new file mode 100644
index 0000000..210c093
--- /dev/null
+++ b/WebContent/mgr/svcmonitor.jsp
@@ -0,0 +1,310 @@
+<%@ page import="java.io.*, java.util.*"%>
+<%@ page language="java" contentType="text/html;charset=utf-8"%>
+<%@ page import="com.eactive.eai.common.EAITable"%>
+<%@ page import="com.eactive.eai.common.message.EAIMessage"%>
+<%@ page import="com.eactive.eai.common.message.ServiceMessage"%>
+<%@ page import="com.eactive.eai.common.message.EAIMessageManager"%>
+<%@ page import="com.eactive.eai.common.monitor.EAIServiceMonitor"%>
+<%@ page import="com.eactive.eai.common.monitor.MonitorVO"%>
+<%@ page import="java.util.HashMap"%>
+
+<%
+ String actionMessage = "";
+ EAIServiceMonitor monitor = EAIServiceMonitor.getInstance();
+ String[] voKey = monitor.getAllMonitorVOKey();
+ String action = request.getParameter("actionType");
+ MonitorVO vo = null;
+ String[] pssCntKey = null;
+ String[] pssErrCntKey = null;
+ HashMap pssCnt = null;
+ HashMap pssCntInPeriod = null;
+ HashMap errPssCnt = null;
+ HashMap errPssCntInPeriod = null;
+
+ String selectedSvcCds = request.getParameter("svcCds");
+ if(selectedSvcCds == null && voKey.length>=1) {
+ selectedSvcCds = voKey[0];
+ }
+
+ if(selectedSvcCds!=null) {
+ vo = monitor.getMonitorVO(selectedSvcCds);
+ if("RESET".equals(action)) {
+ vo.reset();
+ }
+ pssCntKey = vo.getAllPssCntKey();
+ pssCnt = vo.getPssCnt();
+ pssCntInPeriod = vo.getPssCntInPeriod();
+ errPssCnt = vo.getErrPssCnt();
+ errPssCntInPeriod = vo.getErrPssCntInPeriod();
+
+ }
+
+ if("ALLRESET".equalsIgnoreCase(action)){
+ monitor.resetAll();
+ }
+ else if("RESET_TOLIST".equalsIgnoreCase(action)){
+ actionMessage = monitor.resetErrorList();
+ }
+%>
+
+
+
+Service ID/Log Sequence 별 통계 Monitor
+
+
+
+Service ID/Log Sequence 별 통계 데이터
+<% if(actionMessage != null && actionMessage.length() > 0) { %>
+Action result : <%= actionMessage %>
+<% } %>
+
+
+
+
+
diff --git a/WebContent/mgr/svcmonitorbwk.jsp b/WebContent/mgr/svcmonitorbwk.jsp
new file mode 100644
index 0000000..149c1ff
--- /dev/null
+++ b/WebContent/mgr/svcmonitorbwk.jsp
@@ -0,0 +1,407 @@
+<%@ page import="java.io.*, java.util.*"%>
+<%@ page language="java" contentType="text/html;charset=utf-8"%>
+<%@ page import="com.eactive.eai.common.EAITable"%>
+<%@ page import="com.eactive.eai.common.message.EAIMessage"%>
+<%@ page import="com.eactive.eai.common.message.ServiceMessage"%>
+<%@ page import="com.eactive.eai.common.message.EAIMessageManager"%>
+<%@ page import="com.eactive.eai.common.monitor.EAIServiceMonitor"%>
+<%@ page import="com.eactive.eai.common.monitor.MonitorVO"%>
+<%@ page import="java.util.HashMap"%>
+
+<%
+
+ EAIServiceMonitor monitor = EAIServiceMonitor.getInstance();
+ String[] voKey = monitor.getAllBwkClsKey();
+ String action = request.getParameter("actionType");
+
+ String[] pssCntKey = null;
+ String[] pssErrCntKey = null;
+ HashMap pssCnt = null;
+ HashMap pssCntInPeriod = null;
+ HashMap errPssCnt = null;
+ HashMap errPssCntInPeriod = null;
+ HashMap voByBwk = null;
+
+ long[] verTotalByBwk = null;;
+ long[] verTotalInPeriodByBwk = null;
+ double sumOfprocessTimeByBwk = 0;
+ double psvSumOfProcessTimeByBwk = 0;
+ double sumOfprocessTimeInPeriodByBwk = 0;
+ double psvSumOfProcessTimeInPeriodByBwk = 0;
+ double avgOfprocessTimeByBwk = 0;
+ double psvAvgOfProcessTimeByBwk = 0;
+ double avgOfprocessTimeInPeriodByBwk = 0;
+ double psvAvgOfProcessTimeInPeriodByBwk = 0;
+
+ int countEaiSvcCd = 0;
+ int countEaiSvcCdInPeriod = 0;
+
+ String selectedBwkCls = request.getParameter("bwkCls");
+ if(selectedBwkCls == null && voKey.length>=1){
+ selectedBwkCls = voKey[0];
+ }
+
+ if(selectedBwkCls!=null) {
+ voByBwk = monitor.getBwkCls(selectedBwkCls);
+ }
+
+%>
+
+
+
+
+ 업무구분 코드/Log Sequence 별 통계 Monitor
+
+
+
+
+
+ 업무구분 코드/Log Sequence 별 통계 데이터
+
+
+
+
\ No newline at end of file
diff --git a/WebContent/mgr/testcall.jsp b/WebContent/mgr/testcall.jsp
new file mode 100644
index 0000000..a86e12d
--- /dev/null
+++ b/WebContent/mgr/testcall.jsp
@@ -0,0 +1,158 @@
+<%@ page import="java.io.*, java.util.*"%>
+<%@ page language="java" contentType="text/html;charset=utf-8"%>
+<%@ page import="com.eactive.eai.adapter.AdapterVO"%>
+<%@ page import="com.eactive.eai.agent.command.Command"%>
+<%@ page import="com.eactive.eai.agent.testcall.TestCallCommand"%>
+<%@ page import="com.eactive.eai.agent.AgentUtil"%>
+<%@ page import="com.eactive.eai.common.property.PropManager"%>
+<%@ page import="com.eactive.eai.common.testcall.TestCall"%>
+<%@ page import="com.eactive.eai.common.testcall.TestCallManager"%>
+
+<%
+ TestCallManager manager = TestCallManager.getInstance();
+ ArrayList names = new ArrayList();
+ String action = request.getParameter("actionType");
+ String period = request.getParameter("period");
+ Properties prop = new Properties();
+
+ Iterator it = manager.getAllAdapterNames();
+ while(it.hasNext()) {
+ names.add(it.next());
+ }
+ Collections.sort(names);
+
+ if(period == null|| period.length()<=0){
+
+ PropManager pmanager = PropManager.getInstance();
+ long temp = 1000*60*10;
+ try {
+ temp = Long.parseLong(pmanager.getProperty(manager.PROP_GROUP, manager.PROP_PERIOD));
+
+ } catch(Exception e) {}
+
+ period = ""+temp;
+ }
+ String run = "";
+
+ if("UPDATE".equals(action)) {
+ prop.setProperty("PERIOD", period);
+ Command command = new TestCallCommand();
+ command.setArgs(prop);
+ AgentUtil.broadcast(command);
+ }
+ if("START".equals(action)){
+ run = "START";
+
+ prop.setProperty("RUN",run);
+ Command command = new TestCallCommand();
+ command.setArgs(prop);
+ AgentUtil.broadcast(command);
+ }
+ if("STOP".equals(action)){
+ run = "STOP";
+
+ prop.setProperty("RUN",run);
+ Command command = new TestCallCommand();
+ command.setArgs(prop);
+ AgentUtil.broadcast(command);
+ }
+
+ String status = "";
+
+ if(manager.isStarted())
+ status = "시작됨";
+ else
+ status = "종료됨";
+%>
+
+
+
+
+ TestCall Manager
+
+
+
+
+
+ TestCall Configuration
+
+
+
+
\ No newline at end of file
diff --git a/WebContent/mgr/transRuleReload.jsp b/WebContent/mgr/transRuleReload.jsp
new file mode 100644
index 0000000..ab02334
--- /dev/null
+++ b/WebContent/mgr/transRuleReload.jsp
@@ -0,0 +1,103 @@
+<%@ page import="java.io.*, java.util.*"%>
+<%@ page language="java" contentType="text/html;charset=utf-8"%>
+<%@ page import="com.eactive.eai.transformer.engine.TransformEngine"%>
+
+<%
+
+ String action = request.getParameter("actionType");
+ TransformEngine engine = TransformEngine.getInstance();
+
+ String status = "";
+
+ if("START".equals(action)){
+ engine.start();
+
+ }
+ if("STOP".equals(action)){
+ engine.stop();
+ }
+
+ if(engine.isStarted())
+ status = "시작됨";
+ else
+ status = "종료됨";
+%>
+
+
+
+
+ TransRuleReload Manager
+
+
+
+
+
+ TransRuleReload Configuration
+
+
+
+
\ No newline at end of file
diff --git a/WebContent/mgr/transformer/Transform.jsp b/WebContent/mgr/transformer/Transform.jsp
new file mode 100644
index 0000000..2c27a5e
--- /dev/null
+++ b/WebContent/mgr/transformer/Transform.jsp
@@ -0,0 +1,214 @@
+<%--
+<%@ page import="java.io.*, java.util.*"%>
+<%@ page language="java" contentType="text/html;charset=utf-8"%>
+<%@ page import="com.eactive.eai.transformer.layout.Layout"%>
+<%@ page import="com.eactive.eai.transformer.transform.Transform"%>
+<%@ page import="com.eactive.eai.transformer.transform.TransformItem"%>
+<%@ page import="com.eactive.eai.transformer.transform.TransformManager"%>
+<%@ page import="com.eactive.eai.transformer.util.TransformKeys"%>
+<%@ page import="java.util.HashMap"%>
+
+<%!
+ /**
+ *
+ * 문자열을 받아서 널이면 제로스트링을, 아니면 트림된 문자열을 리턴.
+ *
+ * @param String 변환 대상
+ * @return 'String'
+ */
+ public static String checkNull( String str ) {
+
+ String sResult = new String();
+ if ( str == null ) {
+ sResult = "";
+ }
+ else {
+ sResult = str.trim();
+ }
+ return sResult;
+ }
+
+%>
+
+<%
+ TransformManager manager = TransformManager.getManager();
+ String key[] = manager.getAllTransformName();
+
+ String selected = request.getParameter("transformName");
+ if(selected == null && key.length>=1) {
+ selected = key[0];
+ }
+
+ Transform transform = null;
+ ArrayList itemList = new ArrayList();
+ Layout target = null;
+ Iterator it = null;
+
+ if(selected!=null) {
+ transform = manager.getTransform(selected);
+ target = transform.getTargetLayout();
+ itemList = (ArrayList) transform.getTransformItemList();
+ it = transform.getSourceLayoutNames();
+ }
+ int sourceCount = transform.getSourceLayoutCount();
+ String sourceName = "";
+ for (int i=0; i < sourceCount; i++) {
+ if ("".equals(sourceName))
+ sourceName = (String)it.next();
+ else
+ sourceName = sourceName+" , " + (String)it.next();
+ }
+
+%>
+
+
+
+ Transform 변환규칙 Manager
+
+
+
+
+
+ Transform 변환규칙 Configuration
+
+
+
+
+--%>
\ No newline at end of file
diff --git a/WebContent/mgr/transformer/TransformFunc.jsp b/WebContent/mgr/transformer/TransformFunc.jsp
new file mode 100644
index 0000000..7c64062
--- /dev/null
+++ b/WebContent/mgr/transformer/TransformFunc.jsp
@@ -0,0 +1,186 @@
+
+<%@ page import="java.io.*, java.util.*"%>
+<%@ page language="java" contentType="text/html;charset=utf-8"%>
+<%@ page import="com.eactive.eai.transformer.function.FunctionDefinition"%>
+<%@ page import="com.eactive.eai.transformer.function.FunctionManager"%>
+<%@ page import="com.eactive.eai.transformer.function.ParameterDefinition"%>
+<%@ page import="com.eactive.eai.transformer.util.TransformKeys"%>
+<%@ page import="java.util.HashMap"%>
+
+<%!
+ /**
+ *
+ * 문자열을 받아서 널이면 제로스트링을, 아니면 트림된 문자열을 리턴.
+ *
+ * @param String 변환 대상
+ * @return 'String'
+ */
+ public static String checkNull( String str ) {
+
+ String sResult = new String();
+ if ( str == null ) {
+ sResult = "";
+ }
+ else {
+ sResult = str.trim();
+ }
+ return sResult;
+ }
+
+%>
+
+<%
+ FunctionManager manager = FunctionManager.getInstance();
+ String[] alls = manager.getAllFunctionNames();
+
+ String selectedKey = request.getParameter("selectedKey");
+ if(selectedKey == null && alls.length>=1) {
+ selectedKey = alls[0];
+ }
+
+ FunctionDefinition vo = null;
+ ArrayList paramList = new ArrayList();
+
+ if(selectedKey!=null) {
+ vo = manager.getFunctionDefinition(selectedKey);
+ paramList = (ArrayList) vo.getParameterList();
+ }
+
+%>
+
+
+
+
+
+ Transform 변환함수 Manager
+
+
+
+
+
+ 변환함수 관리
+
+
+
+
diff --git a/WebContent/mgr/transformer/layout.jsp b/WebContent/mgr/transformer/layout.jsp
new file mode 100644
index 0000000..7173606
--- /dev/null
+++ b/WebContent/mgr/transformer/layout.jsp
@@ -0,0 +1,218 @@
+<%--
+<%@ page import="java.io.*, java.util.*"%>
+<%@ page language="java" contentType="text/html;charset=utf-8"%>
+<%@ page import="com.eactive.eai.transformer.layout.Layout"%>
+<%@ page import="com.eactive.eai.transformer.layout.LayoutItem"%>
+<%@ page import="com.eactive.eai.transformer.layout.LayoutManager"%>
+<%@ page import="com.eactive.eai.transformer.util.TransformKeys"%>
+<%@ page import="java.util.HashMap"%>
+<%@ taglib uri="netui-tags-databinding.tld" prefix="netui-compat-data"%>
+<%@ taglib uri="netui-tags-html.tld" prefix="netui-compat"%>
+<%@ taglib uri="netui-tags-template.tld" prefix="netui-compat-template"%>
+
+<%!
+ /**
+ *
+ * 문자열을 받아서 널이면 제로스트링을, 아니면 트림된 문자열을 리턴.
+ *
+ * @param String 변환 대상
+ * @return 'String'
+ */
+ public static String checkNull( String str ) {
+
+ String sResult = new String();
+ if ( str == null ) {
+ sResult = "";
+ }
+ else {
+ sResult = str.trim();
+ }
+ return sResult;
+ }
+
+ StringBuffer html = new StringBuffer();
+
+ private void makeItem(LayoutItem item)
+ {
+ makeHTML(item);
+ try{
+ if (!item.isField() && (item.getChildCount() != 0)) {
+ for (int i = 0; i < item.getChildCount(); i++) {
+ if (item.getChild(i).getIndex() == -1) {
+ makeItem(item.getChild(i));
+ }
+ }
+ }
+ }catch (Exception e) {
+ e.printStackTrace();
+ System.out.println("LayoutEdit.jsp error");
+ }
+ }
+
+ private void makeHTML(LayoutItem item) {
+
+ if(item.isMessage())
+ html.append("");
+ else if (item.isGroup())
+ html.append("
");
+ else
+ html.append("
");
+
+ html.append(" | "+item.getSeqNo()+" | ");
+ html.append(" "+checkNull(item.getName())+" | ");
+ html.append(" "+TransformKeys.getLoutItemNodePtrnID(item.getNodeTypeId())+" | ");
+ html.append(" "+TransformKeys.getType(item.getItemTypeId())+" | ");
+ html.append(" "+checkNull(item.getRefInfo())+" | ");
+ html.append(" "+checkNull(item.getOccType())+" | ");
+ html.append(" "+item.getOccMax()+" | ");
+ html.append(" "+item.getOccMin()+" | ");
+ html.append("
");
+
+ }
+%>
+
+<%
+
+
+ LayoutManager manager = LayoutManager.getManager();
+ String key[] = manager.getAllLayoutName();
+
+ String selected = request.getParameter("layoutNames");
+ if(selected == null && key.length>=1) {
+ selected = key[0];
+ }
+
+ Layout layout = null;
+
+ if(selected!=null) {
+ layout = manager.getLayoutByName(selected);
+ }
+
+ LayoutItem root = null;
+ if (layout != null)
+ root = layout.getRootItem();
+
+ String str = "";
+ if (root != null ){
+ makeItem(root);
+ str = html.toString();
+ html = new StringBuffer();
+ }
+
+%>
+
+
+
+
+
+ Transform Layout Manager
+
+
+
+
+
+ Transform Layout Configuration
+
+
+
+
+--%>
\ No newline at end of file
diff --git a/WebContent/mgr/transformer/layoutItem.jsp b/WebContent/mgr/transformer/layoutItem.jsp
new file mode 100644
index 0000000..522e404
--- /dev/null
+++ b/WebContent/mgr/transformer/layoutItem.jsp
@@ -0,0 +1,211 @@
+<%--
+<%@ page import="java.io.*, java.util.*"%>
+<%@ page language="java" contentType="text/html;charset=utf-8"%>
+<%@ page import="com.eactive.eai.transformer.layout.Layout"%>
+<%@ page import="com.eactive.eai.transformer.layout.LayoutItem"%>
+<%@ page import="com.eactive.eai.transformer.layout.LayoutManager"%>
+<%@ page import="com.eactive.eai.transformer.util.TransformKeys"%>
+<%@ page import="java.util.HashMap"%>
+<%@ taglib uri="netui-tags-databinding.tld" prefix="netui-compat-data"%>
+<%@ taglib uri="netui-tags-html.tld" prefix="netui-compat"%>
+<%@ taglib uri="netui-tags-template.tld" prefix="netui-compat-template"%>
+
+<%!
+ /**
+ *
+ * 문자열을 받아서 널이면 제로스트링을, 아니면 트림된 문자열을 리턴.
+ *
+ * @param String 변환 대상
+ * @return 'String'
+ */
+ public static String checkNull( String str ) {
+
+ String sResult = new String();
+ if ( str == null ) {
+ sResult = "";
+ }
+ else {
+ sResult = str.trim();
+ }
+ return sResult;
+ }
+
+%>
+
+<%
+ String layoutName = (String)request.getParameter("layoutName");
+ String itemName = (String)request.getParameter("itemName");
+
+ LayoutManager manager = LayoutManager.getManager();
+ Layout layout = manager.getLayoutByName(layoutName);
+ LayoutItem item = null;
+
+ if(layout != null ) {
+ layout.updateItemNameMap();
+ item = layout.getItemByPath(itemName);
+ } else {
+ System.out.println("layoutItem.jsp ] layout is null - " +layoutName);
+ }
+ if(item == null ) {
+ System.out.println("layoutItem.jsp ] layoutItem is null - " +itemName);
+ }
+
+%>
+
+
+
+
+
+ Transform LayoutItem Manager
+
+
+
+
+
+ Transform LayoutItem Configuration
+
+
+
+
+--%>
\ No newline at end of file
diff --git a/WebContent/mgr/transformer/layoutType.jsp b/WebContent/mgr/transformer/layoutType.jsp
new file mode 100644
index 0000000..3f2ff4a
--- /dev/null
+++ b/WebContent/mgr/transformer/layoutType.jsp
@@ -0,0 +1,149 @@
+<%@ page import="java.io.*, java.util.*"%>
+<%@ page language="java" contentType="text/html;charset=utf-8"%>
+<%@ page import="com.eactive.eai.transformer.layout.LayoutType"%>
+<%@ page import="com.eactive.eai.transformer.layout.LayoutTypeManager"%>
+<%@ page import="com.eactive.eai.transformer.util.TransformKeys"%>
+<%@ page import="java.util.HashMap"%>
+
+<%!
+ /**
+ *
+ * 문자열을 받아서 널이면 제로스트링을, 아니면 트림된 문자열을 리턴.
+ *
+ * @param String 변환 대상
+ * @return 'String'
+ */
+ public static String checkNull( String str ) {
+
+ String sResult = new String();
+ if ( str == null ) {
+ sResult = "";
+ }
+ else {
+ sResult = str.trim();
+ }
+ return sResult;
+ }
+
+%>
+
+<%
+ LayoutTypeManager manager = LayoutTypeManager.getInstance();
+ String [] alls = manager.getAllLayoutTypeName();
+ String action = request.getParameter("actionType");
+
+ String selectedKey = request.getParameter("selectedKey");
+ if(selectedKey == null && alls.length>=1) {
+ selectedKey = alls[0];
+ }
+ if ("reload".equals(action)){
+ manager.reload(selectedKey);
+ }else if ("reloadAll".equals(action)){
+ manager.reload();
+ }else {
+ ;
+ }
+ LayoutType vo = null;
+
+ if(selectedKey!=null) {
+ vo = manager.getLayoutType(selectedKey);
+ }
+
+%>
+
+
+
+
+
+ Transform LayoutType Manager
+
+
+
+
+
+ 레이아웃 유형 관리
+
+
+
+
\ No newline at end of file
diff --git a/WebContent/mgr/useDateControl.jsp b/WebContent/mgr/useDateControl.jsp
new file mode 100644
index 0000000..ad077a0
--- /dev/null
+++ b/WebContent/mgr/useDateControl.jsp
@@ -0,0 +1,77 @@
+<%@ page import="java.io.*, java.util.*"%>
+<%@ page language="java" contentType="text/html;charset=utf-8"%>
+<%@ page import="com.eactive.eai.common.useDate.UseDateControlManager"%>
+<%@ page import="com.eactive.eai.common.useDate.UseDateControlVO"%>
+
+<%
+ UseDateControlManager manager = UseDateControlManager.getInstance();
+ String[] names = manager.getAllKeys();
+ String selectedName = request.getParameter("name");
+ if(selectedName == null && names.length>0) selectedName = names[0];
+ UseDateControlVO vo = manager.get(selectedName);
+%>
+
+
+
+ Use Date Control Manager
+
+
+
+
+
+ Use Date Control Configuration
+
+
+
+
\ No newline at end of file
diff --git a/WebContent/mgr/viewThreadList.jsp b/WebContent/mgr/viewThreadList.jsp
new file mode 100644
index 0000000..02acfac
--- /dev/null
+++ b/WebContent/mgr/viewThreadList.jsp
@@ -0,0 +1,115 @@
+<%@ page import="java.io.*, java.util.*"%>
+<%@ page language="java" contentType="text/html;charset=utf-8"%>
+
+<%!
+ private ThreadGroup getRootThreadGroup() {
+ ThreadGroup group = Thread.currentThread().getThreadGroup();
+ ThreadGroup parent = null;
+ while(true) {
+ parent = group.getParent();
+ if(parent==null) return group;
+
+ group = parent;
+ }
+ }
+
+ private ThreadGroup findThreadGroup(ThreadGroup parent, String path) {
+ if(path.startsWith("/")) {
+ path = path.substring(1);
+ }
+
+ return null;
+ }
+
+
+%>
+
+<%
+ String threadGroupName = request.getParameter("ThreadGroupName");
+
+ ThreadGroup root = getRootThreadGroup();
+ if(threadGroupName==null) threadGroupName=root.getName();
+
+ ThreadGroup[] children = new ThreadGroup[root.activeGroupCount()];
+ root.enumerate(children, true);
+
+ ThreadGroup current = null;
+ for(int i=0;i
+
+
+
+ CurrentThread List
+
+
+
+
+ 쓰레드 전체 리스트 보기
+
+
+
+ |
+ Thread Group List (총: <%=children.length%>개)
+
+ |
+
+
+
+
+ Thread List [<%=current.getName()%>] (총: <%=threads.length%>개)
+
+
+ | No | Thread Name |
+ isAlive |
+ isDaemon |
+ isInterrupted |
+ activeCount |
+
+<%
+ for(int i=0;i
+
+ | <%=(i+1)%> |
+ <%=threads[i].getName()%> |
+ <%=threads[i].isAlive()%> |
+ <%=threads[i].isDaemon()%> |
+ <%=threads[i].isInterrupted()%> |
+ <%=threads[i].activeCount()%> |
+
+<%
+ }
+%>
+
+ |
+
+
+
+
\ No newline at end of file
diff --git a/src/main/java/com/eactive/eai/adapter/service/ApiAdapterService.java b/src/main/java/com/eactive/eai/adapter/service/ApiAdapterService.java
index 5a0df56..033083d 100644
--- a/src/main/java/com/eactive/eai/adapter/service/ApiAdapterService.java
+++ b/src/main/java/com/eactive/eai/adapter/service/ApiAdapterService.java
@@ -309,14 +309,14 @@ public class ApiAdapterService extends HttpAdapterServiceSupport {
result = mapper.writeValueAsString(rootNode);
}
}
-
+
// KJBank는 요청에 대한 응답 (Sync응답, Aync 에 대한 Ack응답) 에 대하여 암호화 하지 않는다. 무조건 안한다. 따라서 아래부분은 구현은 했지만 사용하지 않는다.
//result = doPostEncryption(result, transactionProp); // jwhong Encrypt
return result;
}
- // jwhong decrypt
+ // jwhong decrypt
private String doPreDecryption(String eaiBody, Properties transactionProp, HttpServletRequest request) {
String decryptAlgorithm = transactionProp.getProperty(ENCRYPT_ALGORITHM, "");
diff --git a/src/main/java/com/eactive/eai/authserver/config/AuthorizationServerConfig.java b/src/main/java/com/eactive/eai/authserver/config/AuthorizationServerConfig.java
index 33be4d0..9bd1734 100644
--- a/src/main/java/com/eactive/eai/authserver/config/AuthorizationServerConfig.java
+++ b/src/main/java/com/eactive/eai/authserver/config/AuthorizationServerConfig.java
@@ -104,7 +104,7 @@ public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdap
tokenType = vo.getProperty(PROP_TOKEN_TYPE, "JWT");
}
- endpoints.pathMapping("/oauth/token", "/api/v1/oauth/token");
+// endpoints.pathMapping("/oauth/token", "/auth/oauth/v2/token") ;
endpoints.authenticationManager(authenticationManager);
endpoints.userDetailsService(userDetailsService);
diff --git a/src/main/java/com/eactive/eai/authserver/config/OAuthRequestLoggingFilter.java b/src/main/java/com/eactive/eai/authserver/config/OAuthRequestLoggingFilter.java
index 59278ca..efb0f18 100644
--- a/src/main/java/com/eactive/eai/authserver/config/OAuthRequestLoggingFilter.java
+++ b/src/main/java/com/eactive/eai/authserver/config/OAuthRequestLoggingFilter.java
@@ -1,5 +1,6 @@
package com.eactive.eai.authserver.config;
+import org.apache.commons.lang3.StringUtils;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
@@ -12,7 +13,7 @@ import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collection;
-public class OAuthRequestLoggingFilter extends OncePerRequestFilter {
+public class OAuthRequestLoggingFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
@@ -24,9 +25,10 @@ public class OAuthRequestLoggingFilter extends OncePerRequestFilter {
data.setGrantType(request.getParameter("grant_type"));
data.setScope(request.getParameter("scope"));
data.setUsername(request.getParameter("username"));
+ data.setResource(request.getParameter("resource"));
RequestContextData.ThreadLocalRequestContext.set(data);
-
+
filterChain.doFilter(request, response);
} finally {
RequestContextData.ThreadLocalRequestContext.clear();
diff --git a/src/main/java/com/eactive/eai/authserver/config/RequestContextData.java b/src/main/java/com/eactive/eai/authserver/config/RequestContextData.java
index 19815de..20a261a 100644
--- a/src/main/java/com/eactive/eai/authserver/config/RequestContextData.java
+++ b/src/main/java/com/eactive/eai/authserver/config/RequestContextData.java
@@ -3,12 +3,13 @@ package com.eactive.eai.authserver.config;
import lombok.Data;
@Data
-public class RequestContextData {
+public class RequestContextData {
private String clientId;
private String ipAddress;
private String grantType;
private String scope;
private String username;
+ private String resource;
// Getters and setters
diff --git a/src/main/java/com/eactive/eai/authserver/custom/BearerTokenContoller.java b/src/main/java/com/eactive/eai/authserver/custom/BearerTokenContoller.java
new file mode 100644
index 0000000..bda62ca
--- /dev/null
+++ b/src/main/java/com/eactive/eai/authserver/custom/BearerTokenContoller.java
@@ -0,0 +1,125 @@
+package com.eactive.eai.authserver.custom;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.commons.lang3.StringUtils;
+import org.json.simple.JSONObject;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.oauth2.provider.ClientDetails;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.RestController;
+
+import com.eactive.eai.adapter.http.dynamic.filter.JwtAuthException;
+import com.eactive.eai.authserver.service.OAuth2Manager;
+import com.eactive.eai.common.util.Logger;
+
+@RestController
+@RequestMapping("/auth/oauth/v2")
+public class BearerTokenContoller {
+
+ public static Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT);
+ private final BearerTokenService tokenService;
+
+ public BearerTokenContoller(BearerTokenService tokenService) {
+ this.tokenService = tokenService;
+ }
+
+ // 기본 CA Bearer Token 발급
+ @PostMapping("/token")
+ public ResponseEntity> issueCAToken(@RequestParam MultiValueMap req) throws JwtAuthException {
+ JSONObject resObject = new JSONObject();
+
+ try {
+ String clientId = req.getFirst("client_id"); // .get("client_id");
+ String grantType = req.getFirst("grant_type");
+ if (!StringUtils.equals(grantType, "client_credentials") ) {
+ throw new JwtAuthException("unsupported_grant_type", "Unsupported grant type");
+ }
+
+ ClientDetails clientDetails = OAuth2Manager.getInstance().getClientDeatilsStore().get(clientId);
+ if (clientDetails == null) {
+ throw new JwtAuthException("invalid_client", "Bad client credentials(not found)");
+ }
+
+ String tokenType = "Bearer";
+ String clientSecret = req.getFirst("client_secret");
+ String scopes = req.getFirst("scope");
+ Long expiresIn = Long.valueOf(clientDetails.getAccessTokenValiditySeconds());
+
+ String[] reqScopeArr = org.springframework.util.StringUtils.tokenizeToStringArray(scopes, " ");
+ Set scopeSet = new HashSet();
+ for (String scope : reqScopeArr) {
+ scopeSet.add(scope);
+ }
+ veryfyClient(clientDetails, clientSecret, scopeSet);
+
+ String token = tokenService.generateCAToken(clientId, expiresIn, scopeSet);
+
+ Map tokenMap = new HashMap<>();
+ tokenMap.put("token_type", tokenType);
+ tokenMap.put("access_token", token);
+ tokenMap.put("expires_in", expiresIn);
+ tokenMap.put("scope", scopes);
+ // ... and so on for all key-value pairs
+ resObject.putAll(tokenMap);
+
+ return ResponseEntity.ok(resObject);
+ } catch (JwtAuthException e) {
+ logger.error(e);
+
+ JSONObject errorJson = new JSONObject();
+ errorJson.put("error", e.getCode());
+ errorJson.put("error_description", e.getMessage());
+ resObject.putAll(errorJson);
+ return ResponseEntity.status(401).body(resObject);
+ } catch (Exception e) {
+ logger.error(e);
+
+ JSONObject errorJson = new JSONObject();
+ errorJson.put("error", "invalid_request");
+ errorJson.put("error_description", e.getMessage());
+ resObject.putAll(errorJson);
+ return ResponseEntity.status(500).body(resObject);
+ }
+ }
+
+// // 서비스용 Bearer Token 발급
+// @PostMapping("/service")
+// public ResponseEntity> issueFileToken(@RequestBody Map req) {
+// String fileId = req.get("fileid");
+//
+// String token = tokenService.generateFileToken(fileId);
+//
+// Map tokenMap = new HashMap<>();
+// tokenMap.put("token_type", "Bearer");
+// tokenMap.put("file_token", token);
+// tokenMap.put("expires_in", "599");
+// tokenMap.put("expires_on", "1575593514");
+// tokenMap.put("resource", fileId);
+//
+// return ResponseEntity.ok(tokenMap);
+// }
+ private void veryfyClient(ClientDetails clientDetails, String clientSecret, Set scopeSet) throws JwtAuthException {
+ if (!StringUtils.equals(clientDetails.getClientSecret(), clientSecret)) {
+ throw new JwtAuthException("invalid_client", "Bad client credentials(client_secret is empty or not match)");
+ }
+
+ if (scopeSet.isEmpty()) {
+ throw new JwtAuthException("invalid_client", "Bad client credentials(Scope is empty)");
+ }
+
+ if (!clientDetails.getScope().containsAll(scopeSet)) {
+ throw new JwtAuthException("invalid_client", "Bad client credentials(Include unacceptable scope)");
+ }
+ }
+}
+
diff --git a/src/main/java/com/eactive/eai/authserver/custom/BearerTokenFilter.java b/src/main/java/com/eactive/eai/authserver/custom/BearerTokenFilter.java
new file mode 100644
index 0000000..d993d30
--- /dev/null
+++ b/src/main/java/com/eactive/eai/authserver/custom/BearerTokenFilter.java
@@ -0,0 +1,85 @@
+package com.eactive.eai.authserver.custom;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.json.simple.JSONObject;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import com.eactive.eai.adapter.http.dynamic.filter.JwtAuthException;
+import com.eactive.eai.common.util.Logger;
+
+@Component
+public class BearerTokenFilter extends OncePerRequestFilter{
+
+ public static Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT);
+ private final BearerTokenService tokenService;
+
+ public static final String ERROR_AUTHENTICATION_FAIL = "E.AUTHENTICATION_FAIL";
+ public static final String ERROR_AUTHORIZATION_FAIL = "E.AUTHORIZATION_FAIL";
+ public static final String ERROR_TOKEN_EXPIRED = "E.TOKEN_EXPIRED";
+
+ public BearerTokenFilter(BearerTokenService tokenService) {
+ this.tokenService = tokenService;
+ }
+
+ @Override
+ protected void doFilterInternal(HttpServletRequest request,
+ HttpServletResponse response,
+ FilterChain filterChain) throws IOException, ServletException {
+ JSONObject resObject = new JSONObject();
+
+ try {
+ String auth = request.getHeader("Authorization");
+
+ if (auth != null && auth.startsWith("Bearer ")) {
+ String token = auth.substring(7);
+
+ String clientId = null;
+ String fileId = null;
+
+ if (tokenService.validateCA(token)) {
+ clientId = tokenService.getClientIdFromCA(token);
+ } else {
+ throw new JwtAuthException(ERROR_AUTHENTICATION_FAIL, "Access token is invalid or expired");
+ }
+
+// if (fileId != null) {
+// UsernamePasswordAuthenticationToken authentication =
+// new UsernamePasswordAuthenticationToken(fileId, null, Arrays.asList());
+// SecurityContextHolder.getContext().setAuthentication(authentication);
+// }
+ }
+
+ filterChain.doFilter(request, response);
+ } catch (JwtAuthException jae) {
+ logger.debug(jae.getMessage());
+
+ JSONObject errorJson = new JSONObject();
+ errorJson.put("error", jae.getCode());
+ errorJson.put("error_description", jae.getMessage());
+ resObject.putAll(errorJson);
+ response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401 설정
+ response.setContentType("application/json;charset=UTF-8");
+ response.getWriter().write(resObject.toJSONString());
+ } catch (Exception e) {
+ logger.error(e.getMessage());
+ JSONObject errorJson = new JSONObject();
+ errorJson.put("error", "invalid_request");
+ errorJson.put("error_description", e.getMessage());
+ resObject.putAll(errorJson);
+ response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); // 500 설정
+ response.setContentType("application/json;charset=UTF-8");
+ response.getWriter().write(resObject.toJSONString());
+ }
+ }
+}
+
diff --git a/src/main/java/com/eactive/eai/authserver/custom/BearerTokenInfo.java b/src/main/java/com/eactive/eai/authserver/custom/BearerTokenInfo.java
new file mode 100644
index 0000000..e5a37fb
--- /dev/null
+++ b/src/main/java/com/eactive/eai/authserver/custom/BearerTokenInfo.java
@@ -0,0 +1,49 @@
+package com.eactive.eai.authserver.custom;
+
+import java.util.Set;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class BearerTokenInfo {
+ @JsonProperty("client_id")
+ private String clientId;
+ private Long expiresIn;
+ private Long expiresAt;
+ private Set scopeSet;
+
+ public String getClientId() {
+ return clientId;
+ }
+
+ public void setClientId(String clientId) {
+ this.clientId = clientId;
+ }
+
+ public Long getExpiresIn() {
+ return expiresIn;
+ }
+
+ public void setExpiresIn(Long expiresIn) {
+ this.expiresIn = expiresIn;
+ }
+
+ public Long getExpiresAt() {
+ return expiresAt;
+ }
+
+ public void setExpiresAt(Long expiresIn) {
+ this.expiresAt = System.currentTimeMillis() + (expiresIn * 1000);
+ }
+
+ public Set getScopeSet() {
+ return scopeSet;
+ }
+
+ public void setScopeSet(Set scopeSet) {
+ this.scopeSet = scopeSet;
+ }
+
+ public boolean isExpired() {
+ return System.currentTimeMillis() > expiresAt;
+ }
+}
diff --git a/src/main/java/com/eactive/eai/authserver/custom/BearerTokenService.java b/src/main/java/com/eactive/eai/authserver/custom/BearerTokenService.java
new file mode 100644
index 0000000..3b01368
--- /dev/null
+++ b/src/main/java/com/eactive/eai/authserver/custom/BearerTokenService.java
@@ -0,0 +1,97 @@
+package com.eactive.eai.authserver.custom;
+
+import java.security.SecureRandom;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+import org.springframework.stereotype.Service;
+
+@Service
+public class BearerTokenService {
+
+ // CA 토큰 저장소
+ private final Map CATokenStore = new ConcurrentHashMap<>();
+
+ // File 토큰 저장소
+ private final Map fileTokenStore = new ConcurrentHashMap<>();
+
+// // FileTransfer 토큰 저장소
+// private final Map fileTransferTokenStore = new ConcurrentHashMap<>();
+
+ public String generateCAToken(String clientId, Long expiresIn, Set scopeSet) {
+ String token = UUID.randomUUID().toString();
+
+ BearerTokenInfo CATokenInfo = new BearerTokenInfo();
+ CATokenInfo.setClientId(clientId);
+ CATokenInfo.setExpiresIn(expiresIn);
+ CATokenInfo.setExpiresAt(expiresIn);
+ CATokenInfo.setScopeSet(scopeSet);
+
+ CATokenStore.put(token, CATokenInfo);
+
+ return token;
+ }
+
+ public String generateFileToken(String fileId) {
+ //String token = UUID.randomUUID().toString().replace("-", "");
+ String token = generateTokenString(987);
+ fileTokenStore.put(token, fileId);
+ return token;
+ }
+
+// public String generateFileTransferToken(String fileId) {
+// //String token = UUID.randomUUID().toString().replace("-", "");
+// String token = generateTokenString(654);
+// fileTransferTokenStore.put(token, fileId);
+// return token;
+// }
+
+ public boolean validateCA(String token) {
+ BearerTokenInfo CATokenInfo = CATokenStore.get(token);
+ if (CATokenInfo == null) return false;
+
+ if (CATokenInfo.isExpired()) {
+ CATokenStore.remove(token);
+ return false;
+ }
+ return CATokenStore.containsKey(token);
+ }
+
+ public boolean validateFile(String token) {
+ return fileTokenStore.containsKey(token);
+ }
+
+// public boolean validateFileTransfer(String token) {
+// return fileTransferTokenStore.containsKey(token);
+// }
+
+ public String getClientIdFromCA(String token) {
+ BearerTokenInfo CATokenInfo = CATokenStore.get(token);
+ return CATokenInfo.getClientId();
+ }
+
+ public String getFileIdFromFile(String token) {
+ return fileTokenStore.get(token);
+ }
+
+// public String getFileIdFromFileTransfer(String token) {
+// return fileTransferTokenStore.get(token);
+// }
+
+ private static final String CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+ private static final SecureRandom secureRandom = new SecureRandom();
+
+ public static String generateTokenString(int length) {
+ // Java 8의 IntStream과 Collectors.joining을 사용한 효율적인 방법
+ return IntStream.range(0, length)
+ .map(i -> secureRandom.nextInt(CHARACTERS.length()))
+ .mapToObj(CHARACTERS::charAt)
+ .map(String::valueOf)
+ .collect(Collectors.joining());
+ }
+}
+
diff --git a/src/main/java/com/eactive/eai/authserver/custom/KjbMGOAuth2AccessTokenRequest.java b/src/main/java/com/eactive/eai/authserver/custom/KjbMGOAuth2AccessTokenRequest.java
new file mode 100644
index 0000000..dcd880b
--- /dev/null
+++ b/src/main/java/com/eactive/eai/authserver/custom/KjbMGOAuth2AccessTokenRequest.java
@@ -0,0 +1,63 @@
+package com.eactive.eai.authserver.custom;
+
+import java.io.Serializable;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+@SuppressWarnings("serial")
+public class KjbMGOAuth2AccessTokenRequest implements Serializable {
+ @JsonProperty("grant_type")
+ private String grantType;
+ @JsonProperty("client_id")
+ private String clientId;
+ @JsonProperty("client_secret")
+ private String clientSecret;
+ private String resource;
+ private String scope;
+
+ public String getGrantType() {
+ return grantType;
+ }
+
+ public void setGrantType(String grantType) {
+ this.grantType = grantType;
+ }
+
+ public String getClientId() {
+ return clientId;
+ }
+
+ public void setClientId(String clientId) {
+ this.clientId = clientId;
+ }
+
+ public String getClientSecret() {
+ return clientSecret;
+ }
+
+ public void setClientSecret(String clientSecret) {
+ this.clientSecret = clientSecret;
+ }
+
+ public String getResource() {
+ return resource;
+ }
+
+ public void setResource(String resource) {
+ this.resource = resource;
+ }
+
+ public String getScope() {
+ return scope;
+ }
+
+ public void setScope(String scope) {
+ this.scope = scope;
+ }
+
+ @Override
+ public String toString() {
+ return "KjbMGOAuth2AccessTokenRequest [grantType=" + grantType + ", clientId=" + clientId + ", clientSecret="
+ + clientSecret + ", resource=" + resource + ", scope=" + scope + "]";
+ }
+}
diff --git a/src/main/java/com/eactive/eai/authserver/custom/KjbMGOAuth2AccessTokenResponse.java b/src/main/java/com/eactive/eai/authserver/custom/KjbMGOAuth2AccessTokenResponse.java
new file mode 100644
index 0000000..b99ffe1
--- /dev/null
+++ b/src/main/java/com/eactive/eai/authserver/custom/KjbMGOAuth2AccessTokenResponse.java
@@ -0,0 +1,105 @@
+package com.eactive.eai.authserver.custom;
+
+import java.io.Serializable;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+
+@SuppressWarnings("serial")
+@JsonInclude(Include.NON_NULL)
+public class KjbMGOAuth2AccessTokenResponse implements Serializable {
+ private String responseCode;
+ private String responseMessage;
+ @JsonProperty("access_token")
+ private String accessToken;
+ @JsonProperty("refresh_token")
+ private String refreshToken;
+ @JsonProperty("token_type")
+ private String tokenType;
+ @JsonProperty("expires_in")
+ private Long expiresIn;
+ @JsonProperty("expires_on")
+ private Long expiresOn;
+ private String resource;
+ private String scope;
+
+ public String getResponseCode() {
+ return responseCode;
+ }
+
+ public void setResponseCode(String responseCode) {
+ this.responseCode = responseCode;
+ }
+
+ public String getResponseMessage() {
+ return responseMessage;
+ }
+
+ public void setResponseMessage(String responseMessage) {
+ this.responseMessage = responseMessage;
+ }
+
+ public String getAccessToken() {
+ return accessToken;
+ }
+
+ public void setAccessToken(String accessToken) {
+ this.accessToken = accessToken;
+ }
+
+ public String getrefreshToken() {
+ return refreshToken;
+ }
+
+ public void setrefreshToken(String refreshToken) {
+ this.refreshToken = refreshToken;
+ }
+
+ public String getTokenType() {
+ return tokenType;
+ }
+
+ public void setTokenType(String tokenType) {
+ this.tokenType = tokenType;
+ }
+
+ public Long getExpiresIn() {
+ return expiresIn;
+ }
+
+ public void setExpiresIn(Long expiresIn) {
+ this.expiresIn = expiresIn;
+ }
+
+ public Long getExpiresOn() {
+ return expiresOn;
+ }
+
+ public void setExpiresOn(Long expiresOn) {
+ this.expiresOn = expiresOn;
+ }
+
+ public String getResource() {
+ return resource;
+ }
+
+ public void setResource(String resource) {
+ this.resource = resource;
+ }
+
+ public String getScope() {
+ return scope;
+ }
+
+ public void setScope(String scope) {
+ this.scope = scope;
+ }
+
+ @Override
+ public String toString() {
+ return "KjbMGOAuth2AccessTokenResponse [responseCode=" + responseCode + ", responseMessage=" + responseMessage
+ + ", accessToken=" + accessToken + ", tokenType=" + tokenType + ", expiresIn=" + expiresIn
+ + ", refreshToken=" + refreshToken+ ", expiresOn=" + expiresOn+ ", resource=" + resource+ ", scope=" + scope + "]";
+ }
+}
diff --git a/src/main/java/com/eactive/eai/authserver/custom/KjbMGOAuth2Controller.java b/src/main/java/com/eactive/eai/authserver/custom/KjbMGOAuth2Controller.java
new file mode 100644
index 0000000..f08be14
--- /dev/null
+++ b/src/main/java/com/eactive/eai/authserver/custom/KjbMGOAuth2Controller.java
@@ -0,0 +1,288 @@
+package com.eactive.eai.authserver.custom;
+
+import java.nio.charset.StandardCharsets;
+import java.security.KeyFactory;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.Principal;
+import java.security.PublicKey;
+import java.security.SecureRandom;
+import java.security.Signature;
+import java.security.spec.X509EncodedKeySpec;
+import java.time.LocalDateTime;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.crypto.Cipher;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.math.NumberUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.oauth2.common.OAuth2AccessToken;
+import org.springframework.security.oauth2.common.util.OAuth2Utils;
+import org.springframework.security.oauth2.provider.ClientDetails;
+import org.springframework.security.oauth2.provider.OAuth2Authentication;
+import org.springframework.security.oauth2.provider.OAuth2Request;
+import org.springframework.security.oauth2.provider.endpoint.TokenEndpoint;
+import org.springframework.security.oauth2.provider.token.TokenEnhancer;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+import com.eactive.eai.adapter.http.dynamic.filter.JwtAuthException;
+import com.eactive.eai.authserver.config.IssueLimitOAuth2Exception;
+import com.eactive.eai.authserver.config.AuthorizationServerConfig;
+import com.eactive.eai.authserver.config.RequestContextData;
+import com.eactive.eai.authserver.dao.TokenIssuanceLogDAO;
+import com.eactive.eai.authserver.service.OAuth2Manager;
+import com.eactive.eai.authserver.util.BeanUtils;
+import com.eactive.eai.authserver.vo.ClientVO;
+import com.eactive.eai.common.dao.DAOException;
+import com.eactive.eai.common.logger.EAIDBLogControl;
+import com.eactive.eai.common.util.Logger;
+import com.eactive.eai.common.util.UUID;
+import com.eactive.eai.data.entity.onl.authserver.TokenIssuanceLog;
+
+@Controller
+public class KjbMGOAuth2Controller {
+ public static Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT);
+
+ private static final String HEADER_TRACEID = "traceId";
+
+ private static final String SERVICE_CODE_ACCESS_TOKEN = "00"; // 임시설정
+ @Autowired
+ private TokenIssuanceLogDAO tokenIssuanceLogDAO;
+
+ @RequestMapping(value = "/mapi/oauth2/token2", method = RequestMethod.POST, produces = "application/json; charset=\"UTF-8\"")
+ @ResponseBody
+ public KjbMGOAuth2AccessTokenResponse token(@RequestBody KjbMGOAuth2AccessTokenRequest tokenRequest,
+ HttpServletRequest request, HttpServletResponse response) {
+ if (logger.isDebug()) {
+ logger.debug(tokenRequest.toString());
+ }
+
+ KjbMGOAuth2AccessTokenResponse responseToken = new KjbMGOAuth2AccessTokenResponse();
+ try {
+ if (!StringUtils.equals(tokenRequest.getGrantType(), "client_credentials")) {
+ throw new JwtAuthException(
+ String.format("%d%s%s", HttpStatus.BAD_REQUEST.value(), SERVICE_CODE_ACCESS_TOKEN, "02"),
+ "Invalid Mandatory Field {grantType}");
+ }
+
+ String grantType = tokenRequest.getGrantType();
+ String clientId = tokenRequest.getClientId();
+ String clientSecret = tokenRequest.getClientSecret();
+
+ String[] scopeArr = org.springframework.util.StringUtils.tokenizeToStringArray(tokenRequest.getScope(), " ");
+ Set scopeSet = new HashSet();
+ for (String scope : scopeArr) {
+ scopeSet.add(scope);
+ }
+
+ String[] resourceArr = org.springframework.util.StringUtils.tokenizeToStringArray(tokenRequest.getResource(), ",");
+ Set resourceSet = new HashSet();
+ for (String resource : resourceArr) {
+ resourceSet.add(resource);
+ }
+
+ ClientDetails clientDetails = OAuth2Manager.getInstance().getClientDeatilsStore().get(clientId);
+ verifyClient(clientDetails, clientId, scopeSet);
+
+ String traceId = UUID.randomUUID().toString().replace("-", "");
+ response.setHeader(HEADER_TRACEID, traceId);
+
+ HashMap authorizationParameters = new HashMap();
+ authorizationParameters.put(OAuth2Utils.GRANT_TYPE, grantType);
+ authorizationParameters.put(OAuth2Utils.CLIENT_ID, clientId);
+ authorizationParameters.put("client_secret", clientSecret);
+
+ Set responseType = new HashSet();
+ responseType.add(tokenRequest.getGrantType());
+
+ OAuth2Request authorizationRequest = new OAuth2Request(authorizationParameters, clientId, null, true, scopeSet,
+ resourceSet, "", responseType, null);
+
+ Principal principal = new OAuth2Authentication(authorizationRequest, null);
+ ResponseEntity result = tokenEndpoint().postAccessToken(principal,
+ authorizationParameters);
+ OAuth2AccessToken token = result.getBody();
+ if(StringUtils.isNotBlank(token.getTokenType()) && StringUtils.equalsIgnoreCase(token.getTokenType(), "bearer")) {
+ responseToken.setTokenType("Bearer");
+ } else { responseToken.setTokenType(token.getTokenType()); }
+ responseToken.setAccessToken(token.getValue());
+ responseToken.setExpiresIn(Long.valueOf(token.getExpiresIn()));
+ responseToken.setrefreshToken("");
+ responseToken.setExpiresOn(System.currentTimeMillis() + (token.getExpiresIn() * 1000));
+ responseToken.setScope(tokenRequest.getScope());
+ responseToken.setResource(tokenRequest.getResource());
+ } catch (JwtAuthException e) {
+ response.setStatus(NumberUtils.toInt(StringUtils.left(e.getCode(), 3), HttpStatus.UNAUTHORIZED.value()));
+ responseToken.setResponseCode(e.getCode());
+ responseToken.setResponseMessage(e.getMessage());
+ } catch (Exception e) {
+ logger.error(e.getMessage());
+ response.setStatus(HttpStatus.UNAUTHORIZED.value());
+ responseToken.setResponseCode(
+ String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_ACCESS_TOKEN, "00"));
+ responseToken.setResponseMessage("Unauthorized. [Unknown]");
+ }
+
+ return responseToken;
+ }
+
+ private void verifyClient(ClientDetails clientDetails, String clientId, Set scopeSet)
+ throws JwtAuthException {
+
+ if (StringUtils.isBlank(clientId)) {
+ throw new JwtAuthException(
+ String.format("%d%s%s", HttpStatus.BAD_REQUEST.value(), SERVICE_CODE_ACCESS_TOKEN, "02"),
+ "Invalid Mandatory Field {client_id}");
+ }
+
+
+ if (clientDetails == null) {
+ throw new JwtAuthException(
+ String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_ACCESS_TOKEN, "00"),
+ "Unauthorized. [client not found]");
+ }
+
+ if (scopeSet.isEmpty()) {
+ throw new JwtAuthException(
+ String.format("%d%s%s", HttpStatus.BAD_REQUEST.value(), SERVICE_CODE_ACCESS_TOKEN, "02"),
+ "Invalid Mandatory Field {scope}");
+ }
+
+ if (!clientDetails.getScope().containsAll(scopeSet)) {
+ throw new JwtAuthException(
+ String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_ACCESS_TOKEN, "00"),
+ "Unauthorized. [Bad client credentials(Include unacceptable scope)]");
+ }
+
+ }
+
+ private TokenEndpoint tokenEndpoint() {
+ return BeanUtils.getBean("tokenEndpoint", TokenEndpoint.class);
+ }
+
+ private class TokenIssuanceLogEnhancer implements TokenEnhancer {
+ @Override
+ public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
+ if (!EAIDBLogControl.isEnable()) {
+ logger.warn("DB logging is disabled. Skipping token issuance logging.");
+ return accessToken;
+ }
+
+ String clientId = authentication.getOAuth2Request().getClientId();
+ LocalDateTime startDateTime = LocalDateTime.now().minusHours(24);
+
+ int recentTokenCount = tokenIssuanceLogDAO.findRecentLogsForRestrictionCount(clientId, startDateTime);
+
+ try {
+ ClientVO clientVO = OAuth2Manager.getInstance().getClientInfo(clientId);
+ int dailyTokenLimit = clientVO.getDailyTokenLimit();
+ if (dailyTokenLimit > 0 && recentTokenCount >= dailyTokenLimit) {
+ throw new IssueLimitOAuth2Exception("Token issuance limit exceeded for this client");
+ }
+
+ // JWT 변환 이후의 최종 토큰 값 로깅
+ logTokenIssuance(true, false, "Token issued successfully", accessToken.getValue());
+ } catch (DAOException e) {
+ logger.warn("cannot find clientVo - " + clientId);
+ }
+ return accessToken;
+ }
+ }
+
+ private void logTokenIssuance(boolean isSuccess, boolean isRestrictionExempt, String resultMessage, String accessToken) {
+ try {
+ if (!EAIDBLogControl.isEnable()) {
+ logger.warn("DB logging is disabled. Skipping token issuance logging.");
+ return;
+ }
+ RequestContextData data = RequestContextData.ThreadLocalRequestContext.get();
+ OAuth2Manager.getInstance();
+
+ TokenIssuanceLog log = new TokenIssuanceLog();
+ String clientId = data.getClientId();
+ log.setClientId(clientId);
+
+ if(StringUtils.isNotBlank(clientId)) {
+ ClientVO clientVO = OAuth2Manager.getInstance().getClientInfo(clientId);
+ log.setAppName(clientVO.getClientName());
+ log.setOrgId(clientVO.getOrgId());
+ log.setOrgName(clientVO.getOrgName());
+ }
+
+ log.setGrantType(data.getGrantType());
+ log.setScope(data.getScope());
+ log.setIpAddress(data.getIpAddress());
+ log.setSuccessYn(isSuccess ? "Y" : "N");
+ log.setIssuanceDateTime(LocalDateTime.now());
+ log.setRestrictionExemptYn(isRestrictionExempt ? "Y" : "N");
+ log.setResultMessage(resultMessage);
+ log.setAccessToken(accessToken); // Add access token value to the log
+
+ tokenIssuanceLogDAO.saveTokenIssuanceLog(log);
+ } catch (Throwable th) {
+ logger.error("Error while logging token issuance: " + th.getMessage(), th);
+ }
+ }
+
+ public static void main(String[] args) throws Exception {
+ // First generate a public/private key pair
+ KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
+ // generator.initialize(512, new SecureRandom());
+ //generator.initialize(1024, new SecureRandom());
+ generator.initialize(2048, new SecureRandom());
+ KeyPair pair = generator.generateKeyPair();
+
+ // The private key can be used to sign (not encrypt!) a message. The public key
+ // holder can then verify the message.
+
+ String message = "sSGJDwlJsel5WeXodzd3J3MQ20KYCZpL|2022-12-21T14:36:19+07:00";
+
+ // Let's sign our message
+ Signature privateSignature = Signature.getInstance("SHA256withRSA");
+ privateSignature.initSign(pair.getPrivate());
+ privateSignature.update(message.getBytes(StandardCharsets.UTF_8));
+ System.out.println(
+ "private key=" + Base64.getEncoder().encodeToString(pair.getPrivate().getEncoded()));
+
+ byte[] signature = privateSignature.sign();
+ //System.out.println("signature=" + new String(signature, StandardCharsets.UTF_8));
+ System.out.println("signature=" + Base64.getEncoder().encodeToString(signature));
+
+ // Let's check the signature
+ Signature publicSignature = Signature.getInstance("SHA256withRSA");
+ publicSignature.initVerify(pair.getPublic());
+ publicSignature.update(message.getBytes(StandardCharsets.UTF_8));
+ boolean isCorrect = publicSignature.verify(signature);
+ System.out.println("public key=" + Base64.getEncoder().encodeToString(pair.getPublic().getEncoded()));
+
+ System.out.println("Signature correct: " + isCorrect);
+
+ // The public key can be used to encrypt a message, the private key can be used
+ // to decrypt it.
+ // Encrypt the message
+ Cipher encryptCipher = Cipher.getInstance("RSA");
+ encryptCipher.init(Cipher.ENCRYPT_MODE, pair.getPublic());
+
+ byte[] cipherText = encryptCipher.doFinal(message.getBytes(StandardCharsets.UTF_8));
+
+ // Now decrypt it
+ Cipher decriptCipher = Cipher.getInstance("RSA");
+ decriptCipher.init(Cipher.DECRYPT_MODE, pair.getPrivate());
+
+ String decipheredMessage = new String(decriptCipher.doFinal(cipherText), StandardCharsets.UTF_8);
+
+ System.out.println(decipheredMessage);
+ }
+}
diff --git a/src/main/java/com/eactive/eai/authserver/custom/SecurityConfig.java b/src/main/java/com/eactive/eai/authserver/custom/SecurityConfig.java
new file mode 100644
index 0000000..f2a6183
--- /dev/null
+++ b/src/main/java/com/eactive/eai/authserver/custom/SecurityConfig.java
@@ -0,0 +1,58 @@
+package com.eactive.eai.authserver.custom;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+//
+//@Configuration
+//@EnableWebSecurity
+//public class SecurityConfig {
+//
+// private final BearerTokenFilter tokenFilter;
+//
+// public SecurityConfig(BearerTokenFilter tokenFilter) {
+// this.tokenFilter = tokenFilter;
+// }
+//
+// @Bean
+// public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+//
+// http
+// .csrf().disable()
+// .authorizeRequests()
+// .antMatchers("/auth/token/**").permitAll()
+// .anyRequest().authenticated()
+// .and()
+// .addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class);
+//
+// return http.build();
+// }
+//}
+@Configuration
+@EnableWebSecurity
+public class SecurityConfig extends WebSecurityConfigurerAdapter {
+
+ private final BearerTokenFilter tokenFilter;
+
+ @Autowired
+ public SecurityConfig(BearerTokenFilter tokenFilter) {
+ this.tokenFilter = tokenFilter;
+ }
+
+ @Override
+ protected void configure(HttpSecurity http) throws Exception {
+
+ http
+ .csrf().disable()
+ .authorizeRequests()
+ .antMatchers("/auth/token/**").permitAll()
+ .anyRequest().authenticated()
+ .and()
+ .addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/eactive/eai/custom/adapter/app/impl/JwtTokenCreator.java b/src/main/java/com/eactive/eai/custom/adapter/app/impl/JwtTokenCreator.java
new file mode 100644
index 0000000..47a4ded
--- /dev/null
+++ b/src/main/java/com/eactive/eai/custom/adapter/app/impl/JwtTokenCreator.java
@@ -0,0 +1,477 @@
+package com.eactive.eai.custom.adapter.app.impl;
+
+import java.text.ParseException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Properties;
+import java.util.Set;
+
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.BooleanUtils;
+import org.apache.commons.lang3.StringUtils;
+//import org.codehaus.jackson.map.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.json.simple.JSONObject;
+import org.json.simple.JSONValue;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.oauth2.common.OAuth2AccessToken;
+import org.springframework.security.oauth2.common.util.OAuth2Utils;
+import org.springframework.security.oauth2.provider.ClientDetails;
+import org.springframework.security.oauth2.provider.OAuth2Authentication;
+import org.springframework.security.oauth2.provider.OAuth2Request;
+import org.springframework.security.oauth2.provider.TokenRequest;
+import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
+import org.springframework.security.oauth2.provider.token.TokenEnhancer;
+import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
+import org.springframework.security.oauth2.provider.token.TokenStore;
+import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
+import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
+
+import com.eactive.eai.adapter.app.BusinessApplication;
+import com.eactive.eai.adapter.http.dynamic.filter.ApiAuthFilter;
+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.authserver.util.BeanUtils;
+import com.eactive.eai.authserver.vo.ClientVO;
+import com.eactive.eai.common.util.Logger;
+//import com.nimbusds.jwt.SignedJWT;
+
+/**
+ * JWT 토큰을 발급한다.(인증은 요청시스템에서 수행하고 토큰만 발급)
+ *
+ * @author
+ *
+ */
+public class JwtTokenCreator implements BusinessApplication {
+ public static Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT);
+
+ private static final boolean VERYFY_CLIENT_SECRET = true;
+ private static final String[] GRANT_TYPES = { "client_credentials", "authorization_code", "password",
+ "refresh_token" };
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public Object executeApplication(Properties prop, Object messageBytes, Properties tempProp) throws Exception {
+ // 응답전문용 Json Object
+ JSONObject resObject = new JSONObject();
+
+ try {
+ if (messageBytes == null) {
+ throw new Exception("Message is null.");
+ }
+
+ String message = new String((byte[]) messageBytes);
+ if (StringUtils.isBlank(message)) {
+ throw new Exception("Message is empty.");
+ }
+
+ /**
+ * {
+ * "grant_type":"client_credentials",
+ * "client_id":"WNHZOadBfB9UCPPuq6YArKUW87elXmZX",
+ * "client_secret":"JhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9",
+ * "scope":"manage",
+ * "expires_in":3600,
+ * "resource_ids":"aaa, bbb",
+ * "authorities":"ROLE_ADM, ROLE_DEV",
+ * "user_name":"aaaaaaaaaaaaaa",
+ * "support_refresh_token_yn": "Y",
+ * "refresh_token_expires_in":86400,
+ * "refresh_token":"JhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOj~~",
+ * "iss":"AZABBB0000",
+ * "aud":"ZZAAAA0000"
+ * }
+ */
+ boolean isVerifyClientSecret = VERYFY_CLIENT_SECRET;
+ // 요청전문 Json Object
+ JSONObject jsonObject = (JSONObject) JSONValue.parse(message);
+
+ /* 2022.5.25. yseo
+ * 복합거래여부 확인, 복합거래의 경우 요청전문의 내용을 응답전문에 그대로 복사
+ * 복합거래 : 토큰발급 이후 2차 처리를 위한 Outbound Adapter를 다시 호출하는 거래
+ * 복합거래 대상 EAIServiceCD는 APP Adapter의 프로퍼티(SVC_CD)에 등록해서 관리.
+ */
+ String svcCds = prop.getProperty("SVC_CD");
+ String[] svcCdArr = org.springframework.util.StringUtils.tokenizeToStringArray(svcCds, ",");
+ Boolean isComplex = ArrayUtils.contains(svcCdArr, tempProp.getProperty("EAISvcCd"))?true:false;
+ if(isComplex) {
+ resObject = jsonObject;
+ }
+ logger.debug("1. isComplex : [" + isComplex + "]");
+
+ String grantType = (String) jsonObject.get(OAuth2Utils.GRANT_TYPE);
+ if (!ArrayUtils.contains(GRANT_TYPES, grantType)) {
+ throw new JwtAuthException("unsupported_grant_type", "Unsupported grant type");
+ }
+ logger.debug("2. grantType : [" + grantType + "]");
+
+ String clientId = (String) jsonObject.get(OAuth2Utils.CLIENT_ID);
+ if (StringUtils.isBlank(clientId)) {
+ throw new JwtAuthException("invalid_client", "Bad client credentials(client_id is empty)");
+ /* clientId에 허용된 grantType인지 확인 2022.05.23 yseo*/
+ } else {
+ veryfyGrantType(clientId, grantType);
+ }
+ logger.debug("3. clientId : [" + clientId + "]");
+
+ /* grantType = "client_credentials" 이면 client_secret 체크 2022.05.23 yseo
+ * Mydata 관련해서는 무조건 체크해야함.
+ */
+// if (!StringUtils.equalsIgnoreCase(grantType, "client_credentials")) isVerifyClientSecret = false;
+ if (isVerifyClientSecret) {
+ String clientSecret = (String) jsonObject.get("client_secret");
+ if (StringUtils.isBlank(clientSecret)) {
+ throw new JwtAuthException("invalid_client", "Bad client credentials(client_secret is empty)");
+ }
+ veryfyClientSecret(clientId, clientSecret);
+ logger.debug("4. clientSecret : [" + clientSecret + "]");
+ }
+
+ Set scopeSet = assignScope((String) jsonObject.get(OAuth2Utils.SCOPE));
+ if (scopeSet == null || scopeSet.size() == 0) {
+ if (!StringUtils.equals(grantType, "refresh_token")) {
+ throw new Exception("scope is empty.");
+ }
+ }
+ logger.debug("5. scopeSet : [" + scopeSet + "]");
+
+ Set resourceIdSet = assignResourceIds((String) jsonObject.get("resource"));
+ Set authoritiesSet = assignAuthorities((String) jsonObject.get("authorities"));
+
+ String userName = (String) jsonObject.get("user_name");
+ if (StringUtils.equals(grantType, "password") || StringUtils.equals(grantType, "authorization_code")) {
+ if (StringUtils.isBlank(userName)) {
+ throw new Exception("user_name is empty.");
+ }
+ }
+ logger.debug("6. userName : [" + userName + "]");
+
+ /* 요청정보에 expires_in(토큰유효기간) 값이 없는 경우, client정보에 등록된 유효기간 찾아 대체 2022.05.23 yseo*/
+ String expIn = String.valueOf(jsonObject.get(OAuth2AccessToken.EXPIRES_IN));
+ Long expiresIn = null;
+ if (StringUtils.isNumeric(expIn)) {
+ expiresIn = Long.parseLong(expIn);
+ } else if (StringUtils.isBlank(expIn) || StringUtils.equalsIgnoreCase("null", expIn)) {
+ expiresIn = assignDefaultExpiresIn(clientId, "expires_in");
+ } else {
+ throw new Exception("expires_in is not numeric.");
+ }
+ logger.debug("7. expiresIn : [" + expIn + "]");
+// Long expiresIn = (Long) jsonObject.get(OAuth2AccessToken.EXPIRES_IN);
+// if (expiresIn == null) {
+// expiresIn = assignDefaultExpiresIn(clientId, "expires_in");
+// }
+
+ boolean supportRefreshToken = BooleanUtils.toBoolean((String) jsonObject.getOrDefault("support_refresh_token_yn", "N"));
+ /* 요청정보에 refresh_token_expires_in(리프레시토큰유효기간) 값이 없는 경우, client정보에 등록된 유효기간 찾아 대체 2022.05.23 yseo*/
+ String refreshExpIn = String.valueOf(jsonObject.get("refresh_token_expires_in"));
+ Long refreshTokenExpiresIn = null;
+ if (supportRefreshToken && StringUtils.isNumeric(refreshExpIn)) {
+ refreshTokenExpiresIn = Long.parseLong(refreshExpIn);
+ } else if (supportRefreshToken && (StringUtils.isBlank(refreshExpIn) || StringUtils.equalsIgnoreCase("null", refreshExpIn))) {
+ refreshTokenExpiresIn = assignDefaultExpiresIn(clientId, "refresh_token_expires_in");
+ } else if (supportRefreshToken) {
+ throw new Exception("refresh_token_expires_in is not numeric.");
+ }
+ logger.debug("8. refreshTokenExpiresIn : [" + refreshExpIn + "]");
+// Long refreshTokenExpiresIn = (Long) jsonObject.get("refresh_token_expires_in");
+// if (supportRefreshToken && refreshTokenExpiresIn == null) {
+// refreshTokenExpiresIn = assignDefaultExpiresIn(clientId, "refresh_token_expires_in");
+// }
+
+ String refreshToken = (String) jsonObject.get("refresh_token");
+ if (StringUtils.equals(grantType, "refresh_token") && StringUtils.isBlank(refreshToken)) {
+ throw new JwtAuthException("invalid_token", "refresh_token is empty.");
+ }
+ logger.debug("9. refreshToken : [" + refreshToken + "]");
+
+ String iss = (String) jsonObject.get("iss");
+// if (StringUtils.isBlank(iss)) {
+// iss = assignDefaultStringValue(clientId, "iss");
+// }
+// logger.debug("10. iss : [" + iss + "]");
+//
+ String aud = (String) jsonObject.get("aud");
+// if (StringUtils.isBlank(aud)) {
+// aud = assignDefaultStringValue(clientId, "aud");
+// }
+// logger.debug("11. aud : [" + aud + "]");
+
+ String tokenStr = createToken(grantType, clientId, scopeSet, resourceIdSet, authoritiesSet, userName, expiresIn,
+ supportRefreshToken, refreshTokenExpiresIn, refreshToken, iss, aud);
+// logger.debug("#####created tokenStr==>"+tokenStr);
+
+ //20211122 bearer -> Bearer로 변환철. mydata api준수
+ JSONObject jsonObj = (JSONObject) JSONValue.parse(tokenStr);
+ String tokenType = (String)jsonObj.get("token_type");
+ if(StringUtils.isNotBlank(tokenType) && StringUtils.equalsIgnoreCase(tokenType, "bearer")) {
+ jsonObj.put("token_type","Bearer");
+ }
+ jsonObj.remove("jti");
+ jsonObj.put("resource", (String) jsonObject.get("resource"));
+ if (StringUtils.equals(grantType, "password") || StringUtils.equals(grantType, "authorization_code")) {
+ jsonObj.put("refresh_token_expires_in", refreshTokenExpiresIn-1);
+ }
+ tokenStr = jsonObj.toJSONString();
+ logger.debug("##### Bearer checked tokenStr==>"+tokenStr);
+
+ // 복합거래의 경우 발급된 토큰정보를 "TOKEN_DATAGROUP" 그룹으로 추가
+ if(isComplex) {
+ resObject.put("TOKEN_DATAGROUP", jsonObj);
+ } else {
+ resObject.putAll(jsonObj);
+ }
+ String resStr = resObject.toJSONString();
+
+ return resStr.getBytes();
+ } catch (JwtAuthException e) {
+ logger.error(e);
+
+ JSONObject errorJson = new JSONObject();
+ errorJson.put("error", e.getCode());
+ errorJson.put("error_description", e.getMessage());
+ resObject.putAll(errorJson);
+ return resObject.toJSONString().getBytes();
+ } catch (Exception e) {
+ logger.error(e);
+
+ JSONObject errorJson = new JSONObject();
+ errorJson.put("error", "invalid_request");
+ errorJson.put("error_description", e.getMessage());
+ resObject.putAll(errorJson);
+ return resObject.toJSONString().getBytes();
+ }
+ }
+
+ private Set assignResourceIds(String resourceIds) throws Exception {
+ if (StringUtils.isBlank(resourceIds)) {
+ return null;
+ }
+
+ String[] arr = org.springframework.util.StringUtils.tokenizeToStringArray(resourceIds, ",");
+ Set resourceIdSet = new HashSet();
+ for (String resourceId : arr) {
+ resourceIdSet.add(resourceId);
+ }
+
+ return resourceIdSet;
+ }
+
+ private void veryfyGrantType(String clientId, String grantType) throws JwtAuthException {
+ ClientDetails details = OAuth2Manager.getInstance().getClientDeatilsStore().get(clientId);
+ if (details == null) {
+ throw new JwtAuthException("invalid_client", "Bad client credentials(not found)");
+ }
+
+ if (!details.getAuthorizedGrantTypes().contains(grantType)) {
+ throw new JwtAuthException("unsupported_grant_type", "Unsupported grant type for ClientID");
+ }
+ }
+
+ private void veryfyClientSecret(String clientId, String clientSecret) throws JwtAuthException {
+ ClientDetails details = OAuth2Manager.getInstance().getClientDeatilsStore().get(clientId);
+ if (details == null) {
+ throw new JwtAuthException("invalid_client", "Bad client credentials(not found)");
+ }
+
+ if (!StringUtils.equals(details.getClientSecret(), clientSecret)) {
+ throw new JwtAuthException("invalid_client", "Bad client credentials");
+ }
+ }
+
+ private Long assignDefaultExpiresIn(String clientId, String flag) throws JwtAuthException {
+ ClientDetails details = OAuth2Manager.getInstance().getClientDeatilsStore().get(clientId);
+ Long retLong = null;
+ if (details == null) {
+ throw new JwtAuthException("invalid_client", "Bad client credentials(not found)");
+ }
+
+ if (StringUtils.equals(flag, "expires_in") && StringUtils.isNotBlank(details.getAccessTokenValiditySeconds().toString())) {
+ retLong = new Long(details.getAccessTokenValiditySeconds());
+ } else if (StringUtils.equals(flag, "refresh_token_expires_in") && StringUtils.isNotBlank(details.getRefreshTokenValiditySeconds().toString())) {
+ retLong = new Long(details.getRefreshTokenValiditySeconds());
+ } else {
+ throw new JwtAuthException("invalid_client", "Bad expires_in/refresh_token_expires_in(not found)");
+ }
+ return retLong;
+ }
+
+ private String assignDefaultStringValue(String clientId, String flag) throws Exception {
+ ClientVO client = (ClientVO) OAuth2Manager.getInstance().getClientDeatilsStore().get(clientId);
+ String retStr = "";
+ if (client == null) {
+ throw new JwtAuthException("invalid_client", "Bad client credentials(not found)");
+ }
+
+ if (StringUtils.equals(flag, "iss") && StringUtils.isNotBlank(client.getIssuer())) {
+ retStr = client.getIssuer();
+ } else if (StringUtils.equals(flag, "aud") && StringUtils.isNotBlank(client.getAudience())) {
+ retStr = client.getAudience();
+ } else {
+ throw new Exception("iss/aud is null.");
+ }
+ logger.info("===================>"+flag+" : ["+retStr+"]");
+ return retStr;
+ }
+
+ private Set assignScope(String scopes) throws Exception {
+ if (StringUtils.isBlank(scopes)) {
+ return new HashSet();
+ }
+
+ String[] arr = org.springframework.util.StringUtils.tokenizeToStringArray(scopes, " ");
+ Set scopeSet = new HashSet();
+ for (String scope : arr) {
+ scopeSet.add(scope);
+ }
+
+ return scopeSet;
+ }
+
+ private Set assignAuthorities(String authorities) throws Exception {
+ if (StringUtils.isBlank(authorities)) {
+ return new HashSet();
+ }
+
+ String[] arr = org.springframework.util.StringUtils.tokenizeToStringArray(authorities, ",");
+ Set authoritiesSet = new HashSet();
+ for (String authority : arr) {
+ authoritiesSet.add(new SimpleGrantedAuthority(authority));
+ }
+
+ return authoritiesSet;
+ }
+
+ private String createToken(String grantType, String clientId, Set scopeSet, Set resourceIdSet,
+ Set authoritiesSet, String userName, Long expiresIn, boolean supportRefreshToken,
+ Long refreshTokenExpiresIn, String refreshToken, String issuer, String audience) throws Exception {
+ HashMap authorizationParameters = new HashMap();
+ authorizationParameters.put(OAuth2Utils.GRANT_TYPE, grantType);
+ authorizationParameters.put(OAuth2Utils.CLIENT_ID, clientId);
+// authorizationParameters.put("iss", issuer);
+// authorizationParameters.put("aud", audience);
+ if (StringUtils.equals(grantType, "password") || StringUtils.equals(grantType, "authorization_code")) {
+ authorizationParameters.put("user_name", userName);
+ }
+
+ Set responseType = new HashSet();
+ responseType.add(grantType);
+
+ OAuth2Request authorizationRequest = new OAuth2Request(authorizationParameters, clientId, authoritiesSet, true,
+ scopeSet, resourceIdSet, "", responseType, null);
+
+ Authentication authentication = null;
+ if (StringUtils.equals(grantType, "password") || StringUtils.equals(grantType, "authorization_code")) {
+ User userPrincipal = new User(userName, "", true, true, true, true, authoritiesSet);
+ authentication = new UsernamePasswordAuthenticationToken(userPrincipal, null, authoritiesSet);
+ }
+
+ OAuth2Authentication authenticationRequest = new OAuth2Authentication(authorizationRequest, authentication);
+ authenticationRequest.setAuthenticated(true);
+
+ DefaultTokenServices tokenServices = createDefaultTokenServices(supportRefreshToken, expiresIn,
+ refreshTokenExpiresIn);
+ OAuth2AccessToken accessToken = null;
+ if (StringUtils.equals(grantType, "refresh_token")) {
+ tokenServices.setReuseRefreshToken(true);
+ TokenRequest tokenRequest = new TokenRequest(authorizationParameters, clientId, scopeSet, grantType);
+ accessToken = tokenServices.refreshAccessToken(refreshToken, tokenRequest);
+ } else {
+ accessToken = tokenServices.createAccessToken(authenticationRequest);
+ }
+
+ logger.debug("accessToken=" + accessToken.toString());
+
+ return new ObjectMapper().writeValueAsString(accessToken);
+ }
+
+ private DefaultTokenServices createDefaultTokenServices(boolean supportRefreshToken, Long expiresIn,
+ Long refreshTokenExpiresIn) throws Exception {
+ TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
+ tokenEnhancerChain.setTokenEnhancers(Arrays.asList(tokenEnhancer(), accessTokenConverter()));
+
+ DefaultTokenServices tokenServices = new DefaultTokenServices();
+ tokenServices.setTokenStore(tokenStore());
+ tokenServices.setSupportRefreshToken(supportRefreshToken);
+ tokenServices.setTokenEnhancer(tokenEnhancerChain);
+ if (expiresIn != null) {
+ tokenServices.setAccessTokenValiditySeconds(expiresIn.intValue());
+ }
+ if (supportRefreshToken && refreshTokenExpiresIn != null) {
+ tokenServices.setRefreshTokenValiditySeconds(refreshTokenExpiresIn.intValue());
+ }
+ return tokenServices;
+ }
+
+ private TokenEnhancer tokenEnhancer() {
+// return BeanUtils.getBean("tokenEnhancer", TokenEnhancer.class);
+ return (TokenEnhancer) accessTokenConverter();
+ }
+
+ private JwtAccessTokenConverter accessTokenConverter() {
+ return BeanUtils.getBean("jwtAccessTokenConverter", JwtAccessTokenConverter.class);
+ }
+
+ private TokenStore tokenStore() {
+ return new JwtTokenStore((JwtAccessTokenConverter) accessTokenConverter());
+ }
+
+ public static void main(String args[]){
+
+
+
+// String token = "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImRlZmF1bHRfc3NsX2tleSJ9.ew0KICAiaXNzIjogIkExQUFFUjAwMDAiLA0KICAiYXVkIjoiWlZBQUVNMDAwMCIsDQogICJqdGkiOiJlYmI4NmUyMi03YzZmLTQzZDItOTZiMy05NjU0MTAyZTE1M2MiLA0KICAiZXhwIjoiMTYzNjk2ODcwOSIsDQogICJzY29wZSI6ImJhbmsubGlzdCINCn0.nF0JeCHJHn7yBzhjEnge_Eqi0aAeD_WTt0iUB2V2VPJV9zWgMW-Npr3fCXlmYP5HoTjeQES5ftrxWLlLMaxRPHDKWwafgwLm3eQt9r_x_j0PRn-KpoZ9xTWy3oKCQvP0DN0IQepLZfM214nCrjdSPCV17oUS03cBW0lIDmXeROSR2auAMZvkSQqF74E_QLEKF3XLk0MM3Jz4nZp1bBj3PfJyCxjy2ibp12TN0vHccGmSItqANsbeVn0sQybdIraNzb9bptLwWe8WYyn3XugEA9-bavPOSPwrGgLENBnHnzWj1L8yS0rXuEjydCwf6HMAbpNVHQpicM0Q3xdl5W1PBA";
+// String[] components = token.split("\\s");
+
+ //String r "refresh_token_expires_in":"604800","refresh_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImRlZmF1bHRfc3NsX2tleSJ9.ew0KICAiaXNzIjogIkExQUFFUjAwMDAiLA0KICAiYXVkIjoiWlZBQUVNMDAwMCIsDQogICJqdGkiOiIwMzI5Y2M3Zi04Y2U2LTQxNzQtYTc5Ny1mMWRjNjg1ZmM5NDAiLA0KICAiZXhwIjoiMTYzNjk2ODcwOSINCn0.dQx6kWm1JCEgueZjUsQJYNDBu2i4P1-jVLKgUH6aX2aBFunlUBtk_zMMFIJb3OvQpz8qPc9xhs1MkM2YHdpJlNleHY3GhhjiQMfYeiCU9yOzv-zuyvOY1pCO0McmHJNdFcxoQfHHFNm4yFay3dOAFE0Z3FQ74nwxrgnzKSucrHCKyGz0pHdLWnWxHNrJMZ4NmaSyPRxU3qxdWFe1soxqgTqpW_SQkTl3D5SWXgevc6zihPPpFhCSOJ39RrJlIVSH1IY5NRAm71AoaZ6ooLsLvPNED3Z7rG-g9coKEU_DASeCNslRZrP0D3azqswFeu-np3zFZUTrgP_TNT6FFReEnA";
+ try {
+ JwtTokenCreator jtc = new JwtTokenCreator();
+// String me="\"d\"";
+ String message= "{\"refresh_token_expires_in\":\"86400\",\"refresh_token\":null,\"grant_type\":\"authorization_code\",\"support_refresh_token_in\":\"Y\",\"user_name\":\"aaa\",\"scope\":\"bank.list\",\"expires_in\":\"3600\",\"client_id\":\"ZZAAAAPacQmv3ZVhtPISDSA9jb9AWjak\"}";
+ JSONObject jsonObject = (JSONObject) JSONValue.parse(message);
+ String grantType = (String) jsonObject.get(OAuth2Utils.GRANT_TYPE);
+ String clientId = (String) jsonObject.get(OAuth2Utils.CLIENT_ID);
+// String clientSecret = (String) jsonObject.get("client_secret");
+ Set scopeSet;
+
+ Properties prop = null;
+ byte[] msgBytes = null;
+ msgBytes = message.getBytes();
+ jtc.executeApplication(prop, msgBytes, prop);
+
+// scopeSet = jtc.assignScope((String) jsonObject.get(OAuth2Utils.SCOPE));
+//
+//
+// Set resourceIdSet = jtc.assignResourceIds((String) jsonObject.get("resource_ids"));
+// Set authoritiesSet = jtc.assignAuthorities((String) jsonObject.get("authorities"));
+//
+// String userName = (String) jsonObject.get("user_name");
+//
+//
+// Long expiresIn = Long.parseLong(jsonObject.get(OAuth2AccessToken.EXPIRES_IN).toString());
+// Long refreshTokenExpiresIn = Long.parseLong(jsonObject.get("refresh_token_expires_in").toString());
+//// Long expiresIn = (Long) jsonObject.get(OAuth2AccessToken.EXPIRES_IN);
+//// Long refreshTokenExpiresIn = (Long) jsonObject.get("refresh_token_expires_in");
+// boolean supportRefreshToken = BooleanUtils
+// .toBoolean((String) jsonObject.getOrDefault("support_refresh_token_yn", "Y"));
+//
+// String refreshToken = (String) jsonObject.get("refresh_token");
+//
+// jtc.createToken( grantType, clientId, scopeSet, resourceIdSet,authoritiesSet, userName, expiresIn, supportRefreshToken,
+// refreshTokenExpiresIn, refreshToken);
+
+ } catch (ParseException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (Exception e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/eactive/eai/custom/message/GUIDGeneratorKJB.java b/src/main/java/com/eactive/eai/custom/message/GUIDGeneratorKJB.java
new file mode 100644
index 0000000..71f0dc2
--- /dev/null
+++ b/src/main/java/com/eactive/eai/custom/message/GUIDGeneratorKJB.java
@@ -0,0 +1,83 @@
+package com.eactive.eai.custom.message;
+
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+
+import org.apache.commons.lang3.StringUtils;
+
+import com.eactive.eai.common.server.Keys;
+import com.eactive.eai.common.util.StringUtil;
+import com.eactive.eai.common.util.UUID;
+
+/**
+ * 1. 기능 : UUID Generation Class 2. 처리 개요 : * - 3. 주의사항
+ *
+ * @author :
+ * @version : v 1.0.0
+ * @see : 관련 기능을 참조
+ * @since : :
+ */
+public final class GUIDGeneratorKJB {
+
+ private static final DateTimeFormatter SDF_YYYYMMDDHHMMSS = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
+
+ /*
+ * 그룹사코드(3) + 호스트명(8) + 서버구분번호(2) + 일자(8) + 전문생성시간(6) + 일련번호(6) + 시스템경로번호(3, '001')
+ */
+
+ public static final int GUID_LENGTH = 33; // 시스템경로번호 제외부분
+
+ /**
+ * 1. 기능 : Private 생성자 2. 처리 개요 : - 3. 주의사항 - Instance 생성하지 못함
+ **/
+ private GUIDGeneratorKJB() {
+ }
+
+ public synchronized static String getGUID(String groupCmpCd) {
+ if (StringUtils.isEmpty(groupCmpCd)) groupCmpCd = "034"; //광주은행 그룹회사코드 "034"
+
+ String nowDateTime = SDF_YYYYMMDDHHMMSS.format(ZonedDateTime.now());
+ String strSeq = StringUtils.leftPad(String.valueOf(seq), 6, '0') ;
+ String guid = groupCmpCd + UNIQUE_NUM + nowDateTime + strSeq + "001";
+
+ seq++;
+ if (seq > 999999) {
+ seq = 1;
+ }
+
+ return guid;
+ }
+
+ private static String UNIQUE_NUM;
+ private static int seq = 1;
+ static {
+ String hostname = "";
+ try {
+ hostname = java.net.InetAddress.getLocalHost().getHostName();
+ }
+ catch(Exception e) {
+ hostname = "________";
+ }
+
+ if(hostname.getBytes().length != 8) {
+ if(hostname.getBytes().length < 8) {
+ hostname = StringUtils.leftPad(hostname, 8, '_');
+ } else if(hostname.getBytes().length > 8) {
+ hostname = StringUtils.substring(hostname, 0, 8);
+ }
+ }
+
+ String serverName = System.getProperty(Keys.SERVER_KEY);
+ String instid = "00";
+ if(serverName != null && serverName.length() > 2) {
+ instid = serverName.substring(serverName.length()-2, serverName.length());
+ }
+
+ UNIQUE_NUM = hostname.toUpperCase() + instid;
+ }
+
+ public static void main(String[] args) {
+ String guid = getGUID("051");
+ System.out.println("[" + guid.length() + "]" + guid);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/eactive/eai/custom/message/InterfaceMapperKJB.java b/src/main/java/com/eactive/eai/custom/message/InterfaceMapperKJB.java
new file mode 100644
index 0000000..a28ec4f
--- /dev/null
+++ b/src/main/java/com/eactive/eai/custom/message/InterfaceMapperKJB.java
@@ -0,0 +1,202 @@
+package com.eactive.eai.custom.message;
+
+import org.apache.commons.lang3.StringUtils;
+
+import com.eactive.eai.common.server.EAIServerManager;
+import com.eactive.eai.common.util.StringUtil;
+import com.eactive.eai.message.StandardMessage;
+import com.eactive.eai.message.service.DefaultInterfaceMapper;
+import com.ext.eai.common.stdmessage.STDMessageKeys;
+
+@SuppressWarnings("serial")
+public class InterfaceMapperKJB extends DefaultInterfaceMapper {
+ private static String SESSION_ID = "INTERFACE_COMMON.MCI_SESN_ID";
+ private static String INSTANCE_ID = "INTERFACE_COMMON.MCI_INSTNC_ID";
+
+ @Override
+ public String getEaiSvcCode(StandardMessage standardMessage) {
+ String eaiSvcCode = null;
+ String interfaceId = getInterfaceId(standardMessage);
+ String returnType = getSendRecvDivision(standardMessage); // S | R
+ String inExDivision = getInExDivision(standardMessage); // 1 | 2
+ if (interfaceId == null)
+ interfaceId = "";
+ interfaceId = interfaceId.trim();
+
+ // TODO : site에 맞게 조합해야 함.
+ eaiSvcCode = interfaceId + returnType + StringUtils.defaultString(inExDivision);
+ return eaiSvcCode;
+ }
+
+ @Override
+ public void setGuid(StandardMessage standardMessage, String guid) {
+ if (guid == null)
+ throw new RuntimeException("guid is null");
+ if (guid.length() != 36)
+ throw new RuntimeException("The length of guid is not 36");
+ setItemValue(standardMessage, getPath(GUID), guid);
+ }
+
+ @Override
+ public String getGuid(StandardMessage standardMessage) {
+ String guid = getItemValue(standardMessage, getPath(GUID));
+ if (guid.length() == 36) {
+ guid = guid.substring(0, 33);
+ }
+ return guid;
+ }
+
+ @Override
+ public String getGuidSeq(StandardMessage standardMessage) {
+ String guidseq = null;
+ String guid = getItemValue(standardMessage, getPath(GUID));
+ if (guid.length() == 36) {
+ guidseq = guid.substring(33);
+ } else {
+ guidseq = "000";
+ }
+ return guidseq;
+ }
+
+ @Override
+ public void setGuidSeq(StandardMessage standardMessage, String guidSeq) {
+ setItemValue(standardMessage, getPath(GUID), getGuid(standardMessage) + guidSeq);
+ }
+
+ @Override
+ public String getOrgGuid(StandardMessage standardMessage) {
+ String orgGuid = getItemValue(standardMessage, getPath(GUID_ORG));
+ if (orgGuid.length() == 36) {
+ orgGuid = orgGuid.substring(0, 33);
+ }
+ return orgGuid;
+ }
+
+ @Override
+ public void setOrgGuid(StandardMessage standardMessage, String orgGuid) {
+ if (orgGuid.length() == 36) {
+ orgGuid = orgGuid.substring(0, 33);
+ }
+ setItemValue(standardMessage, getPath(GUID_ORG), orgGuid);
+ }
+
+ @Override
+ public String nextGuidSeq(StandardMessage standardMessage) {
+ String guidSeq = getGuidSeq(standardMessage);
+ if (guidSeq == null)
+ return "";
+ int seq = 0;
+ try {
+ seq = Integer.parseInt(guidSeq) + 1;
+ } catch (NumberFormatException e) {
+ seq = 1;
+ }
+ guidSeq = StringUtil.stringFormat(Integer.toString(seq), true, '0', 3);
+ setGuidSeq(standardMessage, guidSeq);
+ return guidSeq;
+ }
+
+ /**
+ * 표준전문의 복원은 COMMON.ORTR_RESTR_YN 의 value는 (Y/N) 엔진에서 복원여부 의 value는 (1/0)
+ */
+ @Override
+ public String getRecoverYn(StandardMessage standardMessage) {
+ String data = getItemValue(standardMessage, getPath(RECOVER_YN));
+ if ("Y".equals(data)) {
+ return "1";
+ } else {
+ return "0";
+ }
+ }
+
+ @Override
+ public void setRecoverYn(StandardMessage standardMessage, String recoverYn) {
+ if ("1".equals(recoverYn)) {
+ setItemValue(standardMessage, getPath(RECOVER_YN), "Y");
+ } else {
+ setItemValue(standardMessage, getPath(RECOVER_YN), "N");
+ }
+ }
+
+ @Override
+ public String getInstCode(StandardMessage standardMessage) {
+ EAIServerManager server = EAIServerManager.getInstance();
+ if (server != null && server.isMCI()) {
+ return getItemValue(standardMessage, INSTANCE_ID);
+ } else {
+ return "";
+ }
+ }
+
+ @Override
+ public void setInstCode(StandardMessage standardMessage, String instCode) {
+ EAIServerManager server = EAIServerManager.getInstance();
+ if (server != null && server.isMCI()) {
+ setItemValue(standardMessage, INSTANCE_ID, instCode);
+ }
+ }
+
+ @Override
+ public String getSessionId(StandardMessage standardMessage) {
+ EAIServerManager server = EAIServerManager.getInstance();
+ if (server != null && server.isMCI()) {
+ return getItemValue(standardMessage, SESSION_ID);
+ } else {
+ return "";
+ }
+ }
+
+ @Override
+ public void setSessionId(StandardMessage standardMessage, String sessionId) {
+ EAIServerManager server = EAIServerManager.getInstance();
+ if (server != null && server.isMCI()) {
+ setItemValue(standardMessage, SESSION_ID, sessionId);
+ }
+ }
+
+
+ /*
+ * 엔진 에서는 public static final String RESPONSE_TYPE_CODE_N = "0";//정상(normal)
+ * public static final String RESPONSE_TYPE_CODE_E = "1";//오류(error)
+ */
+ @Override
+ public void setResponseType(StandardMessage standardMessage, String responseType) {
+ if (responseType != null && (STDMessageKeys.RESPONSE_TYPE_CODE_N.equals(responseType)
+ || STDMessageKeys.RESPONSE_TYPE_CODE_E.equals(responseType))) {
+ if (STDMessageKeys.RESPONSE_TYPE_CODE_N.equals(responseType)) {
+ setItemValue(standardMessage, getPath(RESPONSE_TYPE), "NR");
+ } else {
+ setItemValue(standardMessage, getPath(RESPONSE_TYPE), "ER");
+ }
+ } else {
+ setItemValue(standardMessage, getPath(RESPONSE_TYPE), responseType);
+ }
+
+ }
+
+ @Override
+ public String getInterfaceId(StandardMessage standardMessage) {
+ String interfaceId = getItemValue(standardMessage, getPath(INTERFACE_ID));
+ if (interfaceId == null)
+ return "";
+ interfaceId = interfaceId.trim();
+ return interfaceId;
+ }
+
+ @Override
+ public String getReqSysCode(StandardMessage standardMessage) {
+ // apiPath의 값에서 추출할 예정 : serviceId = api_path
+ String apiPath = getItemValue(standardMessage, getPath(SERVICE_ID));
+ if (apiPath != null) {
+ String[] split = apiPath.split("/");
+ if (split.length > 2) {
+ String route = split[1].toUpperCase();
+ if (route.length() > 3) {
+ route = route.substring(0, 3);
+ }
+ return route;
+ }
+ }
+ return null;
+ }
+}
\ No newline at end of file
From dff0a1bf98738b5d49847bc771c28af7e9209a24 Mon Sep 17 00:00:00 2001
From: 25W0075 <25W0075@P9801Z.kjbank.dom>
Date: Mon, 8 Dec 2025 09:48:38 +0900
Subject: [PATCH 02/10] =?UTF-8?q?persistent-store-type=EC=9D=84=20memory?=
=?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
WebContent/WEB-INF/weblogic.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/WebContent/WEB-INF/weblogic.xml b/WebContent/WEB-INF/weblogic.xml
index 44b772f..71b418f 100644
--- a/WebContent/WEB-INF/weblogic.xml
+++ b/WebContent/WEB-INF/weblogic.xml
@@ -6,7 +6,7 @@
1800
JSESSIONID_EMS
- none
+ memory
From d6c2ef4cd6b0eaff1d43cf291deb3b8d17bd5e89 Mon Sep 17 00:00:00 2001
From: yunjy-hp
Date: Thu, 11 Dec 2025 10:04:41 +0900
Subject: [PATCH 03/10] =?UTF-8?q?=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20?=
=?UTF-8?q?=EC=9E=90=EC=9B=90=20=EC=A0=9C=EA=B1=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../inflow/InflowControlManagerSBI.java | 425 ------------------
1 file changed, 425 deletions(-)
delete mode 100644 src/main/java/com/eactive/eai/custom/inflow/InflowControlManagerSBI.java
diff --git a/src/main/java/com/eactive/eai/custom/inflow/InflowControlManagerSBI.java b/src/main/java/com/eactive/eai/custom/inflow/InflowControlManagerSBI.java
deleted file mode 100644
index 2b3f635..0000000
--- a/src/main/java/com/eactive/eai/custom/inflow/InflowControlManagerSBI.java
+++ /dev/null
@@ -1,425 +0,0 @@
-package com.eactive.eai.custom.inflow;
-
-import java.time.Duration;
-import java.util.Arrays;
-import java.util.Calendar;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.Properties;
-
-import org.apache.commons.lang3.StringUtils;
-
-import com.eactive.eai.common.dao.DAOFactory;
-import com.eactive.eai.common.exception.ExceptionUtil;
-import com.eactive.eai.common.inflow.CustomBucket;
-import com.eactive.eai.common.inflow.InflowControlDAO;
-import com.eactive.eai.common.inflow.InflowControlManager;
-import com.eactive.eai.common.inflow.InflowTargetVO;
-import com.eactive.eai.common.lifecycle.Lifecycle;
-import com.eactive.eai.common.lifecycle.LifecycleException;
-import com.eactive.eai.common.lifecycle.LifecycleListener;
-import com.eactive.eai.common.lifecycle.LifecycleSupport;
-import com.eactive.eai.common.message.EAIMessage;
-import com.eactive.eai.common.property.PropManager;
-import com.eactive.eai.common.server.EAIServerManager;
-import com.eactive.eai.common.util.Logger;
-import com.eactive.eai.common.util.MessageUtil;
-import com.eactive.eai.message.service.InterfaceMapper;
-import com.ext.eai.common.stdmessage.STDMessageKeys;
-
-import io.github.bucket4j.Bandwidth;
-import io.github.bucket4j.Bucket4j;
-import io.github.bucket4j.local.LocalBucketBuilder;
-
-public class InflowControlManagerSBI implements Lifecycle, com.eactive.eai.common.inflow.Bucket
-{
- static Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT);
-
- private static InflowControlManagerSBI manager;
-
- private HashMap adapterBucketList;
- private HashMap interfaceBucketList;
- private boolean started;
- private LifecycleSupport lifecycle = new LifecycleSupport(this);
-
- /**
- * 1. 기능 : Default Constructor
- * 2. 처리 개요 : C2RServiceVO Rule 정보를 저장하기 위한 HashMap을 초기화한다.
- * 3. 주의사항
- *
- **/
- private InflowControlManagerSBI() {
- adapterBucketList = new HashMap();
- interfaceBucketList = new HashMap();
- }
-
- /**
- * 1. 기능 : InflowControlManagerSBI Singleton Object를 반환하는 getter method
- * 2. 처리 개요 : C2RServiceManager Singleton Object를 반환한다.
- * 3. 주의사항
- *
- * @return InflowControlManagerSBI Singleton object
- **/
- public static InflowControlManagerSBI getInstance() {
- if(manager == null) {
- synchronized(InflowControlManagerSBI.class) {
- if(manager == null) {
- manager = new InflowControlManagerSBI();
- }
- }
- }
-
- return manager;
- }
-
- public static com.eactive.eai.common.inflow.Bucket getBucket() {
- if(manager == null) {
- synchronized(InflowControlManagerSBI.class) {
- if(manager == null) {
- manager = new InflowControlManagerSBI();
- }
- }
- }
-
- return manager;
- }
-
- /**
- * 유량제어 대상인 거래인지 체크한다.(사이트에 맞게 구성)
- * @param eaiServerManager
- * @param eaiMessage
- * @return
- */
- public static Boolean isTargetOfInflowControl(EAIServerManager eaiServerManager, EAIMessage eaiMessage) {
- // 1. 모든 요청거래
- InterfaceMapper mapper = eaiMessage.getMapper();
- String returnType = mapper.getSendRecvDivision(eaiMessage.getStandardMessage()); // S|R
-
- if (STDMessageKeys.SEND_RECV_CD_SEND.equals(returnType)) {
- return new Boolean(true);
- }
-
- return new Boolean(false);
- }
-
- public void start() throws LifecycleException {
- if (started)
- throw new LifecycleException("RECEAICMM201");
-
- // Notify our interested LifecycleListeners
- lifecycle.fireLifecycleEvent(STARTING_EVENT, this);
-
- // C2RServiceVO Table Initializing ....
-
- try {
- init();
- } catch(Exception e) {
- //throw new LifecycleException("RECEAICMM202");
- throw new LifecycleException(ExceptionUtil.getErrorCode(e,"RECEAICMM202"));
- }
-
- started = true;
- // Notify our interested LifecycleListeners
- lifecycle.fireLifecycleEvent(STARTED_EVENT, this);
- }
-
- private void init() throws Exception {
- initAdapter();
- initInterface();
- }
-
- private void initAdapter() throws Exception {
- // Initializing ....
- adapterBucketList = new HashMap();
- InflowControlDAO dao = null;
- dao = (InflowControlDAO) DAOFactory.newInstance().create(InflowControlDAO.class);
- List inflowAdapterVoList = dao.getInflowAdapterList();
-
- for (InflowTargetVO inflowVo : inflowAdapterVoList) {
- if (InflowControlManagerSBI.isInflowTarget(inflowVo)) {
- CustomBucket bucket = makeBucketUsingInflowVO(inflowVo);
- if (bucket != null) {
- adapterBucketList.put(inflowVo.getName(), bucket);
- }
- }
- }
- }
-
- private void initInterface() throws Exception {
- // Initializing ....
- interfaceBucketList = new HashMap();
- InflowControlDAO dao = null;
- dao = (InflowControlDAO) DAOFactory.newInstance().create(InflowControlDAO.class);
- List inflowInterfaceVoList = dao.getInflowInterfaceList();
-
- for (InflowTargetVO inflowVo : inflowInterfaceVoList) {
- if (InflowControlManagerSBI.isInflowTarget(inflowVo)) {
- CustomBucket bucket = makeBucketUsingInflowVO(inflowVo);
- if (bucket != null) {
- interfaceBucketList.put(inflowVo.getName(), bucket);
- }
- }
- }
- }
-
- public synchronized void reloadAdapter() throws Exception {
- if(logger.isWarn()) {
- logger.warn("InflowControlManagerSBI] reload all Started ...");
- }
- initAdapter();
- if(logger.isWarn()) {
- logger.warn("InflowControlManagerSBI] reload all finished ...");
- }
- }
- public synchronized void reloadInterface() throws Exception {
- if(logger.isWarn()) {
- logger.warn("InflowControlManagerSBI] reload all Started ...");
- }
- initInterface();
- if(logger.isWarn()) {
- logger.warn("InflowControlManagerSBI] reload all finished ...");
- }
- }
-
- public synchronized void reloadAdapter(String adapter) throws Exception {
- if (logger.isWarn()) {
- logger.warn("InflowControlManagerSBI] reload Started ...");
- }
- InflowControlDAO dao = null;
- dao = (InflowControlDAO) DAOFactory.newInstance().create(InflowControlDAO.class);
- Map map = dao.getInflowTargetByAdater(adapter);
- if (map != null) {
- Iterator it = map.keySet().iterator();
- String key = "";
- for (int i = 0; it.hasNext(); i++) {
- key = (String) it.next();
-
- InflowTargetVO inflowTarget = map.get(key);
- if (InflowControlManagerSBI.isInflowTarget(inflowTarget)) {
- CustomBucket bucket = makeBucketUsingInflowVO(inflowTarget);
- if (bucket != null) {
- adapterBucketList.put(inflowTarget.getName(), bucket);
- } else {
- adapterBucketList.remove(inflowTarget.getName());
- }
- } else {
- adapterBucketList.remove(inflowTarget.getName());
- }
- }
- }
- if (logger.isWarn()) {
- logger.warn("InflowControlManagerSBI] reload finished ...");
- }
- }
- public synchronized void reloadInterface(String inter) throws Exception {
- if (logger.isWarn()) {
- logger.warn("InflowControlManagerSBI] reload Started ...");
- }
- InflowControlDAO dao = null;
- dao = (InflowControlDAO) DAOFactory.newInstance().create(InflowControlDAO.class);
- Map map = dao.getInflowTargetByInterface(inter);
- if (map != null) {
- Iterator it = map.keySet().iterator();
- String key = "";
- for (int i = 0; it.hasNext(); i++) {
- key = (String) it.next();
-
- InflowTargetVO inflowTarget = map.get(key);
- if (InflowControlManagerSBI.isInflowTarget(inflowTarget)) {
- CustomBucket bucket = makeBucketUsingInflowVO(inflowTarget);
- if (bucket != null) {
- interfaceBucketList.put(inflowTarget.getName(), bucket);
- } else {
- interfaceBucketList.remove(inflowTarget.getName());
- }
- } else {
- interfaceBucketList.remove(inflowTarget.getName());
- }
- }
- }
-
- if (logger.isWarn()) {
- logger.warn("InflowControlManagerSBI] reload finished ...");
- }
- }
-
- /**
- * 1. 기능 : Lifecycle의 stop 메서드로 C2RServiceManager를 종료하는 메서드
- * 2. 처리 개요 : 멤버에 캐싱한 C2RServiceVO Rule 정보를 clear한다.
- * 3. 주의사항
- *
- * @exception LifecycleException 이미 종료된 경우 발생(RECEAICMM203)
- **/
- public void stop() throws LifecycleException {
- // Validate and update our current component state
- if (!started)
- throw new LifecycleException("RECEAICMM203");
-
- // Notify our interested LifecycleListeners
- lifecycle.fireLifecycleEvent(STOPING_EVENT, this);
-
- adapterBucketList.clear();
- interfaceBucketList.clear();
-
- started = false;
- // Notify our interested LifecycleListeners
- lifecycle.fireLifecycleEvent(STOPPED_EVENT, this);
- }
-
- /**
- * 1. 기능 : LifecycleListener를 등록하는 메서드
- * 2. 처리 개요 : LifecycleListener를 등록한다.
- * 3. 주의사항
- *
- * @param listener LifecycleEvent를 수신한 LifecycleListener
- **/
- public void addLifecycleListener(LifecycleListener listener)
- {
- lifecycle.addLifecycleListener(listener);
- }
-
- public LifecycleListener[] findLifecycleListeners()
- {
- return lifecycle.findLifecycleListeners();
- }
-
- public void removeLifecycleListener(LifecycleListener listener)
- {
- lifecycle.removeLifecycleListener(listener);
- }
-
- public boolean isStarted() {
- return this.started;
- }
-
- public String[] getAdapterAllKeys() {
- Iterator it = this.adapterBucketList.keySet().iterator();
- String[] svcCd = new String[this.adapterBucketList.size()];
- for(int i = 0; it.hasNext(); i++){
- svcCd[i] = (String)it.next();
- }
- Arrays.sort(svcCd);
- return svcCd;
- }
- public String[] getInterfaceAllKeys() {
- Iterator it = this.interfaceBucketList.keySet().iterator();
- String[] svcCd = new String[this.interfaceBucketList.size()];
- for(int i = 0; it.hasNext(); i++){
- svcCd[i] = (String)it.next();
- }
- Arrays.sort(svcCd);
- return svcCd;
- }
-
- public void removeAdapter(String adapter) {
- adapterBucketList.remove(adapter);
- }
- public void removeInterface(String inter) {
- interfaceBucketList.remove(inter);
- }
-
- private CustomBucket makeBucketUsingInflowVO(InflowTargetVO inflowTarget) {
- if (InflowControlManagerSBI.isInflowTarget(inflowTarget)) {
- LocalBucketBuilder builder = Bucket4j.builder();
- if (inflowTarget.getThresholdPerSecond() > 0) { // TPS
- builder.addLimit(Bandwidth.simple(inflowTarget.getThresholdPerSecond(), Duration.ofSeconds(1)));
- }
-
- if (inflowTarget.getThreshold() > 0 && StringUtils.isNotBlank(inflowTarget.getThresholdTimeUnit())) {
- builder.addLimit(Bandwidth.simple(inflowTarget.getThreshold(),
- InflowControlManagerSBI.getPeriod(inflowTarget.getThresholdTimeUnit())));
- }
-
- return new CustomBucket(builder.build(), inflowTarget);
- }
-
- return null;
- }
-
- @Override
- public boolean isAdapterPass(String adapter) {
- CustomBucket b = adapterBucketList.get(adapter);
-
- if ( b == null ) return true;
-
- return b.getLocalBucket().tryConsume(1);
- }
-
- @Override
- public boolean isInterfacePass(String inter) {
- CustomBucket b = interfaceBucketList.get(inter);
-
- if ( b == null ) return true;
-
- return b.getLocalBucket().tryConsume(1);
- }
-
- @Override
- public InflowTargetVO getAdapterInflowThreashold(String adapter) {
- CustomBucket b = adapterBucketList.get(adapter);
- if (b == null) return null;
- return b.getInflowTargetVo();
- }
-
- @Override
- public InflowTargetVO getInterfaceInflowThreashold(String inter) {
- CustomBucket b = interfaceBucketList.get(inter);
- if (b == null) return null;
- return b.getInflowTargetVo();
- }
-
- @Override
- public InflowTargetVO getInflowThreashold(String inter) {
- return null;
- }
-
- public InflowTargetVO getAdapterInflow(String adapter) {
- CustomBucket b = adapterBucketList.get(adapter);
- if (b == null) return null;
- return b.getInflowTargetVo();
- }
-
- public InflowTargetVO getInterfaceInflow(String inter) {
- CustomBucket b = interfaceBucketList.get(inter);
- if (b == null) return null;
- return b.getInflowTargetVo();
- }
-
- public static boolean isInflowTarget(InflowTargetVO inflowTarget) {
- if (inflowTarget == null || !inflowTarget.isActivate()) {
- return false;
- }
-
- if (inflowTarget.getThresholdPerSecond() > 0) {
- return true;
- }
-
- if (inflowTarget.getThreshold() > 0 && StringUtils.equalsAnyIgnoreCase(inflowTarget.getThresholdTimeUnit(),
- InflowControlManager.QUOTA_TIMEUNIT_DAY, InflowControlManager.QUOTA_TIMEUNIT_SECOND,
- InflowControlManager.QUOTA_TIMEUNIT_MINUTE, InflowControlManager.QUOTA_TIMEUNIT_HOUR,
- InflowControlManager.QUOTA_TIMEUNIT_DAY, InflowControlManager.QUOTA_TIMEUNIT_MONTH)) {
- return true;
- }
-
- return false;
- }
-
- public static Duration getPeriod(String timeUnit) {
- if (StringUtils.equalsIgnoreCase(timeUnit, InflowControlManager.QUOTA_TIMEUNIT_SECOND)) {
- return Duration.ofSeconds(1);
- } else if (StringUtils.equalsIgnoreCase(timeUnit, InflowControlManager.QUOTA_TIMEUNIT_MINUTE)) {
- return Duration.ofMinutes(1);
- } else if (StringUtils.equalsIgnoreCase(timeUnit, InflowControlManager.QUOTA_TIMEUNIT_HOUR)) {
- return Duration.ofHours(1);
- } else if (StringUtils.equalsIgnoreCase(timeUnit, InflowControlManager.QUOTA_TIMEUNIT_DAY)) {
- return Duration.ofDays(1);
- } else if (StringUtils.equalsIgnoreCase(timeUnit, InflowControlManager.QUOTA_TIMEUNIT_MONTH)) {
- Calendar calendar = Calendar.getInstance();
- return Duration.ofDays(calendar.getActualMaximum(Calendar.DAY_OF_MONTH));
- } else {
- return null;
- }
- }
-}
From 74aeddf8388683808773e467328d8b7e4aaf09f7 Mon Sep 17 00:00:00 2001
From: yunjy-hp
Date: Thu, 11 Dec 2025 11:03:59 +0900
Subject: [PATCH 04/10] =?UTF-8?q?feat:=20=EC=9C=A0=EB=9F=89=EC=A0=9C?=
=?UTF-8?q?=EC=96=B4=20=EA=B7=B8=EB=A3=B9=20=EB=B2=84=ED=82=B7=20=EB=B6=84?=
=?UTF-8?q?=EB=A6=AC=20=EB=B0=8F=20=ED=98=84=ED=99=A9=20=ED=99=94=EB=A9=B4?=
=?UTF-8?q?=20=EA=B0=9C=EC=84=A0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- CustomGroupBucket 클래스 신규 생성: 초당/추가 임계치를 별도 LocalBucket으로 분리하여 개별 토큰 조회 가능
- InflowControlManager 수정: groupBucketList 타입을 CustomGroupBucket으로 변경, makeGroupBucket() 메서드 추가
- InflowGroupBucketService 단순화: 복잡한 리플렉션 로직 제거, CustomGroupBucket 직접 메서드 사용
- 현황 탭에 전체 인스턴스 합산 요약 추가 (활성 인스턴스 수, 초당/추가 임계치 합산)
- 추가 임계치 시간단위에서 '초(SEC)' 옵션 제거
- 저장 완료 후 리스트 화면 이동 제거
---
WebContent/WEB-INF/authserver-servlet.xml | 2 +
kjb-docs/inflow/inflow_control_group_ddl.sql | 51 +++++++++
.../manage/inflow/GroupBucketStatusDTO.java | 101 +++++++++++++++++
.../inflow/InflowGroupBucketController.java | 56 ++++++++++
.../inflow/InflowGroupBucketService.java | 105 ++++++++++++++++++
5 files changed, 315 insertions(+)
create mode 100644 kjb-docs/inflow/inflow_control_group_ddl.sql
create mode 100644 src/main/java/com/eactive/eai/manage/inflow/GroupBucketStatusDTO.java
create mode 100644 src/main/java/com/eactive/eai/manage/inflow/InflowGroupBucketController.java
create mode 100644 src/main/java/com/eactive/eai/manage/inflow/InflowGroupBucketService.java
diff --git a/WebContent/WEB-INF/authserver-servlet.xml b/WebContent/WEB-INF/authserver-servlet.xml
index 4eb1843..1342774 100644
--- a/WebContent/WEB-INF/authserver-servlet.xml
+++ b/WebContent/WEB-INF/authserver-servlet.xml
@@ -21,6 +21,8 @@
base-package="com.eactive.eai.authserver" />
+
diff --git a/kjb-docs/inflow/inflow_control_group_ddl.sql b/kjb-docs/inflow/inflow_control_group_ddl.sql
new file mode 100644
index 0000000..685024c
--- /dev/null
+++ b/kjb-docs/inflow/inflow_control_group_ddl.sql
@@ -0,0 +1,51 @@
+-- ============================================
+-- 유량제어 그룹 테이블 DDL
+-- ============================================
+
+-- ============================================
+-- Oracle
+-- ============================================
+
+-- 유량제어 그룹 테이블
+CREATE TABLE inflow_control_group (
+ group_id VARCHAR2(36) NOT NULL, -- UUID
+ group_name VARCHAR2(100),
+ threshold NUMBER(10),
+ threshold_per_second NUMBER(10),
+ threshold_time_unit VARCHAR2(12),
+ use_yn VARCHAR2(1) DEFAULT '1',
+ modified_by VARCHAR2(50),
+ modified_at TIMESTAMP,
+ CONSTRAINT pk_inflow_control_group PRIMARY KEY (group_id)
+);
+
+COMMENT ON TABLE inflow_control_group IS '유량제어 그룹';
+COMMENT ON COLUMN inflow_control_group.group_id IS '그룹 ID (UUID)';
+COMMENT ON COLUMN inflow_control_group.group_name IS '그룹명';
+COMMENT ON COLUMN inflow_control_group.threshold IS '임계치';
+COMMENT ON COLUMN inflow_control_group.threshold_per_second IS '초당 임계치';
+COMMENT ON COLUMN inflow_control_group.threshold_time_unit IS '임계치 TimeUnit (SEC, MIN, HOU, DAY, MON)';
+COMMENT ON COLUMN inflow_control_group.use_yn IS '사용여부 (1:사용, 0:미사용)';
+COMMENT ON COLUMN inflow_control_group.modified_by IS '수정자';
+COMMENT ON COLUMN inflow_control_group.modified_at IS '수정일시';
+
+-- 그룹-인터페이스 매핑 테이블
+CREATE TABLE inflow_control_group_mapping (
+ interface_id VARCHAR2(100) NOT NULL,
+ group_id VARCHAR2(50) NOT NULL,
+ use_yn VARCHAR2(1) DEFAULT '1',
+ modified_by VARCHAR2(50),
+ modified_at TIMESTAMP,
+ CONSTRAINT pk_inflow_ctrl_grp_mapping PRIMARY KEY (interface_id)
+);
+
+COMMENT ON TABLE inflow_control_group_mapping IS '유량제어 그룹 매핑';
+COMMENT ON COLUMN inflow_control_group_mapping.interface_id IS '인터페이스 ID';
+COMMENT ON COLUMN inflow_control_group_mapping.group_id IS '그룹 ID';
+COMMENT ON COLUMN inflow_control_group_mapping.use_yn IS '사용여부 (1:사용, 0:미사용)';
+COMMENT ON COLUMN inflow_control_group_mapping.modified_by IS '수정자';
+COMMENT ON COLUMN inflow_control_group_mapping.modified_at IS '수정일시';
+
+-- 인덱스
+CREATE INDEX idx_inflow_ctrl_grp_map_grp ON inflow_control_group_mapping (group_id);
+
diff --git a/src/main/java/com/eactive/eai/manage/inflow/GroupBucketStatusDTO.java b/src/main/java/com/eactive/eai/manage/inflow/GroupBucketStatusDTO.java
new file mode 100644
index 0000000..64dd221
--- /dev/null
+++ b/src/main/java/com/eactive/eai/manage/inflow/GroupBucketStatusDTO.java
@@ -0,0 +1,101 @@
+package com.eactive.eai.manage.inflow;
+
+import java.util.List;
+
+public class GroupBucketStatusDTO {
+ private String groupId;
+ private String groupName;
+ private boolean activate;
+ private List buckets;
+
+ public static class BucketInfo {
+ private String type; // "perSecond" | "threshold"
+ private long capacity; // 최대 토큰 수
+ private long availableTokens; // 현재 사용 가능한 토큰 수
+ private String timeUnit; // threshold인 경우 시간 단위
+ private double usagePercent; // 사용률 (100 - availableTokens/capacity * 100)
+
+ public BucketInfo() {}
+
+ public BucketInfo(String type, long capacity, long availableTokens, String timeUnit) {
+ this.type = type;
+ this.capacity = capacity;
+ this.availableTokens = availableTokens;
+ this.timeUnit = timeUnit;
+ this.usagePercent = capacity > 0 ?
+ Math.round((1.0 - (double) availableTokens / capacity) * 10000) / 100.0 : 0;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ public long getCapacity() {
+ return capacity;
+ }
+
+ public void setCapacity(long capacity) {
+ this.capacity = capacity;
+ }
+
+ public long getAvailableTokens() {
+ return availableTokens;
+ }
+
+ public void setAvailableTokens(long availableTokens) {
+ this.availableTokens = availableTokens;
+ }
+
+ public String getTimeUnit() {
+ return timeUnit;
+ }
+
+ public void setTimeUnit(String timeUnit) {
+ this.timeUnit = timeUnit;
+ }
+
+ public double getUsagePercent() {
+ return usagePercent;
+ }
+
+ public void setUsagePercent(double usagePercent) {
+ this.usagePercent = usagePercent;
+ }
+ }
+
+ public String getGroupId() {
+ return groupId;
+ }
+
+ public void setGroupId(String groupId) {
+ this.groupId = groupId;
+ }
+
+ public String getGroupName() {
+ return groupName;
+ }
+
+ public void setGroupName(String groupName) {
+ this.groupName = groupName;
+ }
+
+ public boolean isActivate() {
+ return activate;
+ }
+
+ public void setActivate(boolean activate) {
+ this.activate = activate;
+ }
+
+ public List getBuckets() {
+ return buckets;
+ }
+
+ public void setBuckets(List buckets) {
+ this.buckets = buckets;
+ }
+}
diff --git a/src/main/java/com/eactive/eai/manage/inflow/InflowGroupBucketController.java b/src/main/java/com/eactive/eai/manage/inflow/InflowGroupBucketController.java
new file mode 100644
index 0000000..befff94
--- /dev/null
+++ b/src/main/java/com/eactive/eai/manage/inflow/InflowGroupBucketController.java
@@ -0,0 +1,56 @@
+package com.eactive.eai.manage.inflow;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/manage/inflow/group")
+public class InflowGroupBucketController {
+
+ @Autowired
+ private InflowGroupBucketService bucketService;
+
+ /**
+ * 특정 그룹의 버킷 상태 조회
+ * GET /manage/inflow/group/{groupId}/bucket-status
+ */
+ @GetMapping("/{groupId}/bucket-status")
+ public ResponseEntity> getGroupBucketStatus(@PathVariable String groupId) {
+ GroupBucketStatusDTO status = bucketService.getGroupBucketStatus(groupId);
+
+ if (status == null) {
+ Map error = new HashMap<>();
+ error.put("success", false);
+ error.put("message", "그룹을 찾을 수 없습니다: " + groupId);
+ return ResponseEntity.ok(error);
+ }
+
+ Map result = new HashMap<>();
+ result.put("success", true);
+ result.put("data", status);
+ return ResponseEntity.ok(result);
+ }
+
+ /**
+ * 모든 그룹의 버킷 상태 조회
+ * GET /manage/inflow/group/bucket-status
+ */
+ @GetMapping("/bucket-status")
+ public ResponseEntity> getAllGroupBucketStatus() {
+ List statusList = bucketService.getAllGroupBucketStatus();
+
+ Map result = new HashMap<>();
+ result.put("success", true);
+ result.put("data", statusList);
+ result.put("count", statusList.size());
+ return ResponseEntity.ok(result);
+ }
+}
diff --git a/src/main/java/com/eactive/eai/manage/inflow/InflowGroupBucketService.java b/src/main/java/com/eactive/eai/manage/inflow/InflowGroupBucketService.java
new file mode 100644
index 0000000..1bb4d6f
--- /dev/null
+++ b/src/main/java/com/eactive/eai/manage/inflow/InflowGroupBucketService.java
@@ -0,0 +1,105 @@
+package com.eactive.eai.manage.inflow;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import org.springframework.stereotype.Service;
+
+import com.eactive.eai.common.inflow.CustomGroupBucket;
+import com.eactive.eai.common.inflow.InflowControlManager;
+import com.eactive.eai.common.inflow.InflowGroupVO;
+
+@Service
+public class InflowGroupBucketService {
+
+ public GroupBucketStatusDTO getGroupBucketStatus(String groupId) {
+ InflowControlManager manager = InflowControlManager.getInstance();
+
+ InflowGroupVO groupVo = manager.getGroupInflowThreshold(groupId);
+ if (groupVo == null) {
+ return null;
+ }
+
+ GroupBucketStatusDTO dto = new GroupBucketStatusDTO();
+ dto.setGroupId(groupId);
+ dto.setGroupName(groupVo.getGroupName());
+ dto.setActivate(groupVo.isActivate());
+
+ List buckets = new ArrayList<>();
+
+ // CustomGroupBucket에서 직접 토큰 조회
+ CustomGroupBucket customGroupBucket = getCustomGroupBucket(groupId);
+
+ // perSecond bucket
+ if (groupVo.getThresholdPerSecond() > 0) {
+ long capacity = groupVo.getThresholdPerSecond();
+ long available = capacity;
+ if (customGroupBucket != null) {
+ long tokens = customGroupBucket.getPerSecondAvailableTokens();
+ if (tokens >= 0) {
+ available = Math.min(tokens, capacity);
+ }
+ }
+ buckets.add(new GroupBucketStatusDTO.BucketInfo(
+ "perSecond",
+ capacity,
+ available,
+ "SEC"
+ ));
+ }
+
+ // threshold bucket
+ if (groupVo.getThreshold() > 0) {
+ long capacity = groupVo.getThreshold();
+ long available = capacity;
+ if (customGroupBucket != null) {
+ long tokens = customGroupBucket.getThresholdAvailableTokens();
+ if (tokens >= 0) {
+ available = Math.min(tokens, capacity);
+ }
+ }
+ buckets.add(new GroupBucketStatusDTO.BucketInfo(
+ "threshold",
+ capacity,
+ available,
+ groupVo.getThresholdTimeUnit()
+ ));
+ }
+
+ dto.setBuckets(buckets);
+ return dto;
+ }
+
+ /**
+ * 그룹 버킷 조회 (리플렉션 사용)
+ */
+ public CustomGroupBucket getCustomGroupBucket(String groupId) {
+ try {
+ InflowControlManager manager = InflowControlManager.getInstance();
+ Field field = InflowControlManager.class.getDeclaredField("groupBucketList");
+ field.setAccessible(true);
+ @SuppressWarnings("unchecked")
+ Map groupBucketList =
+ (Map) field.get(manager);
+ return groupBucketList.get(groupId);
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ public List getAllGroupBucketStatus() {
+ InflowControlManager manager = InflowControlManager.getInstance();
+ String[] groupIds = manager.getGroupAllKeys();
+
+ List result = new ArrayList<>();
+ for (String groupId : groupIds) {
+ GroupBucketStatusDTO dto = getGroupBucketStatus(groupId);
+ if (dto != null) {
+ result.add(dto);
+ }
+ }
+ return result;
+ }
+}
From ec16d89d7f39b4d72ba4bad4c1fbd809e36d42f8 Mon Sep 17 00:00:00 2001
From: yunjy-hp
Date: Thu, 11 Dec 2025 11:12:42 +0900
Subject: [PATCH 05/10] =?UTF-8?q?=EC=BB=AC=EB=9F=BC=20=EA=B8=B8=EC=9D=B4?=
=?UTF-8?q?=20=EB=A7=9E=EC=B6=A4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
kjb-docs/inflow/inflow_control_group_ddl.sql | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/kjb-docs/inflow/inflow_control_group_ddl.sql b/kjb-docs/inflow/inflow_control_group_ddl.sql
index 685024c..e258eb9 100644
--- a/kjb-docs/inflow/inflow_control_group_ddl.sql
+++ b/kjb-docs/inflow/inflow_control_group_ddl.sql
@@ -32,7 +32,7 @@ COMMENT ON COLUMN inflow_control_group.modified_at IS '수정일시';
-- 그룹-인터페이스 매핑 테이블
CREATE TABLE inflow_control_group_mapping (
interface_id VARCHAR2(100) NOT NULL,
- group_id VARCHAR2(50) NOT NULL,
+ group_id VARCHAR2(36) NOT NULL,
use_yn VARCHAR2(1) DEFAULT '1',
modified_by VARCHAR2(50),
modified_at TIMESTAMP,
From e8fd8d8278608fd16602a9e1058a273523a6d5e7 Mon Sep 17 00:00:00 2001
From: yunjy-hp
Date: Thu, 11 Dec 2025 11:17:08 +0900
Subject: [PATCH 06/10] =?UTF-8?q?=EB=A9=94=EB=89=B4=20=EA=B2=BD=EB=A1=9C?=
=?UTF-8?q?=20=EC=A0=95=EB=B3=B4=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
kjb-docs/inflow/그룹-유량제어-메뉴-추가-경로.md | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 kjb-docs/inflow/그룹-유량제어-메뉴-추가-경로.md
diff --git a/kjb-docs/inflow/그룹-유량제어-메뉴-추가-경로.md b/kjb-docs/inflow/그룹-유량제어-메뉴-추가-경로.md
new file mode 100644
index 0000000..c408040
--- /dev/null
+++ b/kjb-docs/inflow/그룹-유량제어-메뉴-추가-경로.md
@@ -0,0 +1,5 @@
+## 메뉴 추가 정보
+
+* 경로 : 환경정보 > 유량제어
+* 메뉴 명 : 그룹 별 유량제어
+* URL: /monitoring/onl/admin/inflow/inflowGroupControlMan.view
\ No newline at end of file
From 7b3807300b35bdce5e3635319445dcd832175e28 Mon Sep 17 00:00:00 2001
From: pksup
Date: Thu, 11 Dec 2025 13:32:46 +0900
Subject: [PATCH 07/10] =?UTF-8?q?feat:=20API=20=EC=98=88=EC=99=B8=20?=
=?UTF-8?q?=EC=B2=98=EB=A6=AC=20=EB=B0=8F=20=EC=98=A4=EB=A5=98=20=EC=9D=91?=
=?UTF-8?q?=EB=8B=B5=20=ED=98=95=EC=8B=9D=20=ED=91=9C=EC=A4=80=ED=99=94?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Spring @ControllerAdvice를 사용하여 예외 처리 로직 중앙화
- 4xx, 5xx 오류에 대한 일관된 JSON 응답 형식 적용
- elink-online-common MessageUtil 클래스에 makeJsonErrorMessage 메소드 수정
---
WebContent/WEB-INF/web.xml | 4 +
WebContent/error.jsp | 22 ++
elink-online-common | 2 +-
kjb-errors/.gitignore | 32 +++
kjb-errors/build.gradle | 29 +++
kjb-errors/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59821 bytes
.../gradle/wrapper/gradle-wrapper.properties | 6 +
kjb-errors/gradlew | 234 ++++++++++++++++++
kjb-errors/gradlew.bat | 89 +++++++
kjb-errors/settings.gradle | 1 +
.../eai/kjb/errors/BearerTokenErrorTest.java | 168 +++++++++++++
.../errors/BearerTokenFilterErrorTest.java | 41 +++
.../kjb/errors/BearerTokenSuccessTest.java | 67 +++++
.../errors/EAPIM_ErrorResponse_WorkHistory.md | 218 ++++++++++++++++
.../kjb/errors/EAPIM_ExceptionHandler_Test.md | 99 ++++++++
.../kjb/errors/ErrorResponseValidator.java | 42 ++++
.../errors/ExceptionHandler400ErrorTest.java | 44 ++++
.../errors/ExceptionHandler404ErrorTest.java | 44 ++++
.../errors/ExceptionHandlerSuccessTest.java | 60 +++++
.../errors/OAuth2BearerAuth404ErrorTest.java | 78 ++++++
.../eai/kjb/errors/OAuth2BearerAuthTest.java | 83 +++++++
.../kjb/errors/OAuth2TokenSuccessTest.java | 64 +++++
.../eactive/eai/kjb/errors/TestConfig.java | 34 +++
.../controller/ApiAdapterController.java | 20 +-
.../eai/authserver/config/WebMvcConfig.java | 65 +++++
.../authserver/config/WebSecurityConfig.java | 17 ++
.../custom/BearerTokenContoller.java | 17 +-
.../authserver/custom/BearerTokenFilter.java | 22 +-
.../custom/KjbMGOAuth2Controller.java | 31 +--
29 files changed, 1583 insertions(+), 50 deletions(-)
create mode 100644 kjb-errors/.gitignore
create mode 100644 kjb-errors/build.gradle
create mode 100644 kjb-errors/gradle/wrapper/gradle-wrapper.jar
create mode 100644 kjb-errors/gradle/wrapper/gradle-wrapper.properties
create mode 100644 kjb-errors/gradlew
create mode 100644 kjb-errors/gradlew.bat
create mode 100644 kjb-errors/settings.gradle
create mode 100644 kjb-errors/src/test/java/com/eactive/eai/kjb/errors/BearerTokenErrorTest.java
create mode 100644 kjb-errors/src/test/java/com/eactive/eai/kjb/errors/BearerTokenFilterErrorTest.java
create mode 100644 kjb-errors/src/test/java/com/eactive/eai/kjb/errors/BearerTokenSuccessTest.java
create mode 100644 kjb-errors/src/test/java/com/eactive/eai/kjb/errors/EAPIM_ErrorResponse_WorkHistory.md
create mode 100644 kjb-errors/src/test/java/com/eactive/eai/kjb/errors/EAPIM_ExceptionHandler_Test.md
create mode 100644 kjb-errors/src/test/java/com/eactive/eai/kjb/errors/ErrorResponseValidator.java
create mode 100644 kjb-errors/src/test/java/com/eactive/eai/kjb/errors/ExceptionHandler400ErrorTest.java
create mode 100644 kjb-errors/src/test/java/com/eactive/eai/kjb/errors/ExceptionHandler404ErrorTest.java
create mode 100644 kjb-errors/src/test/java/com/eactive/eai/kjb/errors/ExceptionHandlerSuccessTest.java
create mode 100644 kjb-errors/src/test/java/com/eactive/eai/kjb/errors/OAuth2BearerAuth404ErrorTest.java
create mode 100644 kjb-errors/src/test/java/com/eactive/eai/kjb/errors/OAuth2BearerAuthTest.java
create mode 100644 kjb-errors/src/test/java/com/eactive/eai/kjb/errors/OAuth2TokenSuccessTest.java
create mode 100644 kjb-errors/src/test/java/com/eactive/eai/kjb/errors/TestConfig.java
create mode 100644 src/main/java/com/eactive/eai/authserver/config/WebMvcConfig.java
diff --git a/WebContent/WEB-INF/web.xml b/WebContent/WEB-INF/web.xml
index e4e6473..ebf3069 100644
--- a/WebContent/WEB-INF/web.xml
+++ b/WebContent/WEB-INF/web.xml
@@ -48,6 +48,10 @@
authserver
org.springframework.web.servlet.DispatcherServlet
+
+ throwExceptionIfNoHandlerFound
+ true
+
1
diff --git a/WebContent/error.jsp b/WebContent/error.jsp
index 20cdde1..1363dfc 100644
--- a/WebContent/error.jsp
+++ b/WebContent/error.jsp
@@ -1,5 +1,26 @@
+<%@page import="com.eactive.eai.common.util.MessageUtil"%>
<%@ page import="java.io.*, java.util.*"%>
<%@ page language="java" contentType="text/html;charset=utf-8" isErrorPage="true"%>
+<%
+Integer statusCode = (Integer) request.getAttribute("_authServerStatusCode");
+if (statusCode != null) {
+ response.setStatus(statusCode);
+}
+
+String code;
+if (statusCode != null && statusCode == 401) {
+ code = MessageUtil.ERROR_CODE_AUTH_FAIL;
+} else if (statusCode != null && statusCode == 404) {
+ code = MessageUtil.ERROR_CODE_SERVICE_NOT_FOUND;
+} else {
+ code = MessageUtil.ERROR_CODE_AP_ERROR;
+}
+
+String message = (String)request.getAttribute("errorMessage");
+String jsonErrorMessage = MessageUtil.makeJsonErrorMessage(code, message);
+out.println(jsonErrorMessage);
+%>
+<%--
Error
@@ -16,3 +37,4 @@
<% response.setStatus(200); %>
+ --%>
\ No newline at end of file
diff --git a/elink-online-common b/elink-online-common
index aacc1a3..5274ce8 160000
--- a/elink-online-common
+++ b/elink-online-common
@@ -1 +1 @@
-Subproject commit aacc1a389e145453cf92c18e9d7ffef568cf89e7
+Subproject commit 5274ce811d05e63936057a0bece1ca9cf620f0cc
diff --git a/kjb-errors/.gitignore b/kjb-errors/.gitignore
new file mode 100644
index 0000000..08193f7
--- /dev/null
+++ b/kjb-errors/.gitignore
@@ -0,0 +1,32 @@
+.gradle
+build/
+!gradle/wrapper/gradle-wrapper.jar
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+out/
+!**/src/main/**/out/
+!**/src/test/**/out/
+
+### Eclipse ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+bin/
+!**/src/main/**/bin/
+!**/src/test/**/bin/
+
+### VS Code ###
+.vscode/
+
+### Mac OS ###
+.DS_Store
diff --git a/kjb-errors/build.gradle b/kjb-errors/build.gradle
new file mode 100644
index 0000000..a987ef4
--- /dev/null
+++ b/kjb-errors/build.gradle
@@ -0,0 +1,29 @@
+plugins {
+ id 'java'
+}
+
+group = 'com.eactive.eai'
+version = '1.0.0'
+
+java {
+ sourceCompatibility = '17'
+}
+
+tasks.withType(JavaCompile) {
+ options.encoding = 'UTF-8'
+}
+
+repositories {
+ mavenCentral()
+}
+
+dependencies {
+ implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.2'
+ implementation 'org.apache.httpcomponents.client5:httpclient5:5.2.1'
+
+ testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0'
+}
+
+tasks.named('test') {
+ useJUnitPlatform()
+}
diff --git a/kjb-errors/gradle/wrapper/gradle-wrapper.jar b/kjb-errors/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000000000000000000000000000000000000..41d9927a4d4fb3f96a785543079b8df6723c946b
GIT binary patch
literal 59821
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