Swift 后臺開發(fā) -- 登錄授權(User & Model)

最近一周感覺特別忙,導致很多東西沒有來得及總結粹湃,今天總結下如何通過 Vapor 中的 User 和 Model 來實現(xiàn)一個登錄和授權功能恐仑。如果對 Vapor 環(huán)境搭建和添加 MySQL 數(shù)據(jù)庫不太清楚的,可以看我前面寫的文章再芋。

先上效果圖:

8215D380-4688-4C43-8122-F843E619D52B.png

首先們先在工程目錄 -> Sources -> App -> Models 目錄下創(chuàng)建 Users.swift:

9ADAFE18-C170-47BD-813E-00505B2CA029.png

在這里需要注意的是我創(chuàng)建文件后菊霜,在 Xcode 里面顯示的是 Users.swift ,而不是圖上的 Models/Users.swift济赎,在 Xcode 里面編譯不過,但是在命令行里編譯就過了记某,功能也正常司训,只有把整個工程清空,然后再 build 后液南,構建出來的 Xcode 工程才顯示 Models/Users.swift壳猜,在 Xcode 里面才能正常編譯通過,這個問題的原因不詳滑凉,可能是 Xcode 的原因统扳,也可能是其它,知道的朋友希望能告訴下我畅姊。

然后我們創(chuàng)建一個繼承 Vapor User 的 EBUser 類(如果類名為User時咒钟,實現(xiàn)User protocol 時會出現(xiàn)問題):

final class EBUser: User {
    var id: Node?
    var username: String
    var nickname: String
    var avatar: String
    var password: String
    var exists: Bool = false

    init(node: Node, in context: Context) throws {
        
    }
    func makeNode(context: Context) throws -> Node {
        
    }
    static func prepare(_ database: Database) throws {
        
    }       
    static func revert(_ database: Database) throws {
        
    }       
}

并實現(xiàn)必須實現(xiàn)的 protocol,其中:

var id: Node?

這個屬性是必須提供的若未,他充當?shù)氖菙?shù)據(jù)庫表中的主鍵朱嘴;

var exists: Bool = false

這個屬性說明的是數(shù)據(jù)表是不存在的,Vapor是建議提供的粗合,后續(xù)版本可能會強制必須提供萍嬉。

其中的 init(node: Node, in context: Context) 和 func makeNode(context: Context) throws -> Node 格式是固定的了,如下:

init(node: Node, in context: Context) throws {
    id = try node.extract("id")
    username = try node.extract("username")
    nickname = try node.extract("nickname")
    avatar = try node.extract("avatar")
    password = try node.extract("password")
}

func makeNode(context: Context) throws -> Node {
    return try Node(node: [
        "id": id,
        "username": username,
        "nickname": nickname,
        "avatar": avatar,
        "password": password
        ])
}

這里的參數(shù)名和數(shù)據(jù)表里面的字段是對應的隙疚。

這里重點說下 prepare 和 revert 兩個方法壤追,這兩個方法一個是創(chuàng)建表的,一個是刪除表的供屉,內(nèi)容格式如下:

static func prepare(_ database: Database) throws {
    try database.create("ebusers") { users in
        users.id()
        users.string("username")
        users.string("nickname")
        users.string("avatar")
        users.string("password")
    }
}

static func revert(_ database: Database) throws {
    try database.delete("ebusers")
}

這里需要注意的是行冰,就是表名要在類名上加個s溺蕉,如 ebusers,原因不詳资柔,可能是 bug焙贷, 要不使用時會報一個表名錯誤。prepare 在表還沒有創(chuàng)建過的時候贿堰,執(zhí)行工程就會自動執(zhí)行并創(chuàng)建對應的表辙芍,revert 方法則需要通過配置 arguments 執(zhí)行:

7CC0E09B-D8DF-42B2-AB99-7228849F834C.png

在這一部分內(nèi)容中,User是和 Model 基本相同的羹与,只是實現(xiàn)的協(xié)議不同罷了故硅,而 User 中與 Model 最大不的同點就是 authenticate:

