Goalist Developers Blog

AmazonS3に社内用Mavenリポジトリを作成し、Gradleから使う

こんにちは、毛虫にさされて赤いポツポツ持ちの鈴木です。

タイトルの通り、AmazonS3 で Mavenリポジトリ を作成しGradleから使う手順をかきました。

社内のモジュールに依存するモジュールを開発する際に、サーバー上で依存関係を解決しビルドするために使ったりします。

Qiitaにも同様の内容を投稿済みなので、最新情報はこちらを参照してください。

qiita.com

前提

  • Gradleをインストールしている
  • S3のバケットにファイルをアップロードできるAWS IAMユーザーを作成している
  • 上記IAMユーザーのアクセスキーIDとシークレットアクセスキーを持っている

本記事のゴール

S3上にアップロードしたライブラリを別のGradleプロジェクトからダウンロードできるようにします。

具体的には以下のようなbuild.gradleの設定で「gradle build」コマンドを実行した際にダウンロードできるようにします。

build.gradle

dependencies {
    compile 'jp.co.goalist:library:1.0.0'
}

以下の内容については本記事では扱いません。いつかまた。

  • Jenkinsなどビルドサーバー側での設定
  • S3上のリポジトリへのアクセス管理

Mavenリポジトリへのアップロード

Gradleプロジェクト作成

以下のコマンドを実行します。

mkdir gradle-upload-s3repo
cd gradle-upload-s3repo/
gradle init --type=java-library

生成されたファイルはこんな感じです。

.
├── build.gradle
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle
└── src
    ├── main
    │   └── java
    │       └── Library.java
    └── test
        └── java
            └── LibraryTest.java

この時点で build.gradle の内容は以下の通りです。(不要なコメントと依存ライブラリの設定は削除しています)

build.gradle

apply plugin: 'java'

//依存ライブラリが未定義のため、
//現時点ではrepositoriesとdependenciesは不要ですが、
//後ほど使うので残しておきます。
repositories {
    jcenter()
}

dependencies {
}

確認のため、プロジェクトルートで「gradle build」と打ってあげると、build/libsに「gradle-upload-s3repo.jar 」が生成されます。ここまではjavaプラグインのデフォルト機能です。
確認後、「gradle clean」してビルド結果を削除してあげましょう。

groupId、artifactId、version を決める

Mavenでは、groupIdとartifactIdとversionの組み合わせでライブラリを一意に定義します。

慣習にならい、

キー
groupId jp.co.goalist
artifactId library
version 1.0.0

にしましょう。

build.gradleに以下の設定を追加します。

group = "jp.co.goalist"
version = "1.0.0" 

settings.gradleを以下のように設定します。

settings.gradle

rootProject.name = 'library'

S3上にリポジトリ用のバケットを作成する

手順は省略しますが、リポジトリとして使用するバケットを作成します。

今回は「s3://repository.hoge/maven/」を作成します。

開発環境の設定

~/.gradle/gradle.propertiesにリポジトリのURLとIAMユーザーのアクセスキーIDとシークレットアクセスキーを設定します。
これをプロジェクト側が参照することで、プロジェクトに含めないですむのでセキュリティ的に安全になります。(普通のこと言ってます) あと個々のプロジェクトでアクセスキーIDとか設定したりとかしないですみます。

gradle.properties

goalistRepoUrl=s3://repository.hoge/maven/
awsAccessKeyId = AKIAJWORQEXXXXXXXXXX
awsSecretAccessKey = waTa0aakgK2e5hXXXXXXXXXXXXXXXXXXXXXXXXXX

mavenプラグインのuploadArchives設定

MavenリポジトリへのアップロードにはmavenプラグインのuploadArchivesを使います。
先程設定したリポジトリのURL、IAMユーザーのアクセスキーID、シークレットアクセスキーを参照するように設定します。

build.gradle

apply plugin: 'maven'
uploadArchives {
    repositories {
        mavenDeployer {
            configuration = configurations.deployerJars
            repository(url: goalistRepoUrl) {
              authentication(userName: awsAccessKeyId, password: awsSecretAccessKey)
            }
        }
    }
}

S3へのアクセスには org.springframework.build:aws-maven を使いますので、dependenciesに追加します。
今回は最新版の5.0.0.RELEASEを使います。

The Central Repository Search Engine

build.gradle

configurations {
    deployerJars
}

