Web系 システム 開発 セキュリティ 備忘録
概要
ウェブアプリケーションのセキュリティの基礎や入門的な部分をざっくり書き出しました。
ログイン機能を使う場合やインフラよりのセキュリティは書いていません。
1. 脆弱性の原因と攻撃パターン
脆弱性が生まれる理由
- バグによるもの
- チェック機能不足によるもの
- ディレクトリ・トラバーサル等
能動的攻撃と受動的攻撃
- 能動的攻撃:攻撃者がwebサーバーに対して直接攻撃すること(SQLインジェクション等)
- 受動的攻撃:攻撃者がwebサーバーに対して直接攻撃せず、Webサイトの利用者に罠を仕掛ける
- 単純な受動的攻撃:ユーザーが怪しいサイト閲覧してウィルスに感染
- 正規サイトを悪用する受動的攻撃:攻撃者がwebサーバーに対して不正操作をして、ユーザーが正規サイトに訪れるとウィルスに感染
- サイトをまたがった受動的攻撃:①ユーザーが罠サイトを閲覧②ユーザーが正規サイトに攻撃のリクエスト送信③正規サイトからJSなどの仕掛けを含むレスポンスが返却
2. 脆弱性一覧
- 脆弱性には出力に起因するものと処理に起因するものがある
- 出力に起因する脆弱性には「インジェクション」という単語がつくものが多い
- インジェクションとは、「"」「'」等の終端マークを混入させ処理や出力の構造を変化させる行為
# 処理の流れ クライアントブラウザ ↓ HTTPリクエスト サーバー(入力値検証→処理→出力) ↓ HTTPレスポンス クライアントブラウザ
機能と脆弱性の対応
処理 | 出力 | 対応アプリ | 発生する脆弱性の種類 | 起因する脆弱性 |
---|---|---|---|---|
処理 | 表示/HTML | ブラウザ | クロスサイト・スクリプティング HTTPヘッダ・インジェクション |
出力 |
処理 | DB/SQL | RDB | SQLインジェクション | 出力 |
処理 | 外部コマンド | シェル | OSコマンド・インジェクション | 出力 |
処理 | メール | メール | メールヘッダ・インジェクション | 出力 |
処理 | ファイル | ファイル | ディレクトリ・トラバーサル | 処理 |
重要な処理 | - | - | クロスサイト・リクエストフォージェリ | 処理 |
認証 | - | - | セッションフィクセーション | 処理 |
許可 | - | - | 認可不備 | 処理 |
インジェクション系脆弱性
脆弱性名 | インターフェース | 悪用手口 | データの終端 |
---|---|---|---|
クロスサイト・スクリプティング | HTML | JS等の注入 | < " など |
HTTPヘッダ・インジェクション | HTTP | HTTPレスポンスヘッダの注入 | 改行 |
SQLインジェクション | SQL | SQL命令文注入 | ' など |
OSコマンド・インジェクション | シェルスクリプト | コマンドの注入 | ; | など |
メールヘッダ・インジェクション | sendmailコマンド | メールヘッダ、本文の注入・改変 | 改行 |
3. 入力処理の脆弱性
- Webアプリケーションの入力には、HTTPリクエストとしてわたらせるパラメータ(GET,POST,cookieなど)がある
- 入力処理には次の3つの検証・変換が必要
クライアントブラウザ ↓ HTTPリクエスト ----- |入力| ←ここ |処理| |出力| ----- ↓ HTTPレスポンス クライアントブラウザ
文字エンコーディングの変換
言語 | 自動変換 | スクリプト記述 |
---|---|---|
PHP | pjp.iniなど | mb_convert_encoding |
Perl | - | Encode::decode |
Java | setCharacterEncoding | Stringクラス |
ASP.NET | web.config | - |
4. 表示処理の脆弱性
- 表示処理が原因で発生する問題には次の2つがある
- クロスサイト・スクリプティング
- エラーメッセージからの情報漏洩
クライアントブラウザ ↓ HTTPリクエスト ----- |入力| |処理| |出力| ----- ↓ HTTPレスポンス クライアントブラウザ ←ここ
クロスサイト・スクリプティング
- クロスサイトスクリプティングとは、ウェブページの部分をユーザからの入力をそのままエコーバック(おうむ返し)することによって生成しているアプリケーションのセキュリティ上の不備を利用して、サイト間を横断して悪意のあるスクリプトを注入する攻撃のこと
- クロスサイト・スクリプティングではユーザーに対して次のような被害がある
クロスサイト・スクリプティング対策
- 必須対策(個別):HTMLの要素内容について[<]や[&]をエスケープする、属性値は「"」で囲い[<]や[&]をエスケープする
- 必須対策(共通):HTTPレスポンスに文字エンコーディングを明示する
- 保険的対策:入力値検証、クッキーにHttpOnly属性を付与、TRACEメソッドを無効
5. SQLの脆弱性
- SQLの脆弱性にはSQLインジェクションがある
- SQLインジェクションとは、SQLの呼び出しに不備がある場合発生する脆弱性でSQLが開発者の意図しない形にに改変され実行されること
- SQLインジェクションに脆弱性がある場合、次のような影響を受ける可能性がある
- データベース内の全ての情報が盗まれる
- データベースの内容が書き換えられる
- 承認を回避される(IDとパスワードを用いずにログインされる)
- その他、データーバースサーバー上のファイルの読み書き、プログラム実行などが行われる
クライアントブラウザ ↓ HTTPリクエスト ----- |入力| |処理| |出力| ←ここ ----- ↓ HTTPレスポンス クライアントブラウザ
脆弱性が生まれる原因
文字列リテラル
# 文字列リテラルの例(BOOKSテーブルから著者がO'Reillyのデータを取得) SELECT * FROM BOOKS WHERE AUTHOR = 'O'Reilly' 'O'が文字列リテラルで、Reilly'が文字れるリテラルからはみ出した部分 (「SELECT * FROM BOOKS WHERE AUTHOR = 'O';DELETE FROM BOOKS;」みたいな感じで悪用できる)
数値リテラル
# 数値リテラルの例 SELECT * FROM BOOKS WHERE MONEY <= 20000;DELETE FROM BOOKS 20000が数値リテラル、それ以降が脆弱性を含む意図しない形のSQL
対策
6. まとめ
- 入力値のインジェクション対策はかならず行う(「"」「'」「;」等を含んだ入力値でテストする)
- HTMLの要素内容について[<]や[&]をエスケープする
- HTTPレスポンスに文字エンコーディングを明示する
- 入力値検証、クッキーにHttpOnly属性を付与、TRACEメソッドを無効
- おすすめ書籍
セキュリティの事がひと通り書いてあり、サイトを作成する前に考慮すべき考え事がわかりやすく書いてあります。
iOS SNS シェア 実装方法
概要
SNS等のシェア機能の実装方法の説明です。
実装内容はシェアボタン押下時にアクションシートを表示し、シェアしたい内容を選びシェアするような実装内容です。
Webページを表示しシェアしていますが、それ以外でも同じような実装方法でシェア可能です。
シェア先は次の6個!(mixiは登記簿謄本が必要な為今回は対象外としました)
実装結果イメージ(「safari」と「URLをコピー」はおまけ)
この記事では、「UIApplication sharedApplication」で実装しています。
iOS6移行はUIActivityViewControllerで実装するのが楽だと思うので、iOS6移行は他の記事を参考にする方がお勧めです!
前提条件
TWITTER、FACEBOOK(iOS6以上)
Xcodeのライブラリーに「Social.framework」を追加する必要があります。
(TARGETS->MyApp->Build Phases->Link Binary With Libraries)
Google+、EverNote
Google+とEverNoteは、SDKやサービスの登録が必要となります。
(EverNoteはサービス登録が必要になるので、少し時間と手間がかかります)
簡単な概要は次の通りです。
Google+
名称 | Google+ Platform |
---|---|
URL | https://developers.google.com/+/ |
必要なライブラリ | Security.framework,SystemConfiguration.framework |
手順 | 1. APIs Console プロジェクトを作成する2. SDK をダウンロードして、サンプル アプリを実行する3. SDK のダウンロードとインストール4. プログラム修正 |
参考URL | スタートガイド(iOS)Googleでログインインタラクティブな投稿の作成 |
備考 | 開発用と製品用でIDが異なる。使えないhtmlタグがある。 |
EverNote
名称 | EverNote Developers |
---|---|
URL | https://dev.evernote.com/intl/jp/ |
必要なライブラリ | Security.framework |
手順 | 1.サンドボックスのアカウント登録(アプリごとで共通のアカウントを利用する)2. Evernote API キー取得(アプリごと別々に取得)3. SDK のダウンロードとインストール4.プログラム修正・サンドボックス上のEverNoteでテスト5.サンドボックスからプロダクションに変更依頼(アクティベーション)6.プログラム修正(プロダクションに変更(ホストを「BootstrapServerBaseURLStringSandbox」→「BootstrapServerBaseURLStringUS」)) |
参考URL | スタートガイドサンドボックスアクティベーション(プロダクションAPIキーの有効化)よくある質問 |
備考 | 開発用と製品用でIDが異なる。使えないhtmlタグがある。アクティベーションの申請時にはgoogleアカウントは使用できないので個人名・個人用メールアドレスで依頼アクティベーションの承認待ち時間は約2〜3日 |
プログラム
流れ
- アクションシートの生成
- アクションシートの表示(シェアボタン押下時)
- アクションシートの選択した内容でシェア
サンプルコード
#import <Social/Social.h> #import "EvernoteSession.h" #import "EvernoteUserStore.h" #import "EvernoteNoteStore.h" #import "GTLPlusConstants.h" #import "GPPShare.h" @implementation MainController UIActionSheet *_actionsheet; UIWebView *_webView; - (void)viewDidLoad { [super viewDidLoad]; // 1. アクションシートの生成 _actionsheet = [[UIActionSheet alloc] initWithTitle:@"共有" delegate:self cancelButtonTitle:@"キャンセル" destructiveButtonTitle:nil otherButtonTitles:@"LINE", @"Twitter", @"Facebook", @"Google+", @"メール", @"Safari", @"Evernote", @"URLをコピー", nil]; [_webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://google.co.jp"]]]; } #pragma mark - IBAction // 2. アクションシートの表示(シェアボタン押下時) - (IBAction)shareAction:(id)sender { if([[UIDevice currentDevice].model isEqualToString:@"iPad"]){ [_actionsheet showFromBarButtonItem:sender animated:YES]; } else { [_actionsheet showInView:self.view]; } } #pragma mark UIActionSheetDelegate // 3. アクションシートの選択した内容でシェア -(void)actionSheet:(UIActionSheet*)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex { NSURL *url = [NSURL URLWithString:[_webView stringByEvaluatingJavaScriptFromString:@"document.URL"]]; NSString *title = [_webView stringByEvaluatingJavaScriptFromString:@"document.title"]; if (0 == buttonIndex) { // LINE NSString *contentURL = [self _escapeURL:[url absoluteString]]; NSString *contentTitleText = [self _escapeURL:title]; NSURL *lineURL = [NSURL URLWithString:[NSString stringWithFormat: @"http://line.naver.jp/R/msg/text/%@?%@", contentURL, contentTitleText]]; [[UIApplication sharedApplication] openURL:lineURL]; } else if (1 == buttonIndex) { // TWITTER SLComposeViewController *postVC = [SLComposeViewController composeViewControllerForServiceType:SLServiceTypeTwitter]; [postVC setInitialText:title]; [postVC addURL:url]; [self presentViewController:postVC animated:YES completion:nil]; } else if (2 == buttonIndex) { // FACEBOOK SLComposeViewController *postVC = [SLComposeViewController composeViewControllerForServiceType:SLServiceTypeFacebook]; [postVC setInitialText:title]; [postVC addURL:url]; [self presentViewController:postVC animated:YES completion:nil]; } else if (3 == buttonIndex) { // Google+ id<GPPShareBuilder> shareBuilder = [[GPPShare sharedInstance] shareDialog]; [shareBuilder setPrefillText:title]; [shareBuilder setURLToShare:url]; [shareBuilder open]; } else if (4 == buttonIndex) { // メール NSString *conSbjText = [self _escapeURL:title]; NSString *conBodyText = [self _escapeURL:[url absoluteString]]; NSString *mail = [NSString stringWithFormat:@"mailto:%@?Subject=%@&body=%@", @"", conSbjText, conBodyText]; [[UIApplication sharedApplication] openURL:[NSURL URLWithString:mail]]; } else if (5 == buttonIndex) { // Safari [[UIApplication sharedApplication] openURL:url]; } else if (6 == buttonIndex) { // EverNote EvernoteSession *session = [EvernoteSession sharedSession]; [session authenticateWithViewController:self completionHandler:^(NSError *error) { if (!error && session.isAuthenticated){ // We're authenticated! EvernoteUserStore *userStore = [EvernoteUserStore userStore]; [userStore getUserWithSuccess:^(EDAMUser *user) { // success NSLog(@"Authenticated as %@", [user username]); EDAMNote *note = [[EDAMNote alloc] init]; note.title = title; NSString *conDes = [_webView stringByEvaluatingJavaScriptFromString: @"function a(){l=document.getElementsByTagName('meta');for(i=0;i<=l.length;i++){if(l[i].name=='description') return l[i].content}};a();"]; NSString *conImg = [_webView stringByEvaluatingJavaScriptFromString: @"function a(){l=document.getElementsByTagName('meta');for(i=0;i<=l.length;i++){if(l[i].getAttribute('property')=='og:image') return l[i].content}};a();"]; NSString *enNote = [NSString stringWithFormat:@"<p>%@</p><a href='%@'></a>%@<img src='%@' />", conDes, [self _escapeXML:[url absoluteString]], [self _escapeXML:[url absoluteString]], conImg]; note.content = [NSString stringWithFormat:@"<?xml version=\"1.0\" encoding=\"UTF-8\"?><!DOCTYPE en-note SYSTEM \"http://xml.evernote.com/pub/enml2.dtd\"><en-note>%@</en-note>", enNote]; EvernoteNoteStore *store = [[EvernoteNoteStore alloc] initWithSession:[EvernoteSession sharedSession]]; [store createNote:note success:^(EDAMNote *note) { [[[UIAlertView alloc] initWithTitle:@"Evernoteに保存しました。" message:nil delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show]; } failure:^(NSError *error) { [[[UIAlertView alloc] initWithTitle:@"Evernoteの保存に失敗しました。" message:[error localizedDescription] delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show]; }]; } failure:^(NSError *error) { [[[UIAlertView alloc] initWithTitle:@"Evernoteの保存に失敗しました。" message:[error localizedDescription] delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show]; } ]; } }]; } else if (7 == buttonIndex) { // コピー [[UIPasteboard generalPasteboard] setValue:[url absoluteString] forPasteboardType:@"public.text"]; [[[UIAlertView alloc] initWithTitle:@"URLをコピーしました。" message:nil delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show]; } } #pragma mark - private methods - (NSString *)_escapeURL:(NSString *) url { return (__bridge NSString *)CFURLCreateStringByAddingPercentEscapes(NULL, (__bridge CFStringRef)url, NULL, (CFStringRef)@"!*'();:@&=+$,/?%#[]", kCFStringEncodingUTF8 ); } - (NSString *)_escapeXML:(NSString *) xml { xml = [xml stringByReplacingOccurrencesOfString:@"&" withString:@"&"]; xml = [xml stringByReplacingOccurrencesOfString:@"\""withString:@"""]; xml = [xml stringByReplacingOccurrencesOfString:@"'"withString:@"'"]; xml = [xml stringByReplacingOccurrencesOfString:@">" withString:@">"]; xml = [xml stringByReplacingOccurrencesOfString:@"<" withString:@"<"]; return xml; }
エンコードについて
Line、メール、EverNoteは、直接URLをシェア出来ません。
シェアするためには、htmlのエスケープが必要になります。
urlのエスケープ(line,mail)→16進表記(文字参照)
xmlのエスケープ(EverNote)→実態参照
文字 | 実体参照 | 10進表記(文字参照) | 16進表記(文字参照) |
---|---|---|---|
"&" | & | & | & |
""" | " | " | " |
"<" | < | < | < |
">" | > | > | > |
" " | |   |   |
App Store の iOSアプリ を 公開停止 する 方法
AppStore の アプリ を 公開停止 する 方法
概要
AppStoreに公開中のiOSアプリを取り下げる方法の説明です。
(申請(公開前のレビュー依頼)を取り下げたい場合、Developer Rejectで対応してください)
アプリの公開地域を全て解除することでAppStoreで公開されなくなります。
手順
- iTunes Connectへログイン
- Manage Your Appsを選択
- 公開停止したいアプリを選択
- 「Rights and Pricing」を押下
- Appの使用可能日・価格帯・地域の設定画面が表示されるので公開地域を解除します
- 「特定の地域」リンクを押下(公開中の地域が表示される)
- 「すべての選択を解除」ボタンを押下(全ての地域のチェックが外れる)
- 「Save」ボタン押下
- Statusが「Ready for Sale」から「Developer Removed From Sale」となり、App Infomationに「Delete App」ボタンが表示されます。(「Delete App」を選択しなくてもAppStoreからの公開は停止されました。)
その他
- 私の場合、公開停止の手続きをしてから30分くらいでAppStoreから公開されなくなりました。
- AppStoreに一度も公開されなかったアプリは、公開地域を全解除しても「Delete App」が表示されません。(間違って作ってしまった場合、アプリを消したいけど消せなくて結構辛いです。。。)
Mac で よく使う キーボードショートカット まとめ
Mac(OS X)で個人的によく使うキーボードショートカット(コマンド)を用途別にまとめてみました。
参考URL: OS X のキーボードショートカット
ターミナル
ターミナルでの操作
ショートカットキー | 説明 | 詳細 |
---|---|---|
control + A | 行頭に移動 | 現在の行/段落の先頭に移動 |
control + E | 行末に移動 | 現在の行/段落の末尾に移動 |
option + 右矢印キー | 単語の末尾に移動 | テキスト挿入ポイントを次の単語の末尾に移動 |
option + 左矢印キー | 単語の先頭に移動 | テキスト挿入ポイントを前の単語の先頭に移動 |
control + W | 単語先頭まで削除 | テキスト挿入ポイントから前の単語の先頭まで削除 |
control + D | 1文字削除、ログアウト | カーソル文字を1文字削除、ログアウト |
control + K | 右側全て削除 | カーソルの右側の文字から行/段落の末尾までを削除 |
control + U | 左側全て削除 | カーソルの左側の文字から行/段落の先頭までを削除 |
control + L | 再表示 | 画面をクリアしてカレント行を再表示する |
tab | 予測補完 | コマンドやディレクトリ名を補完する |
control + C | 処理中断 | 実行中の処理を中断する |
control + S | 出力停止 | 画面への出力を中断する |
control + Q | 出力再開 | 画面への出力を再開する |
control + Z | 処理一時停止 | 実行中の処理を一時停止する |
テキスト編集
テキストエディタで文章編集時の操作(viやemacsではない)
ショートカットキー | 説明 | 詳細 |
---|---|---|
command + A | 全選択 | 前面に表示されている Finder ウインドウ (ウインドウが開かれていない場合はデスクトップ) のすべての項目を選択 |
command + C | コピー | 選択した項目/テキストをクリップボードにコピー |
command + V | ペースト | クリップボードの内容をペースト |
command + X | 切り取り | 選択した項目/テキストをクリップボードにコピーしテキストは削除 |
command + Z | 動作を戻す | 取り消す/やり直す |
command + Y | 動作を進める | command + Zと反対の動作 |
command + S | 保存 | ドキュメントを保存 |
command + F | 検索 | 検索ウインドウを開く |
command + 上矢印キー | 先頭に移動 | テキスト挿入ポイントを現在のドキュメントの先頭に移動 |
command + 下矢印キー | 末尾に移動 | テキスト挿入ポイントを現在のドキュメントの末尾に移動 |
command + 左矢印キー | 行頭に移動 | テキスト挿入ポイントを現在のドキュメントの行頭に移動 |
command + 右矢印キー | 行末に移動 | テキスト挿入ポイントを現在のドキュメントの行末に移動 |
option + エリア選択 | 範囲指定エリア選択 | optionを押しながらトラックパッドで範囲指定出来る(行間にまたがって空白を削除したい時に便利) |
アプリケーション
常によく使う操作
ショートカットキー | 説明 | 詳細 |
---|---|---|
command + tab | アプリ切替え | 開いているアプリケーションのリスト (最近使った順番に表示されている) 内を順方向に移動 |
command + N | 新規立上げ | 最前面のアプリケーションで新規作成 |
command + W | 閉じる | 最前面のウインドウを閉じる |
command + Q | 終了 | 最前面のアプリケーションを終了 |
command + D | 保存しないで閉じる | 「開く」ダイアログと「保存」ダイアログで「デスクトップ」フォルダを選択・または、Mac OS X v10.6.8 以前で「保存しない」ボタンを含むダイアログから「保存しない」を選択 |
電源ボタン | スリープ | 電源が入ったら、タップしてスリープ解除/スリープ状態 |
Alfred
Alfredとは、Macで利用できるランチャーアプリです。(App Storeでダウンロードできます)
もっとキーボードだけで操作したい人向けです。
個人的には、標準のショートカットにシャットダウンがないのでAlfredで「S」で検索しシャットダウンしています。(電源ボタン長押しで強制終了は出来るのですが、強制終了は気が引けるので)
その他にもアプリケーションを起動する時に良く利用します。一度使うと手放せなくなると思います。
iPhoneアプリ を iPadアプリ に 対応させる 方法
iPhoeアプリをiPadアプリとしてもAppStoreに配布する方法について書いてあります。 iPad対応するには、Xcodeで「ユニバーサルに変更」するのと「iPad用画像の設定」が必要となります。
環境
Xcode 5.1.1
流れ
- ユニバーサル化
- プログラム修正(修正が必要な場合のみ)
- 申請手続き
1. ユニバーサル化
DeviceをiPhoneからユニバーサルに変更する事でiPadに対応されます。
iPadのTAGETSを作ることで、TAGETS別にプログラムを変更出来るようになります。(「2. プログラム修正」で利用する場合があります)
ユニバーサルにすることによって、既存のiPhone用のxibがそのままiPadでも使えます。
また、iPadでアプリ起動時にiPhoneアプリとして起動するのではなく、iPad用アプリとして起動します。
- 「TARGETS->MyApp」を右クリックで「Depulicate」を選択
- 表示されたダイアログで「Duplicate and Transition to iPad」を選択(TARGETSにMyApp-iPadが追加されます)
- 1で選択した「「TARGETS->MyApp」の「General->Deployment Info->Devices」を「iPhone」から「Universal」に変更(この対応だけで(Depulicateで追加しなくても)iPadに対応します)
- iPadで確認(実機の場合、右下に倍率を変更するボタンが表示されなければiPad用で開かれています)
2. プログラム修正(修正が必要な場合のみ)
iPad用のアプリアイコン画像や起動時画像の追加、iPad用に細かいプログラム修正を行います。
- アプリアイコンの追加
- 起動画像の追加
- 「TARGETS->MyApp->General->Launch Images」で起動画像画像(768X1024,1536X2048,1024X768,2048X1536)を設定
- プログラム修正(iPadで確認した時にレイアウト部分が崩れている場合は修正しましょう)
3. 申請手続き
- iTunes Connetで「Add Version」でバージョンアップ用アプリの追加手続きを行います。(iPhoneアプリと同じ手順)
- Xcodeでアーカイブ化しアプリをアップします。(iPhoneアプリと同じ手順)
- iTunes Connetでステータスが「Upload Received」に変更されると、iPad用のスクリーンショットを追加出来るようになるのでiPad用の画像(1536x2048 or 1536x2008)を設定しましょう。(iPadアプリで新しい手順)
※.1と2についてはわからない場合、App Store に iOSアプリ を 公開する方法の「3. 配布したいアプリの設定」を参照してください。
参考URL
iOS の アプリ内課金(In-App Purchase) 組込方法
概要
「iPhone」や「iPad」でのアプリ内課金(In-App Purchase)の実装方法について書きました。
iOSアプリ内で特定の機能を有料販売するための準備・開発・テストの説明中心です。
全体的な作業時間としては、1日は覚悟したほうが良さそうです。
(課金のタイプによってはサーバー側の開発がないから、APNsよりは少し楽かも??)
- In-App Purchase プログラミングガイド(50ページくらい)
- iTunes Connect In-App Purchase 設定ガイド(50ページくらい)
開発環境
OS : OS X 10.9.2
Xcode : 5.1.1
前提条件
プロダクト(次の2つ)が作成されていること。
- iOS Developer Centerでアプリの登録(Identifiersの登録時に「In-App Purchase」にチェックする(デフォでチェック入ってる))
- iTunes Connectでアプリを作成(ステータスをPrepare for Uploadまで進める)
作成方法はこちら(アプリの登録(1.証明書関連の作成)、アプリを作成(3. 配布したいアプリの設定))
アプリ内課金の種類
全部で5種類。
消費型はゲームの課金でよく使われます。購読型は電子書籍でよく使われます。
今回は、「非消耗型(Non-consumable)プロダクト」で作成します。
名称 | 説明 | 備考 |
---|---|---|
消耗型(Consumable)プロダクト | アプリケーションの実行に伴って消費されていく項目です。Voice over IPアプリケーションで通信できる残り分数や、音声の転送など一度限りのサービスが例として挙げられます。 | 消費アイテム |
非消耗型(Non-consumable)プロダクト | ユーザのすべてのデバイス上で無制限に使用できる項目です。ユーザのすべてのデバイスで使用可能になります。例としては、書籍やゲームレベルなどのコンテンツ、およびアプリケーションの追加機能などがあります。 | 無制限アイテム |
自動更新購読(Auto-renewable subscriptions) | エピソードで構成されるコンテンツです。消耗型プロダクトと同様に、自動更新購読は、ユーザのすべてのデバイスで無制限に利用可能になります。非消耗型プロダクトと異なるのは、自動更新購読には期限があるという点です。新しいコンテンツは定期的に信され、ユーザは購読が有効な期間中、発行されたコンテンツに対してアクセスできます。自動更新購読の期限が近づいてくると、ユーザに代わってシステムにより購読が自動的に更新されます。 | 期間で自動課金 |
非更新購読(Non-renewable subscriptions) | エピソードで構成されるコンテンツを含まない購読です。たとえば、歴史的な写真のデータベースに対するアクセス権や、フライトマップのコレクションなどがあります。ユーザのすべてのデバイスで購読を使用可能にし、ユーザの購入を復元するは、アプリケーション側で対応することになります。このプロダクトタイプは、ユーザのアカウントが既にサーバ上に存在し、このアカウントを使用してコンテンツの復元時にユーザを識別できる場合によく使用されます。購読の期限と期間もアプリケーション(またはサーバ)で実装し、実行することになります。 | 期間で課金(開発者が期間や期限をサーバーで管理) |
無料購読(Free subscriptions) | Newsstandに無料購読のコンテンツを置くための手段です。サインアップしたユーザは、Apple IDに関連付けられたどのデバイスからでも購読できます。期限切れになることはありません。また、購読にはNewsstand対応アプリケーションが必要です。 | 無料、Newsstand対応アプリケーション |
流れ
- 準備
1-1. iTunes ConnectでManage In-App Purchasesの追加
1-2. StoreKitフレームワーク追加
1-3. In-App PurchaseをON - 開発
2-1. チェック処理
2-2. 購入開始処理
2-3. トランザクション処理・リストア処理
2-4. 購入終了処理 - テスト
1. 準備
iTunes Connectの設定やプロジェクトにフレームワークや設定の変更を行います。
1-1. iTunes ConnectでManage In-App Purchasesの追加
iTunes Connectに接続し対象のアプリを開き、アプリ内課金のアイテム情報(Manage In-App Purchases)を登録します。(登録内容は「2. 開発」で利用します。)
「Type」は、今回は「Non-consumable」を選択します。
「Reference Name」は、アイテムの表示名を設定します。(Appleから毎月送信される売上レポートで表示名)
「Product ID」は、一意のグローバル識別子でドメイン名のスタイルを逆にして使用することを推奨しています(例: com.companyname.application.productid)。
「Screenshot」は、購入確認画面のスクリーンショットをアップロードします。(3. テスト実施時にスクリーンショットをキャプチャしアップロードしましょう。)
1-2. StoreKitフレームワーク追加
ライブラリに「StoreKit.framework」を追加します。
(TAGETS > Build Phases > Link Binary With Libraries)
1-3. In-App PurchaseをON
CapabilitiesのIn-App PurchaseをOFFからONに変更します。 (PROJECT > Capabilities > In-App Purchase)
2. 開発
アプリ内課金のプログラム。
参考URL:失敗しない iOS In-App Purchase プログラミング
サンプルコード:SampleInAppPurchase(swift)
2-1. チェック処理
課金イベントにアプリ内課金が使えるかチェックをします。
MyClass.h #import <StoreKit/StoreKit.h> // delegateにSKProductsRequestDelegateとSKPaymentTransactionObserverを追加 @interface MyClass : UIViewController <SKProductsRequestDelegate, SKPaymentTransactionObserver> { } MyClass.m // メソッド名は適当に - (BOOL)_checkInAppPurchase { if (![SKPaymentQueue canMakePayments]) { UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"エラー" message:@"アプリ内課金が制限されています。" delegate:nil cancelButtonTitle:nil otherButtonTitles:@"OK", nil]; [alert show]; return NO; } return YES; }
2-2. 購入開始処理
アイテム情報の取得と購入開始の処理を行います。
MyClass.m // メソッド名は適当に(チェック処理の結果がYESだったらこの処理を呼ぶ) - (void)_startInAppPurchase { // com.companyname.application.productidは、「1-1. iTunes ConnectでManage In-App Purchasesの追加」で作成したProduct IDを設定します。 NSSet *set = [NSSet setWithObjects:@"com.companyname.application.productid", nil]; SKProductsRequest *productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:set]; productsRequest.delegate = self; [productsRequest start]; } #pragma mark SKProductsRequestDelegate - (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response { // 無効なアイテムがないかチェック if ([response.invalidProductIdentifiers count] > 0) { UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"エラー" message:@"アイテムIDが不正です。" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil, nil]; [alert show]; return; } // 購入処理開始(「iTunes Storeにサインイン」ポップアップが表示) [[SKPaymentQueue defaultQueue] addTransactionObserver:self]; for (SKProduct *product in response.products) { SKPayment *payment = [SKPayment paymentWithProduct:product]; [[SKPaymentQueue defaultQueue] addPayment:payment]; } }
2-3. トランザクション処理・リストア処理
App Storeがトランザクションを処理するのを待機し、購入が成功した場合アイテム購入の処理を行います。
MyClass.m // トランザクション処理 #pragma mark SKPaymentTransactionObserver - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions { for (SKPaymentTransaction *transaction in transactions) { switch (transaction.transactionState) { case SKPaymentTransactionStatePurchasing: // NSLog(@"購入処理中"); // TODO: インジケータなど回して頑張ってる感を出す。 break; case SKPaymentTransactionStatePurchased: // NSLog(@"購入成功"); // TODO: アイテム購入した処理(アップグレード版の機能制限解除処理等) // TODO: 購入の持続的な記録 [queue finishTransaction:transaction]; break; case SKPaymentTransactionStateFailed: // NSLog(@"購入失敗: %@, %@", transaction.transactionIdentifier, transaction.error); // TODO: 失敗のアラート表示等 break; case SKPaymentTransactionStateRestored: // リストア処理 // NSLog(@"以前に購入した機能を復元"); [queue finishTransaction:transaction]; // TODO: アイテム購入した処理(アップグレード版の機能制限解除処理等) break; default: [queue finishTransaction:transaction]; break; } } } // リストア処理結果 - (void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error { // NSLog(@"リストア失敗:%@", error); // TODO: 失敗のアラート表示等 } - (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue { // NSLog(@"全てのリストア完了"); // TODO: 完了のアラート表示等 }
購入の持続的な記録には次の方法があります。(消費型プロダクトは維持しません。)
// User Defaultsを使用した値の持続の例) MyClass.m // 購入の持続的な記録 NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; BOOL rocketCarEnabled = [defaults boolForKey:@"enable_rocket_car"]; // 購入アイテム使用時 if (rocketCarEnabled) { // ロケットカーを使用 } else { // 普通の自動車を使用 }
非消耗型プロダクトはAppレシートが推奨ですが、余力がなかったので記述は割愛します。
2-4. 購入終了処理
トランザクションオブザーバの削除を行います。(トランザクションが終了すると呼び出される)
MyClass.m #pragma mark SKPaymentQueue - (void)paymentQueue:(SKPaymentQueue *)queue removedTransactions:(NSArray *)transactions { [[SKPaymentQueue defaultQueue] removeTransactionObserver:self]; }
3. テスト
テストユーザーの作成と実機で確認します。
参考URL:iPad上でアプリ内課金 (In-App Purchase) の実機テストをする方法メモ
- iTunes Connectでテストユーザを作成します。(10 Minute Mailを使うと10分だけ無料でメアドが貰えるので便利かも)
- 実機の端末App IDをサインアウトし、アプリをインストールします。
- 追加した課金のシステムをテストします。テスト実施(課金イベント)時にサインインを求められるので、テストユーザーでサインインします。
- 課金成功のメッセージが表示されれば、無事課金完了です。
テスト実施時の注意事項
参考URL: iOSのアプリ内課金(In App Purchase)での注意点
以上でアプリ内課金(In-App Purchase)の組込作業は終わりです。
お疲れ様でした!
.DS_Store とは
背景
以前書いた記事が自分の思わぬところで多くの人に見ていただけたので、もう少し「.DS_Store」について書きたいと思います。
「.DS_Store」ってなんとなく設定ファイルだと思っていたのですが、そもそもなんだっけ・・・
ということで「.DS_Store」について少し調べました。
過不足や誤記があればご連絡いただけると助かります。
- .DS_Storeとは
- コマンド(defaults write com.apple.desktopservices DSDontWriteNetworkStores true)を利用した場合の影響について
- コマンドを利用した場合の個人的な不具合体験談等
1. .DS_Storeとは
概要
OS Xで作成された独自の形式の隠しファイルです。
アイコンの位置や表示設定などのフォルダ表示オプションに関するメタデータを保持しています。
デフォルトでFinder上でもリモートシステム上でもアクセスするすべてのフォルダに.DS_Storeファイルが作成されます。
Apple側でも問題意識を持っていて、ネットワーク接続時に .DS_Store ファイルの作成を抑制する方法も紹介しています。(defaults write com.apple.desktopservices DSDontWriteNetworkStores trueコマンドの紹介)
作成されることによる問題点
- Macユーザー以外から苦情
- リビジョン管理時のファイル追加による負担
- アーカイブ構造が重要である場合の余計なファイルによる構造的問題
wiki .DS_Storeより抜粋
2. コマンド(defaults write com.apple.desktopservices DSDontWriteNetworkStores true)を利用した場合の影響について
影響範囲
SMB/CIFS、AFP、NFS、WebDAV サーバ
(USB、DropBox等はコマンドを実行しても作成されます。)
影響内容
Finderで予期しない動作が起こることがあります。
一例として、サーバボリューム上の項目の「情報を見る」のコメントを追加したり編集する場合、変更内容は他のクライアントやサーバ自体からは見えません。(ファイル本体ではなくメタデータに影響あり)
Mac OS X: リモートボリュームの「情報を見る」のコメントは表示されないことがあります
削除方法は色々なサイトに乗ってますが、.DS_Storeの説明や影響の記事が少なくて不安になる。。。
3. コマンドを利用した場合の個人的な不具合体験談等
ファイルサーバ上の大きめのサイズのExcelが開けない場合がありました。
ローカルにコピーして、サーバのファイルに上書きして更新は出来ます。
自分以外の人が更新する場合ファイルがデグレードする場合があるので注意が必要です。
私のファイルサーバ上のExcel更新頻度は、週に2〜3回位なので不具合が余りなかったのかもしれません。
私は、これからも.DS_Storeを作成しない方を選びます。
理由は、現在Excelを更新する業務は少ないし、会社の方針もGoogleDriveに移行していく予定なので今後も更新頻度は少なくなるからです。Macに変更した当初はExcelのマクロが使えない事に絶望を覚えましたが気にしないことにしました。
それ以上に、私の会社ではMacユーザーが少ない(1〜2人)ので何を見たのかが他の人に筒抜けの生活なんてもう耐えられないのです!
ファイルサーバ上で頻繁にファイルを変更するようなお仕事をされている方や重要なファイルを扱うような方は、事故を起こす可能性があるのでコマンドは利用せずにそのまま利用するのが良いのかもしれません。