3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

ニフティグループAdvent Calendar 2018

Day 18

Vaporでセッションを利用した認証をつくる

Last updated at Posted at 2018-12-18

NIFTY Advent Calendar 2018 18日目の記事です。
今日は、「Vaporでセッションを利用した認証をつくる」です。
よろしくお願いします。

やること

環境

  • macOS HighSierra (10.13.6)
  • Swift 4.1.2
  • Vapor 3.1.7

はじめに

元記事の作者が作り終わったソースはここにあります。

1.新しいVaporプロジェクトをつくる

vaporにはgithubにあるテンプレートを利用できる機能があります。
以下のコマンドを叩くとテンプレートをCloneしてプエジェクトを作成してくれます。

vapor new vaporAuth --template=vaporberlin/my-first-controller

2.Xcodeプロジェクトをつくる

作ったプロジェクトの中を見るとPackage.swiftがあります。
Xcodeプロジェクトを生成する前に、Package.swift内のパッケージ名を変更する必要があります。

Package.swift
import PackageDescription
let package = Package(
    name: "vaporAuth",  // プロジェクト名
    dependencies: [
        .package(url: "https://github.com/vapor/vapor.git", from: "3.0.0-rc"),
        .package(url: "https://github.com/vapor/leaf.git", from: "3.0.0-rc"),
        .package(url: "https://github.com/vapor/fluent-sqlite.git", from: "3.0.0-rc"),
        .package(url: "https://github.com/vapor/auth.git", from: "2.0.0-rc"),  // added
        .package(url: "https://github.com/vapor/crypto.git", from: "3.0.0")  // added
    ],
    targets: [
        .target(name: "App", dependencies: ["Vapor", "Leaf", "FluentSQLite", "Authentication", "Crypto"]),  // added
        .target(name: "Run", dependencies: ["App"]),
        .testTarget(name: "AppTests", dependencies: ["App"]),
    ]
)

パッケージをアップデートします。時間がかかるので気長に待ちましょう。

vapor update -y

完了すると、自動でXcodeでプロジェクトがたちあがります。

すでにテンプレートが適応されているためUser.swiftをはじめとした基本的なファイルが生成済の状態になっているはずです。

projectName/
├── Package.swift
├── Sources/
│   ├── App/
│   │   ├── Controllers/
│   │   │   └── UserController.swift
│   │   ├── Models/
│   │   │   └── User.swift
│   │   ├── app.swift
│   │   ├── boot.swift
│   │   ├── configure.swift
│   │   └── routes.swift
│   └── Run/
│       └── main.swift
├── Tests/
├── Resources/
│   └── Views/
│       └── userview.leaf
├── Public/
├── Dependencies/
└── Products/

一度VaperをローカルでRunしましょう。:runner_tone1:
Schemeの選択でRun>MyMacを選択し実行。
image.png
localhost:8080でUserlistを入力できる画面が確認できると思います!
image.png

3.Userモデルをつくる

すでにあるUser.swiftを変更していきます。

User.swift
import FluentSQLite
import Vapor
import Authentication  // added
final class User: SQLiteModel {
  var id: Int?
  var email: String  // added
  var password: String  // added
  init(id: Int? = nil, email: String, password: String) {
    self.id = id
    self.email = email
    self.password = password
  }
}
extension User: Content {}
extension User: Migration {}
// 以下を追加
extension User: PasswordAuthenticatable {
  static var usernameKey: WritableKeyPath<User, String> {
    return \User.email
  }
  static var passwordKey: WritableKeyPath<User, String> {
    return \User.password
  }
}
extension User: SessionAuthenticatable {}

4.登録用のビューをつくる

userview.leaf
<!DOCTYPE html>
<html>
  <head>
    <title>Web Auth</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
  </head>
  <body class="container">
    <br />
    <div class="row justify-content-center">
      <div class="col-md-6">
        <div class="card">
          <div class="card-body">
            <h3 class="card-title">Register</h3>
            <form action="/register" method="POST">
              <div class="form-group">
                <label for="email">Email</label>
                <input type="email" name="email" class="form-control" id="email" />
              </div>
              <div class="form-group">
                <label for="password">Password</label>
                <input type="password" name="password" class="form-control" id="password" />
              </div>
              <div class="form-group">
                <input type="submit" class="btn btn-block btn-primary" value="register" />
              </div>
            </form>
          </div>
        </div>
      </div>
    </div>
  </body>
</html>

5.UserControllerを変更する(登録用routeの追加)

登録画面の遷移をつくっていきましょう。

