Search
๐Ÿ“š

EP15. Supabase Auth Middleware

์ƒ์„ฑ์ผ
2024/06/06 07:44
๋งˆ์ง€๋ง‰ ์—…๋ฐ์ดํŠธ
2024/06/09

EP15. Supabase Auth Middleware

์ธ์ฆ ๋ฏธ๋“ค์›จ์–ด๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์ „์— ์‚ฌ์šฉ์ž๋‚˜ ํด๋ผ์ด์–ธํŠธ์˜ ์‹ ์›์„ ํ™•์ธํ•˜๋Š” ์—ญํ• ์„ ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ๋ณดํ˜ธ๋œ ๋ฆฌ์†Œ์Šค์— ์ ‘๊ทผํ•˜๊ธฐ ์œ„ํ•œ ๊ถŒํ•œ์ด ์žˆ๋Š”์ง€๋ฅผ ๊ฒ€์ฆํ•˜๊ณ , ์ธ์ฆ๋˜์ง€ ์•Š์€ ์š”์ฒญ์„ ์ฐจ๋‹จํ•ฉ๋‹ˆ๋‹ค. ์ผ๋ฐ˜์ ์œผ๋กœ ํ† ํฐ์ด๋‚˜ ์„ธ์…˜์„ ์‚ฌ์šฉํ•˜์—ฌ ์ธ์ฆ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.

1. ์ธ์ฆ ๋ฏธ๋“ค์›จ์–ด์˜ ํ•„์š”์„ฑ

AOP(Aspect-Oriented Programming)๋Š” ํ”„๋กœ๊ทธ๋žจ์˜ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๊ณผ ๊ณตํ†ต์ ์ธ ๊ด€์‹ฌ์‚ฌ๋ฅผ ๋ถ„๋ฆฌํ•˜์—ฌ ๋ชจ๋“ˆํ™”ํ•˜๋Š” ํ”„๋กœ๊ทธ๋ž˜๋ฐ ํŒจ๋Ÿฌ๋‹ค์ž„์ž…๋‹ˆ๋‹ค. ์ฃผ์š” ๋ชฉ์ ์€ ๋กœ๊น…, ๋ณด์•ˆ, ํŠธ๋žœ์žญ์…˜ ๊ด€๋ฆฌ ๊ฐ™์€ ํšก๋‹จ ๊ด€์‹ฌ์‚ฌ๋ฅผ ํ•ต์‹ฌ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์—์„œ ๋ถ„๋ฆฌํ•˜์—ฌ ์ฝ”๋“œ์˜ ์žฌ์‚ฌ์šฉ์„ฑ๊ณผ ์œ ์ง€๋ณด์ˆ˜์„ฑ์„ ํ–ฅ์ƒ์‹œํ‚ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

2. ์œ ์ € ํด๋ž˜์Šค ๊ตฌํ˜„

