티스토리 뷰

서버 기반 인증 (Session, Cookie)

HTTP 프로토콜은 요청에 따른 응답을 받으면 연결이 끊어지고(connectionless)

통신이 종료되면 어떠한 상태 정보도 남지 않는다.(stateless)

따라서 로그인 후 다시 웹페이지에 접근하면 로그인 상태가 유지되지 않는다는 문제점이 있다.

HTTP 프로토콜의 인증 문제를 해결하기 위해 사용하는 방법으로 세션과 쿠키를 사용한다.

 

① 사용자가 로그인 한다. (로그인 정보를 서버로 request)

② 서버는 request가 들어오면 DB를 쿼리 하여 사용자를 검증하고 유효할 경우 사용자의 고유한 ID값을 부여하여

    세션 저장소에 저장한 후, 이와 연결되는 세션 ID를 생성하여 response header에 포함시켜 반환한다.

③ 사용자는 서버에서 해당 세션ID를 받아 쿠키에 저장을 한 후, 제한된 end point(인증이 필요한 요청)에 접근할 때 마다

    쿠키를 request header에 포함시켜 보낸다.

④ 서버에서는 쿠키를 받아 세션 저장소에서 검증한 후 요청에 해당하는 데이터를 반환한다.

 

특징

쿠키(서버에 저장된 세션에 접근하기 위한 세션 ID)가 HTTP 요청 중 노출되어도 쿠키 자체에 중요한 정보는 담겨있지 않다. 하지만 쿠키 자체를 훔쳐 세션에 접근하여 중요한 정보를 빼낼 수 있는데, 이를 막기 위해 세션에 유효시간을 넣거나 HTTPS를 사용해 요청을 훔쳐도 그 안의 정보를 보기 힘들게 한다.

 

쿠키를 통해 세션에 접근하면 세션 ID로 사용자를 구분할 수 있으므로 일일이 사용자 정보를 확인할 필요가 없다.

 

서버(메모리 or DB)에 세션을 저장하기 때문에 사용자 수가 많아지면 서버의 부담이 늘어난다. 

또한, 서버 확장성(scalability)이 나빠진다. 서버 사양 업그레이드뿐만 아니라 늘어나는 트래픽을 감당하기 위해 여러 프로세스를 돌리거나, 여러 대의 서버 컴퓨터를 추가하는 것이 어려워진다.

 

CORS(Cross-Origin Resource Sharing)  
쿠키는 단일 도메인 및 서브 도메인에서만 작동하도록 설계되어 여러 도메인에서 관리하기 번거롭다. 

 


토큰 기반 인증 

세션 기반 인증과는 다르게 서버가 사인한 토큰을 이용하여 인증을 수행하는 방식이다.

 

세션 기반 인증의 stateful 서버는 클라이언트로부터의 요청이 있을 때마다 클라이언트의 상태를 유지한다. 사용자가 로그인을 하여 인증을 요청하면 stateful 서버는 인증에 성공하였을 때의 결과(세션)를 메모리 또는 데이터베이스에 유지하기 때문에 서버에 부하가 발생할 수 있다. 하지만 토큰 기반 인증은 stateful 서버와 반대적 개념인 stateless 서버를 사용하며 상태 정보를 유지하지 않는다. 

서버가 전달받은 토큰을 검증만 하면 되기 때문에 서버의 부담을 줄이고 서비스의 확장성을 높일 수 있다.

 

① 사용자가 로그인 한다. (로그인 정보를 서버로 request)

② 서버는 request가 들어오면 사용자를 검증하고 유효할 경우 정상적으로 발급된 토큰임을 증명하는 signature를 갖는 토큰을 

    클라이언트에 반환한다. 

③ 클라이언트는 토큰을 저장하고 서버 요청 시 해당 토큰을 Request header에 담아 서버에 전달한다. 

④ 서버는 토큰을 검증한 후, 요청에 응답한다.

 

특징

Stateless 서버 
Stateful 서버는 클라이언트에게서 요청을 받을 때마다 클라이언트의 상태를 계속해서 유지하고 이 정보를 서비스 제공에 이용한다.

그 예로는 세션을 유지하는 웹서버가 있다. 로그인을 하면 세션에 로그인이 되었다고 저장을 해 두고 서비스를 제공할 때에 그 데이터를 사용한다. 세션은 서버의 메모리나 데이터베이스에 저장한다. 

