#Spring Boot와 JSP

 

 

우연히 Spring Boot를 https://start.spring.io/ 라는 사이트에서

프로젝트를 만들어 보았다.

 

평소같으면 기존 Spring Boot 프로젝트에서

틀이나 내용을 복붙해서 가져왔겠지만

 

새로운 스프링 부트 프로젝트를 만드는 연습을 한다고 생각하고

아무것도 복붙하지 않고 만들어 보았다.

 

그런데 왠걸...?

Hello World조차 화면에 나타내지 못했는데 5시간이 지났다.

아무리 설정파일을 고치고

기존 프로젝트를 참고해 똑같은 라이브러리를 깔고

톰캣을 8버전으로 낮춰도 보고

스프링 부트 버전을 여러번 바꿔도 보았지만

스프링 부트는

도무지 hello.jsp 파일을 찾지 못했다.

다른 점은 한가지 있긴 했다.

WEB-INF 폴더 밑이 아닌 templates 폴더 밑에 jsp 파일을 넣은 것이다.

 

templates 폴더 밑에 html파일은 읽히지만

jsp파일은 읽히지 않았다.

 

스프링 부트에서 jsp를 적용하려면

스프링 프로젝트처럼 webapp/WEB_INF/ 하위에 위치시키고

properties나 yml에서 경로를 설정해줘야 jsp파일을 사용할 수 있었다.

스프링 공식문서에 보면 내장된 서블릿 컨테이너에는 jsp 제한사항이 있다.

스프링 부트는 가능하다면 jsp를 피하고

Thymeleaf와 같은 템플릿 엔진을 사용하라고 권장한다.

국비교육에서 처음 스프링 부트를 배웠을 때

jsp로도 실습을 했었고

templates 폴더 안에 html 파일을 넣고 프로젝트까지 진행도 했었다.

 

원래는 강사님이 스프링 부트에서 jsp를 사용할 때

주의할 점이나 이유에 대해서 설명해 주셨었다.

그러나 시간이 지나면서

다 잊어버렸고

이제서야 희미하게 기억이 났다.

 

역시나

수업을 듣는다고 모든 내용을 흡수할 수 있는 것은 아니다.

이처럼

삽질도 하고 피부로 직접 느껴봐야

피가 되고 살이 되나보다.

스프링 공식문서 링크

https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-developing-web-applications.html#boot-features-spring-mvc-template-engines

 

 

Made by 꿩

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

추상클래스와 인터페이스  (0) 2018.11.11
다형성  (0) 2018.11.03
object vs instance vs class  (3) 2018.10.24

#[Spring] Google reCAPTCHA v3

 

 

오늘은 자동화 공격에 대한

구글이 제공하는 reCAPTCHA에 대해 소개하려 한다.

reCAPTCHA는 자동화 공격을 막기 위한 방법으로

흔히들 아는 "로봇이 아닙니다"를 떠올이면 된다.

자동화 공격은 사람이 아닌 기계로 공격을 하는 것으로

매크로를 만들어 불법적인 프로그램을 사용하거나

로그인을 할 때 비밀번호를 기계가 자동으로 입력하여

계정을 해킹하는 것 등을 말한다.

Google reCAPTCHA v2는

의심스러운 트래픽이 발생하면

'로봇이 아닙니다'를 증명하기 위해

사람만이 판단할 수 있는 이미지를 클릭하는 방식으로 방어하는 방식이다.

하지만 v2 방식은 이용자들에게 불편함을 주는 방식으로

이미지를 클릭해도 다른 이미지가 나와 여러번 클릭하거나

로그인할 때마다 로봇이 아니라는 것을 인증해야 해서

빡친 사람은 나뿐만이 아닐 것이다.

속사정은 모르지만

아마도 사용자를 방해한다는 피드백이 있어

reCAPTCHA v3가 나온 것이 아닐까 추측한다.

reCAPTCHA v3는 v2와 다르게 사용자를 방해하지 않는다.

사용자의 행동(action)을 보고 기계인지 사람인지 판단하는 방법이다.

reCAPTCHA v3는 0 ~ 1.0 까지의 점수를 서버에 보내주는데

0에 가까울수록 기계에 가깝다는 뜻이고

1에 가까울수록 사람에 가깝다고 알려준다.

간단하게 reCAPTCHA v3를 한번 구현해보자.

먼저, Google reCAPTCHA 사이트에 들어가 admin console 사이트를 등록한다.

https://www.google.com/recaptcha/intro/v3.html

 

v3 유형을 선택하고

도메인은 test용이니 localhost를 등록한다.

완료가 되면 사이트 키와 비밀 키를 받는데

설정에서 그 내용을 확인할 수 있다.

그 다음

Spring으로 간단한 maven 프로젝트를 생성한다.

recaptcha.jsp 파일은 다음과 같이 작성하였다.

<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%>
<html>

<head>
    <meta charset="UTF-8">
    <title>구글 리캡챠 테스트</title>
    <script src="https://code.jquery.com/jquery-3.4.1.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
    <script src="https://www.google.com/recaptcha/api.js?render=6Le-q6kUAAAAAFPDTU6lct7ZRRN7vK55hVF4Icp3"></script>
</head>

<body>
    <form action="/robot" method="get">
        <input type="text" name="name" />
        <input type="text" name="g-recaptcha-response" id="g-recaptcha-response" />
        <input type="submit" value="submit" />
    </form>