supabase/api ๋””๋ ‰ํ† ๋ฆฌ ํ•˜์œ„์— entities๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ  user.ts ๊ตฌํ˜„์ฒด ํŒŒ์ผ๋„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
import { User as SupabaseUser } from "https://esm.sh/@supabase/supabase-js@2"; export class ServiceUser { // ServiceUser ํด๋ž˜์Šค๋Š” ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๋‹ด๋Š” ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. // SupabaseUser ํด๋ž˜์Šค๋ฅผ ์ด์šฉํ•ด ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. // ํ•„์š”ํ•œ ์ •๋ณด๊ฐ€ ์žˆ๋‹ค๋ฉด ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. id: string; email: string | null; createdAt: string; constructor(id: string, email: string | null, createdAt: string) { this.id = id; this.email = email; this.createdAt = createdAt; } static fromUser(user: SupabaseUser): ServiceUser { return new ServiceUser( user.id, user.email ?? null, user.created_at, ); } }
TypeScript
๋ณต์‚ฌ

3. ์ธ์ฆ ๋ฏธ๋“ค์›จ์–ด ๊ตฌํ˜„

supabase/api ๋””๋ ‰ํ† ๋ฆฌ ํ•˜์œ„์— middleware๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ  authMiddleware.ts ๊ตฌํ˜„์ฒด ํŒŒ์ผ๋„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
import { ServiceUser } from "../entities/user.ts"; import { supabase } from "./../lib/supabase.ts"; import { Context, Next } from "https://deno.land/x/hono/mod.ts"; // ์ธ์ฆ ๋ฏธ๋“ค์›จ์–ด ํ•จ์ˆ˜ ์ •์˜ export async function authMiddleware(c: Context, next: Next) { // ํด๋ผ์ด์–ธํŠธ๋กœ๋ถ€ํ„ฐ Authorization ํ—ค๋”๋ฅผ ๋ฐ›์•„์˜ต๋‹ˆ๋‹ค. const authHeader = c.req.header("Authorization"); // Authorization ํ—ค๋”๊ฐ€ ์—†๊ฑฐ๋‚˜ Bearer๋กœ ์‹œ์ž‘ํ•˜์ง€ ์•Š์œผ๋ฉด 401 Unauthorized ์‘๋‹ต์„ ๋ณด๋ƒ…๋‹ˆ๋‹ค. if (!authHeader || !authHeader.startsWith("Bearer ")) { return c.json({ code: 401, message: "Unauthorized" }, 401); } // Authorization ํ—ค๋”์—์„œ JWT ํ† ํฐ์„ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค. const jwt = authHeader.split(" ")[1]; // JWT ํ† ํฐ์„ ๊ฒ€์ฆํ•˜๊ณ  ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. const serviceUser = await decodeJWT(jwt); // JWT ํ† ํฐ ๊ฒ€์ฆ์— ์‹คํŒจํ•˜๊ฑฐ๋‚˜ ์‚ฌ์šฉ์ž ์ •๋ณด๊ฐ€ ์—†์œผ๋ฉด 401 Unauthorized ์‘๋‹ต์„ ๋ณด๋ƒ…๋‹ˆ๋‹ค. if (!serviceUser) { return c.json({ code: 401, message: "Unauthorized" }, 401); } // ๊ฒ€์ฆ๋œ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์ปจํ…์ŠคํŠธ์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. c.set("user", serviceUser); // ๋‹ค์Œ ๋ฏธ๋“ค์›จ์–ด ๋˜๋Š” ํ•ธ๋“ค๋Ÿฌ๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค. await next(); } // JWT ํ† ํฐ์„ ๊ฒ€์ฆํ•˜๊ณ  ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ํ•จ์ˆ˜ ์ •์˜ async function decodeJWT(jwt: string): Promise<ServiceUser | null> { // Supabase๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ JWT ํ† ํฐ์„ ๊ฒ€์ฆํ•˜๊ณ  ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. const { data, error } = await supabase.auth.getUser(jwt); // JWT ๊ฒ€์ฆ์— ์‹คํŒจํ•˜๋ฉด ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๋ฅผ ๋กœ๊ทธ์— ์ถœ๋ ฅํ•˜๊ณ  null์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. if (error) { console.error("JWT verification failed:", error.message); return null; } // ์‚ฌ์šฉ์ž ์ •๋ณด๊ฐ€ ์—†์œผ๋ฉด ๋กœ๊ทธ์— ์ถœ๋ ฅํ•˜๊ณ  null์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. if (data.user == null) { console.error("User not exist"); return null; } // ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ServiceUser ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. return ServiceUser.fromUser(data.user); }
TypeScript
๋ณต์‚ฌ

4. ์ธ์ฆ ๋ฏธ๋“ค์›จ์–ด ์ ์šฉ

๋ฏธ๋“ค์›จ์–ด๋Š” Router ๋ ˆ์ด์–ด์—์„œ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค.
supabase/api/routers/userRouter.ts
import { authMiddleware } from "../middleware/authMiddleware.ts"; import { UserController } from "./../controllers/userController.ts"; import { Hono } from "https://deno.land/x/hono/mod.ts"; const userRouter = new Hono(); const userController = new UserController(); // ์‚ฌ์šฉ์ž ์ธ์ฆ ๋ฏธ๋“ค์›จ์–ด๋ฅผ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค. ๐Ÿ‘ˆ๐Ÿ‘ˆ๐Ÿ‘ˆ๐Ÿ‘ˆ๐Ÿ‘ˆ๐Ÿ‘ˆ userRouter.use(authMiddleware); // ๐Ÿ‘ˆ๐Ÿ‘ˆ๐Ÿ‘ˆ๐Ÿ‘ˆ๐Ÿ‘ˆ๐Ÿ‘ˆ userRouter.post( "/v1/fcmToken", (c) => userController.postFCMTokenV1(c), ); userRouter.get( "/v1/fcmToken", (c) => userController.getFCMTokensV1(c), ); userRouter.delete( "/v1/fcmToken", (c) => userController.deleteFCMTokenV1(c), ); export default userRouter;
TypeScript
๋ณต์‚ฌ
โ€ข
"/v1/fcmToken"์— POST, GET, DELETE ์š”์ฒญ์„ ํ•  ๋•Œ, Request Header์— ์œ ํšจํ•œ ์ธ์ฆ ์ •๋ณด๊ฐ€ ์—†์œผ๋ฉด ์šฐ์„ ์ ์œผ๋กœ 401 ์˜ค๋ฅ˜ ์ฝ”๋“œ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

5. ์ปจํŠธ๋กค๋Ÿฌ ์˜์—ญ์—์„œ์˜ ์‚ฌ์šฉ

Router์—์„œ ์ธ์ฆ ๋ฏธ๋“ค์›จ์–ด๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ์„ ์–ธํ•˜๋ฉด, ์ธ์ฆ ๋ฏธ๋“ค์›จ์–ด๊ฐ€ ์‚ฌ์šฉ์ž์˜ ์ธ์ฆ์„ ๊ฒ€์‚ฌํ•˜๊ณ  ๊ทธ ๊ฒฐ๊ณผ๋ฅผ ์ปจํŠธ๋กค๋Ÿฌ์—๊ฒŒ ์ „๋‹ฌํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.
์ปจํŠธ๋กค๋Ÿฌ์—์„œ๋Š” ์ธ์ฆ ๋ฏธ๋“ค์›จ์–ด์—์„œ ๋‹ด์•„์ค€ ์œ ์ € ์ •๋ณด๋ฅผ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
supabase/functions/api/controllers/userController.ts
--- a/supabase/functions/api/controllers/userController.ts +++ b/supabase/functions/api/controllers/userController.ts @@ -2,6 +2,7 @@ import { ResponseCode } from "./../lib/response/responseCode.ts"; import { Context } from "https://deno.land/x/hono/mod.ts"; import { UserService } from "../services/userService.ts"; import { createResponse } from "../lib/response/responseFormat.ts"; +import { ServiceUser } from "../entities/user.ts"; // UserController ํด๋ž˜์Šค๋Š” FCM ํ† ํฐ์„ ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ ์ปจํŠธ๋กค๋Ÿฌ์ž…๋‹ˆ๋‹ค. export class UserController { @@ -14,6 +15,8 @@ export class UserController { // postFCMTokenV1 ๋ฉ”์„œ๋“œ๋Š” FCM ํ† ํฐ์„ ์ถ”๊ฐ€ํ•˜๋Š” API ์—”๋“œํฌ์ธํŠธ์ž…๋‹ˆ๋‹ค. async postFCMTokenV1(c: Context) { + // ์ธ์ฆ ๋ฏธ๋“ค์›จ์–ด๋ฅผ ํ†ตํ•ด ๋„˜์–ด์˜จ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. + const requestUser = c.get("user") as ServiceUser; // ์š”์ฒญ์—์„œ fcmToken์„ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค. const { fcmToken } = await c.req.json(); @@ -29,7 +32,7 @@ export class UserController { } // UserService๋ฅผ ํ†ตํ•ด FCM ํ† ํฐ์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. - const result = await this.userService.addFCMToken("mock_user_id", fcmToken); + const result = await this.userService.addFCMToken(requestUser.id, fcmToken); // ๊ฒฐ๊ณผ์— ๋”ฐ๋ผ ์ ์ ˆํ•œ ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. return c.json( @@ -49,8 +52,10 @@ export class UserController { // getFCMTokensV1 ๋ฉ”์„œ๋“œ๋Š” ํŠน์ • ์‚ฌ์šฉ์ž์˜ FCM ํ† ํฐ๋“ค์„ ๊ฐ€์ ธ์˜ค๋Š” API ์—”๋“œํฌ์ธํŠธ์ž…๋‹ˆ๋‹ค. async getFCMTokensV1(c: Context) { + // ์ธ์ฆ ๋ฏธ๋“ค์›จ์–ด๋ฅผ ํ†ตํ•ด ๋„˜์–ด์˜จ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. + const requestUser = c.get("user") as ServiceUser; // UserService๋ฅผ ํ†ตํ•ด ์‚ฌ์šฉ์ž ID์— ํ•ด๋‹นํ•˜๋Š” FCM ํ† ํฐ๋“ค์„ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. - const tokens = await this.userService.getFCMTokensByUserID("mock_user_id"); + const tokens = await this.userService.getFCMTokensByUserID(requestUser.id); // ํ† ํฐ ๋ชฉ๋ก์„ JSON ํ˜•์‹์œผ๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. return c.json(createResponse(ResponseCode.SUCCESS, "Success", tokens)); @@ -58,6 +63,8 @@ export class UserController { // deleteFCMTokenV1 ๋ฉ”์„œ๋“œ๋Š” FCM ํ† ํฐ์„ ์‚ญ์ œํ•˜๋Š” API ์—”๋“œํฌ์ธํŠธ์ž…๋‹ˆ๋‹ค. async deleteFCMTokenV1(c: Context) { + // ์ธ์ฆ ๋ฏธ๋“ค์›จ์–ด๋ฅผ ํ†ตํ•ด ๋„˜์–ด์˜จ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. + const requestUser = c.get("user") as ServiceUser; // ์š”์ฒญ์—์„œ fcmToken์„ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค. const { fcmToken } = await c.req.json(); @@ -74,7 +81,7 @@ export class UserController { // UserService๋ฅผ ํ†ตํ•ด FCM ํ† ํฐ์„ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค. const result = await this.userService.deleteFCMToken( - "mock_user_id", + requestUser.id, fcmToken, );
Diff
๋ณต์‚ฌ

๊ฐ•์˜ ์ฝ”๋“œ

7
pull