Stateless 서버는 상태를 유지하지 않는다. 서버는 클라이언트 측에서 들어오는 요청만으로만 작업을 처리한다. 클라이언트와 서버의 연결고리가 없기 때문에 서버의 확장성(Scalability)이 높아진다. 


플랫폼 간 권한 공유

대표적인 예로 OAuth가 있다. 페이스북/구글 같은 소셜 계정들을 이용하여 다른 웹서비스에서도 로그인할 수 있다. 

 

모바일 애플리케이션 사용에 편리

세션 기반 인증을 사용하면 쿠키 매니저를 따로 관리해줘야 하지만 토큰을 사용하면 웹 요청 API 헤더에 넣어서 사용해주면 되기 때문에 더 이상 쿠키 매니저를 사용할 필요가 없어진다.


CSRF(Cross-Site Request Forgery, 사이트 간 요청 위조) 방지

사용자가 사이트를 벗어나도 이미 쿠키가 사용자 정보를 가지고 있기 때문에 공격자에게 노출될 수 있다. 공격자가 임의로 다른 URL로 유도하여 비밀번호를 바꾸거나 회원 탈퇴를 할 경우 쿠키가 있기 때문에 서버는 요청을 신뢰하고 작업을 수행하게 된다. 이러한 문제를 해결하기 위해 탈퇴 시 비밀번호를 한 번 더 요구하거나 토큰과 같은 credential을 포함시킬 수 있다.

토큰 기반 인증에서는 헤더 내에 토큰이 포함되어 CSRF를 방지할 수 있다.

CORS(Cross-Origin Resource Sharing)
쿠키는 단일 도메인 및 서브 도메인에서만 작동하도록 설계되어 있기 때문에 여러 도메인에서 관리하기 어렵다. 하지만 토큰 기반 인증은 토큰만 유효하다면 어디서든 작동할 수 있다.

서버 확장 문제 해결

여러 개의 서버에서 한 세션이 첫 번째 서버에 생성되었다고 가정했을 때, 새로운 요청이 발생하고 그 요청이 다른 서버에 전달되면 해당 서버에는 세션 정보가 없을 것이기 때문에 unauthrized 응답을 받을 것이다.

sticky 세션(같은 서버에 세션을 계속 연결시키는 방식)을 사용하여 해결할 수 있지만 토큰 기반 인증에서 요청 토큰은 모든 요청, 모든 서버가 가로채기 때문에 자연스럽게 이러한 문제가 해결된다.

 

토큰 방식의 문제
stateless한 토큰의 특성 때문에 토큰을 강제로 만료시킬 수 없는 문제가 있다.

토큰이 공격자에게 탈취되었다고 가정하면, 공격자는 토큰이 만료될 때까지 서버에 요청을 할 수 있다.

위와 같은 상황을 해결하기 위해 토큰의 만료 주기를 너무 짧게 하면 수시로 로그인을 다시 하게 되어 사용자가 불편해지고, 그렇다고 사용자 편의를 위해 만료 주기를 길게 하면 토큰이 탈취당했을 때 피해가 커지게 된다.
토큰 인증 방식의 단점을 보완하기 위해 보통은 토큰의 타입을 리프레시 토큰과 액세스 토큰으로 나누어 사용하는 방식을 택한다.

이 방식은 서비스를 요청하는 데 사용하는 만료 주기가 짧은 액세스 토큰과 액세스 토큰을 재발급받을 수 있는 보안이 철저하고 만료 주기가 긴 리프레시 토큰을 사용한다. 액세스 토큰은 탈취당하더라도 보안이 철저한 리프레시 토큰을 탈취하지 못하면 공격할 수 있는 시간은 얼마 되지 않기 때문에 피해가 적어진다.

 

토큰 저장 위치
서버가 토큰을 발급해주면, 브라우저에서 사용자/서버 간에 토큰이 전달되는 방식은 크게 두 가지로 나뉜다.

첫 번째는 로그인 성공시 서버가 응답 정보에 토큰을 넣어서 전달하도록 하고, 해당 값을 웹 스토리지(localStorage 혹은 sessionStorage)에 넣고 다음부터 웹 요청을 할 때마다 HTTP 헤더 값에 넣어서 요청하는 방법이다.
이 방법은 구현하기 쉽고 하나의 도메인에 제한되어있지 않다는 장점이 있지만, XSS 해킹 공격을 통하여 해커의 악성 스크립트에 노출이 되는 경우 매우 쉽게 토큰이 탈취될 수 있다. 그냥 localStorage에 접근하면 바로 토큰에 접근할 수 있기 때문이다.

