nirasan's tech blog

趣味や仕事の覚え書きです。Linux, Perl, PHP, Ruby, Javascript, Android, Cocos2d-x, Unity などに興味があります。

Rails で API サーバーの認証の仕組みを作る

はじめに

  • Rails でモバイルアプリのバックエンドに使う API サーバーのユーザー作成と認証の仕組みを作ったメモです
  • Doorkeeper + Sorcery を使った認証の構築とテストまで記述します

導入

新しいプロジェクトを作成
$ rails new -T -B doorkeeper-test

gem のインストール

デフォルトの Gemfile に以下を追記
gem 'doorkeeper'
gem 'sorcery'
group :development, :test do
  gem 'rspec-rails'
  gem 'factory_girl_rails'
  gem 'database_rewinder'
end
インストール
$ bundle install

Sorcery のインストール

# Sorcery の設定ファイルと Sorcery に対応した User モデルの生成
$ rails generate sorcery:install

Doorkeeper のインストール

# 設定ファイルの作成とルートの追加
$ rails generate doorkeeper:install
# マイグレーションファイルの作成
$ rails generate doorkeeper:migration

マイグレーションの実行

$ rake db:migarate

Doorkeeper の設定

Doorkeeper.configure do
  # Change the ORM that doorkeeper will use (needs plugins)
  orm :active_record

  resource_owner_authenticator do
    User.find_by_id(session[:current_user_id]) || redirect_to(login_url)
  end

  resource_owner_from_credentials do
    User.authenticate(params[:username], params[:password])
  end
end
Doorkeeper.configuration.token_grant_types << "password"

ユーザーコントローラの作成

  • ユーザーの作成とユーザー情報表示ページを作成する
  • ユーザー作成は認証なしで、ユーザー情報表示は認証必須のページにする
generate
$ rails g controller users create show
routes.rb
  resources :users, only: [:create, :show]
app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :doorkeeper_authorize!, only: [:show] # show のみ認証が必要

  def create
    @user = User.new(user_params)
    if @user.save
      render json: @user
    else
      head :bad_request
    end
  end

  def show
    render json: User.find(params[:id])
  end

  private

  def user_params
    params.require(:user).permit(:email, :password)
  end
end
app/controllers/application_controller.rb
  • CSRF 対策を無効にする
-  protect_from_forgery with: :exception
+  protect_from_forgery with: :null_session

Doorkeeper の認証用キーの作成

動作確認

  • curl コマンドで動作確認を行う
ユーザーの作成
$ curl -F "user[email]=user1@example.com" -F "user[password]=password" http://127.0.0.1:3000/users
アクセストークンの取得
  • client_id には「Doorkeeper の認証用キーの作成」で作成した Application Id を、client_secret には同じく Secret を指定する
$ curl -F "grant_type=password" \
       -F "client_id=07461c1f68870ea090ed20af49d0680782950119213e2a1af9e1f653c688dca3" \
       -F "client_secret=20dea83f022c9e2a0e6fbec1da0e9c0e20c40e1bf09d2e8db029b461653a88bd" \
       -F "username=user1@example.com" -F "password=password" \
       http://127.0.0.1:3000/oauth/token.json
ユーザーの取得
  • Bearer 以降には「アクセストークンの取得」で取得したトークンの文字列を指定する
$ curl -H "Authorization: Bearer 18afa04c80fbe3528b5d495c24e8badcbaeee12b2866d22d8f220c44543ae01c" http://127.0.0.1:3000/users/1

テスト

rspec の準備
$ rails g rspec:install
rails_helper.rb に追記
  config.before :suite do
    DatabaseRewinder.clean_all
  end

  config.after :each do
    DatabaseRewinder.clean
  end

  config.before :all do
    FactoryGirl.reload
  end
specs_helper.rb に追記
  require 'factory_girl_rails'
  config.include FactoryGirl::Syntax::Methods
factories/users.rb の作成
FactoryGirl.define do
  factory :user do
    sequence(:email) {|n| "user#{n}@example.com" }
    password "password"
  end
end
factories/doorkeeper.rb の作成
FactoryGirl.define do
  factory :access_token, class: Doorkeeper::AccessToken do
    sequence(:resource_owner_id) { |n| n }
    application
    expires_in 1.hours
  end

  factory :application, class: Doorkeeper::Application do
    sequence(:name){ |n| "Application #{n}" }
    redirect_uri 'https://example.com/callback'
  end
end
spec/controllers/user_controller_spec.rb
require 'rails_helper'