public protocol Authenticator {
    static func authenticate(credentials: Credentials) throws -> User
    static func register(credentials: Credentials) throws -> User
}
public protocol User: Entity, Account, Authenticator { }

Authenticator 協(xié)議有兩個方法,一個是授權纵搁,一個是注冊吃衅,然后在 EBUser 中實現(xiàn)這兩個協(xié)議方法:

static func authenticate(credentials: Credentials) throws -> User {
    var user: EBUser?
    switch credentials {
     // 通過密碼和用戶名校驗
    case let credentials as UsernamePassword:
        let fetchedUser = try EBUser.query()
            .filter("username", credentials.username)
            .first()
        if let password = fetchedUser?.password,
            password != "",
            (try? BCrypt.verify(password: credentials.password, matchesHash: password)) == true {
            user = fetchedUser
        }
        
     // 這里可以添加其它方式校驗
        
    default:
        throw UnsupportedCredentialsError()
    }
    
    if let user = user {
        return user
    } else {
        throw IncorrectCredentialsError()
    }
}

static func register(credentials: Credentials) throws -> Auth.User {
    var user: EBUser
    switch credentials {
    case let credentials as UsernamePassword:
        user = EBUser(credentials: credentials)
    default:
        throw UnsupportedCredentialsError()
    }
    
    if try EBUser.query().filter("username", user.username).first() == nil {
        try user.save()
        return user
    } else {
        throw AccountTakenError()
    }
}

這里先不單獨講解這兩個方法使用,結合 main.swift 中的接口內(nèi)容來說腾誉,先看 main.swift 內(nèi)容:

import Vapor
import VaporMySQL
import HTTP
import Auth
import Turnstile
import TurnstileCrypto

let drop = Droplet()
let auth = AuthMiddleware(user: EBUser.self)
drop.addConfigurable(middleware: auth, name: "auth")
let mysql = try VaporMySQL.Provider(config: drop.config)
drop.addProvider(mysql)

drop.get { request in
    let user = try? request.user()
    var dashboardView = try Node(node: [
        "authenticated": user != nil,
        "baseURL": request.baseURL
        ])
    dashboardView["account"] = try user?.makeNode()
    return try drop.view.make("index", dashboardView)
}

drop.get("login") { request in
    return try drop.view.make("login")
}

drop.post("login") { request in
    guard let username = request.data["username"]?.string,
        let password = request.data["password"]?.string else {
            return try drop.view.make("login", ["flash": "Missing username or password"])
    }
    let credentials = UsernamePassword(username: username, password: password)
    do {
        try request.auth.login(credentials)
        return Response(redirect: "/")
    } catch let e {
        return try drop.view.make("login", ["flash": "Invalid username or password"])
    }
}

drop.get("register") { request in
    return try drop.view.make("register")
}

drop.post("register") { request in
    guard let username = request.data["username"]?.string,
        let password = request.data["password"]?.string else {
            return try drop.view.make("register", ["flash": "Missing username or password"])
    }
    let credentials = UsernamePassword(username: username, password: password)
    
    do {
        try _ = EBUser.register(credentials: credentials)
        try request.auth.login(credentials)
        return Response(redirect: "/")
    } catch let e as TurnstileError {
        return try drop.view.make("register", Node(node: ["flash": e.description]))
    }
}

drop.get("logout") { request in
    request.subject.logout()
    return Response(redirect: "/")
}

為了方便使用徘层,我們對 Request 做以下的擴展:

extension Request {
    
    var baseURL: String {
        return uri.scheme + "://" + uri.host + (uri.port == nil ? "" : ":\(uri.port!)")
    }
    
    var subject: Subject {
        return storage["subject"] as! Subject
    }
    
    func user() throws -> User {
        guard let user = try auth.user() as? EBUser else {
            throw Abort.custom(status: .badRequest, message: "Invalid user type.")
        }
        return user
    }
}

在使用 User 的 auth 功能時,我們需要把 AuthMiddleware 添加到 drop 里面:

