1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
SELECT
  DUAL.*,
  TO_CHAR(TO_DATE(DUAL.NEXT_DATE, 'YYYY-MM-DD'+ LEVEL - 1'DY') DAYDAY
FROM (
  SELECT
    CASE 
      WHEN TO_CHAR(TO_DATE('2017-12-12''YYYY-MM-DD'), 'D'= '1'
      THEN TO_CHAR(TO_DATE('2017-12-12''YYYY-MM-DD'+ 1'YYYY-MM-DD')
      WHEN TO_CHAR(TO_DATE('2017-12-12''YYYY-MM-DD'), 'D'= '6'
      THEN TO_CHAR(TO_DATE('2017-12-12''YYYY-MM-DD'+ 3'YYY-MM-DD')
      ELSE TO_CHAR(TO_DATE('2017-12-12''YYYY-MM-DD'+ 1'YYYY-MM-DD')
    END AS NEXT_DATE
  FROM
    DUAL
  ) DUAL
CONNECT BY LEVEL <= 1;
cs

파라미터로 입력되는 날짜의 요일이 금요일일 경우, 다음주 월요일의 날짜와 요일을 출력한다.

Oracle 8g부터 지원하며, 트리처럼 가지를 쳐 뻗어 나가는 구조를 계층형 구조라 부르며, 각 단의 상위 계층과 하위 계층의 관계를 오라클에서는 START WITH와 CONNECT BY로 조회가 가능하다.



1. 구문

- START WITH : 최상위 계층이 되는 row를 말한다. 반복 탐색에 대한 시작 지점을 말하며, AND절로 여러개의 시작 조건을 줄 수 있다. 또한 서브 쿼리를 사용할 수 있다.

- CONNECT BY : 계층형 구조가 어떤 식으로 연결되는지를 기술하는 명령어이다. 함께 쓰이는 PRIOR 키워드는 현재의 레코드를 의미한다.  함께 계층 구조로 표현이 가능하다. 서브 쿼리를 사용할 수 없다.

(ex. PRIOR 상위 = 하위 : 상위 노드로의 탐색 / PRIOR 하위 = 상위 : 하위 노드로의 탐색)


* 조건 기술시

1) 일반 쿼리 조건 : 일반 - WHERE 절 / 그룹핑한 결과에 대한 조건 - HAVING

2) 계층 쿼리 조건 : 일반 - WHERE 절 / 계층형 결과에 대한 조건 - CONNECT BY

 


2. 계층형 쿼리를 위해 필요한 3가지

1) 메뉴 순번 (numbering)

2) 해당되는 메뉴의 계층 : 상위 메뉴와 하위 메뉴를 분리함

3) 세부 메뉴의 상위 그룹에 대한 순번 : 조직 구조를 나타내기 위함



3. 계층형 쿼리의 내부 절차

1) JOIN이 있으면 먼저 처리

2) START WITH 절을 참조하여 최상위 계층의 row를 선택

3) CONNECT BY 구문을 따라 계층 관계를 파악

4) JOIN을 제외한 WHERE 절에 해당하는 row를 걸러냄 (row별로 조건에 맞지 않으면 걸러냄)

  (WHERE는 탐색을 완료한 후 데이터를 필터링하는 키워드이므로, 상황에 따라 불필요한 탐색을 줄일 시에는 CONNECT BY 절에 기술하는 것이 성능상 유리함)



3. 예제

1) 테이블 CREATE




각 지부와 그 지부에 따른 실적을 나타내는 컬럼으로 이루어진 DEPT 테이블과(부서) RESULT 테이블(실적)이 있다고 가정한다.


2) 데이터 INSERT

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
-- DEPT 테이블 PK 추가
CREATE INDEX IDX_DEPT_01 ON DEPT (UPPER_DEPT_CODE);
 
-- RESULT 테이블 PK 추가
ALTER TABLE RESULT ADD CONSTRAINT PK_RESULT PRIMARY KEY (YEAR_DATE, DEPT_CODE);
 
-- DEPT 테이블 DATA INSERT
INSERT INTO DEPT
 SELECT '1000''대표이사'NULL FROM DUAL UNION ALL
 SELECT '1100''영업1본부''1000' FROM DUAL UNION ALL
 SELECT '1200''영업2본부''1000' FROM DUAL UNION ALL
 SELECT '1110''국내영업1부''1100' FROM DUAL UNION ALL
 SELECT '1120''국내영업2부''1100' FROM DUAL UNION ALL
 SELECT '1210''해외영업1부''1200' FROM DUAL UNION ALL
 SELECT '1220''해외영업2부''1200' FROM DUAL UNION ALL
 SELECT '1111''수도권지부''1110' FROM DUAL UNION ALL
 SELECT '1112''강원지부''1110' FROM DUAL UNION ALL
 SELECT '1121''호남지부''1120' FROM DUAL UNION ALL
 SELECT '1122''경남지부''1120' FROM DUAL;
 