<script>
$(document).ready(function(){
    grecaptcha.ready(function() {
      grecaptcha.execute('reCAPTCHA_site_key', {action: 'login'}).then(function(token) {
         console.log(token)
         $.ajax({
            url: '${pageContext.servletContext.contextPath}/robot/token',
            type : 'POST',
            dataType: 'json',
            data : {'token': token},
            success : function(result){
                console.log(result);
            },
            fail: function(e){
                console.log("fail")
            }
          });// end ajax
      });
    });
});
</script>
</body>

</html>

Google DreCAPTCHA_site_key에 할당받은 사이트 키를 넣고

서버와 ajax 통신을 위해 jQuery를 이용했다.

일단 이 상태에서 토큰이 다음처럼 받아져야 한다.

이제 Controller에서 받아 Service 단에서 google reCAPTCHA와 통신을 한다.

통신 방법은 RestTemplate을 사용했으며

Post방식으로 토큰과 비밀키 값을 전송한다.

@Service
public class RecaptchaService {

    public RecaptchaDTO token(String token) {
        String url = "https://www.google.com/recaptcha/api/siteverify";

        RestTemplate restTemplate = new RestTemplate();

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

        MultiValueMap<String, String> map= new LinkedMultiValueMap<String, String>();
        map.add("secret", "secret-key");
        map.add("response", token);

        HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<MultiValueMap<String, String>>(map, headers);

        RecaptchaDTO response = restTemplate.postForObject( url, request, RecaptchaDTO.class );

        return response;
    }

}

받은 자료를 콘솔에 뿌려주면 다음처럼 나오게 될 것이다.

여기서 score를 보면 0.9라는 것을 볼 수 있다.

이는 reCAPTCHA가 사람에 가깝다는 것을 판단한 것이며

이 score를 이용하여 서비스의 동작을 통제하면 될 것이다.

참고자료

https://developers-kr.googleblog.com/2019/01/introducing-recaptcha-v3-new-way-to.html

https://dany-it.tistory.com/302

https://developers.google.com/recaptcha/docs/v3?hl=ko

 

 

Made by 꿩

'IT > 보안' 카테고리의 다른 글

[방화벽] 인바운드 & 아웃바운드  (0) 2022.01.31
OAuth  (0) 2019.06.17
SSL인증서  (0) 2019.03.21
CSRF  (0) 2018.12.25
XSS 공격과 방어  (2) 2018.10.25

#OAuth

 

 

'당신의 비밀번호는 안녕하십니까?'

 

 

이메일, 비밀번호 유출은 매우 빈번하다.

어디 조금만 가입해도 이상한 스팸메일이 날라오며

잊을만 하면

유명한 사이트에서 고객정보 유출 사건이 한번씩 터진다.

 

유명한 사이트도 개인정보 유출과 관련해서 사건사고가 많은데

잘 알지도 못하는 사이트에 개인정보를 입력하고 싶은가???

 

몇몇 사람들은 비슷하거나 똑같은 비밀번호를 사용하고

이메일도 주로 하나 또는 두 개만을 사용한다.

그런데 보안이 제대로 지켜지는 지도 모르는 사이트에

덜컥 회원가입을 하고 로그인 한다면

당신의 이메일과 비밀번호는 안전하겠는가???

 

 

이런 문제를 방지할 수 있는 것이

바로 OAuth이다.

 

위키피디아에 따르면

OAuth는 인터넷 사용자들이 비밀번호를 제공하지 않고

다른 웹사이트 상의 자신들의 정보에 대해 웹사이트나

애플리케이션의 접근 권한을 부여할 수 있는 공통적인 수단으로서 사용되는

접근 위임을 위한 개방형 표준이다.

 

내가 생각하는 OAuth란 쉽게 말해서

인증과 허가를 대신해주는 것이라 생각한다.

 

 

예를 들어,

쇼핑몰 홈페이지가 있다고 가정해보자.

이 쇼핑몰에서 옷을 구매하여 포인트를 적립하고 싶지만

로그인을 해야한다.

게다가 기억력이 안 좋아서

항상 동일한 비밀번호를 사용하기만 한다.

그렇다고 잘 알지도 모르는 사이트에

내 비밀번호를 입력하는 것도 망설여진다.

 

이때 소셜 로그인을 하면

굳이 잘 알지도 모르는 사이트에

비밀번호 입력을 하지 않아도 되고

로그인이 간편해진다.

 

OAuth를 이용한 인증방식에 대해 알아보자.

1. 고객이 카카오 로그인을 선택하면 쇼핑몰 서버는 카카오 로그인 페이지를 전송해준다.

2. 고객은 ID와 비밀번호를 입력해 본인을 인증한다.

3. 카카오 서버는 고객을 인증완료 페이지로 redirect 시킨다.

4. 카카오 서버는 redirect하면서 authorize_code 값을 포함시키고 쇼핑몰 서버는 이것을 수집한다.

5. 쇼핑몰 서버는 authorize_code로 카카오 서버에 access token을 요청한다.

6. 카카오 서버는 쇼핑몰 서버에 access token을 발급해준다.

7. 쇼핑몰 서버는 카카오 API 서버에 access token으로 사용자 정보를 불러온다.

 

정리하자면,

사용자는 카카오에서 로그인을 함으로써 본인을 인증하고

카카오는 인증을 확인하고 사용자 정보 접근을 허가하는 token 값을 발급한다.

token 값으로 쇼핑몰 서버는 사용자 정보를 카카오에서 갖고 오게 된다.

쉽게 말해 쇼핑몰과 사용자 사이에 카카오라는 중간자 역할을 둔 것이다.

 

 

