ページ

2010年3月24日水曜日

Keychain Services 調査 (19) 認証フロー(REST向け)実装開始

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

(前回)Cocoaの日々: Keychain Services 調査 (18) 認証フロー(REST向け)

以前のコードを前回の認証フロー向けに書き直す。新たにプロジェクトを作り、前回までのコードで必要なものをコピーする。

最初にアカウント情報(サービス、ID、パスワード)をまとめて扱えるようにモデルクラスを1つ作る。
LoginAccount.h

@interface LoginAccount : NSObject {

NSString* serviceName;
NSString* loginId;
NSString* password;
}

@property (copy) NSString* serviceName;
@property (copy) NSString* loginId;
@property (copy) NSString* password;

@end


KeychainManager は AccountManager と改名し、LoginAccount を扱うメソッドに変更する。このクラスで KeychainManager からパスワードを取得したり、保存を行ったりする。
AccountManager.h

@class LoginAccount;
@interface AccountManager : NSObject {

}

+ (AccountManager*)sharedManager;
-(BOOL)setPasswordWithLoginAccount:(LoginAccount*)loginAccount;

@end


認証シートのコントローラ AuthenticationWindowController は新しい処理フロー図に合わせて書き直す。

AuthenticationWindowController.h

@class LoginAccount;
@interface AuthenticationWindowController : NSObject {

IBOutlet NSWindow* window_;
IBOutlet NSTextField* loginIdTextField_;
IBOutlet NSSecureTextField* passwordTextField_;

NSWindow* attachedWindow_;
NSString* loginId_;
NSString* password_;

NSString* message_;
BOOL is_canceled_;

}

@property (assign) NSWindow* attachedWindow;
@property (copy) NSString* loginId;
@property (copy) NSString* password;

@property (retain) NSString* message;

-(BOOL)storeLoginAccount:(LoginAccount*)loginAccount;

-(IBAction)login:(id)sender;
-(IBAction)cancel:(id)cancel;
@end


クライアントコードは、-[storeLoginAccount:] を使いユーザからID,パスワードを取得する。ここでの処理がフロー図の右側の点線内の処理に当たる。

今回シートウィンドウはコールバックを使わず、表示中は実行がそこで止まる(ブロック)するようにした。これはデリゲートに nil を指定すると簡単にできる。

(参照)Mac Dev Center: Sheet Programming Topics for Cocoa: Using Application-Modal Dialogs

コードはこんな感じ。

-(BOOL)storeLoginAccount:(LoginAccount*)loginAccount
{
if (loginAccount.loginId) {
self.loginId = loginAccount.loginId;
if (!loginAccount.password) {
[[AccountManager sharedManager]
setPasswordWithLoginAccount:loginAccount];
}
}
[window_ makeFirstResponder:loginIdTextField_];
self.message = @"";

[NSApp beginSheet:window_
  modalForWindow:attachedWindow_
modalDelegate:nil
  didEndSelector:nil
  contextInfo:nil];
[NSApp runModalForWindow:window_];
// dialog is up here (wait for closing)

[NSApp endSheet:window_];
[window_ orderOut:nil];
if (!is_canceled_) {
loginAccount.loginId = self.loginId;
loginAccount.password = self.password;
return YES;

} else {
return NO;
}
}

最後に呼び出し側(クライアント側)のコード。
KeychainSample2AppDelegate.m
-(IBAction)connect:(id)sender
{
LoginAccount* loginAccount = [[[LoginAccount alloc] init] autorelease];

loginAccount.loginId =
[[NSUserDefaults standardUserDefaults] valueForKey:@"loginId"];
loginAccount.serviceName = SERVICE_NAME;

if ([authenticationWindowController_ storeLoginAccount:loginAccount]) {
// TODO: send request
NSLog(@"%@", loginAccount);
} else {
// cancel
NSLog(@"cancend");
}
}


実行してみよう。最初にボタン一つのウィンドウが現れる。

ボタンを押すとID,パスワードを求められる。

login または cancel を押すとシートが閉じられる。それだけ。


今回IDとパスワードが空の場合にエラーメッセージを表示するようにした。
あらかじ NSTextField を貼ってメンバ変数へバインドしておき、エラー時にその変数へエラーメッセージを代入している。

-(IBAction)login:(id)sender
{
NSString* loginId = [loginIdTextField_ stringValue];
NSString* password = [passwordTextField_ stringValue];
if (!loginId  || [loginId length] == 0) {
self.message = @"Username is empty";
[window_ makeFirstResponder:loginIdTextField_];
return;
}
if (!password || [password length] == 0) {
self.message = @"Password is empty";
[window_ makeFirstResponder:passwordTextField_];
return;
}

is_canceled_ = NO;
[NSApp stopModal];
}
当初は ID とパスワードの空チェックにバインドしてあるメンバ変数 self.loginId などを使っていたが、入力直後に loginボタンを押すと、バインドによる同期が間に合わず nil となってしまっていた。この為,直接コントロール(NSTextFiled)をチェックするようにしてある。



ところで認証シートを開いている時に ESCキーを押すと、cancelボタンが一瞬押された状態となり、シートが閉じられた。動作としてはいいのだが、こちらでは何も設定を行っていない。cancelという文字列をみつけて自動的にこのような動作を行っているのだろうか?名前を変えてみたり、ボタンを1つ追加してみたりとしたが、やはり自動的に認識されている。謎だ。

ソースは GitHub からどうぞ。
KeychainSample2 at 2010-03-24 from xcatsan's SampleCode - GitHub

- - - -
今回はパスワードの保存まで手が回らず。次回は Twitpic へのアクセスまで行きたい。