搭建茶屿后端
本文不是关于密码学的,仅仅是关于应用各种最佳应用安全API的指南
引入
作为一个无聊至极的准高中生,中考过后的暑假总感觉有点无聊;遂决定去黑客松欺负小朋友(bushi
简而言之我们的项目后端需要一个用户系统,但我没写过 :(
于是在奋战了一周之后算是学懂了各种诸如 JWT、OAuth2、Session 等认证授权的知识,也对如何设计一个用户系统有了一定的了解。
接下来就是动手实现了。
数据库
我们采用了PostgresSQL作为后端数据库,主要是因为它的性能优越、功能强大,并且有着良好的社区支持。同时,PostgresSQL 也提供了丰富的数据类型和扩展功能,能够满足我们项目的需求。HTTP Server-Side我们就用了Server-Side Swift(老苹果了(悲 并配上了Vapor框架与Fluent做ORM
首先就是用户系统最基本的部分了,一个用户表。
CREATE EXTENSION IF NOT EXISTS pscrypto;
CREATE TABLE users (
id UUID PRIMARY KEY NOT NULL DEFAULT gen_random_uuid(),
);
接下来就是一些用户一般会有的属性了
CREATE EXTENSION IF NOT EXISTS pscrypto;
CREATE TABLE users (
id UUID PRIMARY KEY NOT NULL DEFAULT gen_random_uuid(),
name TEXT NOT NULL, -- Username
email TEXT NOT NULL UNIQUE, -- Email
);
然后就是很重要的一点了,用户的验证
我们一般都会用一个密码 Password来验证用户的身份,最自然而然能想到的方法就是直接存在数据库了
CREATE EXTENSION IF NOT EXISTS pscrypto;
CREATE TABLE users (
id UUID PRIMARY KEY NOT NULL DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
password TEXT NOT NULL, -- here
);
但是现在有个问题:密码是明文存在数据库里的,如果泄漏了后果不堪设想,特别是管理员的密码;怎么办呢?
CREATE EXTENSION IF NOT EXISTS pscrypto;
CREATE TABLE users (
id UUID PRIMARY KEY NOT NULL DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
password_encrypted TEXT NOT NULL,
);
我们回看一下密码验证的过程:
- 用户输入密码
- 前端把需要比对的密码发回来
- 后端判断是否是本来的密码
我们会发现一件事情:密码只需要满足一个验证条件而已,但事实上这不一定需要本来的密码等于前端发来的密码!
那怎么利用这个优势呢?我们翻开密码学教材可以看到一个很重要的概念:哈希算法。
哈希算法(Hashing)是确保密码安全存储和验证的核心。它是一种单向函数,将输入(如密码)转换为固定长度的输出(哈希值)。最重要的是,哈希过程是不可逆的,这意味着无法从哈希值中恢复出原始密码。因此,我们可以通过哈希来确保即使数据库被泄露,攻击者也无法获取用户的明文密码。
- 用户输入密码: 用户在登录时输入密码。
- 前端发送密码哈希: 前端将密码哈希值发送给后端,而不是发送明文密码。前端可以先用客户端的哈希算法对密码进行处理,确保网络传输时不会暴露密码明文。
- 后端验证密码: 后端在接收到密码哈希后,将用户输入的哈希值与数据库中存储的哈希值进行比较。哈希值匹配,则表示密码正确,认证通过。
那我们密码这一关就过啦
-- 加点别的
CREATE TYPE status AS ENUM ('ok', 'banned', 'down');
CREATE TYPE user_role AS ENUM ('user', 'admin', 'moderator');
CREATE OR REPLACE FUNCTION gen_identifier(
prefix TEXT DEFAULT 'user_',
table_name TEXT DEFAULT 'users',
column_name TEXT DEFAULT 'username'
)
RETURNS TEXT AS $$
DECLARE
chars TEXT[] := ARRAY['0','1','2','3','4','5','6','7','8','9',
'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z',
'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z'];
base TEXT := '';
new_identifier TEXT;
identifier_exists BOOLEAN;
query TEXT;
i INTEGER;
BEGIN
LOOP
-- Reset base for each attempt
base := '';
-- Generate an 11-character random string
FOR i IN 0..10 LOOP
base := base || chars[1 + FLOOR(RANDOM() * array_length(chars, 1))::INTEGER];
END LOOP;
new_identifier := prefix || base;
-- Check for uniqueness using dynamic SQL
query := format('SELECT EXISTS(SELECT 1 FROM %I WHERE %I = %L)', table_name, column_name, new_identifier);
EXECUTE query INTO identifier_exists;
IF NOT identifier_exists THEN
RETURN new_identifier;
END IF;
END LOOP;
END;
$$ LANGUAGE plpgsql;
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email TEXT NOT NULL UNIQUE,
username TEXT NOT NULL UNIQUE DEFAULT gen_identifier('user_', 'users', 'username'),
pass_hask TEXT NOT NULL,
user_role user_role NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
status status NOT NULL DEFAULT 'ok',
avatar_url TEXT
);
实现
我们的引用层用的是Swift Vapor。首先我们先来大概写一个ORM吧:
import Vapor
import Fluent
final class User: Model, Content, @unchecked Sendable {
static let schema = "users"
// Fields
@ID(key: .id)
var id: UUID?
@Field(key: "email")
var email: String
@Field(key: "username")
var username: String
@Field(key: "pass_hask")
var passHash: String
@Enum(key: "user_role")
var userRole: UserRole
@Enum(key: "user_status")
var userStatus: UserStatus
@Field(key: "created_at")
var createdAt: Date
@Field(key: "avatar_url")
var avatarUrl: String?
@OptionalChild(for: \.$user)
var userData: UserData?
init() {}
init(
id: UUID? = nil,
email: String,
username: String,
passHash: String,
userRole: UserRole = .user,
userStatus: UserStatus = .ok,
createdAt: Date = Date(),
avatarUrl: String? = nil
) {
self.id = id
self.email = email
self.username = username
self.passHash = passHash
self.userRole = userRole
self.userStatus = userStatus
self.createdAt = createdAt
self.avatarUrl = avatarUrl
}
}
然后我们来implement比对机制吧,我们选择Bcrypt算法
import Vapor
class EncryptionManager {
static func hashPassword(_ pass: String) throws -> String {
return try Bcrypt.hash(pass)
}
static func verifyPassword(_ plaintext: String, hash: String) throws -> Bool {
return try Bcrypt.verify(plaintext, created: hash)
}
}