Kuzunoha-NEのブログ

プログラミングなどの勉強をしてます

【TypeScript】CookieとSessionとSessionIDを使ったログイン情報の保持

こんばんは、葛の葉です。

今回はCookieとSessionとSessionIDを使ったログイン情報の保持を実施します。

前述

普段我々が使用しているWebページでは、ログインを必要とするページが多いかと思います。ログインに一回成功するとその成功したログイン状態というステータスになり、ちょっとした時間は改めてログインをする必要がありません。しかし、Httpはステートレスであり、状態を保持するということが出来ません。そのため、どこかにその情報を保存しておくのですが、今回はそのうち、SessionIDを使った方法を記載してみます。

今回のソースコード

github.com

Cookieとは

WebサーバーがWebブラウザにデータを送り、ブラウザがその値を保持します。それをCookieといいます。CookieWebブラウザの機能の一部であり、Cookieを使えないWebブラウザもあることでしょう(現行のWebブラウザはほとんど持っている)。CookieはNameとValueからなります。複数のNameを持つことが出来ます。また、Cookieには様々な設定を施すことができ、例えば保持する時間や、そのCookieを参照できるドメインの指定、Https通信の時のみ保持させる、と言ったものがあります。

Sessionとは

SessionとはWebサーバーがデータベースなどをもちいて、クライアントとサーバー間で保持しておくべきデータを一時的の保管するならわしをいいます。つまり、WebサーバーやWebブラウザにSessionという機能があるのではありません。Webサーバーがプログラムやミドルウェアを通じてデータベースにデータを保持するという点がCookieとの最大の違いになります。また、Sessionは基本的にログイン状態かどうかというステータスを保持するために使用され、長時間は保存されません。

SessionIDとは

SessionIDとはSessionで保存されたデータを取り出すためのKeyとなるIDのことであり、これはCookieとして保存されます。Cookieに保存されたSessionIDはWebブラウザに保存され、都度、Webサーバーから要求を受けるたびに送信します。WebサーバーはそのSessionIDがデータベース等に存在するかどうかを都度確認をし、存在すればログインは正常であるとして、ログイン状態である旨をクライアントに示します。SessionIDが漏洩したり、あるいは推測されてしまうような単純な値であると、悪意を持った人が、一般ユーザーのログイン状態であるステータスを盗むことが出来、それを使って悪事を働くことが出来てしまうため、留意する必要があります。

WebサーバーからCookieを保持させる。

今回もTypeScriptのWebサーバーhttpを使ってCookieを保持させてみます。

nodejs.org

import http from 'http';
const server = http.createServer();
server.on('request', (req:http.IncomingMessage, res:http.ServerResponse) => {
    res.writeHead(200, {'Set-Cookie': 'myCookie=hogehoge;'});
    res.end('200 OK\n');
    return;
});
server.listen(9000, ()=>{
    console.log('listen http://127.0.0.1:9000\n');
});

4行目res.writeHead(200, {'Content-Type':'text/plain', 'Set-Cookie': 'myCookie=hogehoge;'});こちらがHttpResponseとして返却するHeaderを記載している部分になります。このヘッダーを受け取ったWebブラウザはその情報をCookieに保存します。検証モードがついているWebブラウザならすぐに確認できます。Set-Cookieというレスポンスを受けたブラウザは、その値に書かれた<Name>=<Value>としてブラウザに保存します。先に、Cookieを保存する期限を設けることが出来ると記載しましたが、そのオプション指定が特別存在しない場合はブラウザが閉じられるまでの間となります。

f:id:Kuzunoha-NE:20200104121737p:plain
myCookieというNameとhogehogeという値を持つCookie

SessionIDの発行とCookieに保存。

Cookieの保存方法は先の記載のとおりなので、今回はSessionIDを保存させます。SessionIDはログイン状態を保持する重要なものなので、推測されないような値が正しいです。が今回は適当に作ります。

