UnauthorizedException์ ์ธ์ฆ์ด ํ์ํ๊ฑฐ๋ ๊ถํ์ด ๋ถ์กฑํ ๊ฒฝ์ฐ ์ฌ์ฉํ๋ ์์ธ์
๋๋ค. HTTP 401 ์ํ ์ฝ๋๋ฅผ ๋ฐํํ๋ฉฐ, ๋ก๊ทธ์ธ ํ์, ์ธ์
๋ง๋ฃ, ๊ถํ ๋ถ์กฑ ๋ฑ์ ์ํฉ์ ์ฌ์ฉ๋ฉ๋๋ค.
๊ธฐ๋ณธ ์ฌ์ฉ๋ฒ
๋ณต์ฌ
class UnauthorizedException extends SoException {
constructor(
public message = "Unauthorized",
public payload?: unknown,
);
}
๋ณต์ฌ
@api()
async getMyProfile(ctx: Context) {
if (!ctx.user) {
throw new UnauthorizedException("๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค");
}
return this.findById(ctx.user.id);
}
์ค์ฉ ์์
๊ธฐ๋ณธ ์ธ์ฆ ์ฒดํฌ
๋ณต์ฌ
@api()
async updateMyProfile(
ctx: Context,
name: string,
bio: string
) {
if (!ctx.user) {
throw new UnauthorizedException("๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค");
}
return this.update(ctx.user.id, { name, bio });
}
Guards๋ฅผ ์ฌ์ฉํ ์ธ์ฆ (๊ถ์ฅ)
๊ฐ๋จํ ๋ก๊ทธ์ธ ์ฒดํฌ๋ Guards๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ด ๋ ๊น๋ํฉ๋๋ค:๋ณต์ฌ
// Guards ์ฌ์ฉ (๊ถ์ฅ)
@api({ guards: ["user"] })
async getMyData(ctx: Context) {
// ctx.user๊ฐ ๋ณด์ฅ๋จ
return this.findById(ctx.user!.id);
}
// sonamu.config.ts์์ Guard ์ฒ๋ฆฌ
export default {
server: {
apiConfig: {
guardHandler: (guard, request, api) => {
if (guard === "user" && !request.user) {
throw new UnauthorizedException("๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค");
}
return true;
}
}
}
} satisfies SonamuConfig;
๋ฆฌ์์ค ์์ ๊ถ ๊ฒ์ฆ
๋ณต์ฌ
@api()
async deletePost(ctx: Context, postId: number) {
if (!ctx.user) {
throw new UnauthorizedException("๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค");
}
const post = await this.findById(postId);
if (!post) {
throw new NotFoundException("๊ฒ์๋ฌผ์ ์ฐพ์ ์ ์์ต๋๋ค");
}
// ๋ณธ์ธ์ด ์์ฑํ ๊ฒ์๋ฌผ๋ง ์ญ์ ๊ฐ๋ฅ
if (post.authorId !== ctx.user.id) {
throw new UnauthorizedException(
"๋ณธ์ธ์ด ์์ฑํ ๊ฒ์๋ฌผ๋ง ์ญ์ ํ ์ ์์ต๋๋ค",
{ postId, authorId: post.authorId }
);
}
return this.delete(postId);
}
์ญํ ๊ธฐ๋ฐ ๊ถํ ๊ฒ์ฆ
๋ณต์ฌ
@api()
async deleteUser(ctx: Context, userId: number) {
if (!ctx.user) {
throw new UnauthorizedException("๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค");
}
// ๊ด๋ฆฌ์ ๊ถํ ์ฒดํฌ
if (!ctx.user.isAdmin) {
throw new UnauthorizedException(
"๊ด๋ฆฌ์๋ง ์ฌ์ฉ์๋ฅผ ์ญ์ ํ ์ ์์ต๋๋ค",
{ requiredRole: "admin", currentRole: ctx.user.role }
);
}
// ์๊ธฐ ์์ ์ ์ญ์ ๋ถ๊ฐ
if (ctx.user.id === userId) {
throw new BadRequestException("์๊ธฐ ์์ ์ ์ญ์ ํ ์ ์์ต๋๋ค");
}
return this.delete(userId);
}
๋ณตํฉ ๊ถํ ๊ฒ์ฆ
๋ณต์ฌ
@api()
async publishPost(ctx: Context, postId: number) {
if (!ctx.user) {
throw new UnauthorizedException("๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค");
}
const post = await this.findById(postId);
if (!post) {
throw new NotFoundException("๊ฒ์๋ฌผ์ ์ฐพ์ ์ ์์ต๋๋ค");
}
// ์์ฑ์์ด๊ฑฐ๋ ํธ์ง์ ๊ถํ์ด ์์ด์ผ ํจ
const isAuthor = post.authorId === ctx.user.id;
const isEditor = ctx.user.role === "editor" || ctx.user.role === "admin";
if (!isAuthor && !isEditor) {
throw new UnauthorizedException(
"๊ฒ์๋ฌผ์ ๋ฐํํ ๊ถํ์ด ์์ต๋๋ค",
{
postId,
authorId: post.authorId,
currentUserId: ctx.user.id,
currentRole: ctx.user.role,
requiredCondition: "author or editor/admin role"
}
);
}
return this.update(postId, { status: "published" });
}
์กฐ์ง/ํ ๊ถํ ๊ฒ์ฆ
๋ณต์ฌ
@api()
async addTeamMember(
ctx: Context,
teamId: number,
newMemberId: number
) {
if (!ctx.user) {
throw new UnauthorizedException("๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค");
}
const team = await TeamModel.findById(teamId);
if (!team) {
throw new NotFoundException("ํ์ ์ฐพ์ ์ ์์ต๋๋ค");
}
// ํ ๊ด๋ฆฌ์๋ง ๋ฉค๋ฒ ์ถ๊ฐ ๊ฐ๋ฅ
const membership = await TeamMemberModel.findOne({
teamId,
userId: ctx.user.id
});
if (!membership) {
throw new UnauthorizedException(
"ํ ๋ฉค๋ฒ๊ฐ ์๋๋๋ค",
{ teamId }
);
}
if (membership.role !== "admin" && membership.role !== "owner") {
throw new UnauthorizedException(
"ํ ๊ด๋ฆฌ์๋ง ๋ฉค๋ฒ๋ฅผ ์ถ๊ฐํ ์ ์์ต๋๋ค",
{
teamId,
currentRole: membership.role,
requiredRole: "admin or owner"
}
);
}
return TeamMemberModel.create({
teamId,
userId: newMemberId,
role: "member"
});
}
API ํค ์ธ์ฆ
๋ณต์ฌ
@api()
async getApiData(ctx: Context) {
const apiKey = ctx.headers["x-api-key"];
if (!apiKey) {
throw new UnauthorizedException(
"API ํค๊ฐ ํ์ํฉ๋๋ค",
{ header: "x-api-key" }
);
}
const validKey = await ApiKeyModel.findByKey(apiKey);
if (!validKey) {
throw new UnauthorizedException(
"์ ํจํ์ง ์์ API ํค์
๋๋ค"
);
}
if (validKey.expiresAt && validKey.expiresAt < new Date()) {
throw new UnauthorizedException(
"๋ง๋ฃ๋ API ํค์
๋๋ค",
{ expiresAt: validKey.expiresAt }
);
}
if (!validKey.isActive) {
throw new UnauthorizedException(
"๋นํ์ฑํ๋ API ํค์
๋๋ค",
{ keyId: validKey.id }
);
}
// API ๋ฐ์ดํฐ ๋ฐํ
return this.getDataForApiKey(validKey.id);
}
์๊ฐ ๊ธฐ๋ฐ ์ ๊ทผ ์ ์ด
๋ณต์ฌ
@api()
async accessRestrictedResource(ctx: Context, resourceId: number) {
if (!ctx.user) {
throw new UnauthorizedException("๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค");
}
const subscription = await SubscriptionModel.findByUserId(ctx.user.id);
if (!subscription) {
throw new UnauthorizedException(
"๊ตฌ๋
์ด ํ์ํ ๋ฆฌ์์ค์
๋๋ค",
{ resourceId }
);
}
const now = new Date();
// ๊ตฌ๋
๋ง๋ฃ ์ฒดํฌ
if (subscription.expiresAt < now) {
throw new UnauthorizedException(
"๊ตฌ๋
์ด ๋ง๋ฃ๋์์ต๋๋ค",
{
expiresAt: subscription.expiresAt,
renewUrl: "/subscription/renew"
}
);
}
// ๊ตฌ๋
ํ๋๋ณ ์ ๊ทผ ์ ํ
const resource = await ResourceModel.findById(resourceId);
if (resource.requiredPlan === "premium" && subscription.plan === "basic") {
throw new UnauthorizedException(
"ํ๋ฆฌ๋ฏธ์ ํ๋์ด ํ์ํ ๋ฆฌ์์ค์
๋๋ค",
{
requiredPlan: "premium",
currentPlan: "basic",
upgradeUrl: "/subscription/upgrade"
}
);
}
return resource;
}
IP ๊ธฐ๋ฐ ์ ๊ทผ ์ ์ด
๋ณต์ฌ
@api()
async adminOnlyEndpoint(ctx: Context) {
if (!ctx.user?.isAdmin) {
throw new UnauthorizedException("๊ด๋ฆฌ์ ๊ถํ์ด ํ์ํฉ๋๋ค");
}
// ํน์ IP ๋์ญ์์๋ง ์ ๊ทผ ํ์ฉ
const allowedIPs = ["192.168.1.0/24", "10.0.0.0/8"];
const clientIP = ctx.request.ip;
if (!this.isIPAllowed(clientIP, allowedIPs)) {
throw new UnauthorizedException(
"ํ์ฉ๋์ง ์์ IP ์ฃผ์์
๋๋ค",
{
clientIP,
allowedRanges: allowedIPs
}
);
}
return this.getAdminData();
}
private isIPAllowed(ip: string, allowedRanges: string[]): boolean {
// IP ๋์ญ ์ฒดํฌ ๋ก์ง
return true; // ์ค์ ๊ตฌํ ํ์
}
Guards์ ์๋ ์ฒดํฌ ๋น๊ต
Guards ์ฌ์ฉ (๊ถ์ฅ)
๋ณต์ฌ
// sonamu.config.ts
guardHandler: (guard, request, api) => {
if (guard === "user" && !request.user) {
throw new UnauthorizedException("๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค");
}
if (guard === "admin" && !request.user?.isAdmin) {
throw new UnauthorizedException("๊ด๋ฆฌ์ ๊ถํ์ด ํ์ํฉ๋๋ค");
}
return true;
}
// API ๋ฉ์๋
@api({ guards: ["user"] })
async simpleUserEndpoint(ctx: Context) {
// ์ธ์ฆ ์ฒดํฌ๊ฐ ์๋์ผ๋ก ์๋ฃ๋จ
return this.getData(ctx.user!.id);
}
@api({ guards: ["admin"] })
async adminEndpoint(ctx: Context) {
// ๊ด๋ฆฌ์ ๊ถํ ์ฒดํฌ๊ฐ ์๋์ผ๋ก ์๋ฃ๋จ
return this.getAdminData();
}
์๋ ์ฒดํฌ (๋ณต์กํ ๋ก์ง)
๋ณต์ฌ
@api()
async complexAuthEndpoint(ctx: Context, postId: number) {
if (!ctx.user) {
throw new UnauthorizedException("๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค");
}
const post = await PostModel.findById(postId);
// ๋ณต์กํ ๊ถํ ๋ก์ง: ์์ฑ์์ด๊ฑฐ๋, ํธ์ง์์ด๊ฑฐ๋, ๊ฐ์ ํ ๋ฉค๋ฒ
const isAuthor = post.authorId === ctx.user.id;
const isEditor = ctx.user.role === "editor";
const isSameTeam = await TeamModel.areInSameTeam(
ctx.user.id,
post.authorId
);
if (!isAuthor && !isEditor && !isSameTeam) {
throw new UnauthorizedException("์ ๊ทผ ๊ถํ์ด ์์ต๋๋ค");
}
return post;
}
payload ํ์ฉ ํจํด
๋ก๊ทธ์ธ ์ ๋
๋ณต์ฌ
throw new UnauthorizedException("๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค", {
loginUrl: "/auth/login",
returnUrl: ctx.request.url
});
๊ถํ ์ ๊ทธ๋ ์ด๋ ์ ๋
๋ณต์ฌ
throw new UnauthorizedException(
"ํ๋ฆฌ๋ฏธ์ ๊ธฐ๋ฅ์
๋๋ค",
{
requiredPlan: "premium",
currentPlan: "basic",
upgradeUrl: "/subscription/upgrade",
features: ["๊ณ ๊ธ ๋ถ์", "๋ฌด์ ํ ํ๋ก์ ํธ", "์ฐ์ ์ง์"]
}
);
๋ง๋ฃ ์ ๋ณด
๋ณต์ฌ
throw new UnauthorizedException(
"์ธ์
์ด ๋ง๋ฃ๋์์ต๋๋ค",
{
expiredAt: session.expiresAt,
refreshUrl: "/auth/refresh"
}
);
ํด๋ผ์ด์ธํธ ์๋ต ์์
๊ธฐ๋ณธ ์๋ต
๋ณต์ฌ
{
"statusCode": 401,
"message": "๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค"
}
payload ํฌํจ ์๋ต
๋ณต์ฌ
{
"statusCode": 401,
"message": "ํ๋ฆฌ๋ฏธ์ ํ๋์ด ํ์ํ ๋ฆฌ์์ค์
๋๋ค",
"payload": {
"requiredPlan": "premium",
"currentPlan": "basic",
"upgradeUrl": "/subscription/upgrade"
}
}
401 vs 403
- 401 Unauthorized: ์ธ์ฆ ์์ฒด๊ฐ ์ ๋์๊ฑฐ๋ ๊ถํ์ด ๋ถ์กฑํ ๊ฒฝ์ฐ
- 403 Forbidden: ์ธ์ฆ์ ๋์์ง๋ง ํด๋น ๋ฆฌ์์ค์ ์ ๊ทผํ ๊ถํ์ด ๋ช ์์ ์ผ๋ก ์๋ ๊ฒฝ์ฐ
UnauthorizedException ํ๋๋ก ๋ ๋ค ์ฒ๋ฆฌํ์ง๋ง, ํ์ํ๋ค๋ฉด ์ปค์คํ
ForbiddenException์ ๋ง๋ค ์ ์์ต๋๋ค.
๋ณต์ฌ
// ์ปค์คํ
403 ์์ธ
export class ForbiddenException extends SoException {
constructor(
public message = "Forbidden",
public payload?: unknown,
) {
super(403, message, payload);
}
}
// ์ฌ์ฉ
@api()
async deleteUser(ctx: Context, userId: number) {
if (!ctx.user) {
throw new UnauthorizedException("๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค");
}
if (!ctx.user.isAdmin) {
throw new ForbiddenException(
"์ด ์์
์ ์ํํ ๊ถํ์ด ์์ต๋๋ค",
{ requiredRole: "admin" }
);
}
return this.delete(userId);
}