-
OAuth2 사용해서 react와 함께 소셜로그인 기능 만들기NODE.JS 2021. 10. 29. 21:58
로그인 창에서 많이 볼 수 있는 소셜로그인 기능을 만들어보자.
(spring-react에 이어 두번째 소셜 로그인!)
여기서는 카카오에서 code부분을 access_token으로 생각해서 굉장히 헤메였다...세상에나 마상에나 이런짓을...😵💫
(결국 code로 다시 access_token 받아와서 잘 해결했음!)
REST API
Node js에서 소셜로그인을 구현해내는 방법에는 sdk를 이용한 방법도 존재하지만 Rest api를 통해서 구현하기로 했다.
(백엔드에서 받아서 받은 토큰으로 사용자 정보를 처리할 수 있게)
리액트에서 모든과정은 authorization_code를 받고 나서 access_token을 받아오도록 하고 싶었으나 frontend에서 토큰을 가져오려 시도하면 네이버는 cors오류가 발생했다. 그래서 부득이하게 네이버는 response_type=token으로 설정하여 진행했다. )
(여기서 프론트엔드에서 해당 URL을 부르면 cors오류가 발생한다는 것은 다음 사이트를 참고하여 알게 되었다)
https://developers.naver.com/forum/posts/29165
https://forum.worksmobile.com/kr/posts/100025
프로젝트 계정 만들기
https://dodop-blog.tistory.com/249
프로젝트 생성 과정은 전에 했던 방법과 매우 동일하므로 생략...!
(redirect_uri 는 http://localhost:3000/auth/소셜 코드로 만들어 일단 프론트에서 코드를 받았다)
env.js 생성
export const GOOGLE_CLIENT_ID = "구글 클라이언트 id" export const GOOGLE_CLIENT_SECRET = "구글 클라이언트 secret" export const KAKAO_CLIENT_ID = "카카오 클라이언트 id" export const KAKAO_CLIENT_SECRET = "카카오 클라이언트 secret" export const NAVER_CLIENT_ID = "네이버 클라이언트 id" export const NAVER_CLIENT_SECRET = "네이버 클라이언트 secret" //자신이 설정한 redirect_uri의 공통된 부분 export const OAUTH_REDIRECT_URI = "http://localhost:3000/auth/" //구글에서는 scope를 email, profile로 설정 (token 바로 받아온다) export const GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/auth?client_id=" + GOOGLE_CLIENT_ID + "&redirect_uri=" + OAUTH_REDIRECT_URI + "google" + "&scope=https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile" + "&response_type=token" + "&include_granted_scopes=true" //카카오에서는 response_type을 코드로 하여 보안코드를 먼저 받아왔다. export const KAKAO_AUTH_URL = "https://kauth.kakao.com/oauth/authorize?client_id=" + KAKAO_CLIENT_ID + "&redirect_uri=" + OAUTH_REDIRECT_URI + "kakao" + "&response_type=code" + "&scope=profile_nickname,profile_image,account_email,gender" export const KAKAO_TOKEN_URL = "https://kauth.kakao.com/oauth/token?client_id=" + KAKAO_CLIENT_ID + "&client_secret=" + KAKAO_CLIENT_SECRET + "&redirect_uri=" + OAUTH_REDIRECT_URI + "kakao" + "&grant_type=authorization_code" + "&code=" //네이버에서도 프론트엔드에서 토큰을 받는 것이 불가능했기에 response_type을 토큰으로 해서 먼저 가져왔다. export const NAVER_AUTH_URL = "https://nid.naver.com/oauth2.0/authorize?client_id=" + NAVER_CLIENT_ID + "&redirect_uri=" + OAUTH_REDIRECT_URI + "naver" + "&response_type=token" + "&state=state" export const NAVER_TOKEN_URL = "https://nid.naver.com/oauth2.0/token?client_id=" + NAVER_CLIENT_ID + "&client_secret=" + NAVER_CLIENT_SECRET + "&redirect_uri=" + OAUTH_REDIRECT_URI + "naver" + "&grant_type=authorization_code" + "&code="
authorization code 받아오는 URL과 token을 받아오는 URL을 프론트엔드 env.js파일에 생성해주었다. (gitignore를 설정하여 노출되지 않도록 했다)
페이지 생성해주고 받은 토큰 넘겨주기
<Route exact path="/auth/:social" component={SocialLogin}/>
function SocialLogin(props) { const socialName = props.match.params.social //해당부분은 response_type을 어떤걸로 했느냐에 따라서 달라졌다. var params = socialName === "google" ? props.history.location.hash.substring(1) : socialName === "kakao" ? props.history.location.search.substring(1) : props.history.location.hash.substring(1) const dispatch = useDispatch() useEffect(() => { getAccessToken(params) }, [params]) const getAccessToken = async(params)=>{ if(socialName === "google"){ params = params.split("&") var param = new Array() var key, value for(var i = 0; i<params.length; i++){ key = params[i].split("=")[0] value = params[i].split("=")[1] param[key]=value } dispatch(socialLogin(socialName, param["access_token"])) }else if(socialName === "kakao"){ await axios.post(KAKAO_TOKEN_URL+params.substring(5)) .then(res=>{ dispatch(socialLogin(socialName, res.data.access_token)) }) }else if(socialName === "naver"){ params = params.split("&") var param = new Array() var key, value for(var i = 0; i<params.length; i++){ key = params[i].split("=")[0] value = params[i].split("=")[1] param[key]=value } dispatch(socialLogin(socialName, param["access_token"])) } } return ( <div> </div> ) } export default SocialLogin
google과 naver는 token그 자체를 넘겨주도록 설정했으므로 hash로 받아주고, kakao는 보안코드를 search파트로 넘겨서 받았다.
constant, action, reducer
export const SOCIAL_LOGIN_REQUEST = 'SOCIAL_LOGIN_REQUEST' export const SOCIAL_LOGIN_SUCCESS = 'SOCIAL_LOGIN_SUCCESS' export const SOCIAL_LOGIN_FAIL = 'SOCIAL_LOGIN_FAIL'
export const socialLogin = (socialName, access_token) => async (dispatch)=>{ dispatch({ type:SOCIAL_LOGIN_REQUEST }) try{ const {data} = await axios.post('/api/users/socialLogin', {socialName, access_token}) console.log(data) dispatch({ type:SOCIAL_LOGIN_SUCCESS, payload:data }) localStorage.setItem("userInfo", JSON.stringify(data)) }catch(error){ dispatch({ type:SOCIAL_LOGIN_FAIL, payload: error.response && error.response.data.message ? error.response.data.message : error.message }) } }
case SOCIAL_LOGIN_REQUEST: return {loading:true} case SOCIAL_LOGIN_SUCCESS: return {loading:false, userInfo:action.payload} case SOCIAL_LOGIN_FAIL: return {loading:false, error:action.payload}
해당 action에 대한 변화를 저장해주는 action, reducer를 만들어준다.
Route
userRouter.post('/socialLogin', async(req, res)=>{ try{ const {socialName, access_token} = req.body var fullname, username, email, password, avatar, gender if(socialName === "google"){ const res = await axios.get(`https://www.googleapis.com/oauth2/v1/userinfo?&access_token=${access_token}`) //토큰으로 받아온 정보를 처리 }else if(socialName === "kakao"){ const res = await axios.get("https://kapi.kakao.com/v2/user/me", { headers: { Authorization: `Bearer ${access_token}` } }) //토큰으로 받아온 정보를 처리 }else if(socialName ==="naver"){ const res = await axios.get('https://openapi.naver.com/v1/nid/me',{ headers: { Authorization: `Bearer ${access_token}` } }) //토큰으로 받아온 정보를 처리 } let newUserName = username.toLowerCase().replace(/ /g, '') var user = await User.findOne({email:email}) var token; //이미 존재하는 사용자라면 로그인한 매체가 다를때 오류르 발생시키고, 아니라면 변한 정보를 저장한다. if(user){ if(user.social !== socialName){ return res.status(400).json({message:"You already have an acoount with this email."}) } if(user.avtar!== avatar || user.fullname !== fullname){ user.avatar = avatar user.fullname = fullname user = await user.save() } token = generateToken(user) }else{ //존재하지 않는 사용자라면 정보를 가지고 회원가입 시킨다. const createdUser = new User({fullname, username:newUserName, email, password, social:socialName, gender}) user = await createdUser.save() token = generateToken({id:createdUser._id}) } res.status(200) .send({ token, user:{ ...user._doc, password:'' }, msg:"Social Login Success" }) }catch(err){ return res.status(500).json({message:err.message}) } })
넘겨받은 토큰을 가지고 정보를 가져오도록 한다.
로그인 페이지에 버튼 추가
<div className="auth_social_buttons"> <button className="auth_social_button" style={{backgroundColor:"#3266cf"}}> <a href={GOOGLE_AUTH_URL}> <div style={{color:"white"}}> Continue with GOOGLE </div> <div className="auth_social_button_image"> <img src={google} alt="" /> </div> </a> </button> <button className="auth_social_button" style={{backgroundColor:"#F5DA0C"}}> <a href={KAKAO_AUTH_URL}> <div style={{color:"#2E1413"}}> Continue with KAKAO </div> <div className="auth_social_button_image" style={{border:"2px solid #2E1413"}}> <img src={kakao} alt="" /> </div> </a> </button> <button className="auth_social_button" style={{backgroundColor:"#1AC049"}}> <a href={NAVER_AUTH_URL} style={{color:"white"}}> <div> Continue with NAVER </div> <div className="auth_social_button_image" style={{border:"2px solid white"}}> <img src={naver} alt="" /> </div> </a> </button> </div>
만들어 놓은 해당링크로 버튼을 생성해주면 완료!
( 참고한 사이트 )
https://dukdukz.tistory.com/entry/카카오-로그인-API-사용
https://velog.io/@nara7875/Node.js-kakao-login-api-%EA%B0%80%EC%A0%B8%EC%98%A4%EA%B8%B0
https://devhaks.github.io/2019/05/31/oauth2/
https://developers.naver.com/docs/login/api/api.md
https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api
https://eventhorizon.tistory.com/26
https://www.daleseo.com/google-oauth/
'NODE.JS' 카테고리의 다른 글
Social Media 만들기 - 22) Call 기능 만들기 ( Web RTC사용하기 (peer)) (0) 2021.08.08 Social Media 만들기 - 21) message 기능 만들기 (intersection Observer(infiniteScrolling) , online-offline 정보 가져오기) (0) 2021.08.07 (mongoDB) findOneAndUpdate가 update된 항목을 return 하지 않을 때 (0) 2021.08.05 Social Media 만들기 - 20) socket io 사용(notify 생성하고 전달하기) (0) 2021.08.04 Social Media 만들기 - 18) SavePost 구현하기 (0) 2021.08.01