dependencies {
    deployerJars 'org.springframework.build:aws-maven:5.0.0.RELEASE'//追加
}

これで設定は完了。最終的には以下のようになります。

build.gradle

apply plugin: 'java'
apply plugin: 'maven'

group = "jp.co.goalist"
version = "1.0.0" 

repositories {
    jcenter()
}

configurations {
    deployerJars
}

uploadArchives {
    repositories {
        mavenDeployer {
            configuration = configurations.deployerJars
            repository(url: goalistRepoUrl) {
              authentication(userName: awsAccessKeyId, password: awsSecretAccessKey)
            }
        }
    }
}

dependencies {
    deployerJars 'org.springframework.build:aws-maven:5.0.0.RELEASE'
}

settings.gradle

rootProject.name = 'library'

uploadArchivesタスクを実行

以下のコマンドを実行し、BUILD SUCCESSFULL したら成功です。

gradle uploadArchives

S3を確認すると

s3://repository.hoge/maven/以下にライブラリがアップロードされていると思います。

Mavenリポジトリからダウンロード

今回アップロードしたライブラリに依存するGradleプロジェクトを作成します。

mkdir gradle-upload-s3repo-child
cd gradle-upload-s3repo-child/
gradle init --type=java-library

build.gradleを変更し、S3のリポジトリを参照するための設定と、依存関係の設定をします。

build.gradle

apply plugin: 'java'

repositories {
    jcenter()
    maven {
        url goalistRepoUrl
        credentials(AwsCredentials) {
            accessKey awsAccessKeyId
            secretKey awsSecretAccessKey
        }
    }
}

dependencies {
    compile 'jp.co.goalist:library:1.0.0'
}

以下のコマンドを実行し、BUILD SUCCESSFULなことを確認できたら完了です。

gradle build

さいごに

実運用ではビルドサーバーからしかアップロードできないようにして、開発者は参照のみ出来るようにすればいい感じに運用できると思います。 モジュールを再利用していきましょう。

Swiftの型変換一覧@備忘録

こんちは。渡部です。
ちょいちょい型変換でつまずくので備忘録がてらまとめておきます。

f:id:watabe1028:20170531180439p:plain

Int

Int → String

let int : Int = 23
let string : String = String(int)
print(string)
// 23

Int → Double

let int : Int = 23
let double: Double = Double(int)
print(double)
// 23.0

Int → Float

let int : Int = 23
let float: Float = Float(int)
print(float)
// 23.0

Float

Float → String

let float : Float = 23.5
let string : String = String(float)
print(string)
// 23.5

Float → Int (切り捨て)

let float : Float = 23.5
let int : Int = Int(float)
print(int)
// 23

Float → Double

let float : Float = 23.5
let double : Double = Double(float)
print(double)
// 23.5

Double

Double → String

let double: Double = 23.5
let string : String = String(double)
print(string)
// 23.5

Double → Int (切り捨て)

let double : Double = 23.5
let int : Int = Int(double)
print(int)
// 23

Double → Float

let double : Double = 23.5
let float : Float = Float(double)
print(float)
// 23.5

Bool

Bool → String

let bool : Bool = true
let string : String = String(bool)
print(string)
// true

Bool → Int

let bool : Bool = true
let int : Int = Int(bool)
print(int)
// 1

Bool → Float

let bool : Bool = true
let float : Float = Float(bool)
print(float)
// 1.0

Bool → Double

let bool : Bool = true
let double : Double = Double(bool)
print(double)
// 1.0

まとめ

超個人的な備忘録です。
型変換を忘れがちな人の役に立てればいいなと思います。 決して手抜き記事ではありません。

寿司とチャットするアプリを作ってみた

こんちは。渡部です。
エンジニアの勉強会に行くとよく寿司好きの人に出会います。

f:id:watabe1028:20170531150310p:plain

なので?今回は寿司とチャットするアプリをささっと作ってみます。
ただしローカルでのみ動くものなので固定文言でやります。

チャット画面を1から作るのは大変なので
JSQMessagesViewControllerというライブラリを使って作ります。

こんな感じでやっていきます
・JSQMessagesViewControllerってなに?うまいの?
・どうやって使うのよ?
・JSQMessagesViewControllerをプロジェクトに追加する
・適当にチャット文言を用意する
・ガリガリ書く寿司だけに

れっつらゴー!

JSQMessagesViewControllerってなに?うまいの?

