読者です 読者をやめる 読者になる 読者になる

あすたぴのブログ

astap(あすたぴ)のブログ

私がプログラミングをする理由

あすたぴ です

ふと思い立ったので整理してみます。

別にプログラミングが大好きなわけではない

好きか?って言われたらまぁ好き寄りって感じです。 プログラミングは手段です。 学ぶ理由は目的を遂行するためによりよい方法、より楽な方法を使いたいからです。 目的を遂行するに当たって、よりかっこいい方法をとりたい。みたいな気持ちはあります。

みんなに使ってもらえるようなライブラリとかフレームワークを作りたい。といった気持ちはないです。 世界の天才たちに立ち向かえると思うような頭は持ち合わせていません。

じゃあなんでプログラミングしてるの?

テクノロジーの力は偉大です。 日常生活のなかで周りにはプログラムが多くなってきました。 そういったテクノロジー、技術を理解したいし、自分でも作りたいと思います。

ある程度、出来るようになったら別によくね?

そういった考えもあると思います。

ある程度プログラミングが出来るようになったら、効率の良い人は飯を食うのに困らないかもしれません。 なぜ継続するかといいますと、わからない(理解ができない)ことが多いからです。 例えば、Dockerを使える人は多いと思いますが、DockerがLinux上でどう動いているか。 というところまでわかる人はそれほど多くないのではないかと思います。

そうなると、Dockerを使える。だけであって、 (Dockerと似たようなコンテナ技術とかの)Dockerではないものを使おうとした場合はそれをまた学ぶ必要があります。 私はそれの効率をあげたいと思っています。

技術が使えるようになるために学ぶのではなく、技術を学ぶスピードを上げるために学びます。

最終的にはどうなりたいの?

最終的には、 世界にインパクトを与えたい です。

ポケモンGoはすごかった。 テクノロジーで世界の景色を変えました。(街中の人がポケモンGoをやっている景色という意味)

私はサービスを考える力がある方だと思っていません。 すごいサービスを考えれる人がいた場合に、私はそのサービスを実現できる立場にいたいのです。

そのために必要だと考えていることが以下です。

  • ふつうのシステムだったらサクっと作れる技術力、経験
  • 新しいことを取り込むスピードがはやいこと

ちなみに現職でいまの立場は、この力を付けるのに非常にいい具合です。

Deviseに独自のstrategyを入れる

結論

deviseのデフォルトに沿わない場合は、deviseはいらない。

結局 ほぼwardenだけの話

config/initializers/devise.rb

require 'devise/strategies/media_authenticatable'
~~~
  config.warden do |manager|
    manager.default_strategies(scope: :user).unshift :user_authenticatable
    manager.strategies.add(:user_authenticatable,
                           Devise::Strategies::UserAuthenticatable)
  end

app/models/devise/strategies/user_authenticatable.rb

require 'devise/strategies/authenticatable'
module Devise
  module Strategies
    class MediaAuthenticatable < Authenticatable
      def authenticate!
      # 認証ロジックをかく
      end

      def valid?
      # 認証をする条件を書く
      end
    end
  end
end

これだけかな。

残念なところ

Devise.add_moduleの流儀に従って、モジュールを追加し、strategyだけを追加したくても、モデルが必須だったり(空実装でも定義だけ必要になる)、 DeviseControllerを継承したコントローラーを作ろうとして、routingをadd_moduleから設定しようとした場合、ルーティングを設定するメソッドの実装がActionDispatch::Routingを拡張して書く必要があったり。

まとめ

なんか、そこまでがんばり必要がなさすぎるな。と。 moduleは自分で拡張するものではないんだろうな。

wardenに直接追加するんだったら deviseとは?となる。 もやもやするライブラリだ。

Railsの認証Gem、Deviseとはなんなのか

目的

deivseについての理解を深めて、デフォルト動作ではない認証を作れるようにする。

背景

用意されすぎているライブラリは苦手。 挙動を変えたい場合に出来ること、出来ないことがわかりにくい。 理解したいってこと。

deviseとは

RailsEngineで作られている、Railsの認証ライブラリ。 サクっと認証を用意できる。ために、色々とデフォルト動作が決まっている。 手厚いサポート(機能)があるが、実際そんなにいらないことも多いはず。 ただ、有名すぎてこれを使っちゃうだろう。 wardenをみたあとでは、別にwardenでもいいよね。って気持ちはある。