이에 대한 대안으로 두 번째 방식은 토큰을 쿠키에 넣는 것이다.

쿠키를 사용한다고해서 세션을 관리하는 것은 아니고, 그저 쿠키를 정보 전송수단으로 사용할 뿐이다. 

이 과정에서 서버측에서 응답을 하면서 쿠키를 설정해 줄 때 httpOnly 값을 활성화를 해주면, 네트워크 통신 상에서만 해당 쿠키가 붙게 된다. 따라서 브라우 저상에서는 자바스크립트로 토큰 값에 접근하는 것이 불가능해진다.

 

이 방법의 단점은 쿠키가 한정된 도메인에서만 사용이 된다는 점이다. 이 문제는 토큰이 필요해질 때 현재 쿠키에 있는 토큰을 사용하여 새 토큰을 문자열로 받아올 수 있게 하는 API를 구현하여 해결하면 된다.
또 다른 단점은 XSS의 위험에서 완벽히 해방되는 대신 CSRF 공격의 위험성이 생긴다는 점이다. CSRF는 계정 정보를 탈취하는 것은 아니지만 스크립트를 통해 사이트의 외부에서 사이트의 API를 사용하는 것처럼 모방하는 것이다. 또는, 사이트 내부에서 스크립트가 실행되어 원하지 않는 작업이 수행되게 할 수도 있다.
예를 들면 다음과 같다 => 유저도 모르는 사이 탈퇴 / 덧글을 자동으로 작성 / 포스트를 자동으로 작성 /회원정보 변경
이러한 CSRF는 HTTP 요청 레퍼러 체크, 그리고 CSRF 토큰의 사용을 통하여 방지할 수 있다.


JSON Web Token(JWT)

JSON Web Token은 인증 헤더 내에서 사용되는 토큰 포맷이다. 토큰은 Base64로 인코딩한 String으로 이루어져 있다. 

이 토큰은 두 개의 시스템끼리 안전한 방법으로 통신할 수 있도록 설계하는 것을 도와준다. 

JTW의 장점은 계정 서버와 API 서버가 분리되어 있을 때, API 서버가 계정 서버에게 토큰의 유효성 여부를 물어보지 않고도 스스로 판단할 수 있다는 것이다.

 

Access token은 단순하게 자원에 접근하는 access token만이 아니라, 권한/인증에 대한 token을 말한다. 

Refresh token은 Access token과 똑같은 형태의 JWT이고 Access token의 탈취 문제를 해결하기 위해 발급하는 토큰이다.

처음에 로그인을 완료했을 때 Access token과 동시에 발급되는 Refresh token은 긴 유효기간을 가지면서, 

Access token이 만료됐을 때 새로 발급해주는 열쇠가 된다.

 

JWT는 토큰 자체가 의미를 갖는 Claim기반의 토큰으로 권한과 인증의 역할을 가질 수 있다. 

Claim(권한)은 사용자에 대한 프로퍼티나 속성을 의미한다. 
즉 JWT는 JSON 객체에 요구사항을 작성하고, 어떠한 암호화 방식을 사용해서 문자열로 인코딩을 한 후, 

HTTP header에 추가함으로써 사용자 인증을 요청한다. 서버에서는 이 토큰을 확인한 후 디코딩하여 사용자를 인증하게 된다. 

 

특징 

Stateless

세션/쿠키는 별도의 저장소의 관리가 필요하지만 JWT는 서버 입장에서 요청을 받았을 때 발급한 후 검증만 하면 되기 때문에 

추가 저장소가 필요 없다.

A 서버로 접속했다가 B 서버로 요청을 한다고 해도 문제 생길 것이 없다. 수평으로 쉽게 확장이 가능하다는 의미가 된다. 

 

확장성

토큰 기반으로 하는 다른 인증 시스템에 접근이 가능하다. 

예를 들어 Facebook 로그인, Google 로그인 등은 모두 토큰을 기반으로 인증을 한다. 

이에 선택적으로 이름이나 이메일 등을 받을 수 있는 권한도 받을 수 있다.  