JSQMessagesViewController

github.com

簡単にいうとチャット画面を提供してくれるライブラリです。
なので実装は文言やアイコンなどの設定をすれば
最低限の機能が簡単に使えます。

どうやって使うのよ?

GitHubで落としてゴー!

JSQMessagesViewControllerをプロジェクトに追加する

CocoaPodsで落とします。
使い方がわからない人はこちら

プロジェクトを作ったらPodsでJSQMessagesViewControllerをinstallします。
サンプルとして「ChatTestApp」を作ります。

作ったらターミナルでアプリ直下まで行き、Podsを作成します。

pod init

作成したらJSQMessagesViewControllerを落とすため編集モードで追記し、installします。
編集モードにして、

vi Podfile

これを追記する。

  # Pods for TestApp
  pod ‘JSQMessagesViewController’
end

「:wq」で保存、終了。

インストール!

pod install

これで下準備おけ!

適当にチャット文言を用意する

まんまです。
文言をこんな感じに用意します。

var messages: [JSQMessage] = [
        JSQMessage(senderId: "sushi", displayName: "B", text: "らっしゃい♪"),
        JSQMessage(senderId: "Dummy",  displayName: "A", text: "大将!つぶ貝!"),
        JSQMessage(senderId: "sushi", displayName: "B", text: "ないよ!"),
        JSQMessage(senderId: "Dummy",  displayName: "A", text: "じゃサーモン!"),
        JSQMessage(senderId: "sushi", displayName: "B", text: "ないよ♪"),
        JSQMessage(senderId: "Dummy",  displayName: "A", text: "いか!"),
        JSQMessage(senderId: "sushi", displayName: "B", text: "ないよ♪"),
        JSQMessage(senderId: "Dummy",  displayName: "A", text: "じゃ帰る!"),
        JSQMessage(senderId: "sushi", displayName: "B", text: "ちょwww待てってwww\nマグロあるからwww")
    ]

senderIdの"sushi"が寿司アイコン(相手)、"Dummy"が自分です。
displayNameは表示名ですが、シンプルな方が好きなので
表示させないようにあとで設定します。私情です。
textはチャットで表示される文言です。

ガリガリ書く寿司だけに

コピペでできるように最低限のコードのみ記載します。
説明はコメントを見てもらえばわかるかと。

import JSQMessagesViewController // インポートする

class ViewController: JSQMessagesViewController { // ViewControllerからJSQMessagesViewControllerに変更する
    
