惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

N
Netflix TechBlog - Medium
Spread Privacy
Spread Privacy
Cloudbric
Cloudbric
V
Vulnerabilities – Threatpost
博客园 - 叶小钗
I
Intezer
S
Secure Thoughts
Jina AI
Jina AI
T
Tenable Blog
博客园 - 【当耐特】
WordPress大学
WordPress大学
W
WeLiveSecurity
宝玉的分享
宝玉的分享
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Google DeepMind News
Google DeepMind News
Schneier on Security
Schneier on Security
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
B
Blog RSS Feed
Martin Fowler
Martin Fowler
Hacker News - Newest:
Hacker News - Newest: "LLM"
S
Schneier on Security
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
IT之家
IT之家
小众软件
小众软件
P
Privacy & Cybersecurity Law Blog
V
Visual Studio Blog
S
Securelist
M
MIT News - Artificial intelligence
H
Help Net Security
Scott Helme
Scott Helme
N
News and Events Feed by Topic
SecWiki News
SecWiki News
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
博客园_首页
Microsoft Azure Blog
Microsoft Azure Blog
P
Proofpoint News Feed
博客园 - 司徒正美
Hugging Face - Blog
Hugging Face - Blog
The Cloudflare Blog
T
Tailwind CSS Blog
A
About on SuperTechFans
The Last Watchdog
The Last Watchdog
S
Security @ Cisco Blogs
大猫的无限游戏
大猫的无限游戏
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Apple Machine Learning Research
Apple Machine Learning Research
H
Heimdal Security Blog
T
The Blog of Author Tim Ferriss
Blog — PlanetScale
Blog — PlanetScale

Kevin Blog

产品随想 2 产品力与人才密度 产品随想 1 做营销还是做产品 Play PyTorch Stable Diffusion and ONNX, Ollama on Intel Core Ultra 5 225H Ubuntu 25.04 Play with ROCm, PyTorch, Ollama on Ubuntu 24.04 and 780m 写在新的旅程开始前 想念自然与青春 懒猫微服体验——自由协作的神器 没有光纤的日子怎么上网?自制 Home WI-FI! Swift on Server Tour 5 创建 Users Swift on Server Tour 4 构建 Post Controller Swift on Server Tour 3 构建 Post 的 API 将不懂的日语一拍扫尽,介绍捧读全新的「OCR 工作台」功能 Swift on Server Tour 2 连通你的数据库与服务器 Swift on Server Tour 1 你的第一个 Server App 以及它背后的故事 Swift on Server Tour 0: 为什么这可能是你的好选择 纪念左耳朵耗子 How to learn Japanese by reading Novels and News with the help of Oyomi. Write WebAssembly in Swift and use it in Swift App BenQ WiT ScreenBar Halo 体验报告 记录 2021 年考驾照的体验 捧读的 EPUB 日语轻小说阅读器来了 使用 Go Mobile 开发跨平台 Library 使用 Kotlin Native 开发跨平台 Library 小番茄 - 一个只有陪伴的自习室 保友金豪电脑椅异响维修记 青岛度假指南 2020 「捧读」更新,支持针对任意文本进行语法讲解 一次百元耳机与千元耳机的横评 高刷墨水屏的海信 A5 Pro 体验报告 「捧读」更新 Safari 扩展,轻松阅读日语网页 新 App 「捧读:日语语法学习与分析」的开发幕后思考 明基 WiT ScreenBar Plus 体验 Switch游戏推荐:「健身环大冒险」和「太鼓达人」 妥协是否是一种艺术? 「50 音起源」1.2 更新说明 作客 UX Coffee 分享了自己最近的独立开发者生活 青岛度假指南 2019 我有「快乐自由」 关于小电台这款产品 聊一聊 50 音起源的再设计 极端精英主义的崩塌 Xcode Error: CFBundleSupportedPlatforms or Mach-O LC_VERSION_MIN 自由职业一周年我收获了什么 「50音起源」从起源学习 50 音的 App iOS/Android 青岛疗养(度假)指南 关于独处的一些经验 跨省携猫🐱搬家记 关于 FilmMentor 这款小众产品的想法 选择离开北京,以及为什么可以
Swift on Server Tour 6 关联 User 和 Post
Kevin Zhow · 2023-10-30 · via Kevin Blog