무결성 : HMAC(Hash-based Message Authentication) 기법

JWT Token의 secret 키를 하나라도 입력을 하게 되면 Signature 영역의 글자가 바로 바뀌는 것을 알 수 있다. 

이처럼 JWT Token 은 변조가 되었을 때, 바로 알아차릴 수가 있다.

 

보안

권한을 부여하기 위한 데이터가 JWT안에 모두 담겨있다.

OAuth처럼 인증 서버에서 토큰에 대한 정보를 찾을 필요가 없다. 
정보가 담긴 데이터(JSON 객체)를 암호화해서 HTTP 헤더에 추가시킨다. 하지만
누군가가 토큰을 탈취한다면 그 토큰을 이용해서 권한을 수행할 수가 있다. 
그래서 토큰의 유효시간을 설정할 수 있으며 탈취될 가능성을 줄이기 위해 유효시간을 짧게 해주는 것이 좋다. 


단점

이미 발급된 JWT에 대해서는 돌이킬 수 없다. 

세션/쿠키의 경우 악의적으로 이용될 경우 해당하는 세션을 지우면 된다.

하지만 JWT는 한 번 발급되면 유효기간이 완료될 때 까지는 계속 사용이 가능해서 악의적인 사용자는 유효기간이 지나기 전까지 정보들을 훔쳐갈 수 있다. 
이에 대한 해결책으로 기존의 Access Token의 유효기간을 짧게 하고 Refresh Token이라는 새로운 토큰을 발급한다.

 

Payload 정보가 제한적이다.

Payload는 따로 암호화되지 않기 때문에 디코딩하면 누구나 정보를 확인할 수 있다.

세션/쿠키 방식에서는 유저의 정보가 전부 서버의 저장소에 안전하게 보관되었지만 JWT에서 유저의 중요한 정보들은 Payload에 넣을 수 없다.

세션/쿠키 방식에 비해 JWT의 길이는 길다.

따라서 인증이 필요한 요청이 많아질수록 서버의 자원낭비가 발생하게 된다.

 

JWT의 구조

JWT는 많은 프로그래밍 언어에서 지원하는데, 각 언어의 라이브러리에서 자동으로 인코딩 및 해싱 작업을 해준다. 

 

JWT는 3가지 요소로 구성된다.
Header는 토큰 타입과 암호화 방법을 보관하는 토큰의 한 부분이며, Base-64로 인코딩 된다. 
Payload에는 유저 정보, 상품 정보 등의 다양한 종류의 정보를 넣을 수 있다. Base-64로 인코딩 된다. 
Signature는 Header, Payload, Secret key의 조합이다. Secret key는 반드시 서버에 안전하게 보관되어야 한다. 

 

https://jwt.io/

Header 영역

Header 영역에는 해당 Token에 대한 유형과 알고리즘에 대한 정보가 포함되어 있다. 

위의 Token 은 JWT 공식 홈페이지 에서 가져온 Token으로서 Token의 알고리즘은 HS256이고, 유형은 JWT이다.

알고리즘은 HS256, HS384 등 각 언어별로 지원하는 알고리즘이 있으니 공식 홈페이지에서 확인해서 선택하면 된다.

 

typ : 토큰의 타입을 명시한다. 
alg : 해싱 알고리즘을 명시하는데, 이 알고리즘은 서버에서 토큰을 검증할 때 사용되는 signature에서 사용된다. 


Payload 영역

기본적인 유저에 대한 정보와 함께 추가한 유저의 정보가 포함되어 있다.

각각의 데이터는 속성, 값으로 표현되고 registered, public, private claims으로 구분한다.

Payload 영역은  JWT Token에서는 이미 정해져 있는 이름을 가진 아래의 7개에 대한 정보를 담을 수 있다. 

iss : Token 발행자(Issuer)으로서 해당 필드는 문자열 혹은 URI로 이뤄지면 선택사항이다. 
sub : Token의 제목 (Subject) 으로서 해당 필드의 값은 문자열 혹은 URI로 이루어져 있다. 

          해당 값은 전역적으로나, 발행자 범위에서 유일한 값이어야 한다. 
aud : Token 의 대상자 (Audience) 로서 해당 값에 대한 인증이 이뤄지지 않으면 해당 Token을 이용한 접근이 거부된다. 

          해당 필드는 문자열 혹은 URI로 이루어져 있다. 