    var messages: [JSQMessage] = [
        JSQMessage(senderId: "sushi", displayName: "B", text: "らっしゃい♪"),
        JSQMessage(senderId: "Dummy",  displayName: "A", text: "大将!つぶ貝!"),
        JSQMessage(senderId: "sushi", displayName: "B", text: "ないよ!"),
        JSQMessage(senderId: "Dummy",  displayName: "A", text: "じゃサーモン!"),
        JSQMessage(senderId: "sushi", displayName: "B", text: "ないよ♪"),
        JSQMessage(senderId: "Dummy",  displayName: "A", text: "いか!"),
        JSQMessage(senderId: "sushi", displayName: "B", text: "ないよ♪"),
        JSQMessage(senderId: "Dummy",  displayName: "A", text: "じゃ帰る!"),
        JSQMessage(senderId: "sushi", displayName: "B", text: "ちょwww待てってwww\nマグロあるからwww")
    ]
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 適当につける
        senderDisplayName = "A"
        senderId = "Dummy"
    }

     //アイテムごとに参照するメッセージデータを返す
    override func collectionView(_ collectionView: JSQMessagesCollectionView!, messageDataForItemAt indexPath: IndexPath!) -> JSQMessageData! {
        return messages[indexPath.row]
    }


     //アイテムごとのMessageBubble(背景)を返す
    override func collectionView(_ collectionView: JSQMessagesCollectionView!, messageBubbleImageDataForItemAt indexPath: IndexPath!) -> JSQMessageBubbleImageDataSource! {
        if messages[indexPath.row].senderId == senderId {
            return JSQMessagesBubbleImageFactory().outgoingMessagesBubbleImage(
                with: UIColor(red: 112/255, green: 192/255, blue:  75/255, alpha: 1))
        } else {
            return JSQMessagesBubbleImageFactory().incomingMessagesBubbleImage(
                with: UIColor(red: 229/255, green: 229/255, blue: 229/255, alpha: 1))
        }
    }


     // cell for item
    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        
        let cell = super.collectionView(collectionView, cellForItemAt: indexPath) as! JSQMessagesCollectionViewCell
        
        if messages[indexPath.row].senderId == senderId {
            cell.textView?.textColor = UIColor.white
        } else {
            cell.textView?.textColor = UIColor.darkGray
        }
        return cell
    }
    
    
    // section
    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return messages.count
    }


     // image data for item
    override func collectionView(_ collectionView: JSQMessagesCollectionView!, avatarImageDataForItemAt indexPath: IndexPath!) -> JSQMessageAvatarImageDataSource! {
        
        //senderId == 自分 だった場合表示しない
        let senderId = messages[indexPath.row].senderId
        
        if senderId == "Dummy" {
            return nil
        }
        return JSQMessagesAvatarImage.avatar(with: UIImage(named: "sushi"))
    }


     //時刻表示のための高さ調整
    override func collectionView(_ collectionView: JSQMessagesCollectionView!, attributedTextForCellTopLabelAt indexPath: IndexPath!) -> NSAttributedString! {
        
        let message = messages[indexPath.item]
        if indexPath.item == 0 {
            return JSQMessagesTimestampFormatter.shared().attributedTimestamp(for: message.date)
        }
        if indexPath.item - 1 > 0 {
            let previousMessage = messages[indexPath.item - 1]
            if message.date.timeIntervalSince(previousMessage.date) / 60 > 1 {
                return JSQMessagesTimestampFormatter.shared().attributedTimestamp(for: message.date)
            }
        }
        return nil
    }


     // 送信時刻を出すために高さを調整する
    override func collectionView(_ collectionView: JSQMessagesCollectionView!, layout collectionViewLayout: JSQMessagesCollectionViewFlowLayout!, heightForCellTopLabelAt indexPath: IndexPath!) -> CGFloat {
        
        if indexPath.item == 0 {
            return kJSQMessagesCollectionViewCellLabelHeightDefault
        }
        if indexPath.item - 1 > 0 {
            let previousMessage = messages[indexPath.item - 1]
            let message = messages[indexPath.item]
            if message.date .timeIntervalSince(previousMessage.date) / 60 > 1 {
                return kJSQMessagesCollectionViewCellLabelHeightDefault
            }
        }
        return 0.0
    }

    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
}

画面読み込みですぐにチャットが表示されているはずです。

次にチャットを送受信させて見ます。

     //Sendボタンが押された時に呼ばれる
    override func didPressSend(_ button: UIButton!, withMessageText text: String!, senderId: String!, senderDisplayName: String!, date: Date!) {
        
        //キーボードを閉じる
        self.view.endEditing(true)
        
        //メッセージを追加
        let message = JSQMessage(senderId: senderId, displayName: senderDisplayName, text: text)
        self.messages.append(message!)
        
        //送信を反映
        self.finishReceivingMessage(animated: true)
        
        //textFieldをクリアする
        self.inputToolbar.contentView.textView.text = ""
        
        //テスト返信を呼ぶ
        testRecvMessage()
    }

     //テスト用「マグロならあるよ!」を返す
    func testRecvMessage() {
        
        let message = JSQMessage(senderId: "sushi", displayName: "B", text: "マグロならあるよ!")
        self.messages.append(message!)
        self.finishReceivingMessage(animated: true)
    }

こんな感じになります。
「うに!」と入力し、Sendボタンを押下で、、、

f:id:watabe1028:20170531175132p:plain

こうなる!

f:id:watabe1028:20170531175207p:plain

f:id:watabe1028:20170531175525p:plain

まとめ

世に出てるチャットアプリの大半はJSQMessagesViewControllerを使っているんではないかなーと予想しています。
これからも楽できる便利なライブラリの紹介ができたらなと思います。

Angular CLIをアップデートしたメモ〜angular-cliから@angular/cliへ

こんにちは。イイオです。
この3月末からAngular CLIを使ってAngular2を触り始めた初心者です。

最近では新卒の技術研修の一環でAngularの環境構築サポートを担当しました。

自分で調べながらやってみてね〜と言ったもののあんまり説明不足でしたから
「angular cli 環境構築」でぐぐって上の方に出てくるブログのコマンドをそのまま使って
古い方のangular-cliをインストールしてプロジェクト作成してしまう事態が起きました。