RSpec.describe UsersController, type: :controller do
  describe "GET #show" do
    let!(:application) { create :application }
    let!(:user)        { create :user }
    let!(:token)       { create :access_token, :application => application, :resource_owner_id => user.id }
    it "returns http success" do
      get :show, {format: :json, access_token: token.token, id: user.id}
      expect(response).to have_http_status(:success)
    end
  end
end

さいごに

  • クライアントからは Doorkeeper のトークン情報を見れる必要はないから、中継用のアクションとかを用意したほうがいいかな?

Rails初期化チートシート

プロジェクトの作成

rails new PROJECT_NAME -T -B
cd PROJECT_NAME

git の設定

git init
curl https://www.gitignore.io/api/rails,ruby,osx,linux,vim,sublimetext,rubymine > .gitignore

gemのインストール

cp /path/to/Gemfile ./ #TODO
bundle install

bootstrap の適用

rails g layout:install bootstrap3

font-awesome の使用

vi app/assets/stylesheets/framework_and_overrides.css.scss
 @import "bootstrap-sprockets";
 @import "bootstrap";
+@import "font-awesome";

simple_form

rails g simple_form:install --bootstrap

devise

rails g devise:install
rails g devise user
rake db:migrate

devise の日本語対応

rails g devise:views:locale ja
rails g devise:views:bootstrap_templates

日本語化

curl -L https://raw.github.com/svenfuchs/rails-i18n/master/rails/locale/ja.yml > config/locales/ja.yml

アプリケーション設定

vi config/application.rb
config.time_zone = 'Tokyo'
config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
config.i18n.default_locale = :ja
	
config.generators do |g|
  g.javascripts false
  g.stylesheets false
  g.helper false
  g.test_framework :rspec,
  fixture: true,
  view_specs: false,
  helper_specs: false,
  routing_specs: false,
  controller_specs: true,
  request_specs: true
  g.fixture_replacement :factory_girl, dir: 'spec/factories'
end

devise 用のリンクを app/views/layouts/_navigation_links.html.haml に追加

- if user_signed_in?
  %li= link_to current_user.email, root_path, :class => 'navbar-link'
  %li= link_to 'Edit profile', edit_user_registration_path, :class => 'navbar-link'
  %li= link_to "Logout", destroy_user_session_path, method: :delete, :class => 'navbar-link'
- else
  %li= link_to "Sign up", new_user_registration_path, :class => 'navbar-link'
  %li= link_to "Login", new_user_session_path, :class => 'navbar-link'

kaminari

rails g kaminari:config
rails g kaminari:views  bootstrap3

rspec

rails g rspec:install

テストの設定

spec/rails_helper.rb

RSpec.configure do |config|
  config.before :suite do
    DatabaseRewinder.clean_all
  end

  config.after :each do
    DatabaseRewinder.clean
  end
  
  config.before :all do
    FactoryGirl.reload
  end
end

spec/spec_helper.rb

  require 'factory_girl_rails'
  config.include FactoryGirl::Syntax::Methods

spec/factories/users.rb

actoryGirl.define do
  factory :user do
    sequence(:email) { |i| "user#{i}@example.com" }
    sequence(:password) { |i| "password#{i}" }
  end

end

.pryrc の作成

begin
  require 'hirb'
rescue LoadError
  # Missing goodies, bummer
end

if defined? Hirb
  # Slightly dirty hack to fully support in-session Hirb.disable/enable toggling
  Hirb::View.instance_eval do
    def enable_output_method
      @output_method = true
      @old_print = Pry.config.print
      Pry.config.print = proc do |*args|
        Hirb::View.view_or_page_output(args[1]) || @old_print.call(*args)
      end
    end

    def disable_output_method
      Pry.config.print = @old_print
      @output_method = nil
    end
  end

  Hirb.enable
end

erb2haml

rake haml:replace_erbs

確認用ページ作成

rails g controller welcome index

figaro

bundel exec figaro install
vi config/application.yml
figaro heroku:set -e production

Unity でネイティブプラグインを使って ActivityIndicator の画像変更と画面中央での表示(iOS編)

iOS 側のコード

  • 以下のスクリプトを Unity の Assets/Plugins/iOS に MyActivityIndicator.mm として保存する
  • http://tsubakit1.hateblo.jp/entry/2014/08/19/010141 からの変更点は
    • 親ビューの取得がなんかうまくいかなかったので GetAppController から UnityGetGLViewController に
    • 背景色を半透明の黒に
    • 画像の表示位置を画面中央に
#import "UnityAppController.h"
 
UIImageView *imageView;
UIView *backgroundView;
NSMutableArray *imageList;

const int kImageCount = 4;
NSString* const kImageNameFormat = @"anim%02d.png";

