滑り込みプログラマー

後発組なので劣等感に浸りながら成長を志す人のブログです

ふわっとしていた認証と認可について

今回はいままでふわっとしていた認証と認可について、 加えてOAuthの基本的な流れについてまとめていきたいと思います。

認証と認可の違い

認証とは、「ユーザーがユーザー本人であるか」を証明するフローです。 認証は、ユーザーにサイト独自のパスワードをもたせるパターンと、外部サービスのログイン情報をそのまま使うパターンがあります。

後者には認証APIとOpen IDの2種類が存在します。

認可とは、「ユーザーが特定の操作をすること」を許可したものです。

OAuth入門

認証と認可に関連して、今回は↓の書籍を読んで学んでみました。

【電子版】雰囲気でOAuth2.0を使っているエンジニアがOAuth2.0を整理して、手を動かしながら学べる本 - - BOOTH

某サイトのコピーアプリを練習がてら作っている中でFacebook,Googleログインを実装する必要があったのですが、 なにがなんだかわからないまま実装だけ終えたみたいな感じになってしまったのでちょうど良いなと思い購入しました。

本の内容をまるごと載せるわけにはいかないですし、そもそも僕自身完全には理解しきれてないところがあるので、さわりだけ書こうかなと思います。

そしてOAuthは認可につかうもので認証につかうものではないらしいのでFacebookGoogleログインの話に使える知識は限られています。(書籍内で知る。以下引用)

OAuth は認可のプロトコルであって、認証のプロトコルではありません。すなわち、 「サードパーティアプリから Google Photo を利用する」というときに使うべきものであっ て、「サードパーティアプリに Google アカウントでログインする」というときに使うべ きものではありません。認証の場合は OAuth ではなく、 OpenID Connect を利用すべき です。 では、多くの方が OAuth を認証のためのものと考えているのはなぜでしょう。その理由 は「twitter が OAuth を認証として利用しているから」というのが私の認識です。 P55

というわけで、以下でエンドポイントやフローは、OAuthを使った認証と認可で共通したものをまとめてみました。

基本的な流れ