数ヶ月前の記事がもう使えないとか諸行無常の世界だ。

正式リリースの@angular/cliの方を使いたかったのでアップデートして
プロジェクトのパッケージを変更しました。以下はその際の説明メモです。
公式に書いてある以上のことはしてないです。

github.com

stories 1.0 update · angular/angular-cli Wiki · GitHub

Angular CLIをアップデートする

手順

  1. グローバルの古いangular-cliをアンインストールする
  2. グローバルで@angular/cliをインストールする
  3. ローカルの古いangular-cliを削除し、利用パッケージから外す
  4. ローカルのnode_modules(ローカルに落としたパッケージが入ってるフォルダ)を削除する
  5. ローカルで@angular/cliを利用パッケージに登録する
  6. ローカルの利用パッケージを新しく入れ直す

npm?ローカルとかグローバル?な方はこの辺りを見たら良い気がする

勉強メモ/npmの使い方(node.js=v0.11.16, npm=2.3.0, 2015-03時点) - Qiita

1. グローバルの古いangular-cliをアンインストールする

$ npm uninstall -g angular-cli

2. グローバルで@angular/cliをインストールする

$ npm cache clean
$ npm install -g @angular/cli@latest

3. ローカルの古いangular-cliを削除し、利用パッケージから外す

cd <プロジェクトフォルダ>
npm uninstall --save-dev angular-cli

4. ローカルのnode_modules(ローカルに落としたパッケージが入ってるフォルダ)を削除する

macなら

rm -rf node_modules dist

winなら

rmdir /S/Q node_modules dist

5. ローカルで@angular/cliを利用パッケージに登録する

npm install --save-dev @angular/cli@latest

6. ローカルの利用パッケージを新しく入れ直す

npm install

ng -vでバージョンを確認してみたら