COMMIT;
 
-- RESULT 테이블 DATA INSERT
INSERT INTO RESULT
 SELECT '2017.11''1210'1000 FROM DUAL UNION ALL
 SELECT '2017.11''1220'1000 FROM DUAL UNION ALL
 SELECT '2017.11''1111'1000 FROM DUAL UNION ALL
 SELECT '2017.11''1112'1000 FROM DUAL UNION ALL
 SELECT '2017.11''1121'1000 FROM DUAL UNION ALL
 SELECT '2017.11''1122'1000 FROM DUAL;
 
COMMIT;
cs


3) 쿼리 조회

1
2
3
4
5
6
7
8
9
10
SELECT
  LEVEL,
  DEPT_CODE,
  DEPT_NAME,
  UPPER_DEPT_CODE
FROM
  DEPT
START WITH UPPER_DEPT_CODE IS NULL
CONNECT BY PRIOR DEPT_CODE = UPPER_DEPT_CODE
ORDER BY LEVEL;
cs

- 각 DEPT_CODE를 LEVEL로 계층화하여 조회


1
2
3
4
5
6
7
8
9
10
11
12
13
SELECT
  DEPT_CODE,
  DEPT_NAME,
  UPPER_DEPT_CODE
FROM
  DEPT
START WITH DEPT_CODE = (SELECT DEPT_CODE
                        FROM DEPT
                        WHERE UPPER_DEPT_CODE IS NULL
                        START WITH DEPT_CODE = '1120'
                        CONNECT BY PRIOR UPPER_DEPT_CODE = DEPT_CODE)
CONNECT BY PRIOR DEPT_CODE = UPPER_DEPT_CODE;                        
 
cs

- 최상위 노드 > 각 노드별 하위 계층 조회



4. 오라클 10g부터 계층 구조 쿼리에서 추가되는 기능

1) CONNECT_BY_ROOT : LEVEL이 가장 높은 최상위의 row 정보를 불러오는 키워드

1
2
3
4
5
6
7
8
9
10
SELECT
  LEVEL,
  CONNECT_BY_ROOT DEPT_CODE "ROOT DEPT_CODE",
  DEPT_CODE,
  DEPT_NAME,
  UPPER_DEPT_CODE
FROM
  DEPT
START WITH DEPT_NAME = '대표이사'
CONNECT BY PRIOR DEPT_CODE = UPPER_DEPT_CODE;
cs

DEPT_NAME = '대표이사'를 기준으로 시작하여, DEPT_CODE (부서)와 UPPER_DEPT_CODE (상위부서코드)의 관계를 계층 구조로 조회했다. 따라서 위 그림에서 적색으로 표시한 부분을 보면 부서들이 모두 최상위 코드인 1000 (대표이사)를 가리키는 것을 확인할 수 있다.


2) CONNECT_BY_ISLEAF : row의 최하위 레벨 여부를 리턴한다. 최하위 레벨이면 1, 최하위 레벨이 아니면 0이다.

1
2
3
4
5
6
7
8
9
10
SELECT
  LEVEL,
  CONNECT_BY_ISLEAF "LEAF",
  DEPT_CODE,
  DEPT_NAME,
  UPPER_DEPT_CODE
FROM
  DEPT
START WITH DEPT_NAME = '대표이사'
CONNECT BY PRIOR DEPT_CODE = UPPER_DEPT_CODE;
cs


DEPT_NAME = '대표이사'를 기준으로 시작하여, DEPT_CODE (부서)와 UPPER_DEPT_CODE (상위부서코드)의 관계를 계층 구조로 조회했다. 따라서 위 그림에서 적색으로 표시한 부분을 보면 부서들이 최하위에 있는 부서인 경우 1로 표기되고, 최하위에 있지 않은 부서인 경우 0으로 표기된다. (상단 조직도 참고)


3) SYS_CONNECT_BY_PATH : 현재 row까지의 경로를 쉽게 얻어올 수 있는 키워드이다.

1
2
3
4
5
6
7
8
9
10
SELECT
  LEVEL,
  SYS_CONNECT_BY_PATH(DEPT_NAME, '/'"PATH",
  DEPT_CODE,
  DEPT_NAME,
  UPPER_DEPT_CODE
FROM
  DEPT
START WITH DEPT_NAME = '대표이사'
CONNECT BY PRIOR DEPT_CODE = UPPER_DEPT_CODE;
cs


4) ORDER SIBLING BY : 계층구조 쿼리에서 상관 관계를 유지한 상태로 ORDER BY를 할 수 있는 키워드.

