Sonamu๋ Passport.js๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ํ๋ ์ธ์ฆ ์์คํ
์ ์ ๊ณตํฉ๋๋ค. ์ธ์
๊ธฐ๋ฐ ์ธ์ฆ์ ์ง์ํ๋ฉฐ, ์ฌ์ฉ์ ์ง๋ ฌํ/์ญ์ง๋ ฌํ๋ฅผ ์ปค์คํฐ๋ง์ด์งํ ์ ์์ต๋๋ค.
๊ธฐ๋ณธ ๊ตฌ์กฐ
import { defineConfig } from "sonamu";
export default defineConfig({
server: {
auth: true, // ๊ธฐ๋ณธ ์ธ์ฆ ํ์ฑํ
// ๋๋ ์ปค์คํฐ๋ง์ด์ง
auth: {
userSerializer: async (user, request) => {
// ์ธ์
์ ์ ์ฅํ ๋ฐ์ดํฐ
return { id: user.id, email: user.email };
},
userDeserializer: async (serialized, request) => {
// ์ธ์
์์ ์ฌ์ฉ์ ๋ณต์
return UserModel.findById(serialized.id);
},
},
},
// ...
});
auth ์ค์
๊ฐ๋จํ ํ์ฑํ
ํ์
: boolean | AuthOptions
export default defineConfig({
server: {
auth: true, // ๊ธฐ๋ณธ ์ค์ ์ผ๋ก ํ์ฑํ
},
});
๋นํ์ฑํ:
export default defineConfig({
server: {
auth: false, // ์ธ์ฆ ๋นํ์ฑํ
},
});
auth: true๋ก ์ค์ ํ๋ฉด Sonamu์ ๊ธฐ๋ณธ ์ง๋ ฌํ/์ญ์ง๋ ฌํ๋ฅผ ์ฌ์ฉํฉ๋๋ค.
์ปค์คํ
์ค์
type AuthOptions = {
userSerializer: SerializeFunction<unknown, unknown>;
userDeserializer: DeserializeFunction<unknown, unknown>;
};
userSerializer
์ฌ์ฉ์ ๊ฐ์ฒด๋ฅผ ์ธ์
์ ์ ์ฅ ๊ฐ๋ฅํ ํํ๋ก ๋ณํํฉ๋๋ค.
ํ์
: (user, request) => SerializedUser | Promise<SerializedUser>
export default defineConfig({
server: {
auth: {
userSerializer: async (user, request) => {
// ์ธ์
์ ์ต์ํ์ ์ ๋ณด๋ง ์ ์ฅ
return {
id: user.id,
email: user.email,
};
},
// ...
},
},
});
user: ๋ก๊ทธ์ธ ์ ์ ๋ฌ๋ ์ฌ์ฉ์ ๊ฐ์ฒด
request: Fastify request ๊ฐ์ฒด
๋ฐํ: ์ธ์
์ ์ ์ฅ๋ ์ง๋ ฌํ๋ ๋ฐ์ดํฐ
์ธ์
ํฌ๊ธฐ๋ฅผ ์ค์ด๊ธฐ ์ํด ํ์ ์ ๋ณด๋ง ์ ์ฅํ์ธ์. ๋ณดํต id๋ง์ผ๋ก ์ถฉ๋ถํฉ๋๋ค.
userDeserializer
์ธ์
์์ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๋ณต์ํฉ๋๋ค.
ํ์
: (serialized, request) => User | Promise<User>
export default defineConfig({
server: {
auth: {
userDeserializer: async (serialized, request) => {
// DB์์ ์ ์ฒด ์ฌ์ฉ์ ์ ๋ณด ์กฐํ
return UserModel.findById(serialized.id);
},
// ...
},
},
});
serialized: userSerializer๊ฐ ๋ฐํํ ๋ฐ์ดํฐ
request: Fastify request ๊ฐ์ฒด
๋ฐํ: ๋ณต์๋ ์ฌ์ฉ์ ๊ฐ์ฒด
userDeserializer๋ ๋งค ์์ฒญ๋ง๋ค ํธ์ถ๋๋ฏ๋ก ์ฑ๋ฅ์ ์ํฅ์ ์ค ์ ์์ต๋๋ค. ์บ์ฑ์ ๊ณ ๋ คํ์ธ์.
๊ธฐ๋ณธ ์์
ID๋ง ์ ์ฅ (๊ถ์ฅ)
import { defineConfig } from "sonamu";
export default defineConfig({
server: {
auth: {
userSerializer: async (user, _request) => {
return user.id; // ID๋ง ์ ์ฅ
},
userDeserializer: async (userId, _request) => {
return UserModel.findById(userId);
},
},
},
});
๊ธฐ๋ณธ ์ ๋ณด ์ ์ฅ
export default defineConfig({
server: {
auth: {
userSerializer: async (user, _request) => {
return {
id: user.id,
email: user.email,
role: user.role,
};
},
userDeserializer: async (serialized, _request) => {
return UserModel.findById(serialized.id);
},
},
},
});
์บ์ฑ ์ถ๊ฐ
import { cache } from "sonamu/cache";
export default defineConfig({
server: {
auth: {
userSerializer: async (user, _request) => {
return user.id;
},
userDeserializer: async (userId, _request) => {
// ์ฌ์ฉ์ ์ ๋ณด ์บ์ฑ (1๋ถ)
const cacheKey = `user:${userId}`;
let user = await cache.get(cacheKey);
if (!user) {
user = await UserModel.findById(userId);
await cache.set(cacheKey, user, { ttl: "1m" });
}
return user;
},
},
},
});
๋ก๊ทธ์ธ/๋ก๊ทธ์์
์ธ์ฆ ์ค์ ํ ๋ก๊ทธ์ธ/๋ก๊ทธ์์ API๋ฅผ ๊ตฌํํฉ๋๋ค.
๋ก๊ทธ์ธ
import { api } from "sonamu";
import { login } from "sonamu/auth";
import bcrypt from "bcrypt";
export class AuthModel {
@api({ httpMethod: "POST" })
static async login(email: string, password: string) {
// ์ฌ์ฉ์ ์กฐํ
const user = await UserModel.findByEmail(email);
if (!user) {
throw new BadRequestError("Invalid credentials");
}
// ๋น๋ฐ๋ฒํธ ํ์ธ
const valid = await bcrypt.compare(password, user.passwordHash);
if (!valid) {
throw new BadRequestError("Invalid credentials");
}
// ๋ก๊ทธ์ธ (์ธ์
์์ฑ)
await login(user);
return { success: true, user };
}
}
๋ก๊ทธ์์
import { api } from "sonamu";
import { logout } from "sonamu/auth";
export class AuthModel {
@api({ httpMethod: "POST" })
static async logout() {
await logout();
return { success: true };
}
}
ํ์ฌ ์ฌ์ฉ์
import { api, getUser } from "sonamu";
export class AuthModel {
@api()
static async me() {
const user = getUser();
if (!user) {
throw new UnauthorizedError("Not authenticated");
}
return user;
}
}
โ ์ธ์ฆ API ๊ตฌํ ๊ฐ์ด๋
์ค์ ์์
๊ธฐ๋ณธ ์ค์
import { defineConfig } from "sonamu";
export default defineConfig({
server: {
plugins: {
session: {
secret: process.env.SESSION_SECRET!,
salt: process.env.SESSION_SALT!,
cookie: {
domain: "localhost",
path: "/",
maxAge: 60 * 60 * 24 * 30, // 30์ผ
},
},
},
auth: {
userSerializer: async (user) => user.id,
userDeserializer: async (userId) => {
return UserModel.findById(userId);
},
},
},
});
Role ๊ธฐ๋ฐ ์ธ์ฆ
export default defineConfig({
server: {
auth: {
userSerializer: async (user) => ({
id: user.id,
role: user.role,
}),
userDeserializer: async (serialized) => {
const user = await UserModel.findById(serialized.id);
// Role ๋ณ๊ฒฝ ๊ฐ์ง
if (user.role !== serialized.role) {
// ์ธ์
๊ฐฑ์ ํ์
await updateSession(user);
}
return user;
},
},
},
});
JWT์ ์ธ์
๋ณํ
import jwt from "jsonwebtoken";
export default defineConfig({
server: {
auth: {
userSerializer: async (user) => {
// JWT ํ ํฐ ์์ฑ
const token = jwt.sign(
{ userId: user.id },
process.env.JWT_SECRET!,
{ expiresIn: "7d" }
);
return { id: user.id, token };
},
userDeserializer: async (serialized) => {
// JWT ๊ฒ์ฆ
try {
jwt.verify(serialized.token, process.env.JWT_SECRET!);
} catch {
throw new UnauthorizedError("Invalid token");
}
return UserModel.findById(serialized.id);
},
},
},
});
๊ถํ ์ฒดํฌ
export default defineConfig({
server: {
auth: {
userSerializer: async (user) => user.id,
userDeserializer: async (userId) => {
const user = await UserModel.findById(userId);
// ๋นํ์ฑํ๋ ์ฌ์ฉ์
if (!user.isActive) {
throw new UnauthorizedError("Account deactivated");
}
// ๋ง๋ฃ๋ ๊ณ์
if (user.expiresAt && user.expiresAt < new Date()) {
throw new UnauthorizedError("Account expired");
}
return user;
},
},
apiConfig: {
guardHandler: (guard, request, api) => {
const user = request.user;
if (guard === "admin" && user?.role !== "admin") {
throw new UnauthorizedError("Admin access required");
}
if (guard === "premium" && !user?.isPremium) {
throw new UnauthorizedError("Premium subscription required");
}
},
},
},
});
Session ์ค์
์ธ์ฆ์ ์ธ์
์ ๊ธฐ๋ฐ์ผ๋ก ๋์ํ๋ฏ๋ก session ํ๋ฌ๊ทธ์ธ์ด ํ์ํฉ๋๋ค.
export default defineConfig({
server: {
plugins: {
session: {
secret: process.env.SESSION_SECRET!,
salt: process.env.SESSION_SALT!,
cookie: {
domain: process.env.COOKIE_DOMAIN,
path: "/",
maxAge: 60 * 60 * 24 * 30, // 30์ผ
secure: process.env.NODE_ENV === "production",
httpOnly: true,
sameSite: "strict",
},
},
},
auth: true,
},
});
โ Session ์ค์
์ฃผ์์ฌํญ
1. ์ธ์
ํฌ๊ธฐ
// โ ๋์ ์: ๋๋ฌด ๋ง์ ๋ฐ์ดํฐ ์ ์ฅ
userSerializer: async (user) => ({
id: user.id,
email: user.email,
profile: user.profile, // ํฐ ๊ฐ์ฒด
preferences: user.preferences,
history: user.history,
})
// โ
์ข์ ์: ID๋ง ์ ์ฅ
userSerializer: async (user) => user.id
2. userDeserializer ์ฑ๋ฅ
// โ ๋์ ์: ๋งค๋ฒ DB ์กฐํ
userDeserializer: async (userId) => {
return UserModel.findById(userId); // ๋ชจ๋ ์์ฒญ๋ง๋ค!
}
// โ
์ข์ ์: ์บ์ฑ ์ถ๊ฐ
userDeserializer: async (userId) => {
const cached = await cache.get(`user:${userId}`);
if (cached) return cached;
const user = await UserModel.findById(userId);
await cache.set(`user:${userId}`, user, { ttl: "1m" });
return user;
}
3. ์ธ์
secret
// โ ๋์ ์: ํ๋์ฝ๋ฉ๋ secret
session: {
secret: "my-secret-key",
salt: "my-salt",
}
// โ
์ข์ ์: ํ๊ฒฝ ๋ณ์ ์ฌ์ฉ
session: {
secret: process.env.SESSION_SECRET!,
salt: process.env.SESSION_SALT!,
}
4. null ์ฒ๋ฆฌ
// โ
์ฌ์ฉ์๊ฐ ์์ ์ ์์
userDeserializer: async (userId) => {
const user = await UserModel.findById(userId);
if (!user) {
// ์ธ์
์ ์์ง๋ง ์ฌ์ฉ์ ์ญ์ ๋จ
throw new UnauthorizedError("User not found");
}
return user;
}
๋ค์ ๋จ๊ณ
์ธ์ฆ ์ค์ ์ ์๋ฃํ๋ค๋ฉด: