私がプログラミングをする理由
あすたぴ です
ふと思い立ったので整理してみます。
別にプログラミングが大好きなわけではない
好きか?って言われたらまぁ好き寄りって感じです。 プログラミングは手段です。 学ぶ理由は目的を遂行するためによりよい方法、より楽な方法を使いたいからです。 目的を遂行するに当たって、よりかっこいい方法をとりたい。みたいな気持ちはあります。
みんなに使ってもらえるようなライブラリとかフレームワークを作りたい。といった気持ちはないです。 世界の天才たちに立ち向かえると思うような頭は持ち合わせていません。
じゃあなんでプログラミングしてるの?
テクノロジーの力は偉大です。 日常生活のなかで周りにはプログラムが多くなってきました。 そういったテクノロジー、技術を理解したいし、自分でも作りたいと思います。
ある程度、出来るようになったら別によくね?
そういった考えもあると思います。
ある程度プログラミングが出来るようになったら、効率の良い人は飯を食うのに困らないかもしれません。 なぜ継続するかといいますと、わからない(理解ができない)ことが多いからです。 例えば、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.rb
の devise_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
を継承している。
DeviseController
は ApplicationController
を継承している。
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
と呼んでいて、これを拡張して独自で認証機構を実装することができる。
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するまで呼ばれる。
strategy
は Warden::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を理解していくぞ。