참고자료

https://ko.wikipedia.org/wiki/OAuth

http://tech.devgear.co.kr/delphi_news/449506

https://d2.naver.com/helloworld/24942

 

 

To be continued.........

 

 

Made by 꿩

'IT > 보안' 카테고리의 다른 글

[방화벽] 인바운드 & 아웃바운드  (0) 2022.01.31
[Spring] Google reCAPTCHA v3  (6) 2019.07.07
SSL인증서  (0) 2019.03.21
CSRF  (0) 2018.12.25
XSS 공격과 방어  (2) 2018.10.25

#SSL인증서



포켓몬스터 게임을 하다보면

통신교환을 해야 최종진화가 되는 포켓몬들이 있다.

나는 어떻게 하는지도 모르고

방법도 귀찮아서 통신교환을 전혀 하지 않아

후딘, 강철톤, 괴력몬 등을 한번도 키워본 적이 없다.

진화의 돌 진화나 레벨업 진화만 있었으면 좋겠다.



통신교환을 하는 것은 데이터를 주고 받는 행위이다.

근데 이 데이터를 주고 받을 때

누가 가로채거나 엿본다면 데이터가 유출된다.


HTTP 통신에서도 마찬가지이다.

HTTP 통신은 암호화되지 않은 방법으로 통신하기 때문에

데이터가 쉽게 유출된다.


HTTP의 보안상 문제점을 보완하기 위해 나온 것이 HTTPS이다.

HTTPS는 SSL 프로토콜 위에서 돌아가는 프로토콜을 말하는데

쉽게 말하자면

SSL 인증서를 이용하여 웹에서 데이터를 암호화하여 주고 받을 수 있게 해준다.

SSL 인증서의 역할은 2가지이다.

첫째, 인증서 정보를 통해서 신뢰할 수 있는 서버인지 인증하는 것

둘째, 공개키로 데이터를 암호화하는 것


우선 대칭키와 공개키에 대해서 알아둘 필요가 있다.

대칭키는 암호화, 복호화 모두 하나의 키로 가능하다.

그렇기 때문에 대칭키가 유출이 되면 암호화 하는 의미가 없어진다.


반대로 공개키는 2개의 키 쌍으로 이루어져있다.

보통 다른 사람에게 공개하는 것을 공개키

나만 알고있는 것을 비밀키라 한다.

하나의 키로 암호화를 하게 되면

복호화는 다른 하나의 키로만 가능하다.

두 개의 키 모두 암호화, 복호화가 되지만

자신의 키로 암호화한 것을 복호화할 수 없고

다른 키로 암호화가 된 것만을 복호화 할 수 있다.


공개키와 대칭키에 대해 알아봤다면

SSL 인증서의 동작과정을 살펴보자.



첫번째 과정은 핸드쉐이킹이다.

이 과정은 본격적으로 데이터를 주고 받기 전에

서로가 지원하는 암호화 방식을 알아내고

데이터를 암호화하는 세션 키를 찾는 과정이다.


클라이언트가 서버에 접속을 하게 될 때

클라이언트는 자신이 사용할 수 있는 암호화 방식과

클라이언트가 생성한 랜덤 데이터를 서버에 전송한다.


서버는 클라이언트에게 받은 암호화 방식 중 자신이 지원가능한 암호화 방식을 고른다.

그리고 다시 클라이언트에게

서버가 고른 암호화 방식과

서버가 생성한 랜덤 데이터,

비밀키로 암호화된 SSL 인증서를 전송한다.


클라이언트는 SSL 인증서가

CA에 의해 발급된 인증서인지 확인하고

이미 내장된 CA의 공개키를 이용해 SSL 인증서를 복호화한다.

복호화가 정상적으로 되었다면

해당 서버는 CA에서 인증된 SSL 인증서를 가진 서버라는 것을 인증되는 것이다.


해당 서버가 신뢰할 수 있는 판단이 되는 순간

클라이언트는 자신이 생성한 랜덤 데이터와 서버가 생성한 랜덤 데이터를 조합하여

pre master secret이라는 키를 생성한다.

pre master secret을 공개키로 암호화를 하여 서버에 보내면

서버는 pre master secret을 비밀키로 복호화를 하게 된다.


클라이언트와 서버 모두 pre master secret을 이용하여 master secret을 만들고

master secret을 이용하여 세션 키를 만들게 된다.


여기까지가 핸드쉐이킹 단계이며

다음은 핸드쉐이킹 단계에서 만들었던 세션 키를 가지고

데이터를 암호화하여 주고 받는 세션 단계이다.


세션 단계에서 사용되는 세션 키는 대칭키로 암호화와 복호화가 모두 가능하다.

이미 핸드쉐이킹 단계에서 공개키를 이용한 방식으로 세션키를 만들었기 때문에

세션키를 외부로 노출할 일은 없게 된다.


결국 HTTPS는 공개키와 대칭키를 혼합하여 사용한다.

공개키는 대칭키를 암호화하여 전달하는데 사용이 되고

대칭키는 실제 데이터를 암호화하여 송수신하는데 사용이 된다.

공개키는 많은 컴퓨팅 파워를 사용하기 때문에

실제 데이터를 암호화하여 주고받는 방식은 대칭키를 이용한 방식이 더 좋다.


데이터 전송이 모두 끝나게 되면

통신에 사용하게 된 세션키는 폐기된다.


시간이 된다면 AWS를 이용하여