exp : Token의 유효한 날짜 정보(Expiration time)으로서, 해당 Token을 가지고 요청을 했을 때 현재의 시간을 지났다면

          해당 Token 을 이용한 접근이 거부된다. 일반적으로는 몇 분 이내로 설정을 하며 해당 값은 숫자로만 이루어져야 한다. 
nbf : Token 의 유효 시작 날짜 정보 (Not before)으로서, 해당 Token을 이용하려면 현재의 시간이 이 정보에 기재된 시간과

         같거나 지났어야 한다. exp와 마찬가지로 일반적으로는 몇 분 이내로 설정하며 해당 값은 숫자로만 이루어져야 한다. 
iat : Token의 발행된 날짜 정보(Issued At)으로서, JWT 이 발행된 시간에 대한 정보를 담고 있다. 위의 exp 혹은 nbf와 

        마찬가지로 숫자로만 이뤄져야 한다. 
jti : Token 의 고유 식별자(JWT ID) 로서, 해당 값은 고유한 식별자에 대한 값이 할당되어야 한다. 

       해당 값은 대소문자를 구별하는 문자열로 구성되어 있다.


registered claim  
미리 정의된 claim으로써, 토큰에 대한 정보이다. 

public claim 

공개적인 claim을 명시하는데, 충돌 방지를 위해 URI 형식으로 작성한다. 
private claim 
서버와 클라이언트가 협의한 claim을 명시한다. 


Signature 영역

Header와 Payload는 암호화를 한 것이 아니라 단순히 JSON문자열을 base64로 인코딩한 것뿐이다. 그래서 누구나 이 값을 다시 디코딩하면 JSON에 어떤 내용이 들어있는지 확인할 수 있다. 토큰을 사용하는 경우 이 토큰을 다른 사람이 위변조 할 수 없어야 하므로 이를 검증하기 위한 부분이 Signature 부분이다.

헤더(header)의 인코딩 값과 내용(payload)의 인코딩 값을 "."으로 연결하여 합친 후, Secret 키와 함께 Header에서 alg로 지정한 알고리즘 HS256으로 인코딩하여 만든다.


header, payload, signature의 각 값들을 "."으로 합치면 하나의 JWT가 생성된다. 
이렇게 생성된 JWT를 HTTP header에 추가하여 서버에 요청을 하면 서버에서는 이를 디코딩하여 사용자 인증을 하게 된다. 

JWT는 자체적으로 정보를 갖고 있는 토큰이기 때문에 서버에 저장될 필요가 없다. (쿠키 or local storage 등에 저장한다.)

즉, 서버로부터 독립적이라 할 수 있으며 서버의 부담을 덜어줄 수 있다는 장점이 있다. 

 

JWT Process

일반적으로 JWT 토큰 기반의 인증 시스템은 다음과 같은 프로세스로 이루어진다. 

★ 처음 사용자를 등록할 때 Access token과 Refresh token이 모두 발급되어야 한다.

 

① 사용자가 로그인한다. 

② 서버는 요청을 확인하고 사용자를 검증한 후,

    사용자의 고유한 ID값을 부여하고 기타 정보와 함께 Payload를 작성하여

    암호화할 secret key를 통해 access token을 발급하여 클라이언트로 반환한다.

③ 이후 사용자가 JWT가 요구되는 API를 요청할 때 클라이언트가 Authorization header에 Access token을 담아서 보낸다. 

④ 서버는 Access token(JWT)의 Signature를 secret key를 통해 복호화하여 검증한 후,

    Payload를 디코딩하여 사용자 정보를 확인해 요청한 데이터를 클라이언트로 반환한다. 

 

Access token + Refresh token Process

1. 사용자가 로그인한다. 
2. 서버는 요청을 확인하고 사용자를 검증한 후, Access Token, Refresh Token을 발급한다.

    (일반적으로 회원 DB에 Refresh token을 저장한다.)

3. 사용자는 Refresh Token은 안전한 저장소에 저장 후, Access Token을 헤더에 실어 요청을 보낸다.
4. Access Token을 검증하여 이에 맞는 데이터를 보낸다.


5. 시간이 지나 Access Token이 만료된 후 사용자가 요청을 보내면

    서버는 Access Token이 만료됨을 확인하고 권한 없음을 신호로 보낸다.
