今更感がありますがswift案件に今年かかわり認証周りにもっといい方法は無いかと探していたときにjwtが良さそうだったのでrailsとswiftで実装しました。
当初はknockも使う予定でしたがsorceryとメソッド名が重複していたかだったかで上手く同居させることができなかったためあきらめました
動作環境
macOS
docker for mac
swift4
Xcode9.2
サンプルコード
アプリからログイン後にユーザのプロフィールを表示するサンプル
[github](https://github.com/kkyouhei/auth-sample 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メソッドが実行されます
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などのフィールドが含まれてるので絶対にフィルタリングしてください。
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です。
class Api::UsersController < Api::ApplicationController
before_action :authenticate
def show
render json: current_user
end
end
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
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
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は作成されたら変更しないものなのでこれをユーザモデルに紐付いたキーに変更しかつ有効期限をもたせるようなモデル設計に変える必要がありそうです。
この辺はユーザ作成のサンプルも作るのでその時に一緒に変更します。