SSL 인증서로 HTTPS 서버를 구축해보는 것도 좋은 연습이 될 것 같다.


<참고자료>

https://opentutorials.org/course/228/4894


To be continued.........




Made by 꿩

'IT > 보안' 카테고리의 다른 글

[방화벽] 인바운드 & 아웃바운드  (0) 2022.01.31
[Spring] Google reCAPTCHA v3  (6) 2019.07.07
OAuth  (0) 2019.06.17
CSRF  (0) 2018.12.25
XSS 공격과 방어  (2) 2018.10.25

#Hash Table



해시란 무엇일까?

위키피디아에 따르면 해시 함수에 의해 얻어진 값을 해시라 한다.

그럼 해시 함수란?

임의의 길이의 데이터를 고정된 길이의 데이터로 매핑하는 함수를 말한다.

마지막으로 해시 테이블이란?

해시를 이용한 테이블이겠지..ㅎ


해시 테이블은 해시 값이 인덱스가 되도록 원래의 키 값을 저장한 배열을 말한다.

좀 더 쉽게 풀어서 설명하자면

데이터를 어떠한 알고리즘으로 고유한 숫자를 만든다.

그리고 이것을 인덱스로 사용해 저장하는 방법이다.

배열을 사용하여 데이터를 저장하며

데이터 고유의 인덱스로 접근하게 되므로

매우 빠른 검색 속도를 갖는 자료구조이다.


이미 배열이 생성되어 있고 비어있는 곳을 굳이 채워 넣을 필요가 없기 때문에

삽입 또는 삭제도 효율적으로 수행할 수 있다.

물론 이때문에 충분한 메모리 공간이 필요하다는 단점이 있다.


포켓몬 이름으로 포켓몬의 정보를 해시방법으로 저장한다고 가정해보자.

샤미드, 쥬피썬더, 부스터를 각 이름을 해시 함수에 넣어 인덱스를 구하고

해당 인덱스에 데이터를 저장한다.

나중에 해당 포켓몬의 정보를 얻을 때

이름만 입력하면 바로 데이터를 가져올 수 있다.


그런데 해시 함수의 결과 값은 항상 고유값은 아니라는 문제가 있다.

중복값이 발생할 때를 해시 충돌이라 하고 보통 두가지 해결 방법이 제시된다.

다음과 같이 충돌이 난다고 가정해보자.

버터플과 도나리 모두 해시함수에 넣었을 때

동일한 인덱스 값이 나온다.


이처럼 해시 충돌에 대한 첫번째 해결 방법은 Separate Chaining 방식이 있다.

이는 나중에 들어온 데이터를 추가하는 방법이다.

LinkedList 또는 Tree를 사용하는 방식이 있다.

이 둘을 사용하는 기준은 버킷에 할당된 key-value 쌍의 개수이다.

트리는 기본적으로 메모리 사용량이 많고

데이터 개수가 적을 때

Worst Case를 살펴보면 이 둘의 성능 상 차이가 거의 없으므로

메모리 측면에서 볼 때 LinkedList를 사용하는 것이 맞다.


LinkedList를 이용하면 다음과 같이 저장이 된다.


두번째 방법은 Open Address 방식이 있다.

해시 충돌이 발생하면 다른 해시 버킷에 해당 데이터를 삽입하는 방식이다.

이 방법에서 또 3가지로 나뉘어진다.


1. Linear Probing: 순차적으로 비어있는 버킷 탐색

2. Quadratic Probing: 선형 탐사와 비슷하지만 폭을 제곱수로 넓게 탐색

3. Double hashing Probing: 해시 출동 발생 시 2차 해시 함수 이용하여 새로운 주소 할당


 이중 Linear Probing으로 충돌을 해결하면 다음과 같이 저장된다.


그렇다면 어떤 기준을 가지고 Separate Chaining과 Open Addressing을 사용해야 할까?

둘 모드 Worst Case O(M)이다.

그러나 Open Addressing은 연속된 공간에 데이터를 저장하기 때문에

Separate Chaining보다 캐시 효율이 높다.


따라서 데이터 개수가 적다면 Open Addressing이 성능이 좋지만

데이터 개수가 커질수록 Worst Case 발생빈도가 높아지기 때문에

Open Addressing이 Separate Chaining보다 느려진다.

Separate Chaining은 충돌 빈도를 보조 해시 함수를 이용하여 조절할 수 있기 때문에

Worst Case에 가까운 일이 발생하는 것을 줄일 수 있다.

(보조 해시 함수는 key의 해시 값을 변형하여 해시 충돌 가능성을 줄이는 것이다)


해싱에는 정적 해싱과 동적 해싱이 있다.

정적 해싱은 데이터를 입력하기 전에 일정 크기의 메모리를 미리 할당받는 것이며

해시 테이블의 크기가 할당된 메모리의 크기를 넘어갈 수 있다.


이러한 문제점을 해결하기 위해서 동적 해싱이 등장했는데

동적해싱은 오버플로우 발생시 버킷 안의 엔트리들 재조정하고

동적으로 해시 테이블의 크기를 변화시키면서 성능을 높여주는 방법이다.


동적 해싱은 데이터가 증가해도 검색의 성능이 유지되고 메모리 낭비를 줄일 수 있다는 장점이 있지만

로직이 복잡하고 버킷을 쪼개거나 합치는 과정 중에 부하가 발생하는 단점도 가지고 있다.


참고

https://k39335.tistory.com/18

https://d2.naver.com/helloworld/831311