let auth = AuthMiddleware(user: EBUser.self)
drop.preparations = [EBUser.self, Course.self]

這里添加 MySQL 的配置是時是直接從 config 里面讀壤啊:

let mysql = try VaporMySQL.Provider(config: drop.config)

使用這種方法時趣效,我們需要在工程目錄 -> Sources -> Config 目錄下添加一個 mysql.json 文件,文件內(nèi)容格式如下:

3924F5F0-8AA6-4F4C-9FDF-531B084217C1.png

這里也是和上篇中 MySQL 的配置的不同點猪贪。

我們看首頁的請求實現(xiàn)方法:

drop.get { request in
    let user = try? request.user()
    var dashboardView = try Node(node: [
        "authenticated": user != nil,
        "baseURL": request.baseURL
        ])
    dashboardView["account"] = try user?.makeNode()
    return try drop.view.make("index", dashboardView)
}

我們首先會獲取 request 里的 auth 的 user 對象(這里用到了上面提及的request的擴展方法user()和baseURL屬性)跷敬,如果在請求中沒有獲取的 atuh 的 user,證明用戶還沒有登錄热押,我們則把一個名為 authenticated 的屬性設置為 flase 傳到頁面西傀, 這里的頁面都使用了 leaf 語言來編寫,leaf 的基本語法可以看官網(wǎng)介紹桶癣。

在登錄功能中拥褂,這里一共寫了三個頁面, 首頁(index.leaf)鬼廓、登錄頁(login.leaf)肿仑、注冊頁(register.leaf):

378F0D2C-FC61-494C-B586-D3A1239C400F.png

index.leaf 內(nèi)容:

#extend("base")
#export("body") {
  #if(authenticated) {
<h3>Hi! #(account.username)!</h3>
  }
  ##else() {
    <h3>Hi! Sign up today!</h3>
    #raw() {
      ["name": "EBer"]
      Hello, \(name)!
    }
  }
}

login.leaf 內(nèi)容:

#extend("base")
#export("body") {
<h1>Login</h1>
<form action="/login" method="POST">
  <div class="form-group">
    <label for="username">Username</label>
    <input type="text" class="form-control" name="username" placeholder="Username">
  </div>
  <div class="form-group">
    <label for="password">Password</label>
    <input type="password" class="form-control" name="password" placeholder="Password">
  </div>
  <input type="hidden" name="_csrf" value="{{csrfToken}}">
  <button type="submit" class="btn btn-primary">Login</button>
</form>

<p>Not a user of Exam Bank? <a href="/register">Register Today!</a></p>
}

register.leaf 內(nèi)容:

#extend("base")
#export("body") {
<h1>Register</h1>
<form action="/register" method="POST">
  <div class="form-group">
    <label for="username">Username</label>
    <input type="text" class="form-control" name="username" placeholder="Username">
  </div>
  <div class="form-group">
    <label for="password">Password</label>
    <input type="password" class="form-control" name="password" placeholder="Password">
  </div>
  <input type="hidden" name="_csrf" value="{{csrfToken}}">
  <button type="submit" class="btn btn-primary">Register</button>
</form>
}

base.leaf 內(nèi)容:

<html>
  <head>
    <link rel="stylesheet"  integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
    <link href="/styles/app.css" rel="stylesheet">
    <script type="text/javascript" charset="utf-8" src="/styles/app.js"></script>
    <title>Exam Bank</title>
  </head>
  <body>
    <div class="container">
    <div class="header clearfix">
      <nav>
        <ul class="nav nav-pills pull-right">
          #if(authenticated) {
            <li role="presentation"><a href="/logout">Logout</a></li>
          }
          ##else() {
            <li role="presentation"><a href="/login">Login</a></li>
          }
        </ul>
      </nav>
      <h3 class="text-muted"><a href="/">Exam Bank</a></h3>
    </div>
    #if(flash) {
      <div class="alert alert-danger" role="alert">
        <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
        <span class="sr-only">Error:</span>
        #(flash)
      </div>
    }
    #import("body")
    </div>
  </body>