だがデフォルトの動作を理解し自分の必要な部分だけを残し、ほかはいい感じにカスタマイズが出来るのであれが、deviseを使ったほうが楽だろうなというところ。 それが可能か検証する。

defaultで含まれているモジュール

  • Database Authenticatable
    • 一般的なやつ。passwordで認証をする。DBには暗号化されたパスワードを保存してる
  • Omniauthable
    • omniAuth gemとの連携
  • confirmable
    • 登録時に、確認メールを送る。ログイン時に確認済がどうかを判断する?
  • Recoverable
    • パスワードリセット
  • Registerable
    • 登録、編集、削除
  • Rememberable
    • ユーザーを覚えておくためのトークンを発行したりする。
    • remember me のやつ
  • Trackable
    • サインインしたときのユーザーのIPとかとっておく
  • Timeoutable
    • 時間内にアクティブではないユーザーのセッションを期限切れにする
  • Validatable
    • mailアドレスとパスワードで認証するってことかな?
  • Lockable
    • 指定回数、ログインに失敗したらアカウントをロックする

揃っている。 これらは、(User)モデルで使用するかどうかを指定できるため、簡単につけ外しができる。 ※Tableにカラムがあれば。カラムを消したりしたりすると簡単ではないが(めんどい)。

devise module

Devise.add_moduleメソッドでモジュールを登録できる。 optionは以下。

※以下のリストは正直全然わかってない。

  • model
    • moduleで使用するモデルへのぱす
  • controller
    • moduleで使うコントローラー
  • route
    • moduleの使う route helper
  • strategy
    • strategyを持っているか。 true or false
  • insert_at
    • moduleが含まれる位置

model以外はすべて bool 値でもよく、module名と同じになる。

Deviseの既存のモジュールは lib/devise/modules.rb で読み込まれている。

Devise.with_options model: true do |d|
  # Strategies first
  d.with_options strategy: true do |s|
    routes = [nil, :new, :destroy]
    s.add_module :database_authenticatable, controller: :sessions, route: { session: routes }
    s.add_module :rememberable, no_input: true
  end

  # Other authentications
  d.add_module :omniauthable, controller: :omniauth_callbacks,  route: :omniauth_callback

  # Misc after
  routes = [nil, :new, :edit]
  d.add_module :recoverable,  controller: :passwords,     route: { password: routes }
  d.add_module :registerable, controller: :registrations, route: { registration: (routes << :cancel) }
  d.add_module :validatable

  # The ones which can sign out after
  routes = [nil, :new]
  d.add_module :confirmable,  controller: :confirmations, route: { confirmation: routes }
  d.add_module :lockable,     controller: :unlocks,       route: { unlock: routes }
  d.add_module :timeoutable

  # Stats for last, so we make sure the user is really signed in
  d.add_module :trackable
end

これらは、(User)modelで devise メソッドで定義することで使用できるようになる。

devise :database_authenticatable, :trackable, :omniauthable, omniauth_providers: [:google_oauth2]

このdeviseメソッドはどこからきているのか。

lib/devise/orm/active_record.rb で、active_recordが読み込まれたタイミングのcallbackで、Devise::Models が extends されている。

require 'orm_adapter/adapters/active_record'

ActiveSupport.on_load(:active_record) do
  extend Devise::Models
end

deviseメソッドはなにをしているか。

include Devise::Models::Authenticatable

Models::Authenticatableクラスのinclude

deviseメソッドで指定されたモジュール群の、 include ClassMethods が定義されていた場合、 extends もする。

たとえば、 database_authenticatable の場合だとこんな感じで定義されていて、 Devise::Models.config を呼んで、find_for_database_authentication をモデルに定義する。

      module ClassMethods
        Devise::Models.config(self, :pepper, :stretches, :send_email_changed_notification, :send_password_change_notification)

        # We assume this method already gets the sanitized values from the
        # DatabaseAuthenticatable strategy. If you are using this method on
        # your own, be sure to sanitize the conditions hash to only include
        # the proper fields.
        def find_for_database_authentication(conditions)
          find_for_authentication(conditions)
        end
      end

このモデルに定義したモジュールたちがどう使われるのか。を見ていきたい。

lib/devise/rails/routes.rbdevise_for メソッドから見ていく。

このメソッドは config/routes.rb で以下のような形で呼ぶことになる。