http://blog.naver.com/PostView.nhn?blogId=skyjjw79&logNo=220875535595&parentCategoryNo=&categoryNo=61&viewDate=&isShowPopularPosts=true&from=search

To be continued.........




Made by 꿩













#CSRF



CSRF는 Cross-site request forgery로 사이트 간 요청 위조라 불린다.


위키피디아의 설명에 의하면,

사용자가 자신의 의지와는 무관하게 공격자가 의도한 행위를

특정 웹사이트에 요청하게 되는 공격이라 한다.



나만의 언어로 바꿔보자면

인터넷뱅킹에 로그인한 상태에서

메일을 들어가보니까

평소 내가 사고 싶었던 100cm 냐옹 인형을 싸게 구입할 수 있다는 광고메일이 왔어.

나는 궁금해서 링크를 클릭하겠지?

나는 냐옹 인형을 구입할 수 있는 웹사이트를 요청했지만

알고 봤더니

해커에게 돈을 송금한 요청이 보내진거지.


옥션 사례가 대표적인데

관리자가 로그인을 한 상태이고

자주 가는 사이트가 있었다.

공격자가 사이트에 악성스크립트를 넣은 글을 올렸고

관리자가 그 글을 본 순간

아이디와 비밀번호가 해커는 관리자권한을 획득했고

해커는 관리자로 로그인을 하여 개인정보 데이터를 빼내온 것이다.

XSS와 CSRF는 비슷하면서도 다르다.

둘 모두 악성 스크립트를 이용한다는 점은 동일하지만

XSS는 사용자의 화면에서 악성 스크립트가 실행되는 것이고

CSRF는 사용자 권한을 이용해 악성 스크립트를 서버에 요청을 한다는 점이 다르다.


마치

도둑이 내 물건을 훔쳐간 것과

보이스피싱처럼 내가 도둑에게 돈을 송금해 준 것

이 차이랄까?



물론 CSRF 공격을 막는 방법도 있다.

그 중 하나가 CSRF 토큰을 이용한 방법이다.

CSRF 토큰은 랜덤으로 들어가는 키값인 난수이다.


화면 호출 시 세션에 CSRF 토큰인 난수값이 저장된다.

해당 화면에 대한 요청이 들어올 때

CSRF 토큰 값과 비교해서

토근 값이 일치하면 요청을 실행하고

그렇지 않으면 해당 요청을 막는 방법이다.



앞서 언급한 예시를 이용해 설명해본다면

인터넷뱅킹에 로그인한 상태로 메일의 링크를 클릭했어

근데 그 링크는 해커에게 돈을 송금하도록 요청하는 링크야

서버에서 해커에게 송금하라는 요청을 받았는데

송금하는 화면에서 생성되는 CSRF 토큰을 검사해

근데 해커가 미리 지정한 메일의 링크에 CSRF 토큰인 난수값이 없으면

요청이 금지되는 거지


해커 입장에서는 CSRF 토큰은 난수값으로

랜덤으로 생성되므로 알 수 있는 방법이 없지

그래서 메일 링크에 CSRF 토큰값을 넣을 수가 없어

물론 천재 해커들은 뚫을 방법을 찾을 수도 있겠지만....ㅎ

내가 프로젝트를 수행하면서

CSRF 토큰을 지정하는 방법은 두가지이다.


첫번째는 input hidden으로 넣는방법

1
<input type="hidden" id="csrfToken" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
cs

두번째는 ajax요청시 header에 csrf 토큰을 넣는 방법(javascript)

1
2
3
4
5
6
7
8
9
10
11
<meta id="_csrf" name="_csrf" th:content="${_csrf.token}"/>
<meta id="_csrf_header" name="_csrf_header" th:content="${_csrf.headerName}"/>    
    
    //여기부터는 자바스크립트 
    var csrftoken = $('#_csrf').attr('content');
    var csrfheader = $('#_csrf_header').attr('content');
 
                    //ajax에 추가해야함.
                    beforeSend: function(xhr) {
                        xhr.setRequestHeader(csrfheader, csrftoken);
                    },
cs



To be continued.........




Made by 꿩




'IT > 보안' 카테고리의 다른 글

[방화벽] 인바운드 & 아웃바운드  (0) 2022.01.31
[Spring] Google reCAPTCHA v3  (6) 2019.07.07
OAuth  (0) 2019.06.17
SSL인증서  (0) 2019.03.21
XSS 공격과 방어  (2) 2018.10.25

#[Spring Boot] 카카오페이 API



생각보다 카카오페이 API에 대한 글이 적어서 포스팅을 해보려한다.


스프링 부트에서 카카오페이를 붙이는 데 도움이 되었으면 한다.

본 글은 단건결제 프로세스만 설명할 것이다.


먼저 Kakao Developers를 참고해야한다.

https://developers.kakao.com/docs/restapi/kakaopay-api

카카오페이 API가 어떤 데이터를 요청받고 주는지에 대해 쓰여있다.

----------------------------------------------------------------------------------------------------------------

먼저 결제준비 단계이다.

request는 카카오페이에서 요구하는 정보이다.

해석해보자면

POST방식으로 https://kapi.kakao.com + /v1/payment/approve란 호스트(url 주소)로

Authorization(권한)과 Content-Type을 보내라는 것이다.

이 부분이 header에 해당되는 내용이고

밑에 있는 키와 설명, 타입으로 되어있는 부분은 body로 보내면 된다.


권한은 어디서 얻을 수 있을까?

먼저, KakaoDevelopers에 로그인을 해야한다.