</html>

來看注冊的方法:

drop.post("register") { request in
    guard let username = request.data["username"]?.string,
        let password = request.data["password"]?.string else {
            return try drop.view.make("register", ["flash": "Missing username or password"])
    }
    let credentials = UsernamePassword(username: username, password: password)
    
    do {
        try _ = EBUser.register(credentials: credentials)
        try request.auth.login(credentials)
        return Response(redirect: "/")
    } catch let e as TurnstileError {
        return try drop.view.make("register", Node(node: ["flash": e.description]))
    }
}

post 請求中應該傳username 和 password 兩個參數(shù)過來魔策,再把這兩個參數(shù)轉(zhuǎn)為用戶名密碼證書:

let credentials = UsernamePassword(username: username, password: password)

然后調(diào)用 EBUser.register(credentials: credentials) 對該證書進行注冊律胀,然后我們執(zhí)行request.auth.login(credentials) 方法。

注意蛔外,在執(zhí)行 login(credentials) 方法時雷蹂,會觸發(fā) EBUser 里的 func authenticate(credentials: Credentials) throws -> User 方法來進行授權判斷伟端,判斷證書是否已經(jīng)注冊了,如果沒有注冊則返回相關的錯誤信息匪煌。

再看登錄請求的實現(xiàn):

drop.post("login") { request in
    guard let username = request.data["username"]?.string,
        let password = request.data["password"]?.string else {
            return try drop.view.make("login", ["flash": "Missing username or password"])
    }
    let credentials = UsernamePassword(username: username, password: password)
    do {
        try request.auth.login(credentials)
        return Response(redirect: "/")
    } catch let e {
        return try drop.view.make("login", ["flash": "Invalid username or password"])
    }
}

注冊請求中已經(jīng)包含了登錄责蝠,這里就不重復了党巾。需要理解的就是 request.auth.login(credentials) 方法會觸發(fā) authenticate。

這里霜医, 基本就說完了登錄的全功能了齿拂,我們簡單說下 Model 的功能,Model 和 User 很相似:

public protocol Model: Entity, JSONRepresentable, StringInitializable, ResponseRepresentable { }
public protocol User: Entity, Account, Authenticator { }

它們都實現(xiàn)了 Entity 協(xié)議肴敛, 在上面說授權相關的內(nèi)容前署海,說的內(nèi)容主要都是 Entity 協(xié)議上的東西.

這里主要說的是, Entity 協(xié)議上封裝一些基本的 sql 查詢方法医男,如:

let fetchedUser = try EBUser.query()
                .filter("username", credentials.username)
                .first()

這里就不詳細說這個了砸狞,詳細直接查看官網(wǎng)說明

本文就寫到這里镀梭,因為最近比較忙刀森,所以更新會不定期,歡迎關注~~~

最后附上 EBUser 的完整代碼:

import Foundation
import Vapor
import Auth
import HTTP
import Fluent
import Turnstile
import TurnstileCrypto

enum Error: Swift.Error {
    case userNotFound
    case registerNotSupported
    case unsupportedCredentials
}

final class EBUser: User {
    var id: Node?
    var username: String
    var nickname: String
    var avatar: String
    var password: String
    var exists: Bool = false
    
    init(username: String, nickname: String, avatar: String, password: String) {
        self.username = username
        self.nickname = nickname
        self.avatar = avatar
        self.password = BCrypt.hash(password: password)
    }
    
    init(credentials: UsernamePassword) {
        self.username = credentials.username
        self.password = BCrypt.hash(password: credentials.password)
        self.nickname = ""
        self.avatar = ""
    }

    init(node: Node, in context: Context) throws {
        id = try node.extract("id")
        username = try node.extract("username")
        nickname = try node.extract("nickname")
        avatar = try node.extract("avatar")
        password = try node.extract("password")
    }
    
    func makeNode(context: Context) throws -> Node {
        return try Node(node: [
            "id": id,
            "username": username,
            "nickname": nickname,
            "avatar": avatar,
            "password": password
            ])
    }
    