まずOAuthの基本的な流れを理解するのに必要なものが4つあります。

  • リソースオーナー(ユーザー)

  • クライアント

  • 認可サーバー (OAuth API

  • リソースサーバー

1.書籍内ではユーザー(リソースオーナー)が

2.とある(Google製ではない)画像編集アプリ(クライアント)で

3.Google photo(リソースサーバー)にアップしている画像を加工する

といったフローの中で説明されていました(つまり認可のフロー)が、

今回のFacebookログインなどは認証の話になります。 たとえばGoogleの情報を使ってあるサービスにログインする場合、

1.リソースオーナーが

2.Google(リソースサーバー)のアカウント情報を使って

3.別アプリ(クライアント)へのログインをする

といった流れになります。(たぶんw)

図にするとこんな感じです。

f:id:takaikunoff:20190630181053p:plain

3つのエンドポイント

上記の流れには3つのエンドポイントがあります。

1.認可エンドポイント

2.トークンエンドポイント

3.リダイレクトエンドポイント

認可エンドポイント

クライアントがリソースサーバーへのアクセス権を持ってない場合、クライアントは認可エンドポイントにアクセスします。 そこで、ユーザーがリソースサーバーのパスワードなどを入力して、クライアントにアクセス権を渡すことに同意して、認可コードが発行されます。

上記の図でいうと①,②,③のあたりですね。

認可コードは、「ユーザーがクライアントに対して、リソースサーバーへのアクセス権を渡すことを同意しましたよ。」という証明です。

twitterとかで連携しようとするとよく見る、「このアプリのアクセスを許可しますか?」みたいなアレです。

トークンエンドポイント

認可コードを受け取ったクライアントは、トークンエンドポイントに向かって、使いたい必要なパラメーターと認可コードを投げることで、アクセストークンを取得できます。上記の図で言うと④のあたりです。

ちなみにここではBasic認証を通る必要があります。

Basic認証にはクライアントシークレットとクライアントIDを使います。これは、クライアントを認可サーバーに事前登録することで手に入ります。 事前登録の方法は各認可サーバーによって変わります。一応今回使ったGoogleではGoogle Developer console,Facebookの場合はFacebook for Developerです。

リダイレクトエンドポイント

「アプリへ戻ります」みたいなアレです。

上記の図で言うと②,③あたりになります。ここでクライアントは認可コードを受け取っています。

まとめ

若干無理やりになってしまいましたが、何気なく使ってしまっていた技術のフローがわかったのはもちろん、認証と認可がOAuthという技術を通して整理できたというのと、OpenID、そしてOpen ID Connectなど、さらにキャッチアップすべき内容にも出会ったので1000円にしてはかなり学びの多い書籍だったなと思います。

まだ理解しきれていない部分、実際につかってみないとわからない部分などがありましたが、ネットワーク周りの知識も深めつつ身につけていきたいなという感じです。

生SQLを学んでみた

今回は『スッキリわかるSQL入門』を中心に、生のSQLを学んでみました。 普段はRailsを使っているのでActiveRecordを使うことが多いんですが、ActiveRecordを使うにせよ複雑な検索条件を考えるときにどうしても冗長になってしまったり、 自分のためのデータを抽出したいときなどはSQLを書いてmetabaseなどで見るだけで十分だったりするのでだんだんと必要性を感じました。

というわけで、基本的な部分から個人的に学びになった部分までをActiveRecordを通した操作も絡めながらまとめていきたいと思います。

基本的な構文

こちらは他の記事などにもまとまっているのと、割と直感的で自然とおぼえてしまいそうなのでサラッといきますが

SELECT

データを取得するための構文。SELECT カラム名 FROM テーブル名というのが基本構文。 カラム名を指定しない場合は SELECT * FROM テーブル名で、すべてのレコードを取得することになる。

UPDATE

データを更新するための構文。UPDATE テーブル名 カラム名A = 値, カラム名B = 値というのが基本構文。 指定のカラム名を指定の値に書き換える。

DELETE

データ(レコード)を削除するための構文。DELETE FROM テーブル名 というのが基本構文。 これはレコードをまるごと削除するのでカラム名の指定などはしない。

INSERT

データ(レコード)を挿入するための構文。INSERT INTO テーブル名 (カラム名A, カラム名B, カラム名C) VALUES (値A, 値B, 値C)というのが基本構文。 レコードをまるごと挿入する場合は、カラム名は省略できるが、順番どおりではないとならない。

基本的に自分の目的から必要になるのはSELECTだけになるんですが一応まとめてみました。 ちなみにINSERT以外はActiveRecordの操作でおなじみのWHEREで条件の指定ができます。

SELECTに関してはWHEREの後ろにさらにデータを加工したりする構文をつけたりします。

WHEREに続けて使う構文

WHERE構文は基本的にはWHERE 条件(カラム名 = 値など)のような形で使うと思います。単純に指定のカラム名が指定の値になっているレコードを取り出すときなどですね。

でもそれだけだと、例えば

  • 複数条件をつけたいとき
  • ○○以外という条件で探したいとき *2つの条件のうち、どちらか一方を満たしていれば良いとき

などの条件は使えなくなってしまいます。

では上記3つの条件を満たすときはどのようにすればいいのでしょうか? このあたりは直感的といえば直感的ですが、ActiveRecordの操作になれてると微妙に直感的ではないです。

複数条件をつけたいとき

Railsの場合、複数条件を指定するときは、モデル名.where(条件1, 条件2....)のような書き方でいけたかと思うんですが、WHERE単体で指定できる条件は1つだけのようです。

SQLで複数条件を指定するときはWHERE 条件1 AND 条件2とする必要があるようです。これは1も2も両方満たしてないと引っかかりません。

○○以外という条件で探したいとき

Railsの場合、モデル名.where.not(条件)という書き方をしていました。 SQLの場合も同じようにNOTを使います。

つまりWHERE NOT 条件1となります。

2つの条件のうち、どちらか一方を満たしていれば良いとき

この場合はORを使います。 単純にWHERE 条件1 OR 条件2という感じで使います。

ちなみにRailsではモデル名.where(条件1).or(モデル名.where(条件2))というのが基本的な使い方のようです。

副問合せ

個人的に一番学びになったのがこの副問合せという概念です。

ひとことでいうと 「検索結果を検索条件にして検索をする」という概念です。

例えば、usersテーブルのカラムの1つにユーザーの身長というカラムがあったとします。 その時、身長がもっとも高いユーザーのレコードを取り出したいとなったとします。

この場合、身長の最大値という条件でユーザーを検索するというのが副問合せです。 SQLでいうと SELECT * FROM ユーザー WHERE 身長 = (SELECT MAX(身長) FROM ユーザー)というSQLになります。

Railsの場合、maximumを使うと指定したカラムの最大値がとれるので、それを検索条件にしてwhereすれば良いということになります。

User.where(身長: User.maximum(:身長))みたいな感じですね。

よく考えたら当たり前の話なんですが、この概念がなかったら.orderや.firstなどをつなげてやっていたに違いない。良くても一回最大値を変数に入れてからやっていく方法しか思いつかなかったはずです。

INNER JOIN

あと個人的にはINNER JOINもあまり使ってこなかったと思います。

INNER JOINはアソシエーションが組まれていたりするときに力を発揮します。

例えば、ユーザー(1)とテストの結果(多)でアソシエーションを組まれてたとします。

単純にユーザーに紐付いたテスト結果を条件にしたがって取り出す、みたいなときはUser.テスト結果.where(条件) と書けばいけるんですが、

「ユーザーとテスト結果両方に条件をつけたい」みたいなとき、例えばユーザーテーブルに性別というカラムがあって、「その日にテストを受けた男性のユーザーだけほしい」というとき、

今までだったら

exam_results = ExamResult.where(date: Date.now)
user_ids = exam_results.map { |exam_result| exam_result.user_id }
users = User.where(sex: man, ids: user_ids)  

みたいに書いていたと思います。

INNER JOINを使えば、アソシエーション先の条件も検索条件に含めることができます。

つまり

users = User.joins(:exam_results).where(sex: man, exam_results: { date: Date.now } )

という感じでかけるようになります。

番外編

これは割と前に疑問に思ったときに少し調べたんですが、 kaminariというページネーションを実現する有名なGemがあります。 これもOFFSETとLIMITというSQLを使って実現されています。

page(params[:page])の()内には、見ているページのページ番号が入ります。(ページネーションのリンクをクリックして遷移するとpage番号を持ったURLが発行される) そしてpageメソッドでは何が行われているかというと、 pageメソッドの中にOFFSETとLIMITというSQLが実行されています。

このとき、LIMITはperメソッドで決めた表示件数のことを指し、 OFFSETは、表示件数 × (ページ番号 - 1)を指します。 例えばcontrollerにper(10)と記述している状態で、1ページ目にいるときは、 LIMITは10、OFFSETは0となります。

OFFSETで「0番目のレコードから」 LIMITで「10件目のレコードまで」 という風にテーブルからレコードを取得する、という意味になります。

同じく2番目のページに遷移した際には、 LIMITは10、OFFSETは10 × (2 - 1)なので10 という風になり、 10番目のレコードからスタートして、10件分取得していることになります。 つまりpageメソッドを使うことによって、ページ数に合わせたレコード取得が実現します。

Javascript(ES6)を少し真面目に勉強してみた

概要

最近こちら(https://jsprimer.net/)を使ってJavascript(ES6)を勉強する機会があったので、個人的に理解しづらかったとことをまとめてみます。

 

今回とりあげる項目は主に2つです。

1つ目はPromise

2つ目はreduceメソッド

 

です。

Promise

Promiseは非同期処理の際に、エラーだったときに返す値と成功時に返す値を持つことができるオブジェクトです。

また、成功時にだけ実行したい関数が複数あったときに、コールバックのネストを深くすることなく関数を実行できます。

例えば以下のような場合

function 関数1() {
  関数Aの呼び出し
  .then(() => 関数B())
  .then(() => 関数C())
  .catch((error) => {
    エラー時の処理
  });
}

function 関数A() {
  return new promise((resolve, reject) => {
    最初に行いたい処理
      if (エラーに対する条件) {
        reject(エラー時に返す結果);
      } else if(成功に対する条件) {
        resolve(成功時に返す結果);
      }
  });
}

function 関数B() {

}

function 関数C() {

}

Promiseは関数Aの中で使われています。

return new promiseにコールバックでresolve, rejectの2つの引数をもたせてやることで、 エラー時にはrejectで、成功時にはresolveで値を返すことができます。

上のような場合、関数1を実行することで、関数Aが呼ばれ、成功したときには関数B,関数Cと続けて呼びたいケースになります。 エラー時にはB,Cは呼び出しません。

promiseを持った関数が成功した後に続けたい処理はthenに続けて、失敗したあとに使いたい処理はcatchに続けて書くことができます。

reduceメソッド

reduceメソッドはかなり掴みづらかったです。

reduceメソッドは配列の隣合う値に任意の処理をするメソッド(コールバック関数の引数には処理の累積値が入る)なのですが、

冒頭で貼ったサイトでは、非同期通信で使うHTMLリテラルの入った配列とHTMLリテラルに入れるJavacriptの値をエスケープしたものが入ってる配列いう結構複雑な印象だったのでわかりにくかったんだと思います。

同じことを数字にして行うとだいぶわかりやすくなります。

var numbers = [1,2,3,4];
var result = numbers.reduce(function(累積, 配列の中身1つ1つ) {
 
  return a + b;
 
})
 

この場合、処理は3回行われます。 1回目は1 + 2 2回目は1回目の累積値である3 + 3 3回目は2回目の累積値である6 + 4

最終的に10が返ってくるわけです。

ソースコードが普通のソースコードのように書かれている記事が多かったので、あえて要所要所に日本語をいれてみました。 でも結局使うときは英語になるのでアレですけど、なんか日本語で書かれてた方が頭に入ってくる感じがしますね。

Sorceryを使ったユーザー登録にenumを追加

Sorceryについて

シンプルなログイン機能を作る際にSorceryというgemを使ったので、備忘録としてまとめてみます。  

手順がまとまった記事も結構あるんですが、一応パターンが違うのでどなたかの参考になればと思います。

ログイン認証といえばDeviseが王道っぽかったんですが、ちょっとよくわからずエラー連発でつらみがあり、Sorceryを使うことにしました。

今回は単純にUserテーブルを作成し、

  • ユーザーID(usernameカラムを追加)

  • パスワード

  • ユーザー区分(usertypeカラムを追加)  

でユーザー登録して、ログインの際はユーザIDとパスワードのみという、メールアドレスすら使わないものを考えていたのでSorceryが良いと判断しました。  

Sorceryの特徴として、「Less is more」というのがあります。つまりそういうことです。

使い方

さて、まずはGemfileにsorceryを追加します。

# Gemfile
gem "sorcery"

$ bundle installします。

$ rails g sorcery:installします。

このとき後ろにサブモジュールを追加することで、sorceryに用意されているサブモジュールを追加することができますが、 僕は必要なかったので何もしてません。

そして$rails db:migrate

これでUserモデルも作成されます。

デフォルトの中身はこんな感じ。

class SorceryCore < ActiveRecord::Migration[5.1]
  def change
    create_table :users do |t|
      t.string :email,            :null => false
      t.string :crypted_password
      t.string :salt

      t.timestamps                :null => false
    end

    add_index :users, :email, unique: true 
  end
end

emailをusernameとして扱う

今回認証に使うのはusernameとpasswordになるので、その設定を変える必要があります。

デフォルトだとemailとpasswordなので、よく考えたら最初の時点ではバリデーションが何もないので、emailとして扱われてる部分をパスワードに変えてしまえば良いのです。

というわけでemailカラムをusernameカラムに変更します。

$ rails g migration rename_email_column_to_users

生成されたマイグレーションファイルを変更。

class RenameEmailColumnToUsers < ActiveRecord::Migration[5.1]
  def change
    rename_column :users, :email, :username
  end
end

あとで使うのでenum用のusertypeカラムを追加

$ rails g migration add_column_users

生成されたマイグレーションファイルを変更。

class AddColumnUsers < ActiveRecord::Migration[5.1]
  def change
    add_column :users, :usertype, :integer, null: false, default: 0
  end
end

user.rbにenumを追加

class User < ApplicationRecord
  authenticates_with_sorcery!
  
  enum usertype: {staff:0, manager:1}
end

$ rails db:migrate

add_index部分も変えますがそこは割愛。

今度はsorceryの設定ファイルを変更。

sorcery.rbの

  config.user_config do |user|

    user.username_attribute_names = [:username] #この部分をemailからusernameに変更

※本当はコメントアウトされた行がたくさんありますが、必要箇所だけ抜粋してます。

Routing

僕の場合最初をログイン画面にしたかったのでこんな感じです。

Rails.application.routes.draw do
  root "sessions#new"

  resources :users, only: %i(new create show)
  resources :sessions, only: %i(new create destroy)
end

Controller

まずはuserコントローラー

$ rails g controller users new

users_controllerをこんな感じにします。

class UsersController < ApplicationController
  def new
    @user = User.new
  end

  def show
    @user = User.find(params[:id])
  end

  def create
    @user = User.new(user_params)
    if @user.save
      redirect_to user_path(@user)
    else
      render :new
    end
  end

  private

  def user_params
    params.require(:user).permit(:username, :password,  :usertype) 
  end
end

特別なことはしてないです。強いて言うなら他の方の例と違うのは渡ってくるパラメーターにpassword_comfirmationなどが入っていなかったりemailがなかったりするってところです。あとメッセージの表示がなかったり。初心者なのでとにかくミニマムな状態でやってました。

次はセッション管理のコントローラー

$ rails g controller sessions new

class SessionsController < ApplicationController
  def new
  end

  def create
    user = login(params[:username], params[:password])
    if user
      redirect_back_or_to user_path(user)
    else
      render :new
    end
  end

  def destroy
    logout
    redirect_to root_path
  end
end

View

hamlを使いました

# Gemfile

gem "hamlit"

$ bundle install

ユーザー登録画面、users/new.html.hamlを作成

%h2 Registering New User

= form_for @user do |f|

  .field
    = f.label :username
    = f.text_field :username

  .field
    = f.label :password
    = f.password_field :password
  .field
    = f.label :usertype
 
 # enumのメニューをプルダウンさせるための記述です。
    = f.select :usertype, User.usertypes.keys.to_a {}
  .actions
    = f.submit

%p
  or
  = link_to "login", new_session_path

次にユーザーのマイページにあたるuser/show.html.haml

%h2 User MyPage

= "username: #{@user.username}"
%br
= "usertype: #{@user.usertype}"
%br
= link_to "log out", session_path, method: :DELETE

次にログイン画面 sessions/new.html.haml

%h2 User Login


- if logged_in?
  = current_user.username

= form_tag sessions_path do
  .field
    = label_tag :username
    = text_field_tag :username, params[:username]
  .field
    = label_tag :password
    = password_field_tag :password
  .actions
    = submit_tag "Log in"

%p
  or
  = link_to "Signup", new_user_path

 完了!

これで最低限の機能は整いました。 あとはバリデーションを加えたり、enum含め表示を日本語にしたりする必要がありますがそれは割愛(もしくは次回)

バリデーションは今の時点では必要ないですが、password_comfirmを入力したりする場合は if: -> { new_record? || changes[:crypted_password] }とかを加える必要がありそうです。

てか、passwordはcrypted_passwordに変えなくても認証に使えるのはなぜでしょうか? 誰か教えてください。

当面の趣旨(?)

さて、僕がプログラミングを始めたのは5ヶ月前の話なんですが、

つい2ヶ月ほど前までTechAcademyというものを受講しておりました。

 

※そのことについてはこちらにつらつらと書いてます。よろしければ。

TechAcademyを受講してみたので自分なりの感想と初心者向けの学習手順を紹介してみる。 - Qiita

 

このTechAcademyは効率を重視するもので、そこまで深い知識には入り込んでいなくて、あくまで目標物があってそれを作るのに必要な知識(+α)を学ぶ形になります。

 

知識を身につけるというよりは、PDCAを体感するという感じのですね。

 

もちろん手段を目的にしないためにそれはとても重要なことではあるんですが、知識としてはまだまだ浅すぎるので、今後は独学で増やしていく必要があるわけで、今はまさにその最中です。

 

実際のWebアプリを作るにあたっては複雑なテーブル関係の実装などでつまずきやすいような気がしますが、あくまでブログ開設の目的はアウトプットにあるので、当面の間は知識をブラッシュアップしようとしてがっつりつまずいた点や疑問に感じた点などを書いていこうかなと思っています。

 

プログラミングに限った話ではないと思いますが、学習初心者が挫折しやすいのは

「わからない言葉や概念が出てきて、それを調べようとするとまたわからない言葉が出てきてQiitaやWikipediaなどをたらい回しされる」ことだと、僕は思っています

 

もしこの記事を迷える初心者(同士)の方が見てるなら一つだけお伝えしたいことがあります。

それは「断片的に学ぶのは効率が悪い」ということです。(当たり前?)

 

よく「プログラマーはプロでも調べながら仕事している」ということを聞きますが、我々初心者にとって「調べながらやる」というのは方法として間違っていると思うんです。

 

本当に本当に個人的な見解なので鵜呑みにしないでほしいのですが、プログラミングに大切なのは「理解」であって「暗記」ではないと思うのです。プロが調べながらやっている部分というのはいわゆる「暗記系」に相当する部分で、とはいえ暗記するには多すぎてあまりにも効率が悪い。というわけで「必要になったら該当知識をその都度調べる」というスタイルをとっているだけなのです。(多分そうです。笑)

 

その「調べながらやる」という行為をするにあたって必要になるのが「理解」の部分なんですよ。

 

「理解」が曖昧なままだと、調べること調べること全てはっきりわからなくて、冒頭で記述した「たらい回し」に陥って結局わからないまま終わることになるわけです。

 

というわけで、わからないことが出てきたらググって済ませるなど断片的に解決しようとしないことをお勧めします。

 

わからないことの原因が上位概念(?)のものでそれ自体の理解がふわふわしてたり、単純にわからないことがいっぱいあってその結果いろんなことが理解できない状況になってたり、「理解できない」にもいろいろ種類があると思いますが、体系的に解説されている本とかあると良いかもしれませんね。

 

というわけで、当面の記事の趣旨としては「理解」の部分で僕がつまずいたこと

 

になりそうですね。

ご挨拶

初めまして。takaikunoffと申します。。

 

軽く自己紹介させていただくと

現在大学四年生で、プログラミングを始めたのは四年生の春です。

 

文系学部なので、文系+後発組という感じで、結構滑り込みでエンジニアになった人です。

 

まだ若いとはいえ、それなりに劣等感は感じつつありますが、その劣等感に突き動かされながら生きていこうと考えておりまして、

 

就職したのはとあるソーシャルベンチャー企業で、一応エンジニアという立場ではありますがバリバリITって感じではないんですよ。

 

でもソーシャルベンチャーで社会を変えてみたい、エンジニア以外のスキルも身に付けたいという気持ちもあり、3年前後を目処に会社をできる限り軌道に乗せて、責任を果たした上でその時魅力を感じた企業へと転職しようかなと考えてます。

 

そんなわけで、それなりに道のりが待っているわけですが、そんな歩みをここに綴っていきたい・・・・わけではなく、自分のアウトプットとして使っていきますので、よろしくお願いします。

 

というわけで、何卒よろしくお願いいたします。