$ ng -v
    _                      _                 ____ _     ___
   / \   _ __   __ _ _   _| | __ _ _ __     / ___| |   |_ _|
  / △ \ | '_ \ / _` | | | | |/ _` | '__|   | |   | |    | |
 / ___ \| | | | (_| | |_| | | (_| | |      | |___| |___ | |
/_/   \_\_| |_|\__, |\__,_|_|\__,_|_|       \____|_____|___|
               |___/
@angular/cli: 1.0.0
node: 6.10.2
os: darwin x64
@angular/common: 2.4.10
@angular/compiler: 2.4.10
@angular/core: 2.4.10
@angular/forms: 2.4.10
@angular/http: 2.4.10
@angular/platform-browser: 2.4.10
@angular/platform-browser-dynamic: 2.4.10
@angular/router: 3.4.10
@angular/cli: 1.0.0
@angular/compiler-cli: 2.4.10

無事にアップデートできました。めでてえ〜
何も中身を作り込んでないのであまり問題もなく終わりました。

新卒なのにいきなりAngularをやるみなさんはがんばってくれ!

Angular4でangular-google-mapsを使う

こんにちわ、 ゴーリスト開発のモリツグです。
Angular2もAngular4になってしばらく経ちますが、皆さん元気でしょうか。
RC5のNgModule が導入された時のような事態にはならず安心しました。

今回はAngular4の環境でangular-google-mapsの使い方を紹介したいとおもいます。
私が担当する回では必ずFlexを懐かしんできましたが、今回は残念ながらFlexを懐かしめるのはChangeDetectorRef.detectChanges()がvalidateNow()に似ている点ぐらいでした。

開発環境
・angular 4.1.2
・agm 1.0.0-beta.0
・typescript 2.2.0

インストール

詳しくはこちらになります。
とりあえずインストールします。 古い方のangular2-google-mapsを入れないように注意です。

npm install @agm/core --save

新旧バージョンのselectorの比較などはCHANGELOG.mdにあります

使ってみる

plunkerのDEMOはこちら。
場所を入力⇒表示された候補をクリック⇒地図の表示変更 という感じで動きます。
f:id:t-moritsugu:20170518123817p:plain
GoogleのAPIキー無しでやっているので、いつエラーになるのかは不明です。
ただちゃんと動くのでローカルにコピれば大丈夫です。(agm公式のデモもplunkerでキー無しです)
ローカル環境ではangular-cliを使って作成しましたが、plunkerではsystemjsを利用しています。
app/app.module.ts の以下の部分にAPI_KEYを入れればローカルでも動きます。

app/app.module.ts

@NgModule({
 ...
    AgmCoreModule.forRoot({ // Google Maps JavaScript 
      /*apiKey: 'Your API KEY HERE'*/
    })
 ...
})
export class AppModule { }

Google Maps JavaScript APIを利用できるように設定するのを忘れると以下のようなエラーで怒られます。

Google Maps API error: ApiNotActivatedMapError

あと直接は関係ないのですが、微妙に困ったところはAngularが変更を検知できない現象でした。
このリンク の3番目の解決方法で解決しました。
コード中でchangeDetectorRefを利用しているのはそのためです。

まとめ

angular-google-mapsを使えばあっさりできました。オススメです。
changeDetectorRefはFlexでいうところのvalidateNow()に見えて懐かしかったです。

CloudFront の署名付き URL で S3 にアクセスしてみる

こんにちは、ゴーリストのエンジニアのJPです。
最近 SaaS サービスの開発プロジェクトでインフラ構成の調査を行っているため、その中で話題に上がった AWS のサービスを利用したファイル配信方法を紹介したいと思います。

目次

ターゲット

以下のような疑問をお持ちの方

  • S3 へのアクセスを制限したい
    ( S3 の URL 知ってれば誰でもアクセスできちゃうよね?)
  • Lambda のレスポンスサイズの上限に引っかかる
  • 署名付き URL と Cookie どっちがいいの?

  ※具体的な設定方法は次回以降の記事で記載しますので、ご注意ください。

調査の背景

  • 新規サービスにはファイルをアップロード・ダウンロードする機能がある
  • アプリケーションのユーザーによりダウンロードできるファイルを制限したい
  • バックエンドは AWS の Lambda を使用
  • Lambda にはレスポンスのサイズに 6MB という制限がある → 足りないから DB じゃなくて AWS S3 にファイルを置こう
  • S3 からファイルをダウンロードする際に、ユーザーの権限判定ロジックを埋め込みたい
    どうする?

解決策

下図のように AWS の CloudFront の署名付き URL を利用して S3 のアクセスを制限し、取得者の権限チェックロジックも埋め込もう。

f:id:j-itoh:20170511124644j:plain

署名付き URL についての概要
Amazon の公式ドキュメント:CloudFront を使用してプライベートコンテンツを供給する

インターネットを通じてコンテンツを配信する多くの企業が、選ばれたユーザー(料金を支払っているユーザーなど)のドキュメント、ビジネスデータ、メディアストリーム、またはコンテンツに対して、アクセスを制限する必要があると考えています。CloudFront を使用してこのプライベートコンテンツを安全に供給するには、以下の方法を使用できます。 特別な CloudFront 署名付き URL または署名付き Cookie を使用してプライベートコンテンツにアクセスするようユーザーに要求します。 Amazon S3 コンテンツへのアクセスにユーザーが Amazon S3 URL ではなく CloudFront URL を使用するよう要求します。CloudFront URL を要求することは必須ではありませんが、ユーザーが署名付き URL や署名付き Cookie で指定された制限をバイパスすることを防ぐため、この方法をお勧めします。

CloudFront でできること

S3URL による S3 へのアクセスを無効にする
CloudFront の Distribution のドメイン名を含む URL でしか S3 のファイルにアクセスできない
例)https://sample.cloudfront.net/usagi.png

署名付き URL でのアクセスしか受け付けないようにできる
署名付き URL:公開鍵認証方式により暗号化された文字列を URL のクエリ文字列に含んだもの
例)

https://sample.cloudfront.net/usagi.png?Expires=1493274265&Signature=P-Mz9vHOGzL5HFwUpwoEwaPFDJIqd3T5rCoenGd3JFu50FtmTC9BM~o22rUf3gBn6wLECFgt6b-9bxJKUBc32uzGzy4M0KLBhW9W3CF4G79~sOHGJUKl0HmuLsFn1ZFy606~z5bBcdhp6hI-fJXd2dS4VEP1S~Z~q2rzTR75l3V1WnHnCTxLYWl5smhFavm57WdIan6jnvCRXnVWzWv7pbSz5VAmzx1N5MgQ0fflI1GaYMNel7sErr40BGeTSSoJpdFClWHfuOhclXFYUbq-dTVD4XRp0o4rQHM7uk-jFLTyi0v~jYFGfrYp1p825xwkiM1UYw~kasH5VHJM5Njs7g__&Key-Pair-Id=APKAJRIMZTJQZZQ42AVA