    static func prepare(_ database: Database) throws {
        try database.create("ebusers") { users in
            users.id()
            users.string("username")
            users.string("nickname")
            users.string("avatar")
            users.string("password")
        }
    }
    
    static func revert(_ database: Database) throws {
        try database.delete("ebusers")
    }
    
    static func authenticate(credentials: Credentials) throws -> User {
        var user: EBUser? 
        switch credentials {
        case let credentials as UsernamePassword:
            let fetchedUser = try EBUser.query()
                .filter("username", credentials.username)
                .first()
            if let password = fetchedUser?.password,
                password != "",
                (try? BCrypt.verify(password: credentials.password, matchesHash: password)) == true {
                user = fetchedUser
            }

        default:
            throw UnsupportedCredentialsError()
        }
        
        if let user = user {
            return user
        } else {
            throw IncorrectCredentialsError()
        }
    }

    static func register(credentials: Credentials) throws -> Auth.User {
        var user: EBUser
        
        switch credentials {
        case let credentials as UsernamePassword:
            user = EBUser(credentials: credentials)
        default:
            throw UnsupportedCredentialsError()
        }
        if try EBUser.query().filter("username", user.username).first() == nil {
            try user.save()
            return user
        } else {
            throw AccountTakenError()
        }
    }  
}
最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末报账,一起剝皮案震驚了整個濱河市研底,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌透罢,老刑警劉巖飘哨,帶你破解...
    沈念sama閱讀 218,546評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異琐凭,居然都是意外死亡,警方通過查閱死者的電腦和手機浊服,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,224評論 3 395
  • 文/潘曉璐 我一進店門统屈,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人牙躺,你說我怎么就攤上這事愁憔。” “怎么了孽拷?”我有些...
    開封第一講書人閱讀 164,911評論 0 354
  • 文/不壞的土叔 我叫張陵吨掌,是天一觀的道長。 經(jīng)常有香客問我脓恕,道長膜宋,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,737評論 1 294
  • 正文 為了忘掉前任炼幔,我火速辦了婚禮秋茫,結果婚禮上,老公的妹妹穿的比我還像新娘乃秀。我一直安慰自己肛著,他們只是感情好圆兵,可當我...
    茶點故事閱讀 67,753評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著枢贿,像睡著了一般殉农。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上局荚,一...
    開封第一講書人閱讀 51,598評論 1 305
  • 那天超凳,我揣著相機與錄音,去河邊找鬼危队。 笑死聪建,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的茫陆。 我是一名探鬼主播金麸,決...
    沈念sama閱讀 40,338評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼簿盅!你這毒婦竟也來了挥下?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,249評論 0 276
  • 序言:老撾萬榮一對情侶失蹤桨醋,失蹤者是張志新(化名)和其女友劉穎棚瘟,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體喜最,經(jīng)...
    沈念sama閱讀 45,696評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡偎蘸,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,888評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了瞬内。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片迷雪。...
    茶點故事閱讀 40,013評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖虫蝶,靈堂內(nèi)的尸體忽然破棺而出章咧,到底是詐尸還是另有隱情,我是刑警寧澤能真,帶...
    沈念sama閱讀 35,731評論 5 346
  • 正文 年R本政府宣布赁严,位于F島的核電站,受9級特大地震影響粉铐,放射性物質(zhì)發(fā)生泄漏疼约。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,348評論 3 330
  • 文/蒙蒙 一秦躯、第九天 我趴在偏房一處隱蔽的房頂上張望忆谓。 院中可真熱鬧,春花似錦踱承、人聲如沸倡缠。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,929評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽昙沦。三九已至琢唾,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間盾饮,已是汗流浹背采桃。 一陣腳步聲響...
    開封第一講書人閱讀 33,048評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留丘损,地道東北人普办。 一個月前我還...
    沈念sama閱讀 48,203評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像徘钥,于是被迫代替她去往敵國和親衔蹲。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,960評論 2 355

推薦閱讀更多精彩內(nèi)容