eval族のスコープについてまとめた
ややこしかったので、まとめました。
eval(Bindingなし)
実行中のコンテキストに出現する変数に対しての操作が可能
foo = "foo" eval('p foo') #=> foo
eval(Bindingあり)
以下のように、Bindingオブジェクトを使うことで、以下のようにコンテキストを指定できます。
class C def instance_binding foo = "foooo" binding end end foo = "foo" binding_object = C.new.instance_binding eval('p foo', binding_object) #=> foooo # このようにも書ける binding_object.eval('p foo') #=> foooo
module_eval, class_eval
module_eval
は class_eval
の別名です。
文字列を渡した場合とブロックを渡した場合で、どのスコープで評価されるかが変わります。
文字列を引数とした場合は、レシーバーのスコープで評価されます。
class C; end C.class_eval(<<-EOF) Foo = "bar" def hoge p Foo end EOF Foo = "foo"
ブロックを引数とした場合は、そのコンテキストのスコープで評価されます。 つまり、以下ではトップレベルで定義したことになります。
class C; end C.class_eval do Foo = "bar" def hoge p Foo end end Foo = "foo" C.new.hoge # => warning: already initialized constant Foo # warning: previous definition of Foo was here # "foo" p Object.const_get(:Foo) # => "foo"
instance_eval
module_eval
, class_eval
と同じく、文字列で渡すときとブロックで渡すときで変わります。
module_exec, class_exec
module_exec
は class_exec
の別名です。
ブロックで渡されたときのスコープは、module_eval
と同じです。
文字列での評価はできません。
class C; end C.class_exec do Foo = "bar" def hoge p Foo end end Foo = "foo" C.new.hoge
また、引数をとりブロック引数として、評価する式に値を渡すことができます。
class C; end C.class_exec(:foo) do |foo| define_method(foo) do p "foo" end end C.new.foo #=> "foo"
instance_exec
module_exec
は class_exec
と同じく、引数で値を渡すことができます。
参考
Rubyの定数参照について頭を整理した
最近は、空いた時間にRuby認定試験(Gold)の対策をやっているのですが、 本を読んでいてわかった気になっていた箇所がまぁ、多いこと。。。
練習問題をやってみると、そのへんがはっきりをわかるので良いです。
今回はかなり基本的なことなのですが、定数参照について頭を整理するためにまとめていこうと思います。
Rubyの定数を参照する順番
レキシカルスコープの探索 -> クラス探索 の順番で探索します。
例えば、以下の場合は、CONST_A
が表示されます。
class A NAME = "CONST_A" def name NAME end end class B < A NAME = "CONST_B" end puts B.new.name #=> CONST_A
BクラスにAのメソッド def name ~ end
を追加すると、CONST_B
が表示されます。
また、NAME = "CONST_B"
を消すと、継承チェーンをたどって、CONST_A
が表示されるようになります。
ネストしている場合
もちろんですがネストしている場合、階層が異なるものはそれぞれ別の値が保持されているので、そのスコープに合わせて参照されます。
module M class A CONST = "M::A" def say CONST end end end module M module B class A CONST = "M::B::A" def say CONST end end end end ma = M::A.new puts ma.say # => M::A mba = M::B::A.new puts mba.say # => M::B::A
では、この場合はどうなるかというと
module M CONST = "Hello" end module M class C def say CONST end end end puts M::C.new.say #=> Hello
moduleを再オープンしている場合でも、値は保持されているので Hello が表示されます。
以下のように M::C
と記述するとクラスMの探索は行われないようです。
module M CONST = "Hello" end class M::C def say CONST end end puts M::C.new.say #=> uninitialized constant M::C::CONST (NameError)
Cクラスにいる定数は参照可
module M class C CONST = "Hello" end end class M::C def say CONST end end puts M::C.new.say #=> Hello
includeなどが入ったとき
続いて、includeやprependなどが入ったとき。 こちらも同じく、継承チェーンのどこにクラスが入ってくるかわかれば問題なさそうです。
class B CONST = "Hello B" end module C CONST = "Hello C" end module D CONST = "Hello D" end class A < B include C include D def say CONST end end a = A.new p a.say # => Hello D
呼び出したところから、一番近い Hello D
が参照されます。
A.ancestors => [A, D, C, B, Object, Kernel, BasicObject]
まとめ
定数の参照については、
- 呼び出している箇所はどこか?
- 定数はどのクラス・モジュールにいるのか?
- レキシカルスコープはどうなっているか?
- 継承チェーンはどうなっているか?
を、モジュールのネストも意識しつつ確認していけばよさそうです。
その他
こちらは何が出力されるか?というと、
class A CONST = "Hello A" class << self def name const_get(:CONST) end end end class B < A CONST = "Hello B" end puts B.name #=> Hello B
const_get
は、selfに定義された定数を探索するので、Bクラスの持っているCONST Hello B
が表示されます。
参考
Rubyの認定試験があったので受けてみた
最近、Rubyを原点に戻って学び直しているのですが、 その学習の一環として、Ruby技術者認定試験を受験することにしました。
試験は、javaやoracleと同じような感じでレベル分けがされており、RubyではSilverとGoldの2段階がありました。
詳しくはこちら http://www.ruby.or.jp/ja/certification/examination/
Goldに認定されるには、Sliver,Gold試験両方に受からなければならないということもあり、最初に今回はSilverを受けてきました。
結果は合格で 88/100(75点合格)でした。
90点台はいけるかなと思っていたのですが、どこかのひっかけ問題にまんまと引っかかってしまっていたようです。
Silverはそんなに難易度は高くはありませんでしたが、落ちると受験料16,200円(税込)が吹っ飛ぶので、若干の良いプレッシャーを感じながら学習できました。
試験対策をして効果はあったのか?と聞かれればあった気がします。
私は試験対策がただの覚えゲーにならないよう、最初は試験範囲はあえて意識せず、以下の書籍を復習するようにしました。そのきっかけになったこと。
組み込みクラスのメソッドが試験対策前よりパッとでてくるようになったこと。割と前から知らなかったメソッドも多くありました。
あまり触る機会の無かった、RubyでIOクラス周りの理解が深まったこと。 これに関しては、ファイルをまとめて処理するバッチを作る場面などで役立ちそうな気がしています。
などなど。。。
AngularJS の $http サービスのXSRF対策について調べた
AngularJS の $http サービスのXSRF対策について調べた
AngularJS の $http サービスにはXSRF(CSRF)の対策の機能が実装されています。
(AngularJSでは、cross-site request forgeries
の略を XSRF
と呼んでいる場面が多くみられるので、ここではXSRFで統一します。)
AngularJS側の動作
XHRが実行された際に $http
サービスは、Cookieに XSRF-TOKEN
があれば、 X-XSRF-TOKEN
という名前でHTTPヘッダーにセットします。※ この名前は変更することができます。
サーバ側の対応
送られてきたCookieに含まれる XSRF-TOKEN
の値と HTTPヘッダーのX-XSRF-TOKEN
の値が合っていることを確認します。
自分のドメイン上で実行されるJavaScript以外はCookieを読み込むことが出来ないため、 サーバーはXHRが自分のドメイン上で実行されて送られてきた事を保証することが出来ます。
実装して確認してみる
簡単な実装を行い動作を確認してみます。 テキストボックスに入力した値をXHRでサーバにリクエストし、Tokenの検証が成功したら入力した値を返し、テキストボックスの直下に値を表示させます。 構成としては、sinatraで画面(html+AngularJS)をレンダリングし、そのレンダリングした画面でXHRが実行するようにします。
XHRでメッセージをリクエストし、レスポンスで同じメッセージが返ってくれば成功です。
require 'sinatra' require 'sinatra/reloader' require 'sinatra/cookies' require 'json' require 'active_support' require 'active_support/core_ext' # 画面をレンダリングする get '/' do # Tokenを生成 token = SecureRandom.base64(32) response.set_cookie( 'XSRF-TOKEN', value: token, # jsからcookieを読み取る必要があるので httponly を off にする httponly: false, secure: false ) erb :index end # Tokenの検証に成功したら、入力した文字をそのまま返す post '/add_message' do if same_xsrf_token?(cookies['XSRF-TOKEN']) { message: request_params['message'] }.to_json else status 403 { error: 'CSRF検証に失敗しました' }.to_json end end private def request_params request_body = request.body.read return {} if request_body.blank? JSON.parse(request_body) end def same_xsrf_token?(token) return false if token.blank? http_headers = request.env.select { |k, v| k.start_with?('HTTP_') } token == http_headers['HTTP_X_XSRF_TOKEN'] end
フロント側
<!DOCTYPE html> <html ng-app='App'> <head> <meta charset="utf-8"> <title>angularjs xsrf</title> <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.6/angular.min.js"></script> <script src="js/controller.js"></script> </head> <body ng-controller="AppController"> <input type="text" ng-model="message"> <input type="button" value="送信" ng-click="send_message()"> <h1>{{receive_message}}</h1> </body>
var app = angular.module('App', []); app.controller('AppController', ['$scope', '$http', function($scope, $http){ $scope.send_message = function(){ $http({ method: 'POST', url: 'add_message', data: { message: $scope.message } }) .success(function(data, status, headers, config){ $scope.receive_message = data.message; }) .error(function(data, status, headers, config){ $scope.receive_message = data.error; }); } }]);
動作させてみる
画面で確認
- 無事リクエストしたメッセージが取得できていることを確認しました。
- ちなみに検証に失敗するとこうなります。
リクエストの内容を確認する
POST /add_message HTTP/1.1 Host: localhost:4567 Connection: keep-alive Content-Length: 20 Accept: application/json, text/plain, */* Origin: http://localhost:4567 X-XSRF-TOKEN: VGqbpCECjRRXF6XJFwD4IJA0zxzxdobSr4mnMJieyvI= User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.75 Safari/537.36 Content-Type: application/json;charset=UTF-8 Referer: http://localhost:4567/ Accept-Encoding: gzip, deflate, br Accept-Language: ja,en-US;q=0.9,en;q=0.8 Cookie: XSRF-TOKEN=VGqbpCECjRRXF6XJFwD4IJA0zxzxdobSr4mnMJieyvI=
Cookie XSRF-TOKEN
に保存されているのと同じ値が、 X-XSRF-TOKEN
が付与されていることを確認
JWT(+JWS)について調べた (JWTの検証)
前回、こちらでHMAC SHA256で署名したJWT(+JWS)を作成していきましたが、今回はその検証を行っていきます。
rfc7519
に検証手順があるので、それを参考にしつつ流れを確認しました。
- JWTをheader、payload、signatureに分割
- 分割したheader、payload、signatureをBASE64urlでデコード
- headerから署名アルゴリズムを確認する
- 確認した署名方法でJWTの検証を行なう
これに加えて有効期限の検証も行っていきます。
とにかくコードを書いていきます。 今回はコードの中で説明をしていく形になりますが、順番的にはこんな感じで検証していくのが良いかと思われます。
# -*- coding: utf-8 -*- require 'base64' require 'json' require 'openssl' jwt = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImV4cCI6MTU0MDY1MjQwMH0.a2xzydhHuo2UREDRIdcyKmlihl_1gl_BYCxdQEKmgTE' # JWTをheader、payload、signatureに分割 encoded_header, encoded_payload, encoded_signature = jwt.split('.') # 分割したheader、payload、signatureをBASE64urlでデコード decoded_header = Base64.urlsafe_decode64(encoded_header) # パディングがなくてもよしなにデコードしてくれる decoded_payload = Base64.urlsafe_decode64(encoded_payload) decoded_signature = Base64.urlsafe_decode64(encoded_signature) # headerから署名アルゴリズムを確認する # 今回は形式はJWTでHS256以外のものは対応しないので、それ以外は処理しないようにします header = JSON.parse(decoded_header) payload = JSON.parse(decoded_payload) unless header['typ'] == 'JWT' && header['alg'] == 'HS256' puts 'error: 対象外の形式です。' return end # 確認した署名方法でJWTの検証を行なう secret = 'secret' verification_signature = OpenSSL::HMAC.digest( OpenSSL::Digest::SHA256.new, secret, "#{encoded_header}.#{encoded_payload}" ) if decoded_signature != verification_signature puts 'error: 検証が失敗しました。' return end # 有効期限を設けていれば、その検証も行なう unless Time.now.to_i < payload['exp'] puts 'error: 有効期限が過ぎています。' return end puts '検証OK'
参考
JWT(+JWS)について調べた
JWT とは
JSONオブジェクトで、安全に2者間で情報をやりとりするための表現方法です。 RFC7519で定義されています。
JWS とは
JSON Web Signatureの略、JWTのエンコード形式、デジタル署名、もしくはMACを行ったメッセージの表現 RFC7515で定義されています。 他には、暗号化するための仕様としてJSON Web Encryption (JWE)なんかがあります。
今回は、使われているケースが多いJWT+JWSの組み合わせのパターンでの実装を調べていきたいと思います。
特徴
- コンパクトである
- サイズが小さいので、URL、POSTパラメータ、またはHTTPヘッダー内で送信できます。
- 自己完結されている
- url-safeである
- URLで使用できる文字列のみで構成されています。
どんなときに使うのか
認証
- こちらが一般的な使われ方のようです。
- ユーザーがサービスにログインしJWTを発行してらもらい、その発行されたJWTで他に許可された異なるドメインのリソースを取得する。などの使い方ができます。
情報の交換
OpenID ConnectやOAuthと組み合わせて使うケースもあるようです。
JWTの構造
- ヘッダ
- ペイロード
- 署名
これらをBase64エンコーディング(urlsafe、パディング無し)に変換し、.
区切りで結合します。
こんな形になります。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImV4cCI6MTU0MDY1MjQwMH0.a2xzydhHuo2UREDRIdcyKmlihl_1gl_BYCxdQEKmgTE
ヘッダ
ヘッダには、このトークンのタイプ、使用されているハッシュアルゴリズムなどの情報で構成されます。
この例では、
ハッシュアルゴリズムが、HS256
(HMAC SHA-256アルゴリズム)
トークンのタイプが、JWT
ということを示しています。
{ "alg": "HS256", "typ": "JWT" }
ペイロード
ペイロードは以下のような形式になります。 ペイロードにはクレームを含んでいます。 (クレームとは、エンティティ(通常はユーザー)と追加のメタデータのことを指します。)
クレームには、以下3種類があります。
予約済み
公開済み
個人用
- これらは、それらの使用に同意する当事者間で情報を共有するために作成されたカスタムクレームです。
{ "sub": "1234567890", "name": "John Doe", "admin": true, "exp": 1540652400 }
有効期限(exp)は、 NumericDate
である必要があるので、ここでは、UNIXTIMEを入れています。
https://tools.ietf.org/html/rfc7519#section-4.1.4
署名
先ほど、紹介したJOSE ヘッダ、JWSペイロード、(Base64urlエンコード済)を、
HMAC SHA-256
アルゴリズムで署名し,
[JWS]に指定された形で署名をbase64urlエンコードすると, このエンコード済JWS署名を作り出せます。
RubyでJWTを作ってみる
# -*- coding: utf-8 -*- require 'base64' require 'json' require 'openssl' header = { alg: 'HS256', typ: 'JWT' } payload = { sub: '1234567890', name: 'John Doe', admin: true, exp: 1540652400 } secret = 'secret' encoded_header = Base64.urlsafe_encode64(header.to_json).delete('=') encoded_payload = Base64.urlsafe_encode64(payload.to_json).delete('=') signature = OpenSSL::HMAC.digest( OpenSSL::Digest::SHA256.new, secret, "#{encoded_header}.#{encoded_payload}" ) encoded_signature = Base64.urlsafe_encode64(signature).delete('=') jwt = [ encoded_header, encoded_payload, encoded_signature, ].join('.') puts '[jwt] => ' + jwt
結果
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImV4cCI6MTU0MDY1MjQwMH0.a2xzydhHuo2UREDRIdcyKmlihl_1gl_BYCxdQEKmgTE
値の確認
こちらのjwt.ioというサイトのDebuggerツールで、値の確認ができます。
JWTの検証
こちらは、別の記事でまとめていく予定です。
window.postMessage APIの使い方を調べた
postMessageとは
- HTML5で拡張された機能
- 任意のウィンドウへクロスオリジン通信が可能
- セキュアである
- 双方向の通信チャンネルが確立されてから通信を行う
- 悪意あるWebサイトから知らないうちにされないようにしているため
使い方(送信)
win.postMessage("メッセージ", "http://www.example.com");
メッセージを受け取りたい対象に対してpostMessage
を呼びます。
コードで示している win
は以下を指します。
- 自分のドキュメントウィンドウ内に生成されたiframe
- JavaScriptで開いたポップアップウィンドウ
- 自分のドキュメントウィンドウを含むウィンドウ
- 自分のドキュメントを開いたウィンドウ
第一引数に渡したいメッセージ(文字列)、第二引数に宛先のオリジンを指定します。
受け取り方(受信)
addEventListener
を使って、message
イベントを登録します。
addEventListener('message', function(event) { // 受け取ってからの処理 }, false);
この event
には、以下3つのプロパティが用意されています。
- event.data
- 実際のメッセージ内容(文字列)
- event.origin
- メッセージを送ってきたページの生成元
- event.source
- メッセージを送ってきた window オブジェクトの参照
セキュリティ上の注意点
postMessageの第二引数にワイルドカードを指定しない
postMessageの第二引数に '*' を指定すると、どのオリジンに対してもメッセージを送信することができるため、意図して使わない限り指定しないようにしたほうが良さそうです。
受け取ったメッセージの送信元を確認する
addEventListener
は送られてきたすべてのメッセージを受け取ることができるため、信頼できないページからのメッセージも受け取ることができます。
そのため、受け取ったら必ず以下のように信頼できる送信元であるかチェックをしたほうが良いです。
addEventListener('message', function(event) { if (event.origin !== 'http://www.example.com/') { return; } }, false);
補足
最近は文字列でなくても良い
最新のブラウザではpostMessageの第一引数に、様々なデータオブジェクトを受け付けることができるようになっています。 ただ、IE8,IE9の場合はこの方法は使えないので、互換性を持たせたいのであればJSONを使うほうが良さそうです。 (もう、サポート切れているし意識しなくても良いかな。)
IE8以下は、addEventListener
は使えないので attachEvent
を使う
詳しくはこちら https://qiita.com/39_isao/items/506e08031adfca9a5933