Edited at

railsとjwtでswiftアプリケーションのログイン機能を実装する

More than 1 year has passed since last update.

今更感がありますがswift案件に今年かかわり認証周りにもっといい方法は無いかと探していたときにjwtが良さそうだったのでrailsとswiftで実装しました。

当初はknockも使う予定でしたがsorceryとメソッド名が重複していたかだったかで上手く同居させることができなかったためあきらめました


動作環境

macOS

docker for mac

swift4

Xcode9.2


サンプルコード

アプリからログイン後にユーザのプロフィールを表示するサンプル

githubにコードおいてあります。


起動


rails


docker-compose up

$ cd auth-sample/auth-sample-server

$ docker-compose up


migration

$ docker exec authsampleserver_web_1 ./bin/rake db:migrate


swift


carthage

carthage updateしてプロジェクトをビルドしてrealmなどのライブラリが見つからないと言われたらcarthageでビルドされた各種フレームワークを追加してください。

$ cd auth-sample/auth-sample-client

$ carthage update --platform iOS


コード

railsのAPIモードは使ってません。

アプリのサービスだったとしてもほぼ管理画面が必要になったりするので、プロジェクトを分けたりするとデプロイに気を使ったり、モデル周りのコードが分散してしまったりで走り初めのサービスは同じリポジトリにコードがまとまっていたほうがメリットのほうが大きいためです。

実際にプロジェクトに導入するときはgrapeなどのgemを使って実装し直してもいいかもしれないです。

loginボタンを押下するとIBActionで設定されたloginメソッドが実行されます


LoginViewController.swift

class LoginViewController: UIViewController {

@IBOutlet weak var emailField: UITextField!
@IBOutlet weak var passwordField: UITextField!

let loginURL: String = "http://localhost:3000/api/user_sessions"
lazy var indicator: UIActivityIndicatorView = {
let indicator = UIActivityIndicatorView()
indicator.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
indicator.center = self.view.center
indicator.hidesWhenStopped = true
indicator.activityIndicatorViewStyle = .gray
return indicator
}()
lazy var alertViewController: UIAlertController = {
let alert: UIAlertController = UIAlertController(title: "エラー", message: "ログインに失敗しました", preferredStyle: .alert)
let cancelAction: UIAlertAction = UIAlertAction(title: "OK", style: .cancel, handler: nil)
alert.addAction(cancelAction)
return alert
}()

@IBAction func login(_ sender: Any) {
let param = ["email": self.emailField.text ?? "", "password": self.passwordField.text ?? ""]
self.indicator.startAnimating()
Alamofire.request(self.loginURL, method: .post, parameters: param, encoding: URLEncoding.default).responseJSON { [weak self] response in
self?.indicator.stopAnimating()
guard response.response?.statusCode == 200 else {
if let alertViewController = self?.alertViewController {
self?.present(alertViewController, animated: true, completion: nil)
}
return
}

if let data = response.data {
let decoder: JSONDecoder = JSONDecoder()
if let user = try? decoder.decode(UserCodable.self, from: data) {
User.create(codable: user)
let sb = UIStoryboard(name: "Profile", bundle: nil)
if let vc = sb.instantiateInitialViewController(){
vc.modalTransitionStyle = UIModalTransitionStyle.crossDissolve
self?.present(vc, animated: true, completion: nil)
}
}
}
}
}
}


アプリからリクエストを受けたrailsのコードです。

user_idからjwtトークンを生成して返却します。

アプリはこのトークンを他のリクエスト時にHTTP headerのAuthorizationに追加してリクエストを行います。

jsonにuserオブジェクトを雑にぶん投げてますが本番ではやってはいけません、passwordなどのフィールドが含まれてるので絶対にフィルタリングしてください。


api/user_sessions_controller

class Api::UserSessionsController < Api::ApplicationController

def create
user = User.authenticate(params[:email], params[:password])

if user
token = Jwt::TokenProvider.(user_id: user.id)
render json: {user: user, token: token}
else
render json: {error: 'Error description'}, status: 422
end
end
end


ユーザのプロフィールを取得するAPIです。


user_controller.rb

class Api::UsersController < Api::ApplicationController

before_action :authenticate

def show
render json: current_user
end
end



api/application_controller.rb

class Api::ApplicationController < ActionController::Base

def authenticate
render json: {errors: 'Unauthorized'}, status: 401 unless current_user
end

def current_user
@current_user ||= Jwt::UserAuthenticator.(request.headers)
end
end



service/jwt/user_authenticator.rb

module Jwt::UserAuthenticator

extend self

def call(request_headers)
@request_headers = request_headers

begin
payload, header = Jwt::TokenDecryptor.(token)
return User.find(payload['user_id'])
rescue => e
# log error here
return nil
end
end

def token
@request_headers['Authorization'].split(' ').last
end
end



service/jwt/token_decryptor.rb

module Jwt::TokenDecryptor

extend self

def call(token)
decrypt(token)
end

private
def decrypt(token)
begin
JWT.decode(token, Rails.application.secrets.secret_key_base)
rescue
raise InvalidTokenError
end
end
end

class InvalidTokenError < StandardError; end;



注意点

JWTトークンが第三者に漏れてしまった時にそのユーザのトークンを無効にしないと不正アクセスし放題になってしまいますが、現在の実装方法だとトークンの無効化ができません。

理由はJWTトークンの発行にuser_idを使用しているためです。user_idは作成されたら変更しないものなのでこれをユーザモデルに紐付いたキーに変更しかつ有効期限をもたせるようなモデル設計に変える必要がありそうです。

この辺はユーザ作成のサンプルも作るのでその時に一緒に変更します。