署名付き URL は以下のセキュリティーポリシーを追加することができる

  • 有効期限(開始日時、終了日時)
  • アクセスを許可する IP アドレス

署名付き URL は Java、Node.js、PHP 等の AWS ライブラリで作成する必要がある
具体的な作成方法は省略します。

認証の仕組み

署名付き URL の構造

https:// { Distribution } / { S3 Key } ?Expires= { 有効期限 }&Signature= { 暗号化情報 } &Key-Pair-Id= { アクセスキー ID }
パラメータ 説明
Distribution CloudFront の S3 接続用の Distribution のドメイン名
S3 Key S3 上の目的のファイルまでのパス
有効期限 URLが使えなくなる時刻(UTCで指定しなければならない)
暗号化情報 秘密鍵により暗号化かつ Base64 エンコードされた文字列(※)
アクセスキー ID AWSのルートアカウントにより作成された秘密鍵と公開鍵のペアの ID(認証に使用するキーペアを指定)

※暗号化情報には上表のパラメータが全て含まれている

認証手順

順序 実行サービス 処理内容
1 クライアント 署名付き URL により CloudFront にアクセス
2 CloudFront Distribution でアクセスが許可されている AWS ルートアカウントがアクセスキー ID を保持しているか判定
3 CloudFront 保持している場合、2の ID の公開鍵で Signature を復号化
4 CloudFront 復号化した内容とパラメータを比較( URL の改ざんチェック)
5 CloudFront 4の比較の結果、合わない場合アクセスを拒否(エラーを返す)
6 CloudFront 5が合う場合、有効期限などのポリシーをチェック
7 S3 有効期限内であればファイルを取得

URL の再利用への対策

  • 秘密鍵がなければ Signature を復号化できないため、有効期限等の暗号化情報を改ざんすることはできない
    (公開鍵で暗号化したものは3の公開鍵での復号化はできない)
  • 有効期限内であれば再利用可能(最適な有効期限を検討する必要)

どちらにするかの検討段階で、署名付き URL の以下のメリットが大きいかなと思い、正直 Cookie の方は調べていません。
Cookie のメリットはこれから勉強します。

  • 画像ファイルの URL をそのまま HTML タグのプロパティに書ける
<img src=" { 署名付き URL } "/>

Cookie だとフロント側でロジックが必要になりそう。

活用例

ファイルダウンロード機能において、アプリケーションのユーザー権限によるアクセス制限を行う
※ Lambda では 6MB という制限があるため、ファイルは一律 S3 から転送するようにする

順序 実行サービス 処理内容
1 クライアント ファイルダウンロードリクエストを送信
2 Lambda 権限チェック
3 Lambda 2で権限がある場合、署名付き URL を作成してレスポンスを送信
4 クライアント 署名付き URL にアクセス
5 CloudFront URL の改ざん、有効期限をチェック
6 CloudFront キャッシュの確認・あれば返信(7は実行しない)
7 S3 ファイルを送信

他の実現方法

他の案として Lambda@Edge でセッションをチェックするという方法もありましたが、以下の懸念があったため採用しませんでした。
※ Lambda@Edge とは CloudFront のリクエストと S3 の間に Lambda Function を処理をかませる機能です。

  • Lambda Function の実行時間の上限が0.05秒しかない
  • Node.js でしかロジックを書けない(ランタイムが Node.js にしか対応していない)

感想

署名付き URL でもアプリケーションのユーザー権限によるセキュリティを保つことができることが分かりました。
また、CloudFront を使用することで、キャッシュからの高速なファイル転送が可能となりました( SaaS だったら関係ないかも)。
Lambda のレスポンスサイズの上限を気にする必要がなく、ファイルを DB から取得する方法と、CloudFront 経由のファイル取得、どちらの方が早いのかは気になるところです。

課題としては、ベストな有効期間を検討することが残っています(動かしてみるしかない)。

機会があれば続きとして以下のようなことも書いていきたいです。

  • AWS コンソール上での設定方法
  • Java での署名付き URL の作成方法
  • CloudFront を経由して一括で複数のファイルをダウンロードする際の最も早い方法

XLPagerTabScriptの使い方

こんちは。渡部です。
最近はOSSのライブラリが豊富でありがたいですよね。
UIの参考にもなるし、実際に使用すると工数の削減になります。

