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
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>rowspan example</title>
<script src="https://code.jquery.com/jquery-2.2.1.js"></script>
<script type="text/javascript">
 
$(function(){
    $('#rowTable').each(function() {
        var table = this;
        // rowspan할 tr(세로 기준)
        $.each([1,4], function(c, v) {
            var tds = $('>tbody>tr>td:nth-child(' + v + ')', table).toArray();
            var i = 0, j = 0;
            
            for(j = 1; j < tds.length; j ++) {
                if(tds[i].innerHTML != tds[j].innerHTML) {
                    $(tds[i]).attr('rowTable', j - i);
                    i = j;
                    continue;
                }
                $(tds[j]).hide();
            }
            j --;
            
            if(tds[i].innerHTML == tds[j].innerHTML) {
                $(tds[i]).attr('rowTable', j - i + 1);
            }
        });
    });
});
 
</script>
</head>
<body>
    <h1>중복 데이터 셀 병합</h1>    
    <!-- table id를 적고 제이쿼리에서 id 값으로 불러온다. -->
    <table border="1" style="border-collapse:collapse;border:1px gray solid;" id="rowTable">
        <thead>
            <tr>
                <th>제목</th>
                <th>제목</th>
                <th>제목</th>
                <th>제목</th>
            </tr>
        </thead>
        <tbody>
            <tr>
                <td>1</td>
                <td>2</td>
                <td>3</td>
                <td>4</td>
            </tr>
            <tr>
                <td>a</td>
                <td>b</td>
                <td>c</td>
                <td>d</td>
            </tr>
            <tr>
                <td>a</td>
                <td>2</td>
                <td>3</td>
                <td>d</td>
            </tr>
        </tbody>
    </table>
</body>
</html>
cs

colspan도 이와 같은 방식으로 적용시키면 된다.

1. 정의

엑셀 파일 템플릿을 이용해 엑셀 파일을 쉽게 다운로드 하는 패키지이다.

또한, XML 설정 파일을 통해 엑셀 파일 데이터를 자바 객체로 읽는 장치도 제공한다.

jXLS 라이브러리는 POI 패키지를 기반으로 동작하기 때문에 jXLS 라이브러리 사용시 POI 라이브러리도 함께 사용해야 한다.

XTLS 객체를 사용하는 방식을 선호하는 이유는, 자바의 MVC 패턴을 그대로 활용 가능하기 때문이다.

기존 코드에 view 단을 담당하는 jsp 대신 엑셀 파일을 view로 활용하여 처리하면 된다.

템플릿을 기반으로 하여 엑셀 파일을 그대로 사용 가능하기 때문에 엑셀 기능의 대부분도 사용 가능하다.



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
    <dependency>
        <groupId>net.sf.jxls</groupId>
        <artifactId>jxls-core</artifactId>
        <version>1.0.6</version>
    </dependency>
    <dependency>
        <groupId>org.jxls</groupId>
        <artifactId>jxls-poi</artifactId>
        <version>1.0.9</version>
    </dependency>
    <dependency>
        <groupId>org.jxls</groupId>
        <artifactId>jxls-jexcel</artifactId>
        <version>1.0.6</version>
    </dependency>
    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi</artifactId>
        <version>3.14</version>
    </dependency>
    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi-ooxml</artifactId>
        <version>3.14</version>
    </dependency>
cs

pom.xml 파일에 다섯 개의 라이브러리를 추가해준다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    @RequestMapping(value = "/listExcel.do")
    public void listExcel(HttpServletRequest request,
            HttpServletResponse response, UserSearchVO searchVO,
            ModelMap modelMap) {

        // Service의 해당 메소드로 실현될 쿼리로는 list select문을 그대로 사용하면 된다.
        List<?> listUser = userService.selectUserList(searchVO);
        Map<String, Object> beans = new HashMap<String, Object>();
        
        beans.put("listUser", listUser);
        
        // 엑셀 다운로드 메소드가 담겨 있는 객체
        MakeExcel me = new MakeExcel();
        
        // 인자로 request, response, Map Collection 객체, 파일명, 폴더명, 견본파일을 받는다.
        me.download(request, response, beans, "파일명""폴더명""견본 파일.확장자");
    }
cs


엑셀 다운로드 컨트롤러이다. jsp에 listExcel.do로 하이퍼링크를 걸어준 뒤 클릭하면 해당 컨트롤러로 이동하게 된다.

