이전 프로젝트에서는 무작정 카카오소셜로그인 하나만 구현했엇는데 이번 기회에는 OAuth가 뭔지부터 공부하고자 작성해본다.
❓OAuth가 뭐야
OAuth란 Open Authorization의 약자로, 인터넷 사용자들이 자신의 자격 증명(사용자 이름과 비밀번호)를 공유하지 않고도 웹 사이트나 애플리케이션이 사용자 정보를 접근할 수 있도록 하는 인증 프로토콜이다.
제3자 애플리케이션 접근 허용
사용자들이 자신의 소셜 미디어 계정(구글,페이스북 등)을 통해 다른 웹사이트나 애플리케이션에 로그인 할 때, 이를 가능하게 하는 것이 OAuth이다. 사용자는 자신의 자격증명을 공유하지 않고도 제3자 애플리케이션이 필요한 정보에 접근 할 수 있도록 승인한다.
API 접근 권한 부여
개발자들이 OAuth를 사용하여 다른 서비스의 API에 접근할 수 있다. 예를 들어 트위터 API를 사용하여 트윗을 게시하거나 읽을 때 OAuth를 사용하여 인증한다.
구동방식
세개의 소셜로그인을 구현해본 결과 코드들이 다 비슷하고 크게 다른점이 없는것 같아 대표적으로 카카오로그인 과정을 가져왔다.
기본적인 흐름은 다음과 같다.
- 사용자 인증 : 사용자가 로그인하고, 서비스 제공자는 사용자에게 제3자 애플리케이션이 자신의 정보에 접근하는 것을 허용할 것인지 묻는다.
- 사용자 승인 : 사용자가 접근을 허용하면, 서비스 제공자는 제3자 애플리케이션에 접근 토큰을 발급한다.
- 토큰 사용 : 제3자 애플리케이션은 이 접근 토큰을 사용하여 사용자의 정보에 접근하거나 특정 작업을 수행한다.
나는 백엔드분들과 같이 소셜로그인을 진행하여 따로 kakao developers에 앱을 등록하지 않고 필요한 키들은 백엔드분께 받아서 .env에 저장후 사용했다.
구글을 제외한 네이버,카카오는 앱 등록이 비슷하여 구글링해서 등록해보면 좋을듯하다.
사용하기
카카오로 부터 accessToken을 받기 위한 리다이렉트 페이지가 필요하다.
const commonRoutes = [
<Route path="/" element={<Layout />}>
<Route index element={<Main />} />
<Route path="/sign-in" element={<SignIn />} />
<Route path="/sign-up" element={<SignUp />} />
<Route path="/search" element={<Search />} />
<Route path="/all" element={<TotalProducts />} />
<Route path="/product/:productId" element={<ProductDetailModal />} />
<Route path="/oauth2/naver/redirect" element={<LoginHandler />} />
<Route path="/oauth2/kakao/redirect" element={<LoginHandler />} />
<Route path="/oauth2/google/redirect" element={<LoginHandler />} />
</Route>,
];
나는 카카오,네이버,구글이라 url만 다르게하고 똑같은 페이지로 해줬다. 하나면 되지않을까?라는 생각이 들었는데
앱에 등록하는 리다이렉트 url에 때문에 이렇게 똑같은걸 세개를 만들었다...
이건 백엔드와 진행할경우엔 합의를 보면 좋지않을까 싶음.
백엔드에선 /auth/login/callback/${provider} 이런식으로 api를 던져줬고 나는 저 provider에 알맞게 구글인지 네이버인지 카카오인지 넣어주면 됨
(가끔 공부 제대로 안한 백엔드가 get 만들어서 주는 경우가 있는데. 필요없다고 해주십시오.. 로그인할때마다 백엔드에서 처리하는거기때문에 프론트는 Post만 해주면 됩니다요.
그리고 무조건 앱에 등록하는 url는 프론트 경로와동일해야합니다.!!!)
.env
VITE_REST_API_KEY="발급받은 키 넣으세용"
VITE_KAKAO_REDIRECT_URI="앱에 설정한 리다이렉트 url 넣으세요 (프론트 경로와 동일해야합니다)"
VITE_GOOGLE_REDIRECT_URI="앱에 설정한 리다이렉트 url 넣으세요 (프론트 경로와 동일해야합니다)"
VITE_NAVER_REDIRECT_URI="앱에 설정한 리다이렉트 url 넣으세요 (프론트 경로와 동일해야합니다)"
VITE_GOOGLE_AUTH_CLIENT_ID="발급받은 아이디 넣으세용"
VITE_GOOGLE_SCOPE="https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile"
VITE_GOOGLE_SECRET="발급받은 키 넣으세용"
VITE_NAVER_CLIENT_ID="발급받은 아이디 넣으세용"
VITE_NAVER_CLIENT_SECRET="발급받은 아이디 넣으세용"
VITE_NAVER_STATE="필요없을지도.."
로그인 페이지
const {
VITE_REST_API_KEY,
VITE_KAKAO_REDIRECT_URI,
VITE_GOOGLE_REDIRECT_URI,
VITE_NAVER_REDIRECT_URI,
VITE_GOOGLE_AUTH_CLIENT_ID,
VITE_GOOGLE_SCOPE,
VITE_NAVER_CLIENT_ID,
VITE_NAVER_STATE,
} = import.meta.env;
const KAKAO_AUTH_URL = `https://kauth.kakao.com/oauth/authorize?response_type=code&client_id=${VITE_REST_API_KEY}&redirect_uri=${VITE_KAKAO_REDIRECT_URI}&prompt=login`;
const GOOGLE_AUTH_URL = `https://accounts.google.com/o/oauth2/auth?client_id=${VITE_GOOGLE_AUTH_CLIENT_ID}&redirect_uri=${VITE_GOOGLE_REDIRECT_URI}&response_type=code&scope=${VITE_GOOGLE_SCOPE}`;
const NAVER_AUTH_URL = `https://nid.naver.com/oauth2.0/authorize?response_type=code&client_id=${VITE_NAVER_CLIENT_ID}&state=${VITE_NAVER_STATE}&redirect_uri=${VITE_NAVER_REDIRECT_URI}`;
// 소셜미디어 로그인 버튼
const socialMedia = [
{ name: 'naver', src: '/images/naver.png', url: NAVER_AUTH_URL },
{ name: 'kakao', src: '/images/kakao.png', url: KAKAO_AUTH_URL },
{ name: 'google', src: '/images/google.svg', url: GOOGLE_AUTH_URL },
];
const SignIn = () => {
return (
<FormProvider {...form}>
<div className="w-full bg-mainBlack flex justify-center items-center flex-col">
<div className="w-[460px] flex justify-center flex-col mt-40">
<p className="font-didot text-3xl text-center mb-7">Sign In</p>
<form onSubmit={handleClickSignIn} autoComplete="off">
<AuthInput {...register('email')} type="email" placeholder="이메일">
Email
</AuthInput>
{errors.email && <p className=" text-sm text-red-500 mt-1">{errors.email.message}</p>}
<AuthInput {...register('password')} type="password" placeholder="비밀번호">
password
</AuthInput>
<CommonButton className="w-full rounded-lg bg-mainWhite px-3.5 py-2.5 text-lg font-semibold font-didot text-mainBlack shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 mt-6">
Sign In
</CommonButton>
</form>
</div>
<div className="mt-6">
아직 회원이 아니신가요?
<Link to="/sign-up">
<button type="button" className="font-semibold ml-2">
Sign in
</button>
</Link>
</div>
<div className="flex justify-center items-center mt-10 w-[460px]">
<span className=" w-full h-[1px] bg-mainWhite mr-2"></span>
<p>or</p>
<span className=" w-full h-[1px] bg-mainWhite ml-2"></span>
</div>
<div className="flex flex-row mt-5">
{socialMedia.map(({ name, src, url }) => (
<a key={name} href={url} className="w-[39px] h-[39px] mr-3 cursor-pointer overflow-hidden rounded-full">
<img src={src} alt={name} className="w-full h-full object-cover" />
</a>
))}
</div>
</div>
</FormProvider>
);
};
export default SignIn;
카카오 아이콘을 누르게 되면 카카오 로그인 창이 뜨게 되고 로그인에 성공하면 우리가 설정한 리다이렉트 URI로 이동하게 된다.
리다이렉트 페이지
import instance from '@/api/instance';
import Loading from '@/components/Loading';
import axios from 'axios';
import { useEffect } from 'react';
import { Cookies } from 'react-cookie';
const { VITE_BASE_REQUEST_URL, VITE_GOOGLE_AUTH_CLIENT_ID, VITE_GOOGLE_SCOPE, VITE_GOOGLE_SECRET, VITE_NAVER_STATE } = import.meta.env;
const LoginHandler = () => {
const url = new URL(window.location.href)
const code = url.searchParams.get("code");
const cookies = new Cookies();
const kakaoLogin = async () => {
try {
const response = await instance.post(`${VITE_BASE_REQUEST_URL}/users/login/social/kakao/`,
{
code: code
}
);
cookies.set("accessToken", response.data.access, { path: '/', secure: true, })
cookies.set("refreshToken", response.data.refresh, { path: '/', secure: true, })
window.location.href = "/"
} catch (error) {
console.error("카카오 로그인 실패", error);
}
}
const naverLogin = async () => {
try {
const response = await instance.post(`${VITE_BASE_REQUEST_URL}/users/login/social/naver/`,
{
code: code,
state: VITE_NAVER_STATE
}
);
cookies.set("accessToken", response.data.access, { path: '/', secure: true, })
cookies.set("refreshToken", response.data.refresh, { path: '/', secure: true, })
window.location.href = "/"
} catch (error) {
console.error("네이버 로그인 실패", error);
}
}
const googleLogin = async () => {
try {
const response = await instance.post(`${VITE_BASE_REQUEST_URL}/users/login/social/google/`,
{
code: code,
}
);
if (response.headers['set-cookie']){
console.log(response.headers['set-cookie'])
}
cookies.set("accessToken", response.data.access, { path: '/', secure: true, })
cookies.set("refreshToken", response.data.refresh, { path: '/', secure: true, })
window.location.href = "/"
} catch (error) {
console.error("구글 로그인 실패", error);
}
}
useEffect(() => {
if (url.href.includes("kakao")) {
kakaoLogin()
} else if (url.href.includes("naver")) {
naverLogin()
} else if (url.href.includes("google")) {
googleLogin()
}
}, [])
return (
<div><Loading /></div>
)
}
export default LoginHandler
리다이렉트가 되면 화면에 카카오로부터 받은 accessToken을 부여받을 수 있다. 이는 URL을 통해 제공이 된다.
URL 객체에서 searchParams method를 통해 받아올 수 있다.
백엔드가 있다면 프론트에서 처리해줄건 더 이상 없다.
구글, 카카오, 네이버 이 세개가 다 동일하다,,, 백엔드에서 앱 만들고 발급받은 키 env에 저장해서 리다이렉트 되는곳으로 이동만 되게 하면 나머지는 백엔드 영역이라.. 그렇게 어렵진 않았다.
네이버 리다이렉트 url에서 state를 요구하는데 이게 csrf 토큰을 넣어줘야하는건지 정확히 파악을 못하겠다. 좀 더 알아봐야 할 것같은데 일단 임시로 아무값이나 넣어줘도 로그인이 되긴 한다.
백엔드 없이 프론트혼자서 진행해보려면
React를 활용한 OAuth 2.0 카카오 로그인 연동
이분 블로그를 참고해도 좋을것 같다.
참고
'React' 카테고리의 다른 글
React 파일 이미지 최적화 하기 react-image-file-resizer (2) | 2024.07.10 |
---|---|
React Hook Form를 활용해 폼 관리 하기 + zod (1) | 2024.06.21 |
React-Query 사용하기 (1) | 2024.06.10 |
리액트 랜딩페이지 제작하기 1일차 (0) | 2024.03.22 |
React-quill 사용해서 에디터 구현하기 (1) | 2024.03.14 |