UserController.swift
import Vapor
final class UserController {
  func renderRegister(_ req: Request) throws -> Future<View> {
    return try req.view().render("register")
  }
}
routes.swift
import Vapor
public func routes(_ router: Router) throws {
  let userController = UserController()
  router.get("register", use: userController.renderRegister)
}

Runしてみましょう。以下のような画面ができているはずです!:relaxed:
image.png

補足:Futureって何だ?

UserControllerにFuture<View>といったような記述がでてきました。
VaporはVer3.0からSwiftNIOとよばれる非同期イベント駆動のフレームワークを利用するようになっています。
これによりControllerが返却する型やモデルから取得するデータはFuture型になっています。
Futureは非同期で処理を扱うための約束事のひとつです。重い処理などで将来取得するはずであろうオブジェクトの参照を可能にしてくれます。
Future<View>は「Viewオブジェクトを返却するであろう将来を約束した型」といった表現になります。
くわしくはVaporの公式ドキュメントを見てください。

次に登録の仕組みをつくっていきます。

UserController.swift
import Vapor
import FluentSQL  // added
import Crypto  // added
final class UserController {
  func renderRegister(_ req: Request) throws -> Future<View> {
    ...
  }
  // 以下を追加
  func register(_ req: Request) throws -> Future<Response> {
    return try req.content.decode(User.self).flatMap { user in
      return try User.query(on: req).filter(\User.email == user.email).first().flatMap { result in
        if let _ = result {
          return Future.map(on: req) { _ in
            return req.redirect(to: "/register")
          }
        }
        user.password = try BCryptDigest().hash(user.password)
        return user.save(on: req).map { _ in
          return req.redirect(to: "/login")
        }
      }
    }
  }
}
routes.swift
import Vapor
public func routes(_ router: Router) throws {
  let userController = UserController()
  router.get("register", use: userController.renderRegister)
  router.post("register", use: userController.register) // add
}

Runして/registerにアクセスし、メアド・パスワード入力をしてみてください。
ユーザが新規追加に成功すれば/loginにリダイレクトし、すでに登録済のユーザであれば/loginにリダイレクトするはずです!:relaxed:

補足:map、flatMapって何だ?

基本的な仕組み

Swiftの配列が持つmapやflatMapと名前が同じですが、今回のものはVapor特有のFutureが持つメソッドになっています。
mapはFutureを別のFutureに変換してくれるFutureが持つメソッドです。
flatMapはmapに似ていて、Futureを別のFutureに変換してくれるFutureが持つメソッドで、Futureが入れ子になっていても入れ子にせずに返却してくれるものです。
たとえばFuture>となってしまうものをFutureで返却してくれるものです。

コードの説明

req.content.decode(User.self)でFutureが取得されるのですが、それを非同期に処理するためには常にmapやflatMapのクロージャで閉じて処理をしてあげる必要があります。これがVaporでは随所に出てきます。
Future、mapを駆使することにより将来取得するであろう処理を非同期におこなうことができます。
最終的にreq.redirect(to: "/register")などでFutureに変換してreturnしています。

6.ログイン用のビューを作る

login.leaf
<!DOCTYPE html>
<html>
  <head>
    <title>Web Auth</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
  </head>
  <body class="container">
    <br />
    <div class="row justify-content-center">
      <div class="col-md-6">
        
        <div class="card">
          <div class="card-body">
            <h3 class="panel-title">Login</h3>
            
            <form action="/login" method="POST">
              <div class="form-group">
                <label for="email">Email</label>
                <input type="email" name="email" class="form-control" id="email" />
              </div>
              <div class="form-group">
                <label for="password">Password</label>
                <input type="password" name="password" class="form-control" id="password" />
              </div>
              <div class="form-group">
                <input type="submit" class="btn btn-block btn-success" value="login" />
              </div>
            </form>
          </div>
        </div>
      </div>
    </div>
  </body>
</html>

7.UserControllerを変更する(ログイン遷移の追加)

ログインの遷移をつくっていきます。

UserController.swift
import Vapor
import FluentSQL
import Crypto
final class UserController {
  func renderRegister(_ req: Request) throws -> Future<View> {
    ...
  }
  func register(_ req: Request) throws -> Future<Response> {
    ...
  }
  //以下を追加
  func renderLogin(_ req: Request) throws -> Future<View> {
    return try req.view().render("login")
  }
}
routes.swift
import Vapor
import Authentication  // added
public func routes(_ router: Router) throws {

  let userController = UserController()
  router.get("register", use: userController.renderRegister)
  router.post("register", use: userController.register)
  router.get("login", use: userController.renderLogin) // add

  let authSessionRouter = router.grouped(User.authSessionsMiddleware()) // added
  authSessionRouter.post("login", use: userController.login) // added
}
configure.swift
import Vapor
import Leaf
import FluentSQLite
import Authentication  // added
public func configure(
  _ config: inout Config,
  _ env: inout Environment,
  _ services: inout Services
) throws {
  ...
  try services.register(AuthenticationProvider()) // added
  var middlewares = MiddlewareConfig.default() // added
  middlewares.use(SessionsMiddleware.self) // added
  services.register(middlewares) // added
  config.prefer(MemoryKeyedCache.self, for: KeyedCache.self) // added
}
UserController.swift
final class UserController {

