sinatra, mongoidで簡易ユーザセッション管理

投稿者: | 2015年3月2日

DSC01813

先日の呑み会で Web フレームワークの話が出て、「サーバ挙動からまとめて書けたら便利だよね」という話の流れから久々に sinatra で遊んでみました。

下記、参考にしたページです。

sinatra は簡単に目的の Web アプリケーションが書けるというのが最大のメリットですが、かならずしも「初心者にも簡単」というわけではありません。 それぞれ、何のために何をやっているのかを十分に理解しておかないと変なところで詰まづくと思われます。

かくいう自分も、仕事でも趣味でも Web アプリケーションを書く機会に恵まれず、Rails すらまともに使ったことがない程度の知識。 実は数年前に nginx で Ruby を使ってちょっとしたツールを書こうと思った際に sinatra で一通り動くものを作ったことがありました。数年経つとほとんど忘れていて怖いです。

そんな自分のための覚書です。

セッション管理する基本的な構造

ディレクトリ構成

よく使う構成はテンプレート化してどこかに置いておけばいいんですが、やるたびに忘れているので毎回ゼロから構築してしまいます。かえって手間をかけてしまう…。

    app/
      +- assets/       … CSS や JavaScript のコードが格納されているディレクトリ。
      +- models/
      |    +- user.rb  … MongoDB に格納してもらうオブジェクトを定義してる User クラス
      +- spec/
      +- vender/
      |    +- bundle/  … 利用する gem 置き場
      +- views/
      +- Gemfile       … 利用する gem 定義
      +- app.rb        … Web アプリ本体
      +- config.ru     … Web アプリの定義ファイル
      +- mongoid.yml   … Web アプリが利用する MongoDB の定義ファイル

カレントディレクトリを app ディレクトリにしておいて、次のコマンドを使えばいろいろとできます。

    bundle init             # … このディレクトリを bundle アプリとして設定する
                            # bundle とは依存 gem 一式をローカルに配置して用いる仕組み

    bundle exec shotgun     # … サーバを起動
                            # shotgun はサーバを停止しなくても、スクリプトファイルの
                            # 更新時に自動リロードしてくれる便利な gem

    bundle install --path vender/bundle
                            # … Web アプリに必要な gem ファイルを vender/bundle にインストール
                            # Gemfile に明示的に書いたものをインストールしてくれる

Gemfile の例

使うものを使うだけ定義。ローカルの sinatra を使おうとしたら、バグがあったので最新リビジョンのやつを取ってきています。

    # A sample Gemfile
    source "https://rubygems.org"

    #gem "sass"
    gem "haml"
    gem "bcrypt"
    #gem "coffee-script"
    gem "mongoid"
    gem "shotgun"

    gem 'sinatra', git: 'git@github.com:sinatra/sinatra.git',
      ref: '5f6168bfc92280892e819df524d4508cf9032f6d'
      # for https://github.com/sinatra/sinatra/issues/961

    group :test do
      gem 'rspec'
    end

config.ru の設定

    root = ::File.dirname(__FILE__)
    require ::File.join(root, 'app')
    run Server

MongoDB の設定

MongoDB をインストール後、使う DB やホスト名を指定します。

    development:
      sessions:
        default:
          database: sinatora_test
          hosts:
            - localhost:27017
    production:
      sessions:
        default:
          uri: <%= ENV['MONGOHQ_URL'] %>

HAML テンプレート

views/login.haml

ログイン前に表示されるページのテンプレート。

    %fieldset
      %legend ログイン
      %form{:action => '/session', :method => 'post'}

        %label{:for => "name"} 名前:
        %input{:name => "name", :type => "text", :value => ""}

        %label{:for => "password"} パスワード:
        %input{:name => "password", :type => "password", :value => ""}

        %input{:type => "submit", :value => "ログイン"}

    %fieldset
      %legend 新規登録
      %form{:action => '/regist', :method => 'post'}

        %label{:for => "name"} 名前:
        %input{:name => "name", :type => "text", :value => ""}

        %label{:for => "password"} パスワード:
        %input{:name => "password", :type => "password", :value => ""}

        %input{:type => "submit", :value => "新規登録"}

views/dashboard.haml

ログイン後に表示されるページのテンプレート。

    %h1 Dashboard
    %table.table
      %tbody
        %tr
          %td Name
          %td= @user.name
        %tr
          %td Password(hashed)
          %td= @user.password_hash
    %a(href="/logout") ログアウト

models/user.rb

User クラス

    require 'mongoid'
    require 'bcrypt'
    Mongoid.load!('./mongoid.yml')

    class User
      include Mongoid::Document

      field :name
      field :password_hash
      field :password_salt

      attr_readonly :password_hash, :password_salt

      validates :name, presence: true
      validates :name, uniqueness: true
      validates :password_hash, confirmation: true
      validates :password_hash, presence: true
      validates :password_salt, presence: true

      # パスワードを暗号化するメソッド
      def encrypt_password(password)
        if password.present?
          self.password_salt = BCrypt::Engine.generate_salt
          self.password_hash = BCrypt::Engine.hash_secret(password, password_salt)
        end
      end

      # ユーザーが存在するか
      def self.has_user(name)
        self.where(name: name).first != nil
      end

      # ユーザー認証
      def self.authenticate(name, password)
        user = self.where(name: name).first
        if user && user.password_hash == BCrypt::Engine.hash_secret(password, user.password_salt)
          user
        else
          nil
        end
      end

    end

app.rb

メインスクリプト

    require 'sinatra/base'
    require 'haml'

    require_relative 'models/user'

    class Server < Sinatra::Base

      enable :sessions
      set :session_secret, "My session secret"

      get '/login' do
        session[:user_id] ||= nil
        if session[:user_id]
          redirect '/'
        end
        if params['failed'] == "1"
          @msg = "ログインに失敗しました。"
        elsif params['failed'] == "2"
          @msg = "登録に失敗しました。"
        end
        haml :login
      end

      get '/logout' do
        session[:user_id] = nil
        redirect '/login'
      end

      post '/session' do
        if session[:user_id]
          redirect "/"
        end
        user = User.authenticate(params[:name], params[:password])
        if user
          session[:user_id] = user._id
          redirect '/'
        else
          redirect "/login?failed=1"
        end
      end

      post '/regist' do
        if User.has_user(params[:name])
          redirect "/login?failed=2"
        else
          user = User.new(name: params[:name])
          user.encrypt_password(params[:password])
          if user.save!
            session[:user_id] = user._id
            redirect "/"
          else
            redirect "/login?failed=2"
          end
        end
      end

      get '/' do
        @user = User.where(_id: session[:user_id]).first
        if @user
          haml :dashboard
        else
          redirect '/login'
        end
      end

    end

sinatra や MongoDB が悪いわけではなく、自分の頭が悪いだけなんですが、ひっぱってくるものが多すぎて普段使わないと思い出すだけで頭が痛くなります…(´・ω・`) 「sinatra」「bundle」「Rack」「haml」「slim」「erb」「mongodb」…普段全然使ってないものをひっぱってこようとすると、バックエンドまでちゃんと知らないとまともに使えないのはどの環境でも同じことですが…。 普段から使い始めて、自由自在に使えるようになったらこういう手順も楽チンなんだろうなあと憧れるばかりです。

とりあえず、仕事や趣味のプロジェクトでも比較的絡めやすそうな MongoDB の取り扱いから慣れていこうと考えてます。

コメントを残す

メールアドレスが公開されることはありません。