Yesodにserversession-backend-redisを組み込んで動かしてみたのでメモしておく

学習がてら、HaskellのWebフレームワークであるYesodと、セッション管理をサポートするライブラリであるserversessionを組み合わせて動かしてみました。


今回は、stackのtemplateであるyesodweb/simpleを利用して、redissessionというプロジェクト名にしました。

```haskell
stack new redissession yesodweb/simple
```


今回は、SessionストレージにRedisを使いたかったので、[serversession](https://hackage.haskell.org/package/serversession)というライブラリを使いました。
（このライブラリは長いことメンテナンスされていないようですが、今回はHaskellでのセッション周りの実装例を見てみることが目的なので、気にせず進みます。）

まずは、package.yamlにserversessionパッケージの情報を追記します。
フロントエンドとしてYesod、バックエンドとしてRedisを使用するので、serversession-frontend-yesodとserversession-backend-redisも追記します。
また、serversesion-backend-redisはhedisというパッケージに依存しているので、それも追記します。

```yaml:package.yaml
dependencies:
(中略)
- serversession == 1.0.1
- serversession-frontend-yesod == 1.0
- serversession-backend-redis == 1.0.3
- hedis < 0.11
```

serversession-frontend-yesodとserversession-backend-redisは、今回使用したStackageのresolverのlts-16.20に含まれていなかったため、stack.yamlにも追記します。
また、このままではパッケージ間のバージョン依存関係の不整合によりエラーが発生してしまったので、"allow-newer: true"を設定して、バージョン依存関係の不整合を無視します。
今回はまず動かしてみることが目的なので、これでよしとします。

```yaml:stack.yaml
extra-deps:
- serversession-frontend-yesod-1.0
- serversession-backend-redis-1.0.3

allow-newer: true
```



```haskell:src/Foundation.hs
import ...(略)

-- 以下を追記
import Web.ServerSession.Backend.Redis (RedisStorage(..))
import Web.ServerSession.Frontend.Yesod (simpleBackend, setCookieName, setSecureCookies)
import qualified Database.Redis as Redis

(中略)

data App = App 
    { appSettings    :: AppSettings
    , appStatic      :: Static -- ^ Settings for static file serving.
    , appHttpManager :: Manager
    , appLogger      :: Logger
    , appConn        :: Redis.Connection --追加
    }
```

```haskell:src/Foundation.hs
instance Yesod App where

(中略)

  makeSessionBackend :: App -> IO (Maybe SessionBackend)
  makeSessionBackend = simpleBackend opts . createStorage
    where createStorage appl = RedisStorage (appConn appl) idleTimeoutSec absoluteTimeoutSec
          idleTimeoutSec     = Just $ 60*10
          absoluteTimeoutSec = Just $ 60*30
          opts               = setCookieName "REDISSESSION_ID"
                             . setSecureCookies False
```


```haskell:src/Application.hs
import qualified Database.Redis as Redis

makeFoundation :: AppSettings -> IO App 
makeFoundation appSettings = do
    -- Some basic initializations: HTTP connection manager, logger, and static
    -- subsite.
    appHttpManager <- getGlobalManager
    appLogger <- newStdoutLoggerSet defaultBufSize >>= makeYesodLogger
    appStatic <-
        (if appMutableStatic appSettings then staticDevel else static)
        (appStaticDir appSettings)
    appConn <- Redis.connect Redis.defaultConnectInfo  -- ここを追記                                                                                                                                                                                       

    -- Return the foundation
    return App {..}
```



```haskell:src/Handler/Home.hs
 {-# LANGUAGE NoImplicitPrelude #-}
 {-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE TemplateHaskell #-}
 {-# LANGUAGE MultiParamTypeClasses #-}
 {-# LANGUAGE TypeFamilies #-}
 {-# LANGUAGE QuasiQuotes #-} --追加

import Web.ServerSession.Frontend.Yesod (forceInvalidate, ForceInvalidate(..))
import Import
import Yesod.Form.Bootstrap3 (BootstrapFormLayout (..), renderBootstrap3)
import Text.Julius (RawJS (..))

import Web.ServerSession.Frontend.Yesod (forceInvalidate, ForceInvalidate(..)) 
```

```haskell:src/Handler/Home.hs
getHomeR :: Handler Html
getHomeR = do
    sess <- getSession
    forceInvalidate CurrentSessionId -- これを呼ぶとセッションIDが再採番される
    defaultLayout
        [whamlet|
            <form method=post>
                <input type=text name=key>
                <input type=text name=val>
                <input type=submit>
            <h1>#{show sess}
        |]  

postHomeR :: Handler ()
postHomeR = do
    (key, mval) <- runInputPost $ (,) <$> ireq textField "key" <*> iopt textField "val"
    case mval of
        Nothing -> deleteSession key 
        Just val -> setSession key val 
    liftIO $ print (key, mval)
    redirect HomeR
```

動かしてみます。

Redisを立ち上げておきます。

```
$ redis-server
```

Webサーバを起動します

```
$ stack exec -- yesod devel
```

Webサーバが立ち上がったので、Webブラウザで、localhost:3000にアクセスします。


![スクリーンショット 2021-01-06 23.37.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/444014/3cd28fd5-cf01-83e3-4d45-c72c05950e04.png)

Set-Cookieヘッダーに、「REDISSESSION_ID」フィールドがあります。うまく動いていそうです。
次に、Sessionストレージにデータを保存してみます。
Keyは「hoge」、Valueは「fuga」を指定します。

![スクリーンショット 2021-01-06 23.37.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/444014/9560a1fe-087c-9021-bbf4-877872a409cc.png)

Session情報が画面に表示されました。ちゃんと動いていそうです。
（「forceInvalidate CurrentSessionId」を呼んだので、SetCookieヘッダーの「REDISSESSION_ID」の値が変わっていますが、このタイミングでは本来必要ありません。）

![スクリーンショット 2021-01-06 23.38.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/444014/be33dde7-a24e-fae9-d6af-19a187c72ec1.png)


redis-cliでも、Redisに格納されているデータを確認しておきます。

![スクリーンショット 2021-01-06 22.52.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/444014/d1b25699-fc92-0a64-2c54-22b98f90d3ce.png)

ブラウザで入力したデータが、ちゃんと保存されています。