Kitura Credentials Local

一个 Kitura Credentials 插件,用于使用网页表单进行本地身份验证(例如,检查本地数据库中的凭据)。

此插件仅与 Kitura 2 和 Swift 4 兼容。

用法

用法类似于 Kitura-CredentialsHTTP,您需要在实例化插件时传递一个闭包来验证凭据。有两种方法可以做到这一点。

更简单的方法与 Kitura-CredentialsHTTP 的工作方式最接近。 你传递一个闭包,它接受用户名、密码和回调方法。 用户名和密码将从 URL 编码的 POST 主体中提取,其中包含名为“username”和“password”的值; 如果你希望这些字段在表单的 HTML 中具有不同的名称,请分别设置 usernamePostFieldpasswordPostField 参数来覆盖它们。

let local = CredentialsLocal() { username, password, callback in
    // Check to see if the username is "admin@example.com" and the password is
    // "swordfish". In real use you'd probably be doing something like hashing
    // the password and checking for the credentials in a database.
    if username == "admin@example.com", password == "swordfish" {
        // On success, pass a UserProfile object to the callback.
        let userProfile = UserProfile(id: username, displayName: username, provider: "Local")
        callback(userProfile)
    }
    else {
        // On failure, pass nil to the callback.
        callback(nil)
    }
}

// Override the names of the respective fields.
local.usernamePostField = "emailAddress"
local.passwordPostField = "passphrase"

如果你需要验证除了用户名和密码字段之外的更多字段,或者正在使用 POST 主体以另一种方式编码(例如“multipart/form-data”)的表单,则需要使用第二种方法。 在这种情况下,回调会传递整个 RouterRequest 对象。 这是一个示例,我们在表单上验证用户名和密码字段旁边的“captcha”字段。

let local = CredentialsLocal() { request, callback in
    guard let body = request.body?.asURLEncoded, let userId = body["username"], let pass = body["password"], let cap = body["captchaVal"] else {
        callback(nil)
        return
    }
    if userId != "admin", pass != "swordfish", cap != "123456" {
        callback(nil)
        return
    }
    let userProfile = UserProfile(id: userId, displayName: userId, provider: "Local")
    callback(userProfile)
}

实例化 CredentialsLocal 后,你应该将其作为插件添加到 Credentials 实例中,然后将后一个实例分配给处理应用程序登录操作的路由处理程序。 例如,如果你有一个发布到 /log-in 的表单

let simpleCredents = Credentials()
let simpleCallbackLocal = CredentialsLocal() { userId, password, callback in
    // …
}
simpleCredents.register(plugin: simpleCallbackLocal)
router.post("/log-in", middleware: simpleCredents)

访问限制

请注意,如果你的目标是阻止未经身份验证的用户访问某些页面,则需要自己编写代码来实现。 一个简单的例子是 编写一个 RouterMiddleware 实现,如下所示:

public class Restrictor: RouterMiddleware {
    public func handle(request: RouterRequest, response: RouterResponse, next: @escaping () -> Void) throws {
        guard let _ = request.userProfile else {
            try! response.send("Access denied.").status(.forbidden).end()
            return
        }
        next()
    }
}

然后将其作为中间件附加到应用程序中的 /admin 路径。

完整示例

这是一个示例 main.swift,其中包含更完整的实现示例,带有登录表单。 它还使用上面演示的“Restrictor”中间件。(如果你还不熟悉如何在 Kitura 中使用表单,我建议阅读 相关章节,在 Kitura Until Dawn,我的免费 Kitura 电子书中。)

import Kitura
import Credentials
import CredentialsLocal
import KituraSession

// Initialize a Router
let router = Router()

// Initialize Session and have it be active for all requests
let session = Session(secret: "I like turtles.")
router.all(middleware: session)

// Have the BodyParser middleware be active for all POST requests
router.post(middleware: BodyParser())

// Initialize Credentials and CredentialsLocal and configure them
let simpleCredents = Credentials()
let simpleCallbackLocal = CredentialsLocal() { userId, password, callback in
    // An example "database" of usernames and passwords
    let users = ["John" : "12345", "Mary" : "qwerasdf"]
    if let storedPassword = users[userId] {
        if (storedPassword == password) {
            // Both username and password were vaild
            callback(UserProfile(id: userId, displayName: userId, provider: "Local"))
            return
        }
    }
    // else if userId or password doesnt match
    callback(nil)
}
simpleCredents.register(plugin: simpleCallbackLocal)

// Add access restriction under the "admin" path.
router.all("/admin", middleware: Restrictor())

// Add a simple handler to the base "/admin" path.
router.all("/admin") { request, response, next in
    if let profile = request.userProfile  {
        response.send("\(profile.displayName) is logged in with \(profile.provider)")
    }
    else {
        // This shouldn't have happened because our middleware should have
        // stopped the request when no userProfile was created.
        response.send("This shouldn't have happened.").status(.unauthorized)
    }
    next()
}

// On POST requests to "/log-in", validate the user.
router.post("/log-in", middleware: simpleCredents)

// On GET requests to "/log-in", show a credentials form.
router.get("/log-in") { request, response, next in
    let page = """
<!DOCTYPE html>
<html><body>
    <form method="post" action="/log-in">
        Username: <input type="text" name="username" /><br />
        Password: <input type="password" name="password" /><br />
        <input type="submit" />
    </form>
</body></html>
"""
    response.send(page)
    next()
}

// If the user successfully logged in, redirect them to the "/admin" path.
router.post("/log-in") { request, response, next in
    if let _ = request.userProfile {
        try response.redirect("/admin").end()
    }
    else {
        response.send("Access denied.").status(.unauthorized)
    }
    next()
}

// Kick off Kitura
Kitura.addHTTPServer(onPort: 8080, with: router)
Kitura.run()

升级

此 Credentials 插件的 1.0 版本是一个重定向插件,基本上意味着它会尝试将未经授权的用户重定向到登录表单页面。 我认为这不是一个好主意,因为通常只是向用户显示“访问被拒绝”消息而不是重定向他们更为可取,所以我将插件重构为非重定向插件。 在现实世界中,这意味着你需要意识到升级模块后不会再发生这种重定向,并且你需要实现自己的代码来执行任何类型的访问限制和/或重定向。

故障排除

到目前为止,这段代码只经过了轻微的测试。 由于安全是网络上的主要问题,我强烈建议你在生产项目中使用之前进行广泛的测试。

如果你有任何问题,请通过 GitHub 联系我,在 IBM-Swift Slack 服务器上使用“nocturnal”,或在 Freenode 上使用“_Nocturnal”。 其他联系方式在 我的网站上概述。