내 애플리케이션 -> 개요 -> 앱정보 -> 앱 키 표시를 눌렀을 때 나오는

admin 키가 바로 권한 키이다.


주의할 것은 바로 밑 플랫폼에서

다음과 같이 설정해야한다.


----------------------------------------------------------------------------------------------------------------

kakaoPay.html파일을 만들고 post방식으로 보내는 버튼을 만든다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
 
<h1> kakaoPay api 이용하기 </h1>
 
<form method="post" action="/kakaoPay">
    <button>카카오페이로 결제하기</button>
</form>
 
 
</body>
</html>
cs


----------------------------------------------------------------------------------------------------------------

이제  KakaoPay 클래스를 만들어본다.

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
package org.salem.service;
 
import java.net.URI;
import java.net.URISyntaxException;
 
import org.salem.domain.KakaoPayApprovalVO;
import org.salem.domain.KakaoPayReadyVO;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;
 
import lombok.extern.java.Log;
 
@Service
@Log
public class KakaoPay {
 
    private static final String HOST = "https://kapi.kakao.com";
    
    private KakaoPayReadyVO kakaoPayReadyVO;
    
    public String kakaoPayReady() {
 
        RestTemplate restTemplate = new RestTemplate();
 
        // 서버로 요청할 Header
        HttpHeaders headers = new HttpHeaders();
        headers.add("Authorization""KakaoAK " + "admin key를 넣어주세요~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~!");
        headers.add("Accept", MediaType.APPLICATION_JSON_UTF8_VALUE);
        headers.add("Content-Type", MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8");
        
        // 서버로 요청할 Body
        MultiValueMap<StringString> params = new LinkedMultiValueMap<StringString>();
        params.add("cid""TC0ONETIME");
        params.add("partner_order_id""1001");
        params.add("partner_user_id""gorany");
        params.add("item_name""갤럭시S9");
        params.add("quantity""1");
        params.add("total_amount""2100");
        params.add("tax_free_amount""100");
        params.add("approval_url""http://localhost:8080/kakaoPaySuccess");
        params.add("cancel_url""http://localhost:8080/kakaoPayCancel");
        params.add("fail_url""http://localhost:8080/kakaoPaySuccessFail");
 
         HttpEntity<MultiValueMap<StringString>> body = new HttpEntity<MultiValueMap<StringString>>(params, headers);
 
        try {
            kakaoPayReadyVO = restTemplate.postForObject(new URI(HOST + "/v1/payment/ready"), body, KakaoPayReadyVO.class);
            
            log.info("" + kakaoPayReadyVO);
            
            return kakaoPayReadyVO.getNext_redirect_pc_url();
 
        } catch (RestClientException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (URISyntaxException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        
        return "/pay";
        
    }
    
}
 
cs


1) cid는 가맹점 코드로 카카오페이에 연락해서 받아야 한다.

Test 코드이므로 TC0ONETIME를 넣었고

실결제를 하려면 카카오페이와 제휴 후 받은 cid 코드를 넣으면 된다.


2) Authorization에 위에서 설명한 admin 키를 넣어야 한다.


3)  body 부분에는 내가 결제로 지정할 데이터들을 넣는다.

kakao Developers에 필수라고 써있는 파라미터들은 꼭 넣어줘야한다.


4) HttpEntity<MultiValueMap<StringString>> body = new HttpEntity<MultiValueMap<StringString>>(params, headers);

hearder와 body를 붙이는 방법이다.


5) kakaoPayReadyVO = restTemplate.postForObject(new URI(HOST + "/v1/payment/ready"), body, KakaoPayReadyVO.class);

RestTemplate을 이용해 카카오페이에 데이터를 보내는 방법이다.

post방식으로 HOST + "/v1/payment/ready"에 body(header+body)정보를 보낸다.

정보를 보내고 요청이 성공적으로 이루어지면 카카오페이에서 응답정보를 보내준다.

KakaoPayReadyVO.class는 응답을 받는 객체를 설정한 것이다.

response로 위와 같은 데이터가 들어오므로 이를 객체로 받기 위한 자바 빈을 만들었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package org.salem.domain;
 
import java.util.Date;
 
import lombok.Data;
 
@Data
public class KakaoPayReadyVO {
    
    //response
    private String tid, next_redirect_pc_url;
    private Date created_at;
    
}
 
cs

안드로이드나 ios는 사용하지 않으므로 빼도 상관없다.


6) return kakaoPayReadyVO.getNext_redirect_pc_url();

마지막 return 값으로 redirect url을 불러와 결제가 완료되면 해당 주소로 가게끔 설정해 놓는다.

----------------------------------------------------------------------------------------------------------------

지금까지 만든 클래스가 잘 작동하도록 controller를 만든다.

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
package org.salem.controller;
 
import org.salem.service.KakaoPay;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
 
import lombok.Setter;
import lombok.extern.java.Log;
 
@Log
@Controller
public class SampleController {
    
    @Setter(onMethod_ = @Autowired)
    private KakaoPay kakaopay;
    
    
    @GetMapping("/kakaoPay")
    public void kakaoPayGet() {
        
    }
    
    @PostMapping("/kakaoPay")
    public String kakaoPay() {
        log.info("kakaoPay post............................................");
        
        return "redirect:" + kakaopay.kakaoPayReady();
 
    }
    
