쿠키
쿠키는 브라우저에 저장되는 정보로 Key-Value 쌍으로 구성되어 있고, 유효기간을 가지고 있다. 브라우저에 저장할 수 있는 스토리지는 쿠키 말고도 로컬 스토리지와 세션 스토리지가 있는데, 쿠키 이외에는 서버에서 직접적으로 접근할 수는 없다.
PHP 에서는 $_COOKIE
글로벌 변수를 통해 접근할 수 있기 때문에 별도의 설정없이도 접근할 수 있다. 참고로 쿠키에 중요한 정보를 저장해서는 안 된다. 정말로 중요하다면 세션과 같은 서버 저장소에 저장해야 한다.
setcookie(string key, [ string $value = "" [, int $expires = 0 [ ... ]]]): bool
쿠키를 지정하여 Set-Cookie
헤더에 넣을 때는 setcookie
함수를 쓴다. 제목에는 일부 파라매터를 생략했으나, 쿠키를 저장할 패치, 도메인, httpOnly 등을 설정할 수 있다. 사용법은 아래와 같이 간단하다.
setcookie('message', 'Hello, world');
다만, 응답 헤더에 지정하는 것이기 때문에 Http Message 에서 이미 body 가 지정된 상태에서 쿠키를 설정하는 행위를 하면 정상적으로 동작하지 않을 가능성이 있으니 주의하자. 예를 들어 다음과 같이 사용하면 경고를 발생시킨다.
$message = 'Hello, world';
echo $message;
// PHP Warning: Cannot modify header information - headers already sent by
setcookie('message', $message);
$_COOKIE
클라이언트에서 요청한 쿠키를 탐색해볼 수 있는 글로벌 변수다. 물론 여기서 클라이언트 요청시 요청 헤더에 Cookie
가 포함되어야 함을 주의하자. 브라우저에 쿠키가 있다면, 요청시 알아서 쿠키 정보를 서버에 같이 던져준다. 사실 이는 쿠키가 가진 단점이기도 해서 쿠키가 아닌, 스토리지에서는 이런식으로 사용되지 않는다.
// [ 'message' => 'Hello, world' ]
var_dump($_COOKIE);
세션
세션은 서버에 저장되며, 고유한 키를 갖는다. 이 키를 기반으로 데이터베이스나 파일 등에 데이터가 저장되는데, 기본적으로는 파일에 저장되나 SessionHandlerInterface
를 구현하면 데이터베이스나 다른 저장소에도 충분히 저장할 수 있다. 해당 데이터는 쿠키에 저장되는 데이터보다 훨씬 더 중요한 정보를 갖는다. 예를 들면 사용자 로그인 정보가 대표적이라고 볼 수 있다. 그리고 세션 키는 클라이언트와 쿠키로 소통하는 경우가 일반적이다.
php.ini 에 가면 세션과 관련된 설정들을 볼 수 있는데, 여기서 세션의 이름과 세션 쿠키에 대한 설정, 보안, 유효기간과 같은 것들을 설정할 수 있다.
Http 의 특성상 서버는 클라이언트를 분별할 수 없기때문에 쿠키를 소통의 매개체로 삼는다. 그래서 XSS(Cross-Site Scripting)으로 세션 키가 담긴 쿠키가 노출되면 해킹을 당한것과 다름없게 되니 주의해야한다.
$_SESSION
세션은 서버에 저장되는 정보로써 $_SESSION
글로벌 변수를 통해 알아볼 수 있다. 세션을 사용할 때 주로 사용하는 함수들은 다음과 같다. 언급한 함수들 이외에도 세션과 관련된 함수들은 제법 있으니 매뉴얼을 참고하자.
session_start() |
세션을 시작한다. |
session_regenerate_id() |
세션 아이디를 갱신한다. |
session_destory() |
세션을 삭제한다. |
session_unset() |
세션 변수를 초기화한다. |
// 세션을 시작합니다.
session_start();
$_SESSION['message'] = 'Hello, world';
$_SESSION['foo'] = new stdClass();
/* [
'message' => 'Hello, world',
'foo' => class stdClass#1 (0) {}
] */
var_dump($_SESSION);
// 세션 변수를 초기화 하고,
session_unset();
// 세션을 삭제합니다.
session_destroy();
var_dump($_SESSION); // -> []
SessionHandlerInterface
세션과 관련된 인터페이스는 SessionHandlerInterface, SessionIdInterface, SessionUpdateTimestampHandlerInterface
가 있으나, 중요한 것은 SessionHandlerInterface
다. 아래의 예제처럼 작성하면, PDO(PHP Data Object)
를 통해 데이터베이스에 저장할 수 있다.
/* CREATE TABLE sessions(
`id` VARCHAR(255) UNIQUE NOT NULL,
`payload` TEXT,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
); */
// ini_set('session.gc_maxlifetime', 10);
/**
* Session Handler Interface
*/
class DatabaseSessionHandler implements SessionHandlerInterface
{
/**
* @var PDO $pdo
*/
protected PDO $pdo;
/**
* Create a new DatabaseSessionHandler
*
* @return DatabaseSessionHandler
*/
public function __construct(PDO $pdo)
{
$this->pdo = $pdo;
}
/**
* Open Session
*
* @param string $save_path
* @param string $session_name
*
* @return bool
*/
public function open($save_path, $session_name)
{
return true;
}
/**
* read session payload
*
* @param string $session_id
*
* @return string
*/
public function read($session_id)
{
$sth = $this->pdo->prepare('SELECT * FROM sessions WHERE `id` = :id');
if ($sth->execute([ ':id' => $session_id ])) {
if ($sth->rowCount() > 0) {
$payload = $sth->fetchObject()->payload;
} else {
$this->pdo->prepare('INSERT INTO sessions(`id`) VALUES(:id)')->execute([ ':id' => $session_id ]);
}
}
return $payload ?? '';
}
/**
* write session data
*
* @param string $session_id
* @param string $session_data
*
* @return bool
*/
public function write($session_id, $session_data)
{
return $this->pdo->prepare('UPDATE sessions SET `payload` = :payload WHERE `id` = :id')->execute([ ':payload' => $session_data, ":id" => $session_id ]);
}
/**
* run Session GC
*
* @param int $maxlifetime
*
* @return bool
*/
public function gc($maxlifetime)
{
$sth = $this->pdo->prepare('SELECT * FROM sessions');
if ($sth->execute()) {
while ($row = $sth->fetchObject()) {
$timestamp = strtotime($row->created_at);
if (time() - $timestamp > $maxlifetime) {
$this->destroy($row->id);
}
}
return true;
}
return false;
}
/**
* destroy Session
*
* @param string $session_id
*
* @return bool
*/
public function destroy($session_id)
{
return $this->pdo->prepare('DELETE FROM sessions WHERE `id` = :id')->execute([ ':id' => $session_id ]);
}
/**
* close Session
*
* @return bool
*/
public function close()
{
return true;
}
}
SessionUpdateTimestampHandlerInterface
이 녀석을 별도로 언급하는 이유는 은행과 같은 중요한 곳에서는 세션 활성화 시간(대체로 10분)을 지정해놓고 하기 때문이다. 세션 시작시 타임스탬프를 지정해놓고, 일정 시간이 지나면 세션을 종료시키는 것이다. 그 기준은 session.gc_maxlifetime
이 될 수도 있고 임의로 지정해도 된다.
세션의 유효기간이 지나면 더는 해당 세션 데이터에는 접근할 수 없도록 해야한다. 유효기간이 지났을 때 세션 데이터를 삭제할 지는 중요하지 않고, 접근의 측면에서 바라보면 된다.
class DatabaseSessionTimestampHandler extends DatabaseSessionHandler implements SessionUpdateTimestampHandlerInterface
{
/**
* SessionUpdateTimestampHandlerInterface::updateTimestamp
*
* @param string $session_id
* @param string $session_data
*
* @return bool
*/
public function updateTimestamp($session_id, $session_data)
{
return $this->pdo->prepare('UPDATE sessions SET updated_at = CURRENT_TIMESTAMP WHERE id = ?')->execute([ $session_id ]);
}
/**
* SessionUpdateTimestampHandlerInterface::validateId
*
* @param string $session_id
*
* @return bool
*/
public function validateId($session_id)
{
$sth = $this->pdo->prepare('SELECT * FROM sessions WHERE id = ?');
if ($sth->execute([ $session_id ])) {
if ($session = $sth->fetchObject()) {
return (time() - strtotime($session->updated_at)) < (int) ini_get('session.gc_maxlifetime');
}
}
return false;
}
}
validateId()
메서드는 읽기, 쓰기 등 세션에 관한 것에 접근할 때 호출되며, updateTimestamp()
메서드는 세션에 값을 새로 쓸 때는 호출되지 않고, 세션에 재접근할 때 호출된다. 마치 은행에 로그인했을때 세션의 값을 바꾸지 않고 어떤 행위를 하는(재접근)시에 수명이 연장되고, 시간의 경과된 이후 다시 접근을 요청하였으나 세션이 만료(validateId()
실패)되는 것과 같다. 물론 세션이 만료 된 경우에는 자동으로 새로운 세션을 발급해주지만 말이다.
php.ini
session.use_strict_mode
이것을 설정하면 초기화되지 않은 세션을 사용할 수 없도록 한다. 세션 보안과 관련된 주요 옵션은 여러가지가 있겠지만, 먼저해볼 것은 초기화되지 않은 세션을 임의로 사용자가 만들어보는 것이다. session_start()
로 만들어진 것이 아니라, 공격자가 세션을 직접 만들어서 보낼 수도 있다. 우선 아래의 설정을 모두 비활성화 한다.
; Whether to use strict session mode.
; Strict session mode does not accept uninitialized session ID and regenerate
; session ID if browser sends uninitialized session ID. Strict mode protects
; applications from session fixation via session adoption vulnerability. It is
; disabled by default for maximum compatibility, but enabling it is encouraged.
; https://wiki.php.net/rfc/strict_sessions
session.use_strict_mode = 1
; Whether to use cookies.
; http://php.net/session.use-cookies
session.use_cookies = 1
; This option forces PHP to fetch and use a cookie for storing and maintaining
; the session id. We encourage this operation as it's very helpful in combating
; session hijacking when not specifying and managing your own session id. It is
; not the be-all and end-all of session hijacking defense, but it's a good start.
; http://php.net/session.use-only-cookies
session.use_only_cookies = 1
그 다음 Get 요청으로 다음과 같은 URL 형태로 보내보자. 놀랍게도 123456789 라는 세션 키를 가진 세션이 생성될 것이다. 여기서 PHPSESSID 는 세션의 이름이다. 따라서 위와같은 옵션은 모두 활성화하는 것이 좋다.
/?PHPSESSID=123456789
session.cookie_httponly
이것은 브라우저의 자바스크립트에서 세션 쿠키를 확인할 수 없도록 설정한다.
; Whether or not to add the httpOnly flag to the cookie, which makes it inaccessible to browser scripting languages such as JavaScript.
; http://php.net/session.cookie-httponly
session.cookie_httponly = 1
session.cookie_secure
https 를 사용한다면, 쿠키 보안을 활성화 해놓는 것이 좋다.
; http://php.net/session.cookie-secure
session.cookie_secure = true
더 읽을거리
https://www.inflearn.com/course/php7-reboot