extern "C" {
    void InitMyActivityIndicator();
    void StartMyActivityIndicator();
    void EndMyActivityIndicator();
}
 
void InitMyActivityIndicator()
{
    imageList = [NSMutableArray array];
    for (NSInteger i = 0; i < kImageCount; i++) {
        NSString *imagePath = [NSString stringWithFormat:kImageNameFormat, i];
        UIImage *img = [UIImage imageNamed:imagePath];
        [imageList addObject:img];
    }
    UIImage *img = [imageList objectAtIndex:0];
    CGSize cs = img.size;
    CGRect pr = [[UIScreen mainScreen] bounds];
    CGRect rect = CGRectMake(
        (pr.size.width / 2 - cs.width / 2),
        (pr.size.height / 2 - cs.height / 2),
        cs.width,
        cs.height
    );
    imageView = [[UIImageView alloc]initWithFrame:rect];
    imageView.image = imageView.image = [imageList objectAtIndex:0];
    imageView.animationImages = imageList;
    imageView.animationDuration = 0.5;
    imageView.animationRepeatCount = 0;
}

void CreateBackgroundView () {
    CGRect pr = [[UIScreen mainScreen] bounds];
    backgroundView = [[UIView alloc] init];
    backgroundView.frame = CGRectMake(0, 0, pr.size.width, pr.size.height);
    backgroundView.backgroundColor = [UIColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:0.5];
    [UnityGetGLViewController().view addSubview:backgroundView];
} 
 
void StartMyActivityIndicator() {
    InitMyActivityIndicator();
    CreateBackgroundView();
    [UnityGetGLViewController().view addSubview:imageView];
    [imageView startAnimating];
}

