CookieHandler를 이용한 쿠키 관리
자바 플랫폼의 경우, URL을 통한 오브젝트 액세스는 일련의 프로토콜 핸들러에 의해 관리된다. URL의 첫 부분은 사용되는 프로토콜을 알려주는데, 예를 들어 URL이 file:
로 시작되면 로컬 파일 시스템 상에서 리소스를 액세스할 수 있다. 또, URL이 http:로 시작되면 인터넷을 통해 리소스 액세스가 이루어진다. 한편, J2SE 5.0은 시스템 내에 반드시 존재해야 하는 프로토콜 핸들러(http, https, file, ftp, jar 등)를 정의한다.
J2SE 5.0은 http 프로토콜 핸들러 구현의 일부로 CookieHandler
를 추가하는데, 이 클래스는 쿠키를 통해 시스템 내에서 상태(state)가 어떻게 관리될 수 있는지를 보여준다. 쿠키는 브라우저의 캐시에 저장된 데이터의 단편이며, 한번 방문한 웹 사이트를 다시 방문할 경우 쿠키 데이터를 이용하여 재방문자임을 식별한다. 쿠키는 가령 온라인 쇼핑 카트 같은 상태 정보를 기억할 수 있게 해준다. 쿠키에는 브라우저를 종료할 때까지 단일 웹 세션 동안 데이터를 보유하는 단기 쿠키와 1주 또는 1년 동안 데이터를 보유하는 장기 쿠키가 있다.
J2SE 5.0에서 기본값으로 설치되는 핸들러는 없으나, 핸들러를 등록하여 애플리케이션이 쿠키를 기억했다가 http 접속 시에 이를 반송하도록 할 수는 있다.
CookieHandler
클래스는 두 쌍의 관련 메소드를 가지는 추상 클래스이다. 첫 번째 쌍의 메소드는 현재 설치된 핸들러를 찾아내고 각자의 핸들러를 설치할 수 있게 한다.
getDefault()
setDefault(CookieHandler)
보안 매니저가 설치된 애플리케이션의 경우, 핸들러를 얻고 이를 설정하려면 특별 허가를 받아야 한다. 현재의 핸들러를 제거하려면 핸들러로 null을 입력한다. 또한 앞서 얘기했듯이 기본값으로 설정되어 있는 핸들러는 없다.
두 번째 쌍의 메소드는 각자가 관리하는 쿠키 캐시로부터 쿠키를 얻고 이를 설정할 수 있게 한다.
get(URI uri, Map<String, List<String>> requestHeaders)
put(URI uri, Map<String, List<String>> responseHeaders)
get()
메소드는 캐시에서 저장된 쿠기를 검색하여 requestHeaders
를 추가하고, put()
메소드는 응답 헤더에서 쿠키를 찾아내어 캐시에 저장한다.
여기서 보듯이 핸들러를 작성하는 일은 실제로는 간단하다. 그러나 캐시를 정의하는 데는 약간의 추가 작업이 더 필요하다. 일례로, 커스텀 CookieHandler
, 쿠키 캐시, 테스트 프로그램을 사용해 보기로 하자. 테스트 프로그램은 아래와 같은 형태를 띠고 있다.
-
import java.io.*;
-
import java.net.*;
-
import java.util.*;
-
-
public class Fetch {
-
if (args.length == 0) {
-
}
-
CookieHandler.setDefault(new ListCookieHandler());
-
connection = url.openConnection();
-
obj = connection.getContent();
-
}
-
}
먼저 이 프로그램은 간략하게 정의될 ListCookieHandler
를 작성하고 설치한다. 그런 다음 URL(명령어 라인에서 입력)의 접속을 열어 내용을 읽는다. 이어서 프로그램은 또 다른 URL의 접속을 열고 동일한 내용을 읽는다. 첫 번째 내용을 읽을 때 응답에는 저장될 쿠키가, 두 번째 요청에는 앞서 저장된 쿠키가 포함된다.
이제 이것을 관리하는 방법에 대해 알아보기로 하자. 처음에는 URLConnection
클래스를 이용한다. 웹 상의 리소스는 URL을 통해 액세스할 수 있으며, URL 작성 후에는 URLConnection
클래스의 도움을 받아 사이트와의 통신을 위한 인풋 또는 아웃풋 스트림을 얻을 수 있다.
-
String urlString = ...;
-
// .. read content from stream
접속으로부터 이용 가능한 정보에는 일련의 헤더들이 포함될 수 있는데, 이는 사용중인 프로토콜에 의해 결정된다. 헤더를 찾으려면 URLConnection
클래스를 사용하면 된다. 한편, 클래스는 헤더 정보 검색을 위한 다양한 메소드를 가지는데, 여기에는 다음 사항들이 포함된다.
getHeaderFields()
- 가용한 필드의Map
을 얻는다.
getHeaderField(String name)
- 이름 별로 헤더 필드를 얻는다.
getHeaderFieldDate(String name, long default)
- 날짜로 된 헤더 필드를 얻는다.
getHeaderFieldInt(String name, int default)
- 숫자로 된 헤더 필드를 얻는다.
getHeaderFieldKey(int n) or getHeaderField(int n)
- 위치 별로 헤더 필드를 얻는다.
일례로, 다음 프로그램은 주어진 URL의 모든 헤더를 열거한다
-
import java.net.*;
-
import java.util.*;
-
-
public class ListHeaders {
-
if (args.length == 0) {
-
}
-
Map<String,List<String>> headerFields =
-
connection.getHeaderFields();
-
Set<String> set = headerFields.keySet();
-
Iterator<String> itor = set.iterator();
-
while (itor.hasNext()) {
-
headerFields.get(key));
-
}
-
}
-
}
ListHeaders
프로그램은 가령 http://java.sun.com 같은 URL을 아규먼트로 취하고 사이트로부터 수신한 모든 헤더를 표시한다. 각 헤더는 아래의 형태로 표시된다.
Key: <key> / [<value>]
따라서 다음을 입력하면,
>> java ListHeaders http://java.sun.com
다음과 유사한 내용이 표시되어야 한다.
Key: Set-Cookie / [SUN_ID=192.168.0.1:269421125489956; EXPIRES=Wednesday, 31- Dec-2025 23:59:59 GMT; DOMAIN=.sun.com; PATH=/] Key: Set-cookie / [JSESSIONID=688047FA45065E07D8792CF650B8F0EA;Path=/] Key: null / [HTTP/1.1 200 OK] Key: Transfer-encoding / [chunked] Key: Date / [Wed, 31 Aug 2005 12:05:56 GMT] Key: Server / [Sun-ONE-Web-Server/6.1] Key: Content-type / [text/html;charset=ISO-8859-1]
(위에 표시된 결과에서 긴 행은 수동으로 줄바꿈한 것임)
이는 해당 URL에 대한 헤더들만을 표시하며, 그곳에 위치한 HTML 페이지는 표시하지 않는다. 표시되는 정보에는 사이트에서 사용하는 웹 서버와 로컬 시스템의 날짜 및 시간이 포함되는 사실에 유의할 것. 아울러 2개의 ‘Set-Cookie’ 행에도 유의해야 한다. 이들은 쿠키와 관련된 헤더들이며, 쿠키는 헤더로부터 저장된 뒤 다음의 요청과 함께 전송될 수 있다.
이제 CookieHandler
를 작성해 보자. 이를 위해서는 두 추상 메소드 CookieHandler: get()
과ㅓ put()
을 구현해야 한다.
public void put( URI uri, Map<String, List<String>> responseHeaders) throws IOException
public Map<String, List<String>> get( URI uri, Map<String, List<String>> requestHeaders) throws IOException
우선 put()
메소드로 시작한다. 이 경우 응답 헤더에 포함된 모든 쿠키가 캐시에 저장된다.put()
을 구현하기 위해서는 먼저 ‘Set-Cookie’ 헤더의 List
를 얻어야한다. 이는 Set-cookie
나 Set-Cookie2
같은 다른 해당 헤더로 확장될 수 있다.
List<String> setCookieList = responseHeaders.get("Set-Cookie");
쿠키의 리스트를 확보한 후 각 쿠키를 반복(loop)하고 저장한다. 쿠키가 이미 존재할 경우에는 기존의 것을 교체하도록 한다.
-
if (setCookieList != null) {
-
Cookie cookie = new Cookie(uri, item);
-
// Remove cookie if it already exists in cache
-
// New one will replace it
-
for (Cookie existingCookie : cache) {
-
...
-
}
-
cache.add(cookie);
-
}
-
}
여기서 ‘캐시’는 데이터베이스에서 Collections Framework에서 List
에 이르기까지 어떤 것이든 될 수 있다. Cookie
클래스는 나중에 정의되는데, 이는 사전 정의되는 클래스에 속하지 않는다.
본질적으로, 그것이 put()
메소드에 대해 주어진 전부이며, 응답 헤더 내의 각 쿠키에 대해 메소드는 쿠키를 캐시에 저장한다.
get()
메소드는 정반대로 작동한다. URI에 해당되는 캐시 내의 각 쿠키에 대해, get()
메소드는 이를 요청 헤더에 추가한다. 복수의 쿠키에 대해서는 콤마로 구분된(comma-delimited) 리스트를 작성한다. get()
메소드는 맵을 반환하며, 따라서 메소드는 기존의 헤더 세트로 Map
아규먼트를 취하게 된다. 그 아규먼트에 캐시 내의 해당 쿠키를 추가해야 하지만 아규먼트는 불변의 맵이며, 또 다른 불변의 맵을 반환해야만 한다. 따라서 기존의 맵을 유효한 카피에 복사한 다음 추가를 마친 후 불변의 맵을 반환해야 한다.
get()
메소드를 구현하기 위해서는 먼저 캐시를 살펴보고 일치하는 쿠키를 얻은 다음 만료된 쿠키를 모두 제거하도록 한다.
-
// Retrieve all the cookies for matching URI
-
// Put in comma-separated list
-
StringBuilder cookies = new StringBuilder();
-
for (Cookie cookie : cache) {
-
// Remove cookies that have expired
-
if (cookie.hasExpired()) {
-
cache.remove(cookie);
-
} else if (cookie.matches(uri)) {
-
if (cookies.length() > 0) {
-
cookies.append(", ");
-
}
-
cookies.append(cookie.toString());
-
}
-
}
이 경우에도 Cookie
클래스는 간략하게 정의되는데, 여기에는 hasExpired()
와 matches()
등 2개의 요청된 메소드가 표시되어 있다. hasExpired() 메소드는 특정 쿠키의 만료 여부를 보고하고, matches() 메소드는 쿠키가 메소드에 패스된 URI에 적합한지 여부를 보고한다.
get()
메소드의 다음 부분은 작성된 StringBuilder 오브젝트를 취하고 그 스트링필드 버전을 수정 불가능한 Map에 put한다(이 경우에는 해당 키 ‘Cookie’를 이용).
-
// Map to return
-
Map<String, List<String>> cookieMap =
-
new HashMap<String, List<String>>(requestHeaders);
-
-
// Convert StringBuilder to List, store in map
-
if (cookies.length() > 0) {
-
List<String> list =
-
cookieMap.put("Cookie", list);
-
}
다음은 런타임의 정보 표시를 위해 println
이 일부 추가되어 완성된 CookieHandler
정의이다.
-
import java.io.*;
-
import java.net.*;
-
import java.util.*;
-
-
public class ListCookieHandler extends CookieHandler {
-
-
// "Long" term storage for cookies, not serialized so only
-
// for current JVM instance
-
private List<Cookie> cache = new LinkedList<Cookie>();
-
-
/**
-
* Saves all applicable cookies present in the response
-
* headers into cache.
-
* @param uri URI source of cookies
-
* @param responseHeaders Immutable map from field names to
-
* lists of field
-
* values representing the response header fields returned
-
*/
-
-
public void put(
-
URI uri,
-
Map<String, List<String>> responseHeaders)
-
-
List<String> setCookieList =
-
responseHeaders.get("Set-Cookie");
-
if (setCookieList != null) {
-
Cookie cookie = new Cookie(uri, item);
-
// Remove cookie if it already exists
-
// New one will replace
-
for (Cookie existingCookie : cache) {
-
if((cookie.getURI().equals(
-
existingCookie.getURI())) &&
-
(cookie.getName().equals(
-
existingCookie.getName()))) {
-
cache.remove(existingCookie);
-
break;
-
}
-
}
-
cache.add(cookie);
-
}
-
}
-
}
-
-
/**
-
* Gets all the applicable cookies from a cookie cache for
-
* the specified uri in the request header.
-
*
-
* @param uri URI to send cookies to in a request
-
* @param requestHeaders Map from request header field names
-
* to lists of field values representing the current request
-
* headers
-
* @return Immutable map, with field name "Cookie" to a list
-
* of cookies
-
*/
-
-
public Map<String, List<String>> get(
-
URI uri,
-
Map<String, List<String>> requestHeaders)
-
-
// Retrieve all the cookies for matching URI
-
// Put in comma-separated list
-
StringBuilder cookies = new StringBuilder();
-
for (Cookie cookie : cache) {
-
// Remove cookies that have expired
-
if (cookie.hasExpired()) {
-
cache.remove(cookie);
-
} else if (cookie.matches(uri)) {
-
if (cookies.length() > 0) {
-
cookies.append(", ");
-
}
-
cookies.append(cookie.toString());
-
}
-
}
-
-
// Map to return
-
Map<String, List<String>> cookieMap =
-
new HashMap<String, List<String>>(requestHeaders);
-
-
// Convert StringBuilder to List, store in map
-
if (cookies.length() > 0) {
-
List<String> list =
-
cookieMap.put("Cookie", list);
-
}
-
}
-
}
퍼즐의 마지막 조각은 Cookie
클래스 그 자체이며, 대부분의 정보는 생성자(constructor) 내에 존재한다. 생성자 내의 정보 조각(비트)들을 uri 및 헤더 필드로부터 파싱해야 한다. 만료일에는 하나의 포맷이 사용되어야 하지만 인기 있는 웹 사이트에서는 복수의 포맷이 사용되는 경우를 볼 수 있다. 여기서는 그다지 까다로운 점은 없고, 쿠키 경로, 만료일, 도메인 등과 같은 다양한 정보 조각을 저장하기만 하면 된다.
-
this.uri = uri;
-
this.name = nameValue.substring(0, nameValue.indexOf('='));
-
this.value = nameValue.substring(nameValue.indexOf('=')+1);
-
this.path = "/";
-
this.domain = uri.getHost();
-
-
for (int i=1; i < attributes.length; i++) {
-
nameValue = attributes[i].trim();
-
int equals = nameValue.indexOf('=');
-
if (equals == -1) {
-
continue;
-
}
-
if (name.equalsIgnoreCase("domain")) {
-
if (uriDomain.equals(value)) {
-
this.domain = value;
-
} else {
-
if (!value.startsWith(".")) {
-
value = "." + value;
-
}
-
uriDomain =
-
uriDomain.substring(uriDomain.indexOf('.'));
-
if (!uriDomain.equals(value)) {
-
"Trying to set foreign cookie");
-
}
-
this.domain = value;
-
}
-
} else if (name.equalsIgnoreCase("path")) {
-
this.path = value;
-
} else if (name.equalsIgnoreCase("expires")) {
-
try {
-
this.expires = expiresFormat1.parse(value);
-
try {
-
this.expires = expiresFormat2.parse(value);
-
"Bad date format in header: " + value);
-
}
-
}
-
}
-
}
-
}
클래스 내의 다른 메소드들은 단지 저장된 데이터를 반환하거나 만료 여부를 확인한다.
-
public boolean hasExpired() {
-
if (expires == null) {
-
return false;
-
}
-
return now.after(expires);
-
}
-
-
StringBuilder result = new StringBuilder(name);
-
result.append("=");
-
result.append(value);
-
return result.toString();
-
}
쿠키가 만료된 경우에는 ‘match’가 표시되면 안 된다.
-
public boolean matches(URI uri) {
-
-
if (hasExpired()) {
-
return false;
-
}
-
-
if (path == null) {
-
path = "/";
-
}
-
-
return path.startsWith(this.path);
-
}
스펙이 도메인과 경로 양쪽에 대해 매치를 수행할 것을 요구한다는 점에 유의해야 한다. 단순성을 위해 여기서는 경로 매치만을 확인한다.
Cookie
아래는 전체 Cookie
클래스의 정의이다.
-
import java.net.*;
-
import java.text.*;
-
import java.util.*;
-
-
public class Cookie {
-
-
String name;
-
String value;
-
URI uri;
-
String domain;
-
Date expires;
-
String path;
-
-
-
-
-
/**
-
* Construct a cookie from the URI and header fields
-
*
-
* @param uri URI for cookie
-
* @param header Set of attributes in header
-
*/
-
this.uri = uri;
-
this.name =
-
nameValue.substring(0, nameValue.indexOf('='));
-
this.value =
-
nameValue.substring(nameValue.indexOf('=')+1);
-
this.path = "/";
-
this.domain = uri.getHost();
-
-
for (int i=1; i < attributes.length; i++) {
-
nameValue = attributes[i].trim();
-
int equals = nameValue.indexOf('=');
-
if (equals == -1) {
-
continue;
-
}
-
if (name.equalsIgnoreCase("domain")) {
-
if (uriDomain.equals(value)) {
-
this.domain = value;
-
} else {
-
if (!value.startsWith(".")) {
-
value = "." + value;
-
}
-
uriDomain = uriDomain.substring(
-
uriDomain.indexOf('.'));
-
if (!uriDomain.equals(value)) {
-
"Trying to set foreign cookie");
-
}
-
this.domain = value;
-
}
-
} else if (name.equalsIgnoreCase("path")) {
-
this.path = value;
-
} else if (name.equalsIgnoreCase("expires")) {
-
try {
-
this.expires = expiresFormat1.parse(value);
-
try {
-
this.expires = expiresFormat2.parse(value);
-
"Bad date format in header: " + value);
-
}
-
}
-
}
-
}
-
}
-
-
public boolean hasExpired() {
-
if (expires == null) {
-
return false;
-
}
-
return now.after(expires);
-
}
-
-
return name;
-
}
-
-
public URI getURI() {
-
return uri;
-
}
-
-
/**
-
* Check if cookie isn't expired and if URI matches,
-
* should cookie be included in response.
-
*
-
* @param uri URI to check against
-
* @return true if match, false otherwise
-
*/
-
public boolean matches(URI uri) {
-
-
if (hasExpired()) {
-
return false;
-
}
-
-
if (path == null) {
-
path = "/";
-
}
-
-
return path.startsWith(this.path);
-
}
-
-
StringBuilder result = new StringBuilder(name);
-
result.append("=");
-
result.append(value);
-
return result.toString();
-
}
-
}
이제 조각들이 모두 확보되었으므로 앞의 Fetch
예제를 실행할 수 있다.
>> java Fetch http://java.sun.com Cookies: {Connection=[keep-alive], Host=[java.sun.com], User-Agent=[Java/1.5.0_04], GET / HTTP/1.1=[null], Content-type=[application/x-www-form-urlencoded], Accept=[text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2]} Cache: [] Adding to cache: SUN_ID=192.168.0.1:235411125667328 Cookies: {Connection=[keep-alive], Host=[java.sun.com], User-Agent=[Java/1.5.0_04], GET / HTTP/1.1=[null], Cookie=[SUN_ID=192.168.0.1:235411125667328], Content-type=[application/x -www-form-urlencoded], Accept=[text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2]} Cache: [SUN_ID=192.168.0.1:235411125667328]
(위에 표시된 결과에서 긴 행은 수동으로 줄바꿈한 것임)
‘Cache’로 시작되는 행은 저장된 캐시를 나타낸다. 저장된 쿠키가 즉시 반환되지 않도록 put()
메소드 전에 get()
메소드가 어떻게 호출되는지에 대해 유의하도록 할 것.
쿠키와 URL 접속을 이용한 작업에 관해 자세히 알고 싶으면 자바 튜토리얼의 Custom Networking trail(영문)을 참조할 것. 이는 J2SE 1.4에 기반을 두고 있으므로 튜토리얼에는 아직 여기서 설명한 CookieHandler
에 관한 정보가 실려 있지 않다. Java SE 6 ("Mustang")(영문) 릴리즈에서도 기본 CookieHandler
구현에 관한 내용을 찾아볼 수 있다.