在上一章中我们创建了 UserModel,并构建了 UserController,但尚未构建起 UserPost 之间的关系。在这一章中,我们将在 Vapor 中完成一对多的关系构建。

本章代码可以在 Github 中找到。

修改 Post 的数据结构

要修 Post 表结构,我们首先需要创建一个新的 Migration 文件。

创建 Sources/App/Migrations/3_AddUserIDToPost.swift 文件,添加如下代码:

import Fluent 

struct AddUserIDToPost: AsyncMigration { 
    func prepare(on database: Database) async throws { 
        try await database.schema(Post.schema)  
            .field("user_id", .uuid, .references(User.schema, "id")) 
            .update()
    }

    func revert(on database: Database) async throws { 
       try await database.schema(Post.schema)
          .deleteField("user_id")
          .update()
    }
}

这种方式将会给 Post 表添加一个可为空的 user_id 字段,虽然这种方式简单直接,但同时也意味着,我们容忍了 Post 可能不属于任何用户的情况。

prepare 方法中,我们使用 references 方法来创建了一个外键约束,这个外键将会引用 User 表中的 id 字段,确保了 user_id 在数据库层面的正确性,避免我们意外插入一个不存在的 user_id

revert 方法中,我们使用 deleteField 方法来删除 user_id 字段,这个方法会同时删除它的外键约束。

随后,我们应当修改 configure.swift 将这条 Migration 添加到其中

app.migrations.add([CreatePost(), CreateUser(), AddUserIDToPost()])

现在,运行 vapor run migrate 即可进行数据库的迁移。

修改 Post Model

完成了数据库 Schema 的修改后,我们需要进一步修改 Post Model

编辑 Sources/App/Models/Post.swift 文件如下:

final class Post: Model, Content {
    static let schema = "posts"

    @ID(key: .id)
    var id: UUID?

    @OptionalParent(key: "user_id")
    var user: User?

    @Field(key: "content")
    var content: String
    
    @Timestamp(key: "created_at", on: .create)
    var createdAt: Date?

    init() { }

    init(id: UUID? = nil, content: String) {
        self.id = id
        self.content = content
    }
}

因为我们在给 Post 的 Schema 添加 user_id 字段时允许其为空,因此在 Post Model 中,我们使用 @OptionalParent 来表明 user 是不一定存在的。

与此同时,@OptionalParent(key: "user_id") 表明了 user_id 和 User Model @ID 之间的对应关系,这与 Schema 中的 .field("user_id", .uuid, .references(User.schema, "id")) 形成对应关系。

修改 User Model

import Vapor
import Fluent

final class User: Model, Content {
    static let schema = "users"
    
    @ID(key: .id)
    var id: UUID?
    
    @Field(key: "username")
    var username: String
    
    @Field(key: "password_hash")
    var passwordHash: String

    @Children(for: \.$user)
    var posts: [Post]
    
    @Timestamp(key: "created_at", on: .create)
    var createdAt: Date?
    
    init() {}
    
    init(id: UUID? = nil, username: String, passwordHash: String) {
        self.id = id
        self.username = username
        self.passwordHash = passwordHash
    }
}

在 User Model 中,@Children(for: \.$user) 表明了 User 和 Post 之间的一对多关系,同时也说明了,这种关系是通过 Post Model 中的 user 属性来进行关联的。

现在,我们有两种方式可以创建 Post 了,一种是通过 Post Model 的 user 属性:

let user = User(username: "hellouser", passwordHash: try await app.password.async.hash("123456"))
try await user.save(on: app.db)
let post = Post(content: "Hello, World!")
post.$user.id = try user.requiredID()

另一种是通过 User Model 的 posts 属性

let user = User(username: "hellouser", passwordHash: try await app.password.async.hash("123456"))
try await user.save(on: app.db)
let post = Post(content: "Hello, World!")
user.$posts.create(post, on: app.db)

不管采用哪一种,我们都需要在保存 Post 前知道 user_id

使用 BasicAuthorization 进行用户验证

在 PostController 中,我们需要修改 create 方法,使其能够接收 user_id 参数,并将其与 Post 关联起来。

那么如何将 user_id 传入到 create 方法中呢?

直接让客户端提供 user_id 参数肯定是不可以的,因为这样的话,客户端只需要伪造 user_id 就可以假装成别的用户发布,这显然是不安全的。