1
2
3
4
5
6
7
8
9
10
SELECT
  LEVEL,
  DEPT_CODE,
  DEPT_NAME,
  UPPER_DEPT_CODE
FROM
  DEPT
START WITH DEPT_NAME = '대표이사'
CONNECT BY PRIOR DEPT_CODE = UPPER_DEPT_CODE
ORDER SIBLINGS BY DEPT_NAME;
cs

* ORDER BY로 정렬하면 계층구조 관계가 유지되지 않는다.


1
2
3
4
5
SELECT
    -- 문자열 끼워넣기
    REGEXP_REPLACE('0927''(^.{2})''\1-') AS DAYDAY
FROM
    DUAL;
cs

 

 

SQL Developer에서 어느 순간부터 오라클 접속이 안되기 시작하면서, 제목과 같이 the password has expired라는 오류가 계속 뜨기 시작했다.

귀찮아서 계속 외면했으나 오라클 공부를 위해 해결법을 찾아보기로 했다.


1. Oracle sqlplus에서 문제점 해결

cmd에서 sql plus에 접속한다. 이 때, 접속하는 명령어는 다음과 같다.

 sqlplus "/as sysdba

 


접속 후, 아래의 명령어를 이용해 기간이 만료된 계정의 비밀번호를 변경한다.

 alter user (userName) identified by (password);

 

 

2. 접속 확인

다시 오라클 프로그램에 접속해 바꾼 비밀번호를 입력하고 해당 계정이 잘 접속되는지 확인한다.