devise_for :users, controllers: { sessions: 'users/sessions' }

devise_forの責任としては、指定したリソース(モデル)を使用した認証関連のルーティングの設定をすること。

devise_for :users とだけ定義した場合は、 User モデルを見て、ルーティングを設定する。 ルーティングの他は Devise::Mapping の生成をする。

mappingはscopeごとに生成され、以下のようなものになる。

mapping = Devise.mappings[:user]
mapping.name #=> :user
mapping.as   #=> "users"
mapping.to   #=> User
mapping.modules  #=> [:authenticatable]

認証ロジックを自作したい。

ここまで見てきたわかったことは、 deviseに処理を追加したい場合は、モジュールを作る。 モジュールには、スコープのモデルに追加したいメソッド、 スコープを使用してのルーティングの追加、 ルーティングに使用するコントローラーの追加、 スコープを認証する際のstrategyの定義ができる。

単純に考えると、モジュールを作るかー。となりそうなものだが、 database_authenticatable を継承したりしていい感じに出来ないの?って思ったりする。

そこらへんを見ていきたい。

モデルに対して、 database_authenticatable を deviseメソッドで指定すると、以下のルーティングが作成される。

    new_user_session GET      /sign_in(.:format)  devise/sessions#new
        user_session POST     /sign_in(.:format)  devise/sessions#create
destroy_user_session DELETE   /sign_out(.:format) devise/sessions#destroy

Deviseに用意されているControllerは DeviseController を継承している。 DeviseControllerApplicationController を継承している。

DeviseControllerは warden へアクセスするヘルパーメソッドが用意されている。

自作するために。

strategyモジュールを作って、モデルに読み込ませる。 認証をするControllerでDeviseControllerを継承し、 warden.authenticate , sign_in 等のヘルパーメソッドを呼ぶ。

こんな感じ。 というかこれだけの事をするのに devise が必要か?というといらない。

devise とはなんだったのか

wardenのヘルパー。

deviseの決めた挙動での認証システムの高速な実装。

strategy, routing等まで含めた認証実装の簡易化。

たとえば、社内システムでSSOを実装する。となった場合に、1度実装すれば簡単に使いまわすことが可能。 という感じだろうか。

まとめ

読んでいないソースが大半なので、確定ではないが、 deviseの標準を大きく外れるようであれば、無理にdeviseを使う必要もないとう判断になった。 wardenで十分ぽい。

RackとWardenについて

Rackについてはこちらをみた。

じぶんの解釈

Rackとはwebサーバーを作る際のお作法(インターフェース)。

インターフェースに従ってWebサーバーを作成することで、サーバーを交換可能。 ミドルウェアを共通化できる。 という感じだろうか

Warden

General Rack Authentication Framework

Rackの上で構築される認証のフレームワーク。 有名なdeviseはwardenのラッパーになる。

Rackミドルウェアとして作られている。 session情報は env['rack.session'] Wardenは env[warden] にオブジェクトを入れる。 このオブジェクトを用いて、認証を行うことができる。

env['warden'].authenticated?               # Ask the question if a request has been previously authenticated
env['warden'].authenticated?(:foo)         # Ask the question if a request is authenticated for the :foo scope
env['warden'].authenticate(:password)      # Try to authenticate via the :password strategy.  If it fails proceed anyway.
env['warden'].authenticate!(:password)     # Ensure authentication via the password strategy. If it fails, bail.

認証が成功されると、 user オブジェクトへアクセスが出来る。 これは nil 以外なら何でもよい。

env['warden'].authenticate(:password) この記述ではパスワードで認証を行っている。 この、どのように認証を行うか。を strategy と呼んでいて、これを拡張して独自で認証機構を実装することができる。

strategy

Rackアプリからwadenを利用していく

ためしにやっていく

Rackアプリ

config.ru というファイル名で以下を作成

require 'rack'


app = Proc.new do |env|
    ['200', {'Content-Type' => 'text/html'}, ['A barebones rack app.']]
end

run app

rackup コマンドで config.ru を読み込んでサーバーを起動する。

vagrant-ubuntu-trusty-64% rackup
Puma starting in single mode...
* Version 3.8.2 (ruby 2.3.0-p0), codename: Sassy Salamander
* Min threads: 0, max threads: 16
* Environment: development
* Listening on tcp://localhost:9292
Use Ctrl-C to stop
vagrant-ubuntu-trusty-64% curl http://localhost:9292/
A barebones rack app.%