엑셀 다운로드가 담겨 있는 MakeExcel 이라는 객체는 이미 존재하는 라이브러리가 아닌 다운로드를 위해 임의로 만든 객체이다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class MakeExcel {
    public void download(HttpServletRequest request, HttpServletResponse response,
                    Map<String, Object> bean, String fileName, String templateFile)
                    throws ParsePropertyException, InvalidFormatException {
        
        String tempPath = request.getSession().getServletContext().getRealPath("/WEB-INF/templete");
        
        try {
            InputStream is = new BufferedInputStream(new FileInputStream(tempPath + "\\" + templateFile));
            XLSTransformer xls = new XLSTransformer();
            Workbook workbook = xls.transformXLS(is, bean);
            
            response.setHeader("Content-Disposition""attachment; filename=\"" + fileName + ".xlsx\"");
            
            OutputStream os = response.getOutputStream();
            
            workbook.write(os);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
cs


임의로 생성한 MakeExcel 클래스의 소스이다. InputStream과 OutputStream 객체를 사용해 엑셀 다운로드를 가능하도록 해준다.

tempPath 변수로 값을 복사한 getRealPath() 메소드에 엑셀 템플릿이 들어갈 경로를 적어주면 된다.

엑셀 템플릿에는 실제로 jstl 형식으로 파라미터를 전달하듯 Map에 담은 객체를 ${객체명.파라미터} 를 사용해

jsp list를 출력하듯 엑셀 파일 내에 그대로 적어주면 된다.

1. 정의

AOP는 IoC, DI와 더불어 Spring 프레임워크의 3대 핵심 기술중 하나이다.


전통적인 설계 방법을 따랐던 경우에는 트랜잭션의 분리가 어려웠기 때문에 객체 간의 결합도를 조금이라도 낮추기 위해 관점 지향 프로그래밍을 도입하게 되었다.


주 업무가 아닌, 부가 업무가 응집력이 강한 경우에는 소스가 복잡해진다. OOP (Object Oriented Programming - 객체 지향 프로그래밍)의 보완적 개념으로 관점 지향 프로그램을 사용하고 있다. (클래스들이 공통으로 갖는 기능이나 절차 등을 하나의 것으로 묶어 빼내 별도로 관리하려는 목적)


보조 업무의 코드를 주 업무 코드에서 분리해 작성하고, 필요시에만 도킹(Docking)하여 사용하고, 스프링 프레임워크 내에서 구현하는 방법으로는 xml 스키마를 기반으로 구현하는 방법과 @Aspect 어노테이션을 이용해 구현하는 방법으로 두 가지가 있다.


가장 대표적인 부가 업무의 예로는 로그인, 트랜잭션, 보안, 캐싱 등의 작업이 있다.



2. 특징

순번 

 장점

 1

 부가 업무 소스가 한 곳으로 응집되어 응집력이 높아진다.

 2

 코드가 깔끔해지고 가독성이 높아진다.



3. 실행 순서

Spring 프레임워크에서 AOP의 구현은 프록시를 이용해 구현한다.

ㄱ. 수행할 어드바이스를 프록시에 요청한다.

ㄴ. 핵심 기능을 수행하기 전에 사용할 공통 기능을 수행한다.

ㄷ. 공통 기능을 수행한 후 핵심 기능 쪽으로 가서 핵시 기능 로직을 수행한다.

ㄹ. 핵심 기능을 수행한 후 다시 프록시로 가서 공통 기능을 수행한다.



4. 종류 (Spring이 지원하는 AOP)

ㄱ. AspectJ (eclipse.org/aspectj)

ㄴ. JBoss AOP (labs.jboss.com)

ㄷ. Spring AOP (www.springframework.org)

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


1. 정의

@RequestBody 어노테이션은 컨트롤러의 메소드 내에서 해당 어노테이션이 붙은 객체를 바인딩시

리턴값을 view로 자동 바인딩 되지만, 컨트롤러에 @ResponseBody 어노테이션이 선언되어 있다면

리턴 값은 view를 통해 출력되지 않고 HTTP Response Body에 직접 쓰여진다.

이 때, 해당 메소드 리턴값의 데이터 타입에 따라 MessageConverter에서 변환이 이뤄진 후 쓰여진다.


주로 jsp에서 비동기 통신(ajax)시 사용한다. application/json을 지원한다.

어노테이션을 <mvc:annotation-driven />을 사용하고 있다면, HttpMessageConverter를 기본으로 등록하기 때문에,

별도의 설정이 필요 없이 Jackson 라이브러리만 추가하면 된다.



2. 예제


1
2
3
4
5
6
7
8
9
10
<dependency>
    <groupId>org.codehaus.jackson</groupId>
        <artifactId>jackson-mapper-asl</artifactId>
        <version>1.9.13</version>
</dependency>
<dependency>
    <groupId>org.codehaus.jackson</groupId>
    <artifactId>jackson-core-asl</artifactId>
    <version>1.9.13</version>
</dependency>
cs


pom.xml 에 두 개의 라이브러리를 추가한다.


1
2
3
4
5
6
7
8
9
10
11
<!-- @ResponseBody 어노테이션이 있는 Controller는 리턴값이 MessageConvert로 설정된 
    MappingJacksonHttpMessageConverter 에서 JSON 으로 변환 작업이 이뤄진다. -->
<beans:bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter" >
    <beans:property name="messageConverters">
        <!-- MappingJacksonHttpMessageConverter : MessageConverter의 종류중 하나.
                                                Jackson's ObjectMapper를 사용한다.
                                                request, repsonse를 JSON으로 변환할 떄 사용하는 MessageConverter.
                                                application / json을 지원한다. -->
        <beans:bean class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter"/>
    </beans:property> 
</beans:bean>    
cs


context.xml에 해당 bean을 추가한다.


1
2
3
4
5
6
7
@RequestMapping(value = "/index.do")
public @ResponseBody List memberIdCheck() throws Exception {
        
    List<Map<StringString>> list = new ArrayList<Map<StringString>>();
    
    return list;
}
cs


컨트롤러를 위와 같이 작성한다. 컨트롤러까지 작성한 후, 매핑된 주소를 url에 입력해보면 JSON임을 확인할 수 있다.


Model 객체를 세션에 저장했다가 다시 활용하게 해주는 기능이다.

서블릿은 기본적으로 상태를 유지하지 않으려 하는데, 상태를 유지해줘야 하는 경우에 사용한다.



1. @ModelAttributes

Command Object. 클라이언트측 파라미터를 1:1로 전담하는 property에 담아내는 방식이다.

해당 어노테이션을 붙이면 자동으로 바인딩 되고, 중간 과정을 생략한다. (인자에 넣기만 하면 된다.)

getter / setter 등의 과정을 알아서 생략할 수 있다.


Enum 객체 같은 경우여도 파라미터 이름만 알면 자동으로 매핑해준다.

그러나, 만약 같은 객체가 2개 이상 전달되는 폼 구성인 경우 굉장히 까다로워진다.

세션으로 사용자 파라미터를 List 형태로 저장하게 되는데, DB 입력 후 @ModelAttributes가 적용되지 않는다. (컬렉션을 지원하지 않기 떄문에)


1
2
3
4
5
6
7
// @ModelAttributes는 해당 값을 복수로 받고 싶을 땐
// 오로지 Java Bean 객체를 배열로 선언하는 방법 뿐이다.
public class A {
    private String[] id;
    private String[] pw;
    ...
}
cs


=> 해당 값을 복수로 받고 싶다면, 오로지 Java Bean 객체를 배열로 선언하는 방법 밖에는 없다.

=> 유동적으로 변하는 <form>에는 대응이 까다로워진다.

=> 해당 파라미터에 문제가 있을 경우, 타 단일 객체와 같이 배열이 아닌 배열 속의 객체에 바인딩 에러를 처리해주는 기술이 별도로 필요하다.



2. @SessionAttributes

어노테이션에서 설정한 Model 객체를 세션에 저장하는 역할을 한다. 해당 모델 객체를 사용할 시,

세션에서 불러와 사용하며 view에서도 이름으로 접근 가능하다. @SessionAttributes를 컨트롤러에 붙이고 모델명을 인자로 넣어준다.


 

 용도

 1

 Spring framework에서 제공하는 form 태그 라이브러리 이용시

 2

 몇 단계에 걸쳐 완성되는 form 구성시

 3

 지속적으로 사용자의 입력 값을 유지하고 싶을 떄


스프링에서 상태 유지를 위해 제공하며, 객체 위치는 view와 Controller 사이에 존재한다.

해당 어노테이션이 붙은 컨트롤러는 지정하는 세션명을 @RequestMapping으로 설정한 모든 view에서 공유하고 있어야 한다.

(공유하지 않을 시 에러가 발생함)


=> * 해결 방법

ㄱ. 해당 컨트롤러에서 맨 처음 읽어들이는 Model 객체를 통해 수동적으로 파라미터를 보내주는 것.

(반드시 첫 번째로 해당하는 view를 통해야 한다는 제약 조건)

ㄴ. @ModelAttributes 어노테이션이 붙은 메소드 사용 : 해당 컨트롤러로 접근하는 모든 요청에 어노테이션이 붙은

메소드 command() 로 메소드의 리턴값을 설정된 모델명으로

자동 포함해주는 역할을 담당한다.

클라이언트 접근시 어노테이션이 붙은 메소드의 리턴값을 보장받는다.


해당 어노테이션의 설정값과 동일한 이름을 Model 객체에서 발견시 세션 값으로 자동 변경시켜준다.

그 Model 객체가 세션값으로 대체되면, 세션값을 지우기 전까지 저장된 해당 값을 불러오게 된다.


* @SessionAttributes 사용 예시

- 고객 입력 폼 등에서 잘못 입력되어 경고창을 띄우고 다시 페이지를 만든다고 생각할 때, 최초 입력한 정보를 세션에 저장해두고 이를 바탕으로 입력 화면을 다시 띄우는 경우

- 등록 화면이 여러 페이지에 걸쳐 입력되는 경우, 미리 모델 객체를 생성해 첫번쨰 화면 내용을 객체에 저장해두고

   다음 입력 화면에서 나머지 고객 정보를 입력해 세션 객체에 추가로 저장하는 경우 (유효성 검사 시에도 유리함)


* @ModelAttributes와 같이 컬렉션 프레임워크를 지원하지 않는다. (List 지원 안 함)



3. SessionStatus 객체

사용 완료된 객체는 사용 후 SessionStatus.setComplete(); 메소드로 제거해줘야 한다. 수동으로 제거해주지 않으면 값이 계속 남아 있게 된다.

소프트웨어 개발에 중점을 둔 보안 보증 프로세스. 2004년부터 진행하는 모든 개발 프로젝트에 필수 정책으로 사용되고 있다.

기존의 소프트웨어 개발 주기인 SDLC(Software Development Lifecycle)에 보안을 고려한 SDL(Security Development Lifecycle) 모델을

별도로 구성해 기존 개발 주기에 소프트웨어 보안 및 개인정보 보호 기능을 통합시켰다.

SW 개발 보안의 중요성을 뒷받침하는 대표적인 사례. 자체적으로 개발한 개발생명주기를 제품 개발 전 과정에 적용토록 하여

보안 취약점의 원인을 제거하도록 했다. 개발 과정에서 MS-SDL을 적용한 운영체제 및 DB서버의 보안 취약점이 감소하는 효과가 있다.



1. Pre-SDL

소프트웨어 개발팀의 구성원들이 보안의 기초와 최신 보안 동향에 대한 정보를 매년 1회 교육을 받을 수 있도록 한다.


순번

보안 교육이 포함해야 하는 내용

 1

 시큐어 설계

 2

 위협 모델링

 3

 시큐어 코딩

 4

보안 테스팅

5

프라이버시



2. 요구사항

신뢰성 있는 소프트웨어를 구축하기 위해 기본 보안 요구사항, 프라이버시 요구사항을 정의한다.


순번 

 필수 항목

 1

 SDL 방법론 적용 여부 결정

 2

 보안 책임자(Security Advisor) 선정

 3

 보안 팀(Security Champion) 선정

 4

 버그 리포팅 도구 정의

 5

 보안 버그 경계(Security bug bar) 정의

 6

 보안 위험 평가

 7

 보안 계획서 작성

 8

 버그 추적 시스템 정의



3. 설계

구현에서부터 배포에 이르는 동안 수행해야 하는 작업 계획을 수립하는 단계이다.


순번

 항목

 1

보안 설계 검토

 2

 방화벽 정책 준수

 3

 위협 모델링

 4

 위협 모델 품질 보증

 5

 위협모델 검토 및 승인

 6

 보안 설계서 문서 작성

 7

 보단 디폴트 인스톨 실행

 8

 모든 샘플 코드의 보안 검토 수행

 9

 안전하지 않은 함수와 코딩 패턴 알림

 10

 설계 변화 요구에 관한 보안 관련 사항 문서화

 11

 위협 모델을 통해 찾아진 취약성을 위한 작업 목록 작성


* 공격 표면(Attack Surface) 계획 수립

- 공격 표면이라는 개념적으로 정보 및 금융 자산, 지적 재산, 또는 비즈니스 수행 역량에 존재하는 잠재적 공격 지점을 확인하는 과정의 의미.

위협 모델링에서 공격 표면 관리는 일종의 네트워크, 애플리케이션, 시스템 등 공격자들이 데이터나 정보를 수집하는 지점을 확인하고 공격을 예방하는 것



4. 구현

보안 및 프라이버시 문제점을 발견하고 제거하기 위해 개발 Best Practice를 수립하고 따르도록 한다.


순번

 항목

 1

 최신 버전의 빌드 도구 사용

 2

 금지된 API 사용 회피

 3

 'Execute' 허가를 통한 SQL 안전하게 사용

 4

 저장된 프로시저에서의 SQL 사용

 5

 안전하게 소프트웨어를 사용하기 위해 필요한 사용자 정보 식별

 6

 사용자 중심의 보안 문서 계획

 7

 보안 형상 관리에 관한 정보 생성

 8

 자동화된 금지 API 변환 실행

 9

 프로젝트 팀 전체와 모든 모범 사례와 정책에 대해 정의, 문서화, 토론 등



5. 검증

코드가 이전 단계에서 설정한 보안과 프라이버시를 지키는지, 보안 및 프라이버시 테스팅과 보안 푸쉬(Security Push), 문서 리뷰를 통해 확인한다.

보안 푸쉬는 팀 전체에 걸쳐 위협 모델 갱신, 코드 리뷰, 테스팅에 초점을 맞춘 작업이다.


순번

 항목

 1

커널-모드 드라이버를 위한 테스팅 완료

 2

 COM 객체 테스팅 수행

 3

 인증된 사이트 크로스 도메인 스크립팅을 위한 테스팅

 4

 애플리케이션 검증 테스트 수행

 5

 파일 Fuzzing 수행

 6

 위협 모델 검토 및 수정 등

 7

 보안 테스팅 계획 완료

 8

 침투 테스팅 수행

 9

 시큐어 코드 검토

 10

 보안 푸쉬를 시작하기 전 모든 코드에 대한 우선 순위 결정

 11

 보안 문서 계획서 검토 등


1. 정의

대한민국 공공 부분 정보화 사업시 플랫폼별 표준화된 개발 프레임워크.

과거 플랫폼 기반의 정보화 구축 사업시 수행 업체의 자체 프레임워크를 사용해 정보 시스템이 구축되어, 유지보수 등 여러가지 문제점이 있었기 때문에 전자정부프레임워크를 개발하게 되었다. 최근에는 모바일 개발 프레임워크도 출시되었다.


정보 시스템 개발을 위해 필요한 기능, 아키텍쳐를 미리 만들어 제공하기 위해 제작되었다. 또한, 효율적인 어플리케이션 구축을 지원한다.


* 프레임워크 vs 라이브러리

- 프레임워크 : 어플리케이션의 틀과 구조를 프레임워크에서 제어하고, 프레임워크 위에서 개발자의 코드가 동작한다.

- 라이브러리 : 개발자의 코드 안에서만 재사용됨을 총칭한다.



2. 특징

순번

 내용

 1

5개 서비스 그룹, 34개의 서비스로 구성

 2

 전자정부 프로젝트에 최적화된 오픈 소스 소프트웨어 선정

 3

경량화된 개발프레임워크로서 사실상 업계 표준에 가까운 Spring 프레임워크를 적용

출처: http://netframework.tistory.com/entry/3-전자-정부-프레임워크의-소개 [Programming is Fun]

경량화된 개발 프레임워크. 업계 표준에 가까운 Spring framework 적용

 4

 DI (Dependency Injection) 방식의 의존 관계 처리

 5

AOP 지원

 6

MVC Model2 아키텍쳐 구조 제공 및 다양한 UI 클라이언트 연계 지원

 7

 전자정부 개발 프레임워크 표준 연계 인터페이스 정의


'Framework > Spring' 카테고리의 다른 글

@ResponseBody  (0) 2017.06.28
@SessionAttributes 와 SessionStatus 객체  (0) 2017.06.26
파일 다운로드  (4) 2017.03.14
Ajax :: Ajax (Asynchronos JavaScript And XML)  (0) 2017.01.10
Framework :: JSON (JavaScript Object Notation)  (0) 2017.01.10

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

 

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

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

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

 

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

비대칭 암호화인 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

1. context.xml 설정


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
<?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">
    
    <!-- 모든 경로는 지정한 경로명이 붙으므로 해당 경로를 피하기 위해 업로드 파일들을 resources/ 폴더 아래 넣음 -->
    <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/"/>
        <property name="suffix" value=".jsp"/>
        <property name="order" value="1"/>
    </bean>
    
    <!-- BeanNameViewResolver : view 와 동일한 이름을 갖는 bean을 view 객체로 사용
                                custom view 클래스인 UtilFile을 view로 사용해야 하기 때문에 mapping함 -->
    <bean class="org.springframework.web.servlet.view.BeanNameViewResolver">
        <property name="order" value="0" />
    </bean>
 
    <!--custom view 클래스 -->
    <bean id="downloadView" class="com.cafe24.smart.util.UtilFile"/>
 
</beans>
cs



2. 파일 다운로드 하이퍼링크


1
2
3
4
5
6
7
8
<html>
<head>
    <title>title</title>
</head>
<body>
    <a href="<c:url value='/re/fileDownload?reCode=${reward.reCode}'/>">${reward.reDocument}</a>
</body>
</html>
cs


화면에는 업로드된 파일 경로 전체가 보이는 것처럼 느껴지겠지만 Controller에서 해당 파일명 + 확장자까지만 보이게 잘라놓은 상태이다.

해당 파일명을 클릭하면 /re/fileDownload 경로를 값으로 받는 컨트롤러 메소드로 이동하며 get 방식으로 pk컬럼 값을 받게 된다.



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
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
// AbstractView : 스프링 MVC 사용시 DispatcherServlet 기능
//                : requestURI에 따라 컨트롤러로 분기하고 로직 처리 후 Resolver를 사용하여
//                : 해당 jsp 파일을 찾아 응답하게 되는데 그 사이 시점을 잡아 처리하는 부분의 기능
public class UtilFile extends AbstractView {
//  파일 다운로드
    @Override
    protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request,
                        HttpServletResponse response) throws Exception {
        
        setContentType("application/download; charset=utf-8");
        
        File file = (File) model.get("downloadFile");
        
        response.setContentType(getContentType());
        response.setContentLength((int) file.length()); 
        
        String header = request.getHeader("User-Agent");
        boolean b = header.indexOf("MSIE"> -1;
        String fileName = null;
        
        if (b) {
            fileName = URLEncoder.encode(file.getName(), "utf-8");
        } else {
            fileName = new String(file.getName().getBytes("utf-8"), "iso-8859-1");
        }
        
        response.setHeader("Conent-Disposition""attachment); filename=\"" + fileName + "\";");
        response.setHeader("Content-Transter-Encoding""binary");
        
        OutputStream out = response.getOutputStream();
        FileInputStream fis = null;
        
        try {
            fis = new FileInputStream(file);
            
            FileCopyUtils.copy(fis, out);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException ioe) {
                    ioe.printStackTrace();
                }
            }
            
            out.flush();
        }
    }
}
cs



4. 파일 다운로드


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Controller
public class RewardController {
//  파일 다운로드 하는 메소드
    @RequestMapping(value = "/re/fileDownload", method = RequestMethod.GET)
//  get 방식 하이퍼링크 경로로 보낸 PK 값을 인자로 받음
    public ModelAndView reDocumentDown(@RequestParam(value="reCode"int reCode) {
        
//      pk 값으로 해당 도메인 객체의 파일 전체 경로 값을 받은 후
        Reward reward = rewardService.reListByReCodeServ(reCode);
        
//      전체 경로를 인자로 넣어 파일 객체를 생성
        File downFile = new File(reward.getReDocument());
        
        System.out.println("RewardController reDocumentDown reCode : " + reCode);
        
//      생성된 객체 파일과 view들을 인자로 넣어 새 ModelAndView 객체를 생성하며 파일을 다운로드
//      (자동 rendering 해줌)
        return new ModelAndView("downloadView""downloadFile", downFile);
    }
}
cs


+ Recent posts