1
2
3
4
5
6
<!-- 해당 달 날짜를 THIS_MONTH로 출력 -->
SELECT
  TRUNC (SYSDATE, 'MM'+ LEV - 1 AS THIS_MONTH
FROM (SELECT LEVEL AS LEV
      FROM DUAL
      CONNECT BY LEVEL <= TO_CHAR (LAST_DAY (SYSDATE), 'DD'));
cs






1. 24시간 조회

1
2
3
4
5
6
<!-- 24시간을 A_HOUR로 조회 -->
SELECT
  TO_CHAR(LEVEL, '00') AS A_HOUR
FROM
  DUAL
CONNECT BY LEVEL <= 24;
cs



2. 5분 단위의 분 조회 (00분 ~ 55분)

1
2
3
4
5
6
7
8
9
10
11
<!-- 5분 단위의 00분 ~ 55분을 A.MNT로 조회
    전체 범위를 0에서 55까지 구한 뒤, 5로 나눈 나머지가 0인 것들로만 받다 -->
SELECT
  A.MNT
FROM (SELECT
        TO_CHAR(LEVEL, '00') AS MNT
      FROM
        DUAL
      CONNECT BY LEVEL <= 55) A
WHERE
  MOD(A.MNT, 5= 0;
cs



1. 금주의 월~금요일과 월~금요일 날짜 조회

1
2
3
4
5
6
7
8
<!-- 금주 월 ~ 금요일을 컬럼 DAYDAY로, 금주 월 ~ 금 날짜를 WEEKLY_DT로 조회 -->
SELECT
  TO_CHAR(SYSDATE - 7 + LEVEL + (SELECT NEXT_DAY(SYSDATE, 1- SYSDATE
                                  FROM DUAL), 'DY''NLS_DATE_LANGUAGE=korean') AS DAYDAY,
  TO_CHAR(SYSDATE - 7 + LEVEL + (SELECT NEXT_DAY(SYSDATE, 1- SYSDATE
                                  FROM DUAL), 'MM/DD') AS WEEKLY_DT
FROM DUAL
CONNECT BY LEVEL <= 5;
cs




2. 금주 월~금요일 날짜 조회

1
2
3
4
5
6
<!-- 금주 월 ~ 금 날짜를 WEEKLY_DT로 조회 -->
SELECT
  TO_CHAR(SYSDATE - 7 + LEVEL + (SELECT NEXT_DAY(SYSDATE, 1- SYSDATE
                                  FROM DUAL), 'YYYY-MM-DD') AS WEEKLY_DT
FROM DUAL
CONNECT BY LEVEL <= 5;
cs


 

 

3. 해당 날짜에 맞는 요일 조회

1
2
3
4
5
SELECT
    TO_CHAR(TO_DATE('20170927''YYYYMMDD'+ LEVEL - 1'DY') DY
FROM
    DUAL
CONNECT BY LEVEL <= 1;
cs

\

 

HashMap은 입력된 데이터를 출력할 때 key값을 가지고 출력하기 때문에 출력하는 순서가 정해져 있지 않으나,

LinkedHashMap은 입력한 순서대로 데이터가 쌓여 있는 것을 확인할 수 있는 객체이다. 또한 HashMap의 장점을 그대로 가지고 있다.


Iterator로 출력시에도 HashMap은 순서가 없으나 LinkedHashMap은 입력된 순서에 맞게 값을 출력한다.

동일한 key의 중복을 막아주고, 특정 key의 값을 찾고자 할 떄 for문을 들어 전체 값을 검색할 필요가 없다.


순서대로 입력되어 있기 때문에, 전체 값을 순서대로 operation 해야할 때 유용하다.

HashMap 클래스를 상속 받아 만들어져 있고, Iterator를 사용하여 가져오더라도 입력시의 순서에 맞게 값을 가져올 수 있다.

LinkedList로 저장되며, HashMap과 마찬가지로 다중스레드를 사용시 Synchronized가 되지 않는다.


JDK 1.4 이상부터 사용 가능한 Collection 객체이다.


* removeEldestEntry() : put() 할 때 불리는 메소드. 들어온 순서를 기억하고 가장 오래된 값을 그 자리에 방금 들어온 값으로 대체하는 메소드.



2. 예제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
 
public class LinkedHashMapExample {
    public static void initData(Map<StringString> map) {
        map.put("key1""1");
        map.put("key2""2");
        map.put("key3""3");
        map.put("key4""4");
        map.put("key5""5");
        map.put("key6""6");
        map.put("key7""7");
        map.put("key8""8");
        map.put("key9""9");
        map.put("key10""10");
    }
    
    public static void printResult(Map<StringString> map) {
        Set<String> set = map.keySet();
        Iterator<String> iter = set.iterator();
        
        while (iter.hasNext()) {
            String key = ((String)iter.next());
            String value = map.get(key);
            
            System.out.println("key : " + key + ", value : " + value);
        }
    }
    
    public static void main(String[] args) {
        System.out.println("==========HashMap Test=========");
        
        Map<StringString> hashMap = new HashMap<StringString>();
        
        initData(hashMap);
        
        printResult(hashMap);
        
        System.out.println("==========LinkedHashMap Test=====");
        
        Map<StringString> linkedHashMap = new LinkedHashMap<StringString>();
        
        initData(linkedHashMap);
        
        printResult(linkedHashMap);
    }
}
cs


포트폴리오 중 하나인 그룹웨어 시스템에서 로그인시 보안을 위해 간단하게라도 암호화 알고리즘을 구현해야겠다고 생각했다.

 

알고리즘의 종류에는 대칭 암호화와 비대칭 암호화가 있는데,

대칭 암호화는 빠르다는 장점이 있지만 자주 키를 바꿔야 해 관리가 어렵다는 단점이 있고

비대칭 암호화는 대칭에 비해 다소 느리지만 안전하고 키 관리가 단순하다는 장점이 있다.

 

계정 정보가 해킹당할 시 사내 정보가 유출될 수 있다는 점을 염두해 안전하다는 부분에 초점을 맞췄고,

비대칭 암호화인 RSA 알고리즘을 구현하기로 했다.  

 

 

* RSA 라이브러리 참고 사이트 : http://www-cs-students.stanford.edu/~tjw/jsbn/ 

 

 

1. 로그인 전 공개키 생성


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@Controller
public class HomeController {
    final static Logger log = LoggerFactory.getLogger(HomeController.class);
 
    /**
     * @author GREEN GO
     * @since 2017.03.14
     */
    @RequestMapping(value = "/", method = RequestMethod.GET)
    public String homeCtrl(Locale locale, HttpServletRequest request, HttpServletResponse response, Model model,
            HttpSession session) throws Exception, NoSuchAlgorithmException {
 
        session = request.getSession();
 
        KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
 
        generator.initialize(1024);
 
        KeyPair keyPair = generator.genKeyPair();
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PublicKey publicKey = keyPair.getPublic();
        PrivateKey privateKey = keyPair.getPrivate();
 
        // RSA 개인키
        session.setAttribute("_RSA_WEB_Key_", privateKey);
 
        log.debug("HomeController _RSA_WEB_Key_ : " + request.getAttribute("setAttribute"));
 
        RSAPublicKeySpec publicSpec = (RSAPublicKeySpec) keyFactory.getKeySpec(publicKey, RSAPublicKeySpec.class);
 
        String publicKeyModulus = publicSpec.getModulus().toString(16);
        String publicKeyExponent = publicSpec.getPublicExponent().toString(16);
 
        // 로그인 폼 hidden setting
        request.setAttribute("RSAModulus", publicKeyModulus);
        // 로그인 폼 hidden setting
        request.setAttribute("RSAExponent", publicKeyExponent);
 
        log.debug("HomeController RSAModulus getAttribute : " + request.getAttribute("RSAModulus"));
        log.debug("HomeController RSAExponent getAttribute : " + request.getAttribute("RSAExponent"));
 
        return "member/mm_login";
    }
}
cs


로그인 페이지가 열리기 전 컨트롤러에서 공개키를 생성한 후, 로그인 form에 request 범위에 담아 hidden 타입으로 적어준다. 

 

 

2. 로그인


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!DOCTYPE HTML>
<html>
<head>
    <link href="<c:url value='/resources/css/bootstrap.min.css'/>" rel="stylesheet">
    <link href="<c:url value='/resources/css/landing-page.css'/>" rel="stylesheet">
    <link href="<c:url value='/resources/font-awesome/css/font-awesome.min.css'/>" rel="stylesheet" type="text/css">
    <link href="https://fonts.googleapis.com/css?family=Lato:300,400,700,300italic,400italic,700italic" rel="stylesheet" type="text/css">
    
    <!-- RSA javascript library / cdn 올리는 순서 틀리면 안 됨 -->
    <script src="<c:url value='/resources/js/rsa/jsbn.js'/>"></script>
    <script src="<c:url value='/resources/js/rsa/rsa.js'/>"></script>
    <script src="<c:url value='/resources/js/rsa/prng4.js'/>"></script>
    <script src="<c:url value='/resources/js/rsa/rng.js'/>"></script>
    
    <script src="<c:url value='/resources/js/jquery-3.1.1.min.js'/>"></script>
    <script>
        $(document).on('click','#loginBtn',function() {
        
        // 로그인 버튼 클릭시 서버로 전송되기 전에 계정 정보 암호화 후 서버로 전송
        // $("#loginBtn").click(function() {
            var mmCode = $("#mmCode").val();
            var mmPassword = $("#mmPassword").val();
         
            // RSA 암호키 생성
            var rsa = new RSAKey();
            
            rsa.setPublic($("#RSAModulus").val(), $("#RSAExponent").val());
            
            // 계정 정보 암호화
            var secondMmCode = rsa.encrypt(mmCode);
            var secondMmPassword = rsa.encrypt(mmPassword);
            
            // console.log('secondMmCode : ' + secondMmCode);
            // console.log('secondMmPassword : ' + secondMmPassword);
            
            // Restful 기술을 이용한 컨트롤러를 이용해 ajax로 form 정보를 전송하고, 유효성 검사 후
            // 아이디, 비밀번호 일치하면 메인 페이지로 이동 (실패시 alert 창 띄움)
            $.ajax({ 
                  type: "post",  
                  url: "/smart/mm/loginRSA",
                  dataType: "json",
                  data: {"secondMmCode": secondMmCode, "secondMmPassword": secondMmPassword},
                  success: function(data) {    
                      
//                       alert('data.state : ' + data.state);
                     
                      if(data.state == true) {
                          location.href = "<c:url value='/member/mm_login'/>"
                      } else if(data.state == false) {
                         THIS.oWin.alert("로그인","로그인에 실패했습니다. <br> 아이디와 패스워드를 확인하세요.");
                      } else {
                         THIS.oWin.alert("로그인","잘못된 경로로 접근했습니다. <br>암호화 인증에 실패했습니다."); 
                      } 
                  } 
            });
        });
    </script>
</head>
<body>
    <div class="intro-header">
       <div class="container">
        <div class="row">
               <div class="col-lg-12">
                   <div class="intro-message">
                       <h1>Smart Groupware-System</h1>
                       <hr class="intro-divider">
                    <div class="col-md-4 col-md-offset-4">
                        <div class="panel panel-default">
                            <div class="panel-body">
                                <h5 class="text-center" style="color:black;">SIGN UP</h5>
                                <!-- 컨트롤러에서 생성했던 공개키를 form에 hidden 타입으로 배치-->
                                <form class="form form-signup" role="form" id="mmlogin" action="member/mm_login" method="post">
                                    <div class="form-group">
                                        <input type="hidden" id="RSAModulus" value="${RSAModulus}">
                                        <input type="hidden" id="RSAExponent" value="${RSAExponent}">
                                        <div class="input-group">
                                            <span class="input-group-addon"><span class="glyphicon glyphicon-user"></span></span>
                                            <input type="text" class="form-control" name="mmCode" id="mmCode" value="2"/>
                                        </div>
                                    </div>
                                    <div class="form-group">
                                        <div class="input-group">
                                            <span class="input-group-addon"><span class="glyphicon glyphicon-lock"></span></span>
                                            <input type="password" class="form-control" name="mmPassword" id="mmPassword" value="123456"/>
                                        </div>
                                    </div>
                                    <input type="button" id="loginBtn" class="btn btn-sm btn-primary btn-block" value="로그인"/>
                                </form>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>
</body>
</html>
cs


컨트롤러에서 jsp 페이지로 보낸 hidden 타입들을 form 내에 배치하고, hidden 타입 데이터와 계정 정보를 ajax로 보내

Restful 기술을 이용한 컨트롤러에서 DB 정보와 비교한 후 성공시 메인 페이지로 이동하고 실패시 alert 창을 띄운다.

 

 

3. 계정 정보 복호화


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
package com.cafe24.smart;
 
import javax.crypto.Cipher;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
 
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
 
import java.security.PrivateKey;
import java.util.HashMap;
import java.util.Map;
 
import com.cafe24.smart.member.domain.Department;
import com.cafe24.smart.member.domain.Member;
import com.cafe24.smart.member.service.MemberService;
 
@RestController
public class HomeRestController {
    private static final Logger log = LoggerFactory.getLogger(HomeRestController.class);
 
    @Autowired
    MemberService memberService;
 
    // 로그인 체크
    @RequestMapping(value = "mm/loginRSA", method = RequestMethod.POST)
    @ResponseBody
    // 암호화된 키들을 다시 복호화 하여 DB 값들과 비교 => 있으면 로그인
    public Map<String, Object> mmLoginRSACtrl(HttpServletRequest request, Map<String, Object> map) {
 
        Map<String, Object> params = new HashMap<String, Object>();
 
        String mmCode = request.getParameter("secondMmCode");
        String mmPassword = request.getParameter("secondMmPassword");
 
        HttpSession session = request.getSession();
 
        // 로그인 전에 세션에 저장했던 개인키를 getAttribute
        PrivateKey privateKey = (PrivateKey) session.getAttribute("_RSA_WEB_Key_");
 
        log.debug("HomeRestController mmLoginRSACtrl privateKey : " + privateKey);
 
        // 개인키(아이디)가 들어오지 않은 경우
        if (privateKey == null) {
            
            params.put("state"false);
            // 개인키(아이디)가 들어온 경우
        } else {
            try {
                // 암호화 처리된 사용자 계정을 복호화 처리
                int _mmCode = Integer.parseInt(decryptRsa(privateKey, mmCode));
                int _mmPassword = Integer.parseInt(decryptRsa(privateKey, mmPassword));
 
                Member member = new Member();
                Department department = new Department();
                member.setMmCode(_mmCode);
 
                // 복호화한 사원코드로 값을 가져와서 DB의 사원 패스워드와 같은지 비교
                params = memberService.mmLoginServ(member);
 
                log.debug("HomeRestController mmLoginRSACtrl _mmCode : " + _mmCode);
                log.debug("HomeRestController mmLoginRSACtrl _mmPassword : " + _mmPassword);
 
                if (params != null) {
                    log.debug("mmLoginRSACtrl params not null");
 
                    session.setAttribute("mmCode", _mmCode);
                    // session.setAttribute("mmName", params.get("mmName"));
 
                    member = memberService.mmContentByMmCodeServ(_mmCode);
                    session.setAttribute("mmName", member.getMmName());
 
                    // 부서정보세팅
                    department = memberService.mmDpDetailServ(member.getDpCode());
                    session.setAttribute("dpName", department.getDpName());
                }
 
                log.debug("mmLoginRSACtrl mmCode session : " + session.getAttribute("mmCode"));
                log.debug("mmLoginRSACtrl mmCode session : " + session.getAttribute("mmName"));
 
                params.put("state"true);
            } catch (Exception e) {
                params.put("state"false);
 
                e.printStackTrace();
            }
        }
 
        log.debug("HomeRestController mmLoginRSACtrl params : " + params);
 
        return params;
    }
 
    public String decryptRsa(PrivateKey privateKey, String securedValue) {
 
        String decryptedValue = "";
 
        log.debug("mmLoginRSACtrl decryptRsa privateKey : " + privateKey);
        log.debug("mmLoginRSACtrl decryptRsa securedValue : " + securedValue);
 
        try {
            Cipher cipher = Cipher.getInstance("RSA");
 
            // 암호화 된 값 : byte 배열
            // 이를 문자열 form으로 전송하기 위해 16진 문자열(hex)로 변경
            // 서버측에서도 값을 받을 때 hex 문자열을 받아 다시 byte 배열로 바꾼 뒤 복호화 과정을 수행
            byte[] encryptedBytes = hexToByteArray(securedValue);
 
            cipher.init(Cipher.DECRYPT_MODE, privateKey);
 
            byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
 
            // 문자 인코딩
            decryptedValue = new String(decryptedBytes, "utf-8");
        } catch (Exception e) {
            e.printStackTrace();
        }
 
        log.debug("mmLoginRSACtrl decryptRsa decryptedValue : " + decryptedValue);
 
        return decryptedValue;
    }
 
    // 16진 문자열을 byte 배열로 변환
    public static byte[] hexToByteArray(String hex) {
 
        if (hex == null || hex.length() % 2 != 0) {
            return new byte[] {};
        }
 
        byte[] bytes = new byte[hex.length() / 2];
 
        log.debug("mmLoginRSACtrl hexToByteArray bytes : " + bytes);
 
        for (int i = 0; i < hex.length(); i += 2) {
            byte value = (byte) Integer.parseInt(hex.substring(i, i + 2), 16);
 
            bytes[(int) Math.floor(i / 2)] = value;
        }
 
        log.debug("mmLoginRSACtrl hexToByteArray final bytes : " + bytes);
 
        return bytes;
    }
}
cs


계정 정보와 hidden 타입으로 받아온 공개키들을 복호화한다.


'BackEnd > Java' 카테고리의 다른 글

Java :: NULL vs isEmpty() 차이  (0) 2018.01.16
LinkedHashMap  (0) 2017.06.29
Java :: Spring 파일 업로드 (multipart-form/data)  (2) 2017.03.14
가변인수 (Java 5.0 이상)  (0) 2017.02.13
Java :: 열거형 (Enumeration) (Java 5.0 이상)  (0) 2017.02.13

스프링 프레임워크의 내장 객체인 CommonsMultipartResolver 를 이용한 파일 업로드 방식이다.


1. pom.xml 설정

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
 
    <!-- MultipartHttpServletRequset -->
    <dependencies>
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.0.1</version>
        </dependency>
 
        <dependency>
            <groupId>commons-fileupload</groupId>
            <artifactId>commons-fileupload</artifactId>
            <version>1.2.2</version>
        </dependency>
    </dependencies>
 
</project>
cs

pom.xml 파일에서

ㄱ. commons-io.jar

ㄴ. commons-fileupload.jar

두 가지 .jar를 추가한다.



2. context.xml 설정

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:mvc="http://www.springframework.org/schema/mvc"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns="http://www.springframework.org/schema/beans"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
 
    <!-- MultipartResolver -->
    <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
        <property name="maxUploadSize" value="100000000"/>
        <property name="maxInMemorySize" value="100000000"/>
    </bean>
 
</beans>
cs

context.xml에서 CommonsMultipartResolver 객체를 추가한다.


CommonsMultipartResolver는 스프링 프레임워크에 내장되어 있는 MultipartResolver 이다.


* maxUploadSize : 한 번에 최대 올릴 수 있는 파일 사이즈

  maxInMemorySize : 해당 경로에 최대로 저장할 수 있는 파일 사이즈



3. form 파일 업로드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<html>
<head>
    <title>title</title>
</head>
<body>
 
    <!-- form enctype="multipart/form-data" 을 꼭 적어줘야 함 -->
    <form class="form-horizontal" method="post" action="<c:url value='/re/add'/>" enctype="multipart/form-data">
        <!-- input type="file" 이라고 꼭 저어줘야 함 -->
        <input type="file" class="form-control1" id="uploadFile" name="uploadFile" style="border:0px solid black;"/>
        
        <button type="submit" class="btn btn-default">등록</button>
        <button type="reset" class="btn btn-default">취소</button>
    </form>
 
</body>
</html>
cs

form enctype="multipart/form-data"라고 꼭 적어줘야 한다. 그렇지 않으면 form이 전송되지 않는다.

마찬가지로 전송할 파일 타입도 file이라고 적어줘야 한다.



4. MultipartResolver 클래스 작성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
package com.cafe24.smart.util;
 
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.URLEncoder;
import java.util.Map;
 
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
 
import org.springframework.stereotype.Component;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.servlet.view.AbstractView;
 
import com.cafe24.smart.approve.domain.Draft;
import com.cafe24.smart.reward.domain.Reward;
 
// @Component > @Service
//              : 스프링 프레임워크가 관리하는 컴포넌트의 일반적 타입 
//              : 개발자가 직접 조작이 가능한 클래스의 경우 해당 어노테이션을 붙임
//              : ( <=> @Bean : 개발자가 조작이 불가능한 외부 라이브러리를 Bean으로 등록시 사용)
@Component
public class UtilFile {
    String fileName = "";
    
//  프로젝트 내 지정된 경로에 파일을 저장하는 메소드
//  DB에는 업로드된 전체 경로명으로만 지정되기 때문에(업로드한 파일 자체는 경로에 저장됨)
//  fileUpload() 메소드에서 전체 경로를 리턴받아 DB에 경로 그대로 저장   
    public String fileUpload(MultipartHttpServletRequest request,
                                        MultipartFile uploadFile, Object obj) {
        String path = "";
        String fileName = "";
        
        OutputStream out = null;
        PrintWriter printWriter = null;
        
        try {
            fileName = uploadFile.getOriginalFilename();
            byte[] bytes = uploadFile.getBytes();
            path = getSaveLocation(request, obj);
            
            System.out.println("UtilFile fileUpload fileName : " + fileName);
            System.out.println("UtilFile fileUpload uploadPath : " + path);
            
            File file = new File(path);
            
//          파일명이 중복으로 존재할 경우
            if (fileName != null && !fileName.equals("")) {
                if (file.exists()) {
//                    파일명 앞에 업로드 시간 초단위로 붙여 파일명 중복을 방지
                    fileName = System.currentTimeMillis() + "_" + fileName;
                    
                    file = new File(path + fileName);
                }
            }
            
            System.out.println("UtilFile fileUpload final fileName : " + fileName);
            System.out.println("UtilFile fileUpload file : " + file);
            
            out = new FileOutputStream(file);
            
            System.out.println("UtilFile fileUpload out : " + out);
            
            out.write(bytes);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (out != null) {
                    out.close();
                }
                if (printWriter != null) {
                    printWriter.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        
        return path + fileName;
    }
    
//  업로드 파일 저장 경로 얻는 메소드
//  업로드한 파일의 경로가 도메인 별로 달라야 했기 때문에 도메인의 형을 비교하여 파일 저장 정로를 다르게 지정함
    private String getSaveLocation(MultipartHttpServletRequest request, Object obj) {
        
        String uploadPath = request.getSession().getServletContext().getRealPath("/");
        String attachPath = "resources/files/";
        
//      Reward인 경우
        if (obj instanceof Reward) {
            attachPath += "reward/";
//      Approval인 경우
        } else if(obj instanceof Draft) {
            attachPath += "approval/";
//      Document인 경우            
        } else {
            attachPath += "document/";
        }
        
        System.out.println("UtilFile getSaveLocation path : " + uploadPath + attachPath);
        
        return uploadPath + attachPath;
    }
}
cs

시간이 촉박했기 때문에 파일의 경로를 암호화하는 UUID 객체는 사용하지 않았다.


getSaveLocation() 메소드에서 저장될 경로를 지정할 때 해당 프로젝트의 경로를 변수 uploadPath로 값 복사하였는데

로컬에서는 (본인의 컴퓨터) 에서는 아무리 해당 경로를 찾아봐도 업로드 했던 파일이 나오지 않는다.

그러나 cafe24 같은 서버에 프로젝트를 호스팅 하면 지정 경로로 업로드가 잘 되니 걱정하지 않아도 된다.



5. form에서 보낸 파일 업로드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Controller
public class RewardController {
//  파일을 업로드하는 컨트롤러 클래스 메소드
    @RequestMapping(value = "re/add", method = RequestMethod.POST)
//  인자로 MulfiPartFile 객체, MultipartHttpServletRequest 객체, 업로드 하려는 도메인 클래스를 받는다
    public String reAddProCtrl(@RequestParam("uploadFile") MultipartFile uploadFile,
                                    MultipartHttpServletRequest request, Reward reward) {
        
        System.out.println("RewardController reAddProCtrl uploadFile : " + uploadFile);
        System.out.println("RewardController reAddProCtrl reward : " + reward);
        
//      UtilFile 객체 생성
        UtilFile utilFile = new UtilFile();
        
//      파일 업로드 결과값을 path로 받아온다(이미 fileUpload() 메소드에서 해당 경로에 업로드는 끝났음)
        String uploadPath = utilFile.fileUpload(request, uploadFile, reward);
        
//      해당 경로만 받아 db에 저장
        int n = rewardService.reAddServ(uploadPath, reward);
        
        System.out.println("RewardController reAddProCtrl n : " + n);
        System.out.println("RewardController reAddProCtrl uploadPath : " + uploadPath);
        
        return "redirect:listAll";
    }
}
cs

이로서 업로드가 끝난다.

'BackEnd > Java' 카테고리의 다른 글

LinkedHashMap  (0) 2017.06.29
RSA 암호화 구현  (0) 2017.03.14
가변인수 (Java 5.0 이상)  (0) 2017.02.13
Java :: 열거형 (Enumeration) (Java 5.0 이상)  (0) 2017.02.13
초기화 블록 (Initialization block)  (0) 2017.02.13

+ Recent posts