server.on('request', (req:http.IncomingMessage, res:http.ServerResponse) => {
    const sid = createSessionId();
    res.writeHead(200, {'Set-Cookie': `sessionId=${sid};`});
    res.end('200 OK\n');
    return;
});

const createSessionId:() => string = () => {
    const newSessionId =`hogehoge_${new Date().toISOString()}`;
    nodeCache.set(newSessionId, "", 60);
    return newSessionId;
}

今回はNodeCacheというライブラリを使います。KVSのようなもののようです。KeyにSessionIDを渡しておき、中身は空の値にしておきます。

10行目nodeCache.set(newSessionId, "", 60);set関数は以下になってます。

## 第一引数がkey
## 第二引数がvalue
## 第三引数がオプションで保存秒数

www.npmjs.com

SessionIDはNodeCache内とCookie内に保存されました。これをログインが成功したタイミングで渡すようにしてあげます。また、ログインが必要なページにはCookieからSessionIDを取得し、NodeCache内に検索をかけます。SessionIDがNodeCache内に存在していなければログイン状態が保持されていないことになるので、ログインを求めるような表示をします。

import http from 'http';
import NodeCache from 'node-cache';

const server = http.createServer();
const nodeCache = new NodeCache();

server.on('request', (req:http.IncomingMessage, res:http.ServerResponse) => {
    switch (req.url){
        case '/':
            const cookies = cookieParser(req.headers.cookie);
            const sessionId = cookies.sessionId;
            if (nodeCache.has(sessionId)){
                res.writeHead(200);
                res.end('200 Loggined\n');
            } else {
                res.writeHead(200);
                res.end('200 Not Loggined\n');
            }
        break;
        case '/login':
            const sid = createSessionId();
            res.writeHead(200, {'Set-Cookie': `sessionId=${sid};`});
            res.end('200 Cookie Set\n');
        break;
        default:
            res.writeHead(404);
            res.end('404 Not Found\n');
        break;
    }
    return;
});

interface Cookies {
    sessionId:string
}

const cookieParser:(str:string|undefined) => Cookies = (str)=> {
    if(!str) return {sessionId:''};
    const cookies = {sessionId:''};
    str.split(';').forEach((cookie)=>{
        let parts = cookie.split('=');
        let key = parts[0];
        let value = parts[1];
        let part = {[key]: value}
        Object.assign(cookies, part);
    });
    return cookies;
}

const createSessionId:() => string = () => {
    const newSessionId =`hogehoge_${new Date().toISOString()}`;
    nodeCache.set(newSessionId, "", 60);
    return newSessionId;
}

server.listen(9000, ()=>{
    console.log('listen http://127.0.0.1:9000\n');
});

http://127.0.0.1:9000にアクセスすると200 Not Logginedと表示されます。http://127.0.0.1:9000/loginにアクセスするとCookieにSessionIDがセットされます。再びhttp://127.0.0.1:9000にアクセスすると200 Logginedと表示されます。60秒放置した状態で3度、http://127.0.0.1:9000にアクセスすると200 Not Logginedとなります。

f:id:Kuzunoha-NE:20200110214156g:plain
擬似的にログインを再現する

まず、http://127.0.0.1:9000にアクセスするとCookie内のSessionIDを確認します。SessionIDがない場合やSessionIDがNodeCacheに存在しない場合は200 Not Logginedと表示しています。SessionIDが存在する場合は200 Logginedと表示されます。http://127.0.0.1:9000/loginにアクセスするとCookieとNodeCacheにSessionIDを設定します。

あとはユーザー情報とパスワード情報をデータベースから引っ張ってきて、正しい情報ならSessionIDをセットするようにして、そうでなければ認証に失敗した旨を返せば良いです。

ちょっとやってみておもしろかった

今回作成したものは結構単純なものだけど、かなり大変だった。こういったものはライブラリに任せるのが一番楽だと思う。一方で、中身がよくわかってない状態でライブラリを使うのは良くないと思ってた。本当はもう少し複雑なんだと思うけど、まずはこんな感じで終わりにしようと思う。