6. 사용자는 Refresh Token과 Access Token을 함께 서버로 보낸다.
7. 서버는 받은 Access Token이 조작되지 않았는지 확인한 후, Refresh Token과 사용자의 DB에 저장되어 있던 Refresh Token을 비교한다. Token이 동일하고 유효기간도 지나지 않았다면 새로운 Access Token을 발급해준다.
8. 사용자는 새로운 Access Token을 헤더에 실어 다시 API 요청을 진행한다. 

 

Access Token 만료가 될 때마다 권한 없음을 확인할 필요 없이, 사용자가 Access Token의 Payload를 통해 유효기간을 확인하여

프런트엔드 단에서 API 요청 전에 토큰이 만료됐다면 바로 재발급 요청을 할 수도 있다. 


JWT와 OAuth
JWT는 토큰 유형이고 OAuth는 토큰을 발급하고 인증하는 방법을 설명하는 일종의 프레임워크이다.

기존의 /outh/token endpoint에 의해 발급되는 모든 토큰은 일종의 OAuth 프레임워크에 의해 관리된다고 볼 수 있다.

{ 
"token_type":"bearer", 
"access_token":"eyJ0eXAiOiJKV1QiLCJh", 
"expires_in":20, 
"refresh_token":"fdb8fdbecf1d03ce5e6125c067733c0d51de209c" 
} 

위의 토큰이 기존 OAuth에서 주로 사용하는 bearer 기반의 토큰 방식이다. 

다만 JWT는 토큰 자체에 유저 정보를 담아서 HTTP 헤더로 전달하기 때문에 유저 세션을 유지할 필요가 없고 가볍게 데이터를 주고받을 수 있다는 장점이 있다.

 

Access Token과 Refresh Token의 저장

API요청을 허가하는데 Access Token을 사용하고, 액세스 토큰이 만료된 후 새로운 액세스 토큰을 얻기 위해 Refresh Token을 사용한다.

 

Access Token은 리소스에 직접 접근할 수 있도록 해주는 정보만을 가지고 있다. 

즉, 클라이언트는 Access Token이 있어야 서버 자원에 접근할 수 있다. 

Access Token은 짧은 수명을 가지며, 만료기간을 가진다. 

주로 세션에 담아서 관리한다.

 

Refresh Token은 새로운 Access Token을 발급받기 위한 정보를 가진다. 

즉, 클라이언트가 Access Token이 없거나 만료되었다면 Refresh Token을 통해 Auth Server에 요청해서 발급받을 수 있다. 

Refresh Token 또한 만료기간이 있지만 길다. 

Refresh Token은 중요하기 때문에 외부에 노출되지 않도록 엄격하게 관리해야 하므로 주로 데이터베이스에 저장한다.
​ 


참고 사이트

https://tansfil.tistory.com/58

https://artoria.us/25

https://swalloow.github.io/implement-jwt

https://sanghaklee.tistory.com/47

 

 

 

'do > term' 카테고리의 다른 글

type assertion  (0) 2019.04.14
REST API  (0) 2019.04.04
서버 기반 인증, 토큰 기반 인증 (Session, Cookie / JSON Web Token)  (3) 2019.04.02
Boilerplate  (0) 2019.04.01
Singleton pattern, Scaffolding  (0) 2019.03.31
SSR, CSR / Cookie, Session  (0) 2019.03.30
댓글
  • 프로필사진 에드 신 정리 잘 되어 있네요! 감사합니다 :) 2019.09.30 13:34 신고
  • 프로필사진 기네스 안녕하세요 정리된 내용 잘 읽었습니다.
    그런데 혹시
    Access token + Refresh token Process 부분에
    8. 서버는 새로운 Access Token을 헤더에 실어 다시 API 요청을 진행한다.
    => 8. 사용자는 새로운 Access Token을 헤더에 실어 다시 API 요청을 진행한다.
    이거를 잘못 쓰신 것 아닌지요?
    2019.12.02 23:11
  • 프로필사진 사용자 dooo.park 맞습니다. 알려주셔서 감사합니다. 수정하였습니다 2019.12.04 00:47 신고
댓글쓰기 폼
공지사항
Total
19,854
Today
1
Yesterday
30
링크
TAG
more
«   2021/06   »
    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      
글 보관함