今回はXLPagerTabScriptというライブラリの使い方を紹介します。
どんどこやっていきます。(パクリ)

f:id:watabe1028:20170430235401p:plain

こんな感じでやっていきます
・XLPagerTabScriptってなに?うまいの?
・どうやって使うのよ?
・XLPagerTabScriptをプロジェクトに追加する
・遷移するViewControllerを用意する
・Storyboardを設定する
・ガリガリ書く

れっつらゴー!

XLPagerTabScriptってなに?うまいの?

XLPagerTabScript github.com

一言で例えると、「カスタマイズしやすいタブ遷移ライブラリ」です。
カスタマイズしやすい点としてはUIViewControllerがそのまま使えることです。
よくライブラリを使うと、よくわからないクラスを継承していて
結局Wiki通りに使うことしかできないものが多々あります。(未熟なので)

また、今も頻繁に更新されているのでOSが上がった際にも安心です。

どうやって使うのよ?

GitHubで落としてゴー!

XLPagerTabScriptをプロジェクトに追加する

CocoaPodsで落とします。
使い方がわからない人はこちら

プロジェクトを作ったらPodsでXLPagerTabScriptをinstallします。
サンプルとして「TabTestApp」を作ります。

作ったらターミナルでアプリ直下まで行き、Podsを作成します。

pod init

作成したらXLPagerTabScriptを落とすため編集モードで追記し、installします。
編集モードにして、

vi Podfile

これを追記する。

  # Pods for TestApp
  pod ‘XLPagerTabStrip’
end

「:wq」で保存、終了。

インストール!

pod install

これで下準備おけ!

遷移するViewControllerを用意する

まんまです。
UIViewControllerを3つ追加します。
今回はわかりやすく
「RedViewController」
「BlueViewController」
「GreenViewController」
の3つを追加し、それぞれviewに色を設定します。
Storyboardにも追加し、関連付けてください。

Storyboardを設定する

まず、ベースとなる(デフォルトで入っている)ViewControllerに
Collection ViewとScroll Viewを追加します。
ついでにAutoLayoutもつけちゃってください。

Scroll VIewを設定します。
ます、追加したScroll Viewの上で右クリックし、
「 New Referencing Outlet」をViewControllerに繋ぎます。
その際に「containerView」と「view」の選択肢が出るので、「containerView」を選択します。

Collection Viewはclassを「ButtonBarView」、Moduleを「XLPagerTabStrip」を選択します。

f:id:watabe1028:20170430234311p:plain

その後、ScrollViewと同様に
「 New Referencing Outlet」をViewControllerに繋ぎ、
出てくる選択肢の「buttonBarView」を選択します。

これで準備ができました。

ガリガリ書く

もちろん、ソース無しでは動きません。
が、結構簡単にかけます。コピペでできるように最低限のコードのみ記載します。

まずはベースとなるViewControllerに以下を記述します。

import XLPagerTabStrip // インポートする

class ViewController: ButtonBarPagerTabStripViewController {    // 「ButtonBarPagerTabStripViewController」に変更する

    override func viewDidLoad() {
        
        // もしタブのカスタマイズをするなら
        // ここに追記する

        super.viewDidLoad()
    }
    
    /// ViewControllers
    override func viewControllers(for pagerTabStripController: PagerTabStripViewController) -> [UIViewController] {
        
        // 追加したViewControllerを指定
        let firstVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "redViewController")
        let secondVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "blueViewController")
        let thirdVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "greenViewController")
        
        // ViewControllersに入れる
        let childViewControllers:[UIViewController] = [firstVC, secondVC, thirdVC]
        return childViewControllers
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
}

次に追加した各ViewControllerに以下を記述します。
RedViewControllerを例にしてます。

import XLPagerTabStrip  //インポートして

class RedViewController: UIViewController, IndicatorInfoProvider {    // 「IndicatorInfoProvider」を追加
    
    // タブのタイトルを設定
    var itemInfo: IndicatorInfo = "RED"

    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    // これはお約束。コピペでおけ!
    func indicatorInfo(for pagerTabStripController: PagerTabStripViewController) -> IndicatorInfo {
        return itemInfo
    }
}

これで実行してください。

タブでスルスル遷移します。
スワイプでも遷移するので試してみてください。

まとめ

少ないコードで劇的に工数が減らせる最高のライブラリですね。
これからも楽をするためにバンバンライブラリを取り入れて行きたいと思います!