SmallStyle


2011-07-11

_ Devise で認証が必要な URL に対して .NET のアプリケーションからアクセスする

Rails3 + Devise で認証が必要なサービスに対して,.NET で作った Windows アプリからアクセスする方法について,認証が必要な Web インターフェースと,Web API の提供を同時に実現した場合に,Devise を利用していて動作させるための自分なりの整理.

作成する Windows アプリは特定の URL にデータを POST するアプリケーション.そのサービスはユーザー認証が必要なサービスで,データの POST にも当然認証が必要となる.サービス側は Rails で構築していて,認証エンジンには Devise を利用し, Devise が標準で対応している Basic 認証を利用することにした.(実際にサービスとして外に出すのであれば,Basic 認証を利用する場合は SSL 通信での利用も考えないといけない).

Rails3 + Devise で簡単な Web アプリを作る

まずは Rails3 と Devise を使って簡単な Web アプリケーションを作る.確認のためのサンプルなので,単純なテキストデータの CRUD アプリケーションとする.

$ rails new sample
$ cd sample

Gemfileに "gem 'devise' を追記して,bundle install を実行する.

$ bundle install

依存するライブラリのインストールがすんだら,Devise のイニシャライザーをインストールする.

$ rails generate devise:install

ユーザー管理をするモデルを作成する.ここでは User クラスとする.作成できたらデータベースの migrate を行う.またログインに必要な画面についても,標準的なものが Devise で用意されているのでそれを利用する.

$ rails generate devise user
$ rake db:migrate
$ rails generate devise:views

あとはアプリケーションとして必要なものを作成していく.ここではごく簡単なテキストを追加していくようなものなので scaffold でサクっと作ってしまう.

$ rails generate scaffold memo message:text user:references
$ rake db:migrate

root を MemosController の index とするため,config/route.rb に root :to => "memos#index" と定義し, public/index.html は削除しておく.あとは認証が必要となるように設定する.MemosController クラスに対して,before_filter を定義し,あとはログインしたユーザーの情報のみを返すようにそれぞれ修正します.

class MemosController < ApplicationController
	
	before_filter :authenticate_user!
	
	# GET /memos
	# GET /memos.xml
	def index
		@memos = current_user.memos
		respond_to do |format|
			format.html # index.html.erb
			format.xml  { render :xml => @memos }
		end
	end
	
	# GET /memos/1
	# GET /memos/1.xml
	def show
		@memo = current_user.memos.find(params[:id])
		respond_to do |format|
			format.html # show.html.erb
			format.xml  { render :xml => @memo }
		end
	end
	
	# GET /memos/new
	# GET /memos/new.xml
	def new
		@memo = Memo.new
		respond_to do |format|
			format.html # new.html.erb
			format.xml  { render :xml => @memo }
		end
	end
	
	# GET /memos/1/edit
	def edit
		@memo = current_user.memos.find(params[:id])
	end
	
	# POST /memos
	# POST /memos.xml
	def create
		@memo = current_user.memos.build(params[:memo])
		respond_to do |format|
			if @memo.save
				format.html { redirect_to(@memo, :notice => 'Memo was successfully created.') }
				format.xml  { render :xml => @memo, :status => :created, :location => @memo }
			else
				format.html { render :action => "new" }
				format.xml  { render :xml => @memo.errors, :status => :unprocessable_entity }
			end
		end
	end
	
	# PUT /memos/1
	# PUT /memos/1.xml
	def update
		@memo = current_user.memos.find(params[:id])
		respond_to do |format|
			if @memo.update_attributes(params[:memo])
				format.html { redirect_to(@memo, :notice => 'Memo was successfully updated.') }
				format.xml  { head :ok }
			else
				format.html { render :action => "edit" }
				format.xml  { render :xml => @memo.errors, :status => :unprocessable_entity }
			end
		end
	end
	
	# DELETE /memos/1
	# DELETE /memos/1.xml
	def destroy
		@memo = current_user.memos.find(params[:id])
		@memo.destroy
		respond_to do |format|
			format.html { redirect_to(memos_url) }
			format.xml  { head :ok }
		end
	end