  ...

  // 以下を追加
  func login(_ req: Request) throws -> Future<Response> {
    return try req.content.decode(User.self).flatMap { user in
      return User.authenticate(
        username: user.email,
        password: user.password,
        using: BCryptDigest(),
        on: req
      ).map { user in
        guard let user = user else {
          return req.redirect(to: "/login")
        }
        try req.authenticateSession(user)
        return req.redirect(to: "/profile")
      }
    }
  }
}

ここでセッションにユーザがいなければ/loginにリダイレクト。いれば/profileにリダイレクトとしています。
次に/profileをつくっていきましょう。

8.プロフィール用のビューをつくる

profile.view
<!DOCTYPE html>
<html>
  <head>
    <title>Web Auth</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
  </head>
  <body class="container">
    <br />
    <div class="row justify-content-center">
      <div class="col-md-6">
        <div class="card">
          <div class="card-body">
            <h3 class="card-title">Profile</h3>
            <p>Email: #(user.email)</p>
          </div>
        </div>
      </div>
    </div>
  </body>
</html>

9.UserControllerを変更する(プロフィール用routeの追加)

UserController.swift
public func routes(_ router: Router) throws {
  let userController = UserController()
  router.get("register", use: userController.renderRegister)
  router.post("register", use: userController.register)
  router.get("login", use: userController.renderLogin)
  let authSessionRouter = router.grouped(User.authSessionsMiddleware())
  authSessionRouter.post("login", use: userController.login)
  let protectedRouter = authSessionRouter.grouped(RedirectMiddleware<User>(path: "/login"))
  protectedRouter.get("profile", use: userController.renderProfile)
}
UserController.swift
final class UserController {
  
  ...

  // 以下を追加
  func renderProfile(_ req: Request) throws -> Future<View> {
    let user = try req.requireAuthenticated(User.self)
    return try req.view().render("profile", ["user": user])
  }
}

10.UserControllerを変更する(ログアウト用routeの追加)

UserController.swift
final class UserController {

  ...

  // 以下を追加
  func logout(_ req: Request) throws -> Future<Response> {
    try req.unauthenticateSession(User.self)
    return Future.map(on: req) { return req.redirect(to: "/login") }
  }
}
routes.swift
public func routes(_ router: Router) throws {
  
  ...

  router.get("logout", use: userController.logout) // added
}

最後にログアウトボタンをつけます。

profile.leaf
<!DOCTYPE html>
<html>
  <head>
    <title>Web Auth</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
  </head>
  <body class="container">
    <br />
    <div class="row justify-content-center">
      <div class="col-md-6">
        <div class="card">
          <div class="card-body">
            <h3 class="card-title">Profile</h3>
            <p>Email: #(user.email)</p>
            <a href="/logout" class="btn btn-block btn-danger">
              logout
            </a>
          </div>
        </div>
      </div>
    </div>
  </body>
</html>

Runしてみてください!:runner_tone1:
ユーザ登録→ログイン→プロフィール確認→ログアウトがセッション管理で実現できていますよ!:slight_smile:
これでVaporでのセッション認証はバッチリです!!:muscle:

さいごに

実際、伝えたかったことは補足のFutureとmap、flatMapの部分でした:wink:
正直、今回の仕組みをつくって感じたことは「複雑だな」という印象です。
非同期の仕組みをFutureやmapで実現することは効率的な処理をする上で必要なのだとは思いますが、他のメジャーなWEBフレームワークと比較してしまうとどうしても面倒な記述が必要になりSwiftの簡潔な文法が生かされていないような気になります。
ただし今後async,awiitの仕組みがSwift5で提供されるため、Vaporがこの仕組みを導入してくれればかなりこの部分の複雑さは少なくなるのではないかと思います。そうなれば楽しいServerSideSwiftが実現するかもですね!:relaxed:

これを機にVaporやServerSideSwiftに興味を持っていただけると幸いです!!
長文にお付き合いいただきありがとうございました!!

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?