    @GetMapping("/kakaoPaySuccess")
    public void kakaoPaySuccess(@RequestParam("pg_token"String pg_token, Model model) {
        log.info("kakaoPaySuccess get............................................");
        log.info("kakaoPaySuccess pg_token : " + pg_token);
        
    }
    
}
 
cs

kakaoPaySuccess.html까지 만들어 놓으면

우선 카카오페이 결제가 정상적으로 작동이 될 것이다.

<url에 pg_token이 들어왔는지 확인하자!

ex) http://localhost:8080/kakaoPaySuccess?pg_token=8cf5a737f5fd9151f2ca>




지금까지는 카카오페이에 "갤럭시S9 제품 2100원 결제해주세요~"라고 요청했을때

카카오페이에서 알아서 결제하도록 만든 것이다.


결제가 완료되면 결제완료 창에

결제가 어떤 방식으로 이루어졌는지?

어떤 카드를 써서 결제를 했는지?

등등 결제정보를 카카오페이에서 받아와야하지 않겠는가?

----------------------------------------------------------------------------------------------------------------

이제 결제승인 단계로 가본다.

매커니즘은 결제준비 단계와 크게 다르지 않지만

결제완료 후 받아오는 pg_token과 tid가 필수적으로 있어야 한다.


동일하게 KakaoPay 클래스에 kakaoPayInfo메소드를 추가했다.

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
package org.salem.service;
 
import java.net.URI;
import java.net.URISyntaxException;
 
import org.salem.domain.KakaoPayApprovalVO;
import org.salem.domain.KakaoPayReadyVO;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;
 
import lombok.extern.java.Log;
 
@Service
@Log
public class KakaoPay {
 
    private static final String HOST = "https://kapi.kakao.com";
    
    private KakaoPayReadyVO kakaoPayReadyVO;
    private KakaoPayApprovalVO kakaoPayApprovalVO;
    
