너의 개발은/PHP

PHP: 쿠키와 세션

쿠키

쿠키브라우저에 저장되는 정보로 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

 

 

PHP 7+ 프로그래밍: 리부트 - 인프런

기초 문법부터 내장 함수, 웹 보안, 게시판 만들기까지 PHP 언어를 시작하는 분들을 위해 바이블이 될 수 있게 만들어보고자 하는 마음으로 이번 강좌를 만들어보았습니다. 입문 웹 개발 프로그��

www.inflearn.com

 

PHP 7+ 프로그래밍: 객체지향 - 인프런

PHP 객체지향, 내장 클래스, PSR, Composer, MVC(Model, View, Controller)까지 모던 PHP를 익히기 위한 근간을 이야기합니다. 초급 프로그래밍 언어 알고리즘 PHP 객체지향 알고리즘 온라인 강의 모던 PHP 프로��

www.inflearn.com

'너의 개발은 > PHP' 카테고리의 다른 글

PHP: 파일 업로드와 다운로드  (0) 2020.06.10
PHP: 데이터베이스 (MySQLi, PDO)  (0) 2020.06.10
PHP: 쿠키와 세션  (0) 2020.04.29
PHP: HTML 폼 (GET, POST)  (0) 2020.04.29
PHP: 객체 비교와 복사  (0) 2020.04.25
PHP: 참조 (WeakReference)  (0) 2020.04.25