반응형
01. passport
기존의 node처럼 passport로 로그인을 구현 해보겠당 ㅎㅎ
NestJS에서 가드(guard)란 애플리케이션의 최전선에서 말그대로 애플리케이션을 보호하는 역할을 담당한다.
NestJS로 들어오는 요청은 컨트롤러(controller) 단에 도달하기 전에 반드시 가드를 거쳐가도록 되어 있다.
가드를 이용하면 컨트롤러가 요청을 처리하기 전에 안전하지 않은 요청을 효과적으로 차단할 수 있다.
결론은 node에서 썻던 유저인증용 미들웨어랑 같다 라고 볼수 있다.
1. LocalAuthGuard
- canActivate: 로그인 시도시 실행되는 메서드
- 로그인 성공하면 세션에 사용자 정보를 저장(logIn)
- 주로 @UseGuards(LocalAuthGuard)로 로그인 라우트를 보호할 때 사용
// auth/local-auth.guard.ts
import { ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {
async canActivate(context: ExecutionContext): Promise<boolean> {
const can = await super.canActivate(context);
if (can) {
const req = context.switchToHttp().getRequest();
console.log(req);
await super.logIn(req);
}
return true;
}
}
2. LocalStrategy
- validate: 로그인 시도시 실행되는 메서드
- 이메일과 비밀번호로 사용자 인증
- 인증 실패시 UnauthorizedException 발생
import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) {
super({ usernameField: 'email', passwordField: 'password' });
}
async validate(email: string, password: string, done: CallableFunction) {
const user = await this.authService.validateUser(email, password);
if (!user) {
throw new UnauthorizedException();
}
return done(null, user);
}
}
3. LocalSerializer
- 세션 관리를 위한 직렬화/역직렬화 처리
- serializeUser: 로그인 성공 시 세션에 저장할 정보 선택 (보통 ID만)
- deserializeUser: 세션에서 ID를 가져와 사용자 전체 정보 조회
- 매 요청마다 실행되어 req.user에 사용자 정보를 채움
import { Injectable } from '@nestjs/common';
import { PassportSerializer } from '@nestjs/passport';
import { InjectRepository } from '@nestjs/typeorm';
import { Users } from 'src/entities/Users';
import { Repository } from 'typeorm';
import { AuthService } from './auth.service';
@Injectable()
export class LocalSerializer extends PassportSerializer {
constructor(
private readonly authService: AuthService,
@InjectRepository(Users) private usersRepository: Repository<Users>,
) {
super();
}
// 유저의 id만 뽑아서 세션 저장
serializeUser(user: Users, done: CallableFunction) {
done(null, user.id);
}
// 세션에서 id를 받아서 사용자 데이터를 꺼내옴
async deserializeUser(userId: string, done: CallableFunction) {
return await this.usersRepository
.findOneOrFail({
where: { id: +userId },
select: ['id', 'email', 'nickname', 'name', 'image', 'role', 'phone'],
relations: ['mentos', 'mentos.mentoringPrograms'],
})
.then((user) => {
done(null, user);
})
.catch((err) => done(err));
}
}
5. 전체 인증 흐름
- 로그인 시도 → LocalAuthGuard
- LocalStrategy에서 인증
- 인증 성공 → LocalSerializer.serializeUser로 세션 저장
- 이후 요청마다 LocalSerializer.deserializeUser로 사용자 정보 복원
02. next-auth
next-auth와 혼합해서 사용해보려 했지만 로컬로그인은 성공햇는데 소셜로그인을 하려니 자꾸 next-auth의 로직으로가서 찾아보니 passport에서 next-auth의 로직을 전부 넣어놔야한다는데... 너무 비효율일꺼같아서 솔직히 포기했다.
일단 로컬로그인만 코드를 남겨보려고 한다.
//_component
'use client';
import React from 'react';
import { SessionProvider } from 'next-auth/react';
type Props = {
children: React.ReactNode;
};
export default function AuthSession({ children }: Props) {
return <SessionProvider>{children}</SessionProvider>;
}
//src/middeware.ts
import { NextResponse } from 'next/server';
import { auth } from './auth';
export async function middleware() {
const session = await auth();
if (!session) {
return NextResponse.redirect('http://localhost:3000/login');
}
}
export const config = {
// 로그인을 해야만 들어갈수 잇는 페이지
matcher: ['/messages', '/mypage'],
};
// auth.ts
import { faker } from '@faker-js/faker';
import NextAuth from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
export const {
handlers: { GET, POST }, // API 라우트 핸들러
auth, // 미들웨어/클라이언트에서 세션 확인용
signIn, // 프로그래밍 방식 로그인용
} = NextAuth({
// 콜백을 위해서
secret: process.env.NEXTAUTH_SECRET,
// 커스텀 페이지 경로 설정
pages: {
// 로그인창으로 보낼때
signIn: '/login',
// 회원가입창으로 보내야할때
newUser: '/signup/terms',
},
providers: [
CredentialsProvider({
async authorize(credentials) {
const authResponse = await fetch(
`${process.env.NEXT_PUBLIC_API_BASE_URL}/users/login`,
{
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: credentials.username,
password: credentials.password,
}),
}
);
// 로그인 실패시
if (!authResponse.ok) {
return null;
}
// 로그인 성공시 사용자 데이터 처리
const user = await authResponse.json();
return {
...user,
email: user.email,
name: user.nickname,
image: faker.image.url(),
};
},
}),
],
});
// app/api/auth/[...nextauth]
export { GET, POST } from '@/auth';
// 이페이지의 주소는 get /api/auth/route
// 로그인페이지
const onSubmit = useCallback(
async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
try {
await signIn('credentials', {
username: email,
password,
redirect: false,
});
router.replace('/');
} catch (error) {
setMessage('아이디 또는 비밀번호가 틀립니다.')
}
},
[email, password, router]
);
반응형