    public String kakaoPayReady() {
 
        RestTemplate restTemplate = new RestTemplate();
 
        // 서버로 요청할 Header
        HttpHeaders headers = new HttpHeaders();
        headers.add("Authorization""KakaoAK " + "admin key를 넣어주세요~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~!");
        headers.add("Accept", MediaType.APPLICATION_JSON_UTF8_VALUE);
        headers.add("Content-Type", MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8");
        
        // 서버로 요청할 Body
        MultiValueMap<StringString> params = new LinkedMultiValueMap<StringString>();
        params.add("cid""TC0ONETIME");
        params.add("partner_order_id""1001");
        params.add("partner_user_id""gorany");
        params.add("item_name""갤럭시S9");
        params.add("quantity""1");
        params.add("total_amount""2100");
        params.add("tax_free_amount""100");
        params.add("approval_url""http://localhost:8080/kakaoPaySuccess");
        params.add("cancel_url""http://localhost:8080/kakaoPayCancel");
        params.add("fail_url""http://localhost:8080/kakaoPaySuccessFail");
 
         HttpEntity<MultiValueMap<StringString>> body = new HttpEntity<MultiValueMap<StringString>>(params, headers);
 
        try {
            kakaoPayReadyVO = restTemplate.postForObject(new URI(HOST + "/v1/payment/ready"), body, KakaoPayReadyVO.class);
            
            log.info("" + kakaoPayReadyVO);
            
            return kakaoPayReadyVO.getNext_redirect_pc_url();
 
        } catch (RestClientException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (URISyntaxException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        
        return "/pay";
        
    }
    
    public KakaoPayApprovalVO kakaoPayInfo(String pg_token) {
 
        log.info("KakaoPayInfoVO............................................");
        log.info("-----------------------------");
        
        RestTemplate restTemplate = new RestTemplate();
 
        // 서버로 요청할 Header
        HttpHeaders headers = new HttpHeaders();
        headers.add("Authorization""KakaoAK " + "admin key를 넣어주세요~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~!");
        headers.add("Accept", MediaType.APPLICATION_JSON_UTF8_VALUE);
        headers.add("Content-Type", MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8");
 
        // 서버로 요청할 Body
        MultiValueMap<StringString> params = new LinkedMultiValueMap<StringString>();
        params.add("cid""TC0ONETIME");
        params.add("tid", kakaoPayReadyVO.getTid());
        params.add("partner_order_id""1001");
        params.add("partner_user_id""gorany");
        params.add("pg_token", pg_token);
        params.add("total_amount""2100");
        
        HttpEntity<MultiValueMap<StringString>> body = new HttpEntity<MultiValueMap<StringString>>(params, headers);
        
        try {
            kakaoPayApprovalVO = restTemplate.postForObject(new URI(HOST + "/v1/payment/approve"), body, KakaoPayApprovalVO.class);
            log.info("" + kakaoPayApprovalVO);
          
            return kakaoPayApprovalVO;
        
        } catch (RestClientException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (URISyntaxException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        
        return null;
    }
    
}
 
cs


1) kakaoPayApprovalVO = restTemplate.postForObject(new URI(HOST + "/v1/payment/approve"), body, KakaoPayApprovalVO.class);

응답정보를 받기 위해 KakaoPayApprovalVO 클래스를 만든다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package org.salem.domain;
 
import java.util.Date;
 
import lombok.Data;
 
@Data
public class KakaoPayApprovalVO {
    
    //response
    private String aid, tid, cid, sid;
    private String partner_order_id, partner_user_id, payment_method_type;
    private AmountVO amount;
    private CardVO card_info;
    private String item_name, item_code, payload;
    private Integer quantity, tax_free_amount, vat_amount;
    private Date created_at, approved_at;
    
    
}
 
cs

여기서 amount 와 card_info는 JSONObject로 전송받기 때문에

따로 AmountVO, CardVO라는 객체를 만들어 준다.

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
package org.salem.domain;
 
import lombok.Data;
 
@Data
public class AmountVO {
 
    private Integer total, tax_free, vat, point, discount;
}
 
package org.salem.domain;
 
import lombok.Data;
 
@Data
public class CardVO {
    
    private String purchase_corp, purchase_corp_code;
    private String issuer_corp, issuer_corp_code;
    private String bin, card_type, install_month, approved_id, card_mid;
    private String interest_free_install, card_item_code;
    
 
}
 
cs

----------------------------------------------------------------------------------------------------------------

Controller에서 model.addAttribute를 이용하여

화면 쪽에 정보를 전송한다.

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
package org.salem.controller;
 
import org.salem.service.KakaoPay;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
 
import lombok.Setter;
import lombok.extern.java.Log;
 
@Log
@Controller
public class SampleController {
    
    @Setter(onMethod_ = @Autowired)
    private KakaoPay kakaopay;
    
    
    @GetMapping("/kakaoPay")
    public void kakaoPayGet() {
        
    }
    
    @PostMapping("/kakaoPay")
    public String kakaoPay() {
        log.info("kakaoPay post............................................");
        
        return "redirect:" + kakaopay.kakaoPayReady();
 
    }
    
    @GetMapping("/kakaoPaySuccess")
    public void kakaoPaySuccess(@RequestParam("pg_token"String pg_token, Model model) {
        log.info("kakaoPaySuccess get............................................");
        log.info("kakaoPaySuccess pg_token : " + pg_token);
        
        model.addAttribute("info", kakaopay.kakaoPayInfo(pg_token));
        
    }
    
}
 
cs


----------------------------------------------------------------------------------------------------------------

이제 kakaoPaySuccess.html에 결제승인된 정보를 나타내보자.

Controller에서 보내온 데이터를 Thymeleaf를 이용해 표현했다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
 
카카오페이 결제가 정상적으로 완료되었습니다.
 
결제일시:     [[${info.approved_at}]]<br/>
주문번호:    [[${info.partner_order_id}]]<br/>
상품명:    [[${info.item_name}]]<br/>
상품수량:    [[${info.quantity}]]<br/>
결제금액:    [[${info.amount.total}]]<br/>
결제방법:    [[${info.payment_method_type}]]<br/>
 
 
 
<h2>[[${info}]]</h2>
 
</body>
</html>
cs


결제가 완료되면 화면은 다음과 같이 나올 것이다.




To be continued.........




Made by 꿩






# 추상클래스와 인터페이스



추상화란 형태를 잡는 것이다.

미술에서 추상화는 구체적인 형상을 나타내는 것이 아니라

점, 선, 면, 색과 같은 순수한 조형 요소로 표현한 것이다.

프로그래밍에서 추상화도 마찬가지이다.

구체적인 코드를 짜는 것이 아니라

기본 틀을 잡고 가는 것이다.


추상클래스와 인터페이스는 모두 추상화에 포함된다.

추상클래스는 메소드의 내용을 구현할 수 있는 것과 달리

인터페이스는 명시만 되어있다.

인터페이스가 추상클래스보다 더 추상화된 형태라고 보면 된다.


그렇다면 왜 추상화란 개념이 프로그래밍에 적용이 될까?

바로 다른 사람과의 커뮤니케이션 때문이다.


같이 프로그램 코드를 짜고 있는 상황에서

다른 사람이 어떤 이름의 객체, 메소드를 만드는 지 모른다면

다른 사람의 만든 객체를 내가 쓸 수 없다.

혹은 다른 사람의 일이 다 끝나야 내가 코드를 완성시킬 수 있다.


그러나 인터페이스를 이용하여 먼저 약속을 해놓으면

상대방의 코드 내용을 알지 못해도

나는 내 코드를 완성시킬 수 있다.

일반적으로는

인터페이스 -> 추상클래스 -> 구현클래스

이 3단계로 프로그램 코드를 완성했다.

그러나

자바 8버전 부터는 인터페이스에 디폴트 메소드가 생겼다.

인터페이스도 제한적이지만 메소드를 구현할 수 있게 되면서

추상클래스를 꼭 만들 필요성이 없어졌다.

그렇다면 추상클래스를 이제 쓸 필요가 없어졌는가?

아니다.

인터페이스가 추상클래스의 역할 전부를 대체할 수 없다.

인터페이스는 여전히 상수변수만을 쓸 수 있다.

그렇기에 객체변수를 선언할 필요가 있는 경우엔

추상클래스가 필요하다.


참고로 자바 8버전부터 디폴트 메소드로 인해

스프링에서 몇몇 추상클래스가 Deprecated 되었다.

한마디로 더이상 못쓴다는 것이다.


예를 들어,

WebMvcConfigurerAdapter라는 추상클래스가 있다.


자바 8버전을 베이스로 만든 스프링 5버전 부터는 쓸 수 없다고 나온다.

이 추상클래스가 Deprecated 된 이유는

앞서 말했듯이 바로 디폴트 메소드의 추가로 인해 생겼다.

혹여나 옛날 코드를 참고해 코드를 짜면 에러가 생길 수 있으므로 참고하길 바란다.


To be continued.........




Made by 꿩



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

Spring Boot와 JSP  (0) 2019.08.02
다형성  (0) 2018.11.03
object vs instance vs class  (3) 2018.10.24

+ Recent posts