void EndMyActivityIndicator()
{
    [imageView removeFromSuperview];
    [backgroundView removeFromSuperview];
}
画像の取り込み手順
  • iOS 側で表示する画像は XCode のプロジェクト内に取り込む必要がある
  • 上記のコードでは前回の Plugins/Android/res/drawable/*.png を使いまわす想定
  • 以下手順
    • Unity で Platform を iOS にしてビルド
    • XCode でプロジェクトを開く
    • Images.xcassets を選択し
    • 中央ペインで右クリック
    • Import を選んで前記の画像を選択して Open で取り込む

Unity 側のコード

  • 以下のスクリプトを任意の GameObject にアタッチする
  • StartActivityIndicator で表示、StopActivityIndicator で非表示
using UnityEngine;
using System.Collections;
using System.Runtime.InteropServices;

public class ActivityIndicator : SingletonMonoBehaviour<ActivityIndicator>{

    #if UNITY_IOS
    [DllImport("__Internal")]
    static extern void InitMyActivityIndicator();

    [DllImport("__Internal")]
    static extern void StartMyActivityIndicator();

    [DllImport("__Internal")]
    static extern void EndMyActivityIndicator();
    #endif

    public void Start()
    {
        #if UNITY_IOS
        InitMyActivityIndicator ();
        DontDestroyOnLoad(gameObject);
        #endif
    }

    public void StartActivityIndicator(){
        #if UNITY_IOS
        StartMyActivityIndicator();
        #endif
    }

    public void StopActivityIndicator()
    {
        #if UNITY_IOS
        EndMyActivityIndicator();
        #endif
    }
}

Unity でネイティブプラグインを使って ActivityIndicator の画像変更と画面中央での表示(Android編)

はじめに

  • Unity で OS ネイティブの読み込み中アニメーションを表示するメソッドとして Handheld.StartActivityIndicator がある。
  • これは Android では画面左上に表示されてしまうので、画面中央に表示させられるようにネイティブプラグインを作ったメモ。

バージョン

ネイティブプラグインの作り方

プラグインからリソースを参照する

  • jar 形式のファイルにはリソースが含まれないため、R.id などで参照するとエラーになる
  • リソースを参照したい場合は http://sixeight.hatenablog.com/entry/2013/10/12/215456 こちらの通り、Unity の Assets/Plugins/Android/res にファイルを配置し、プラグイン側からは getResources().getIdentifier() を使って参照するとうまくいく
  • 今回はこのおかげで表示したい画像を Unity 側で差し替えやすくなったのが嬉しかった

画面中央に任意の画像の Progress Dialog を表示する

Progress Dialog の拡張クラス

  • 前記の通り R による参照を使えないので、初期化時に context から取得している
package com.example.activityindicator;

import android.app.ProgressDialog;
import android.content.Context;
import android.graphics.drawable.AnimationDrawable;
import android.os.Bundle;
import android.widget.ImageView;

public class MyCustomProgressDialog extends ProgressDialog {
  private static int layoutId;
  private static int animationId;
  private static int drawableId;

  private AnimationDrawable animation;

  public static MyCustomProgressDialog ctor(Context context) {
    MyCustomProgressDialog dialog = new MyCustomProgressDialog(context);
    dialog.setIndeterminate(true);
    dialog.setCancelable(false);
    layoutId = context.getResources().getIdentifier("view_custom_progress_dialog", "layout", context.getPackageName());
    animationId = context.getResources().getIdentifier("animation", "id", context.getPackageName());
    drawableId = context.getResources().getIdentifier("custom_progress_dialog_animation", "drawable", context.getPackageName());
    return dialog;
  }

  public MyCustomProgressDialog(Context context) {
    super(context);
  }

  public MyCustomProgressDialog(Context context, int theme) {
    super(context, theme);
  }

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(layoutId);

    ImageView la = (ImageView) findViewById(animationId);
    la.setBackgroundResource(drawableId);
    animation = (AnimationDrawable) la.getBackground();
  }

  @Override
  public void show() {
    super.show();
    animation.start();
  }

  @Override
  public void dismiss() {
    super.dismiss();
    animation.stop();
  }
}

呼び出し用クラス

package com.example.activityindicator;
import android.app.Activity;

import com.unity3d.player.UnityPlayer;

public class ActivityIndicator {

    private static MyCustomProgressDialog _dialog;

    public static void show() {
        final Activity activity = UnityPlayer.currentActivity;
        activity.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                if (_dialog == null) {
                    _dialog = MyCustomProgressDialog.ctor(activity);
                }
                _dialog.show();
            }
        });
    }

    public static void hide() {
        if (_dialog == null) return;
        final Activity activity = UnityPlayer.currentActivity;
        activity.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                _dialog.hide();
            }
        });
    }
}

Unity への取り込み

Unity 側のコード

    void StartActivityIndicator () {
        #if UNITY_ANDROID
        AndroidJavaClass androidNativePlugin = new AndroidJavaClass("com.example.activityindicator.ActivityIndicator");
        androidNativePlugin.CallStatic("show");
        #endif
    }

    void StopActivityIndicator () {
        #if UNITY_ANDROID
        AndroidJavaClass androidNativePlugin = new AndroidJavaClass("com.example.activityindicator.ActivityIndicator");
        androidNativePlugin.CallStatic("hide");
        #endif
    }

paperclip_database の導入メモ

はじめに

  • ActiveRecord のオブジェクトに画像ファイルを添付する gem の Paperclip で、画像の保存先をデータベースにする gem の paperclip_database の導入メモ。
  • Paperclip は導入済みで、User モデルの avatar カラムに添付画像情報を入れているものとする。

バージョン

  • Rails 4.2
  • Paperclip 4.2.1
  • paperclip_database 2.3.1

インストール

  • Gemfile
gem "paperclip_database", "~> 2.0"
  • インストール
bundle install

マイグレーションファイルの作成と実行

rails generate paperclip_database:migration User avatar
rake db:migrate

モデルの設定

  • :storage オプションを追加する
has_attached_file :avatar, :storage => :database

画像を返すアクションとルートの作成

ルートの作成

  • routes.rb で users リソースが定義済みの場合、member でルートの追加
resources :users do
  member do
    get :avatars
  end
end

アクションの作成

  • UsersController で paperclip_database のコントローラ拡張ミックスインのインポートと画像返却用のアクション定義メソッドの追記
class UsersController < ApplicationController
  include Paperclip::Storage::Database::ControllerClassMethods
  downloads_files_for :user, :avatar

テンプレートで画像の呼び出し

  • @user.image.url(:style) だと URL エンコード済みの文字列が出てきてしまうので、普通にルート名で URL を指定する。
<% image_tag(images_user_url(@user, :style => :medium)) %>

NGUI の UITweener の Animation Curve に任意の曲線を設定する

はじめに

  • Unity + NGUI で TweenPosition や TweenAlpha などの UITweener を継承したコンポーネントで Animation Curve の曲線をプログラムから設定する方法についてメモ。

バージョン

  • Unity 4.6.1f1
  • NGUI 3.7.6

設定方法

  • Animation Curve の曲線は AnimationCurve クラスで設定する
  • コンストラクタは AnimationCurve(Keyframe[])
  • Keyframe は曲線上の任意の点で Keyframe(x, y) で x が時間軸、y が変化量になる。
    • Keyframe の x は、開始地点が 0 で、終了地点が 1 になる。duration が 3秒 なら「x:0 = 0秒」で「x:1 = 3秒」となる。
    • Keyframe の y は、開始地点が 0 で、終了地点が 1 になる。TweenAlpha で変化前が 1 で変化後の値が 0 なら「y:0 = 1」で「y:1 = 0」となる。

コード例

開始から半分の時間まで変化しない場合

GameObject go = GameObject.Find("Path/To/Any/GameObject");
UITweener tw = TweenAlpha.Begin(go, 1f, 0.5f);
tw.animationCurve = new AnimationCurve(
	new Keyframe(  0f, 0f),
	new Keyframe(0.5f, 0f),
	new Keyframe(  1f, 1f)
);

点滅する場合

GameObject go = GameObject.Find("Path/To/Any/GameObject");
UITweener tw = TweenAlpha.Begin(go, 1f, 0.5f);
tw.animationCurve = new AnimationCurve(
	new Keyframe(  0f, 0f),
	new Keyframe(0.2f, 1f)
	new Keyframe(0.4f, 0f),
	new Keyframe(0.6f, 1f)
	new Keyframe(0.8f, 0f),
	new Keyframe(  1f, 1f)
);

Keyframe に入る角度と出る角度の指定をする

  • Keyframe のコンストラクタで Keyframe(x, y, inTangent, outTangent) というパターンが有り inTangent と outTangent で曲線の入る角度と出る角度を指定できる。
  • inTangent, outTangent の単位は tangent なので、角度を指定したい場合は変換する必要がある。

角度からタンジェントへの変換

// 角度からタンジェントへ
float DegreesToTan (float degrees) {
	return Mathf.Tan(DegreesToRadians(degrees));
}
// 角度からラジアンへ
float DegreesToRadians (float degrees) {
	return degrees * Mathf.PI / 180f;
}

コード例

イーズアウト

GameObject go = GameObject.Find("Path/To/Any/GameObject");
UITweener tw = TweenAlpha.Begin(go, 1f, 0.5f);
tw.animationCurve = new AnimationCurve(
	new Keyframe(0f, 0f, 0f, DegreesToTan(70f)),
	new Keyframe(1f, 1f, DegreesToTan(10f), 0f)
};

イーズイン

GameObject go = GameObject.Find("Path/To/Any/GameObject");
UITweener tw = TweenAlpha.Begin(go, 1f, 0.5f);
tw.animationCurve = new AnimationCurve(
	new Keyframe(0f, 0f,                0f, 0f),
	new Keyframe(1f, 1f, DegreesToTan(70f), 0f)
};

波を描く

GameObject go = GameObject.Find("Path/To/Any/GameObject");
UITweener tw = TweenAlpha.Begin(go, 1f, 0.5f);
tw.animationCurve = new AnimationCurve(
	new Keyframe(   0f,   0f,                 0f,                 0f),
	new Keyframe(0.25f, 0.5f, DegreesToTan(-85f), DegreesToTan(-85f)),
	new Keyframe(0.5f,  0.5f, DegreesToTan(-85f), DegreesToTan(-85f)),
	new Keyframe(0.75f, 0.5f, DegreesToTan(-85f), DegreesToTan(-85f)),
	new Keyframe(   1f,   1f,                 0f,                 0f)
};

NGUI で UILabel の文字量に従って縦幅だけ変動させるスクリプト

using UnityEngine;
using System.Collections;

/// <summary>
/// UILabel の横幅を固定して text に従った縦幅に設定するスクリプト
/// </summary>
public class UILabelHeightFitter : MonoBehaviour {

    /// <summary>
    /// リサイズ対象の UILabel
    /// </summary>
    public UILabel label;

    private string beforeText = "";

	void Update () {
        // UILabel.text が更新されるたびにリサイズを実行
        if (label.text != beforeText) {
            ResizeHeight();
            beforeText = label.text;
        }
	}

    /// <summary>
    /// UILabel.text の文字量に従って UILabel.height を変更する。
    /// 簡単のため「UILabel.fontSize == 文字の幅 == 文字の高さ」という前提で計算しているので、
    /// 半角全角の混在や可変幅フォントの使用によりずれが生じる場合がある
    /// </summary>
    void ResizeHeight () {
        int line = 1;
        int chara = 0;
        int maxChara = label.width / (label.fontSize + label.spacingX);
        string text = label.text;
        for (int i = 0; i < text.Length; i++) {
            if (text[i] != '\n') {
                chara++;
                if (chara >= maxChara) {
                    chara = 0;
                    line++;
                }
            } else {
                chara = 0;
                line++;
            }
        }
        label.height = (label.fontSize + label.spacingY) * line;
    }
}