end

また,現在のログインユーザーなどがわかるようにヘッダーなどを layout/appliation.html.erb に追記しておく.

...
<div>
<% if user_signed_in? %>
<%= current_user.email %> でログイン中です.<%= link_to "ログアウト", destroy_user_session_path %>
<% else %>
<%= link_to "ログイン", new_user_session_path  %> | <%= link_to "新規登録", new_user_registration_path%>
<% end %>
</div>
<p class="notice"><%= notice %></p>
<p class="alert"><%= alert %></p>
...

ここまでできたらとりあえず起動してみてサービス側は一通り動作することが確認できる.

$ rails server

新規ユーザーの登録,ログイン,ログアウトなどが可能となっていることがわかる.

この時点ではまだ Devise は Basic 認証に対応させていないため,試しに curl などでアクセスしてみるとログイン画面に転送されることが確認できる.

$ curl --basic --user foo@example.com:password -d memo[message]=hello http://127.0.0.1:3000/memos.xml
<?xml version="1.0" encoding="UTF-8"?>
<errors>
  <error>You need to sign in or sign up before continuing</error>
</errors>

Devise を Basic 認証でも利用出来るようにするには,config/initializers/devise.rb で,config.http_authenticatable = true を設定する.設定を変更したら,サーバーを再始動し再度 curl などでアクセスしてみると,ログイン後の画面が取得できることが確認できます.これでサービス側は完了.

<?xml version="1.0" encoding="UTF-8"?>
<memo>
  <created-at type="datetime">2011-06-29T00:00:00Z</created-at>
  <id type="integer">1</id>
  <message>test</message>
  <updated-at type="datetime">2011-06-29T00:00:00Z</updated-at>
</memo>

C#.NETクライアントアプリを作る

今回は Basic 認証が動作するか確認するためだけなので,ユーザー名とパスワード,送信するテキストメッセージは固定として以下のようなアプリケーションにしてみる.

using System;
using System.Text;
using System.Net;
using System.Collections.Specialized;
class MemoClient
{
  static void Main()
  {
    WebClient client = new WebClient();
    //client.Credentials = new NetworkCredential("foo@example.com", "password");
    client.Headers.Add("Authorization", "Basic " + Convert.ToBase64String(Encoding.ASCII.GetBytes("foo@example.com:password")));
    client.Encoding = Encoding.UTF8;
    NameValueCollection kv = new NameValueCollection();
    kv.Add("memo[message]", "Hello World!");
    client.UploadValues(new Uri("http://127.0.0.1:3000/memos"), kv);
  }
}

アプリケーションを実行すると,正しく認証されてメッセージが DB に書き込まれていることが確認できます..Net での開発に非常に疎いので,これが正しいのかわかりませんが,検索してみつかる NetworkCredential を利用した認証情報では,サーバー側では認識されず認証が通りませんでした.そこでリクエストヘッダーに直接 Authorization を追加してユーザー名とパスワードを送信したところ,正しく動作しました.

Devise で Basic 認証を有効にした場合,ブラウザでアクセスした場合は,設定変更前と変わらないフォーム認証のままになっています.通常,Basic 認証を利用する場合は,認証のダイアログが表示され,そこにユーザー名とパスワードを入力しますが,この辺がどうやら異なるようです.試しに,サービス側の認証を Devise ではなく,ActionController の authenticate_or_request_with_http_basic を利用するようにしてみました.こちらの方法だとブラウザでアクセスしたときもダイアログが表示され,クライアントアプリでも NetworkCredential を利用した認証情報を利用することができました.

curl での Basic 認証では問題なく利用できたのに Windows クライアントアプリからだと NetworkCredential が利用できないというのがいまいち理解できないのだけれどなんでなんだろう….


最近の日記