我们暂且通过最简单的 BasicAuthorization 来解决这个问题,BasicAuthorization 需要客户端提供 username 和 password 两个字段,并将其进行 Base64 编码后放在 HTTP Header 中:

计算格式如下

base64(username:password)

以上面的用户信息 happyuser:123456 为例,计算后的 Header 结果如下:

Authorization: Basic aGFwcHl1c2VyOjEyMzQ1Ng==

服务器端验证用户名和密码后,才会允许客户端创建对应用户的 Post。

BasicAuthorization 作为一种业界通用实践,Vapor 已经提供了 Model Authenticatable 来帮助我们完成这个功能

修改 Sources/App/Models/User.swift 在底部增加如下代码:

extension User: ModelAuthenticatable {
    static let usernameKey = \User.$username
    static let passwordHashKey = \User.$passwordHash

    func verify(password: String) throws -> Bool {
        try Bcrypt.verify(password, created: self.passwordHash)
    }
}

usernameKeypasswordHashKey 分别表明了 User Model 中的哪两个属性用于存储用户名和密码。当客户端提供用户名和密码时,Vapor 会自动将其与 User Model 中的 usernameKeypasswordHashKey 进行对应,从而完成用户验证。

接下来我们需要确保 Post 在创建前,用户已经通过了验证,这可以通过在 PostController 的路由中,加入权限验证相关的 Middleware 来完成,Vapor 已经提供了 User.authenticator() 来帮助我们完成 BasicAuthorization 验证的功能。

修改 Sources/App/Controllers/PostController.swift 中 boot 代码如下

func boot(routes: RoutesBuilder) throws {
    routes.group("posts") { posts in
        posts.get(use: index)
        posts.grouped(User.authenticator()).post(use: create)
    }
}

posts 路由组中,当客户端请求 posts 路由组中的 post 方法前,User.authenticator() 会进行 BasicAuthorization 验证,如果没有通过验证,Vapor 会返回 401 错误。

修改 PostController

修改 Sources/App/Controllers/PostController.swift 中的 create 方法如下:

func create(req: Request) async throws -> Post {
    let user = try req.auth.require(User.self)

    let postData = try req.content.decode(Post.CreateDTO.self)
        
    let post = Post(content: postData.content)

    post.$user.id = try user.requireID()
    
    try await post.create(on: req.db)
    
    return post
}

create 方法中,我们首先通过 req.auth.require(User.self) 获取到了已经通过验证的用户,然后通过 req.content.decode(Post.CreateDTO.self) 获取到了客户端传入的 Post 数据,最后通过 post.$user.id = try user.requireID() 将 Post 和 User 关联起来。

完善 Post 的单元测试

修改 Tests/AppTests/PostTests.swift 如下:

@testable import App
import XCTVapor

final class PostTests: XCTestCase {
    var app: Application!

    override func setUp() async throws {
        app = Application(.testing)
        try configure(app)

        try await app.autoRevert()
        try await app.autoMigrate()
    }

    
    override func tearDown() async throws {
        app.shutdown()
    }
    

    func testCreatePost() async throws {
        let user = User(username: "hellouser", passwordHash: try await app.password.async.hash("123456"))
        try await user.save(on: app.db)
        let postDTO = Post.CreateDTO(content: "Post created from test")

        try app.test(.POST, "posts", beforeRequest: { req in
            try req.content.encode(postDTO)
            req.headers.basicAuthorization = BasicAuthorization(username: user.username, password: "123456")
        }, afterResponse: { res in
            XCTAssertEqual(res.status, .ok)
            
            let post = try res.content.decode(Post.self)
            
            XCTAssertEqual(postDTO.content, post.content)
            XCTAssertEqual(try user.requireID(), post.$user.id)
        })
    }
}

testCreatePost 中,我们首先创建了一个用户,然后创建了一个 Post DTO,接着通过 app.test 方法,模拟了客户端的请求,最后验证了 Post 的 User 来判断是否创建成功。

总结

在这一章中,我们学习了如何在 Vapor 中构建一对多的关系,同时也学习了如何使用 BasicAuthorization 来进行用户验证。但这种方式虽然简单,但也有很多缺点,在下一章中,我们会学习使用 BearerAuthentication 来对用户进行验证,并进一步修复 User Model 的安全隐患。

本章代码可以在 Github 中找到。