기존 코드는 https://www.npmjs.com/package/@react-oauth/google 라이브러리를 활용하여 구글 소셜 로그인만 구현한 상태였다.
이번에 카카오, 네이버 등등의 소셜 로그인을 구현하려고 했는데 앞전에 구현한 구글 소셜 로그인 기능 코드가 확장성이 좀 떨어지는것 같아 고민끝에 리팩토링을 하게 되었다.
기존의 과정은 이렇다,
기존 과정
1) React에서 버튼을 클릭하여 구글 로그인 시도 -> 성공시 구글 소셜로그인 토큰을 받음
import { GoogleLogin } from '@react-oauth/google';
<GoogleLogin
onSuccess={credentialResponse => {
console.log(credentialResponse); // 여기에 구글 토큰이 딸려있음
}}
onError={() => {
console.log('Login Failed');
}}
/>;
2) 받은 토큰을 서버측에(nestjs) 전달하여 google-auth-library라는 라이브러리를 활용해 토큰을 디코딩하여 유저 정보 추출
3) jwt accessToken과 refreshToken을 새롭게 발급하여 유저정보와 함께 리턴
나는 왜 이렇게 구현했었나?
물론 @react-oauth/google 라이브러리에서 accessToken과 refreshToken값을 받아와 그대로 사용할 수 있겠고 이러는게 개발부분에서 편리함이 있겠지만, 다른 로컬 로그인 유저들과 비교해서 토큰 관리에 어려움이 생길것 같다는 느낌을 받았다.
그래서 accessToken과 refreshToken을 새롭게 서버측에서 발급하여 리턴해주는 식으로 개발했었다.
바뀐 과정
토큰 관리측면에서 서버측에서 새롭게 발급해주는건 정말 좋은것 같았다.
하지만 소셜 로그인이 구글 하나만 있는게 아니라는 문제점이 있었다.
현재 코드는 너무 구글 로그인 하나만 생각하여 짠 코드인것 같았다.
바꾼 코드에대해서 알아보자.
1) google-auth-library 라이브러리 제거
const GoogleSocialAuthBtn = () => {
const handleGoogleLogin = () => {
window.location.href = 'http://localhost:3001/login/google';
};
return (
<div>
<button onClick={handleGoogleLogin}>구글로 로그인</button>
</div>
);
};
export default GoogleSocialAuthBtn;
href로 서버쪽 주소와 연결시켜줬다.
참고로 href로 안하면 cors에러가 발생한다;
2) auth.controller.ts에서 @Get('/login/:social')로 요청을 받아준다.
import { Controller, Get, Req, Res, UseGuards } from '@nestjs/common';
import { Request, Response } from 'express';
import { AuthService } from './auth.service';
import { DynamicAuthGuard } from './guards/dynamic-auth.guard';
import { IOAuthUser } from './interface/auth-service.interface';
@Controller()
export class AuthController {
constructor(
private readonly authService: AuthService, //
) {}
@Get('/login/:social')
@UseGuards(DynamicAuthGuard) // 이부분
loginOAuth() {}
@Get('/login/:social/callback')
@UseGuards(DynamicAuthGuard)
handleRedirect(
@Req() req: Request & IOAuthUser, //
@Res() res: Response,
) {
return this.authService.loginOAuth({ req, res });
}
}
DynmaicAuthCuard 파일은 아래와 같다.
import { CanActivate, ExecutionContext } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
class GoogleAuthGuard extends AuthGuard('google') {}
class KakaoAuthGuard extends AuthGuard('kakao') {}
class NaverAuthGuard extends AuthGuard('naver') {}
const DYNAMIC_AUTH_GUARD = {
google: new GoogleAuthGuard(),
kakao: new KakaoAuthGuard(),
naver: new NaverAuthGuard(),
};
export class DynamicAuthGuard implements CanActivate {
canActivate(context: ExecutionContext) {
// context가 req,res가 담겨있는 내용으로 바뀌게됨
const { social } = context.switchToHttp().getRequest().params;
if (!DYNAMIC_AUTH_GUARD[social]) {
throw new Error(`Unsupported social provider: ${social}`);
}
// object literal lookup : 오브젝트 리터럴을 찾는 방법
return DYNAMIC_AUTH_GUARD[social].canActivate(context);
}
}
이렇게해서 좀 더 유연하게 소셜로그인 인증 기능을 사용할 수 있게됬다.
DynamicAuthGuard는 JwtGooglestrategy 가드와 연동되어있다.
import { PassportStrategy } from '@nestjs/passport';
import { Profile, Strategy } from 'passport-google-oauth20';
export class JwtGoogleStrategy extends PassportStrategy(Strategy, 'google') {
constructor() {
super({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: 'http://localhost:3001/login/google/callback', // 서버쪽 url
scope: ['email', 'profile'],
});
}
validate(accessToken: string, refreshToken: string, profile: Profile) {
// console.log('accessToken:', accessToken);
// console.log(refreshToken);
// console.log(profile);
return {
id: profile?.id,
name: profile?.displayName,
email: profile?.emails[0].value,
avatar: profile?.photos[0].value,
age: 0,
};
}
}
여기서 주석부분안 callbackURL이 중요하다. 서버쪽 url주소를 적어줘야 바로 리다이렉트가 되서 서버쪽에서 validate된 유저 정보를 활용하여 작업해줄 수 있다.
(이 callbackURL이 프론트쪽으로 콜백시켜주는건지, 서버쪽으로 콜백시켜주는건지 햇갈려서 많이 헤맸음)
import { Controller, Get, Req, Res, UseGuards } from '@nestjs/common';
import { Request, Response } from 'express';
import { AuthService } from './auth.service';
import { DynamicAuthGuard } from './guards/dynamic-auth.guard';
import { IOAuthUser } from './interface/auth-service.interface';
@Controller()
export class AuthController {
constructor(
private readonly authService: AuthService, //
) {}
@Get('/login/:social')
@UseGuards(DynamicAuthGuard)
loginOAuth() {}
@Get('/login/:social/callback') // 이부분으로 리다이렉트되서 처리됨
@UseGuards(DynamicAuthGuard) // 마찬가지로 이부분도 똑같이 해줘야 jwt가드 인가가 되서 유저 정보가 들어감
handleRedirect(
@Req() req: Request & IOAuthUser, //
@Res() res: Response,
) {
// console.log(req.user); 해보면 유저 정보가 들어가있음
return this.authService.loginOAuth({ req, res });
}
}
3) return this.authService.loginOAuth({ req, res });에서 해당 이메일이 db에 저장되어있지 않다면 가입시키고, JWT accessToken과 refreshToken을 발급하여 토큰을 포함한 콜백 url로 리다이렉트 시켜줬다.
this.setRefreshToken({ user, res });
const token = this.getAccessToken({ user });
res.redirect(
`http://localhost:3000/login/google/callback?token=${token}`,
); // 프론트쪽 url로 리다이렉트
4) 프론트쪽에서 callback url을 라우터로 설정하여 useEffect로 url에있는 토큰값을 활용하여 유저 정보를 다시 받아온다던가,
저장한다던가 하는식으로 활용해주었다.
끝으로, 처음 코드를 바꾸기전에는 graphql+nestjs로 구글 소셜로그인을 구현해줬었는데,
아무래도 구글측에서 graphql 소셜로그인 api를 내주는게아닌 ,rest api로 내주는거다보니깐 막상 구현해보니 복잡하게 구현만했지, 속없는 알맹이였다. (대충 겉에만 graphql이였다는 뜻.. 통신만 뮤테이션,쿼리문으로 했다.)
다른 소셜로그인 api도 이와 다르지 않다 생각하여 rest api쪽으로 코드를 바꿨다.
'기록' 카테고리의 다른 글
supabase,zustand로 user 로그인 처리한거를 react-query로 변경 (0) | 2025.01.23 |
---|---|
'Set'을 활용해서 기존 배열중에서 삭제된 배열값 구하기 (0) | 2024.11.19 |
'parameter' , 'argument' 정의 (2) | 2024.11.18 |