curlでリクエストを投げると、レスポンスが確認できる。

wardenの追加

require 'rack'
require 'warden'

app = Proc.new do |env|
    ['200', {'Content-Type' => 'text/html'}, ['A barebones rack app.']]
end

failure_app = Proc.new do |env|
    ['401', {'Content-Type' => 'text/html'}, ['fail.']]
end

use Rack::Session::Cookie, :secret => "replace this with some secret key"
use Warden::Manager do |manager|
  manager.default_strategies :password, :basic
  manager.failure_app = failure_app
end

run app

wardenを使う際には、デフォルトのstrategyと認証が失敗した場合に呼ばれる、rack endpoint を指定する。

sessionシリアライズロジックと認証するユーザー

config.ru の run.app の前あたりに以下を入れる

Warden::Manager.serialize_into_session do |user|
  user.id
end

Warden::Manager.serialize_from_session do |id|
  User.get(id)
end

Userクラスの定義もその下らへんに書く。

class User
  attr_accessor :id, :password

  USER_MAPPING = {
    a: 'hogehoge',
    b: 'mogemoge',
  }

  def initialize(id, pass)
    self.id = id
    self.passworkd = pass
  end

  def self.get(id)
    new(id, USER_MAPPING[id.to_sym])
  end
end

これで認証のもとなるUserの準備ができたので、認証ロジックを作りたいとおもう。

認証をしてみる

strategyは複数設定が可能。 その中の1つでも成功するか、すべての戦略を通るか、戦略がfailするまで呼ばれる。

strategyWarden::Strategies::Base を継承して作られる。 実装が必要なのは、 valid?authenticate! の2つ。

valid?

valid? メソッドは strategy の実行条件(ガード)。 宣言シない場合、strategy は常に実行される。

上記の例では、 username, passwordのどちらか1つでもあれば戦略を実行します。

authenticate!

認証ロジック。 利用可能なメソッドがいくつかある。

  • request
  • session
  • params
  • env

  • halt!

    • strategyの実行を止める。後続のstrategyは実行されない
  • pass
    • strategyを実行しない
  • success!
    • 認証に成功。userオブジェクトをsessionに格納し、ログインする。halt!する
  • fail!
    • 認証失敗。halt!
  • redirect!
    • 別のURLへリダイレクトする。halt!
  • custom!

    • わからん
  • headers

    • headerを設定する
  • errors
    • errorオブジェクトへのアクセス

では、実際に strategy を書いてみる。 こんな感じのコードをまた run.app の前あたりにいれる。

Warden::Strategies.add(:password) do

  def valid?
    params['id'] || params['password']
  end

  def authenticate!
    if User.authenticate(params['id'], params['password'])
      success! User.get(params['id'])
    else
      fail!('failll')
    end
  end
end

User.authenticateを実装する

  def self.authenticate(id, pass)
    return false unless password = USER_MAPPING[id.to_sym]
    !!(password == pass)
  end

Rack endpointを修正する

app = Proc.new do |env|
  env['warden'].authenticate!
  ['200', {'Content-Type' => 'text/html'}, ['A barebones rack app.']]
end

やってみる

vagrant-ubuntu-trusty-64% curl "http://localhost:9292/?id=a&password=hogehoge"
A barebones rack app.%

vagrant-ubuntu-trusty-64% curl "http://localhost:9292/?id=b&password=hogehoge"
fail.%

id=a, password=hogehogeは認証が成功し、id=b, password=hogehogeはfailしている。

scope

複数のユーザー(タイプ?)をログイン可能にする。

defautユーザーとAdminユーザーで認証ロジックを変えたい場合など、 スコープ(defalt, admin)でそれぞれ定義が可能。

callbacks

  • after_set_user
  • after_authentication
  • after_fetch
  • before_failure
  • after_failed_fetch
  • before_logout
  • on_request

これらのタイミングにcallbackを仕込める。

Warden::Manager.after_set_user do |user, auth, opts|
  unless user.active?
    auth.logout
    throw(:warden, :message => "User not active")
  end
end

set_userは認証に成功し、env[‘warden’].user にユーザーオブジェクトを入れる時のこと。 そのあとに上記のcallbackが呼ばれる。

まとめ

これだけ見ると非常にシンプル。 deviseを理解するために、wardenを見た。 次はdeviseを理解していくぞ。