항해 99 78일차 12/05일
오늘은 카카오톡 소셜로그인을 프론트와 맞춰봤다.
기존 코드
import { Router } from 'express';
import passport from 'passport';
import dotenv from 'dotenv'
import callback from '../../utils/kakaojwt'
import passportCogfig from '../../passport/index';
//패스포트 임포트 해줘야 가능하다.
passportCogfig();
dotenv.config()
const router = Router();
// kakao로 요청오면, 카카오 로그인 페이지로 가게 되고, 카카오 서버를 통해 카카오 로그인을 하게 되면, 다음 라우터로 요청한다.
router.get('/',passport.authenticate('kakao', {scope: ['profile_nickname', 'profile_image'],})); //scope 속성
router.get('/callback'callback)
//index.ts
import kakao from './kakaoSrategy';
export default () => {
kakao(); //카카오 실행
};
//kakaoSrategy.ts
import passport from 'passport';
import dotenv from 'dotenv';
import { User } from '../database/models/user';
import { profile } from '../interface/user';
dotenv.config();
const KakaoPassport = require('passport-kakao').Strategy;
//Strategy => 카카오톡 스토리지 안에있는 클래스 적용
//KakaoPassport => 인터페이스 및 클래스 사용
export default () => {
//passport 카카오가 정한 인터페이스
passport.use(
new KakaoPassport(
{
clientID: process.env.KAKAO_ID!,
callbackURL: process.env.KAKAO_URL!,
},
async (accessToken: string, refreshToken: string, profile:profile, done: any) => {
try {
const profileImg = profile._json.properties.profile_image;
const kakaoId = profile._json.id;
const nickname = profile._json.properties.nickname;
const exUser = await User.findOne({
// 카카오 플랫폼에서 로그인 했고 & snsId필드에 카카오 아이디가 일치할경우
where: { kakaoId, provider: 'kakao' },
});
// 이미 가입된 카카오 프로필이면 성공
if (exUser) {
done(null, exUser); // 로그인 인증 완료
} else {
// 가입되지 않는 유저면 회원가입 시키고 로그인을 시킨다
const newUser = await User.create({
kakaoId: kakaoId,
nickname: nickname,
provider: 'kakao',
profileImg: profileImg,
});
done(null, newUser); // 회원가입하고 로그인 인증 완료
}
} catch (err) {
console.error(err);
done(err);
}
}
)
);
};
//kakaojwt.ts
import { NextFunction, Request, Response } from "express";
import passport from "passport";
import jwt from "jsonwebtoken";
import { Users } from "../interface/user";
import bcrypt from 'bcrypt';
import User from "../database/models/user";
export default(req:Request, res:Response, next:NextFunction) => {
try {
console.log("test");
passport.authenticate(
"kakao",
{ failureRedirect: "/login" }, // 실패하면 '/user/login''로 돌아감.
async (err:Error, user:Users) => {
console.log(user)
if (err) return next(err);
const { userId } = user;
const accessToken = jwt.sign(
{ userId: userId },
process.env.JWT_KEY!,
{ expiresIn: "3h" }
);
const refreshToken = jwt.sign(
{ userId: userId },
process.env.JWT_KEY!,
{ expiresIn: "7d" }
);
const salt = bcrypt.genSaltSync(Number(process.env.SALT_ROUND!));
const RefreshToken = bcrypt.hashSync(refreshToken, salt);
await User.update({RefreshToken}, {where : {userId}})
res.cookie("refreshToken", refreshToken);
res.cookie("accessToken", accessToken);
// result = { userId, accessToken, refreshToken, nickname };
res.status(200).json({
message: "로그인 성공",
accesstoken: accessToken,
refreshtoken: refreshToken,
});
// res.redirect(
// );
}
)(req, res, next);
} catch (error) {
next(error);
}
};
프론트와 맞춰보기 전에 예상을 했을 때 짯던 코드는
프론트에서 우리 /kakao로 요청을 보내면 /kakao get 라우터에 있는 passport.authenticate가 실행 되며 index.ts로 이동해서 kakao()함수를 실행한다.
그러면 kakaoSrategy.ts로 이동을 해서 로직을 수행하고 kakao/callback을 호출하면서 kakaojwt.ts에있는 토큰 발급 로직과 쿠키에 토큰을 전송 해주는 방식으로 회원가입과 로그인을 백앤드에서 동시에 처리하는 로직으로 구현 하였고,
프론트는 인가코드를 받아서 백앤드로 보내주는 로직으로 구현이 되어있기도 하고 백앤드에서 짠 로직으로 맞춰보았지만 페이지 리다이렉트 되지 않고 오류를 뱉어내며 여러가지 문제가 많았다.
그래서 프론트에서 구현 했는 프론트에서 직접 카카로 api로 호출해서 로그인을 하면 인가 코드를 백엔드로 보내주는 방식으로 구현을 했다.
//kakaojwt.ts
import jwt from "jsonwebtoken";
import bcrypt from 'bcrypt';
import axios from "axios";
import { NextFunction, Request, Response } from "express";
import User from "../database/models/user";
import { Unauthorized } from "./exceptions";
export default async (req:Request, res:Response, next:NextFunction) => {
try{
//토큰 생성 및 토큰 전달 함수
const kakaojwt = async (userId:number)=>{
const AccessToken = jwt.sign(
{ userId: userId },
process.env.JWT_KEY!,
{ expiresIn: "3h" }
);
const RefreshToken = jwt.sign(
{ userId: userId },
process.env.JWT_KEY!,
{ expiresIn: "7d" }
);
const salt = bcrypt.genSaltSync(Number(process.env.SALT_ROUND!));
const refreshToken = bcrypt.hashSync(RefreshToken, salt);
await User.update({refreshToken}, {where : {userId}})
res.cookie("refreshToken", RefreshToken);
res.cookie("accessToken", AccessToken);
res.status(200).json({
message: "로그인 성공",
accesstoken: AccessToken,
refreshtoken: RefreshToken,
})
}
const {authorization}=req.headers;
if(!authorization) throw new Unauthorized("토큰 정보가 없습니다.")
const { data: kakaoUser } = await axios('https://kapi.kakao.com/v2/user/me', {
headers: {
Authorization: `Bearer ${authorization}`,
},
}); //유저 정보를 받아온다
console.log(kakaoUser,"<=카카오 에서 받아옴")
const profileImg:string = kakaoUser.properties.profile_image
const kakaoId:number = kakaoUser.id
const nickname:string = kakaoUser.properties.nickname
const provider:string = 'kakao'
const email:string = kakaoUser.kakao_account.email
const exUser = await User.findOne({
where : {kakaoId, provider}
})
if(!exUser){
const newUser = await User.create({kakaoId,nickname,provider,profileImg,email});
console.log(newUser,"<=저장한 값")
const { userId } = newUser;
console.log("회원가입 했어여~")
kakaojwt(userId)
}else{
const { userId } = exUser
console.log("회원가입 되어있어여~")
kakaojwt(userId)
}
}catch(err){
console.log(err)
next(err)
}
}
흐름을 보면
1.프론트에서 인가 코드를 받아서 헤더에 authorization키값에 인가 코드를 담아서 백앤드로 넘겨준다.
2.req.headers로 받아온 토큰을 구조분해 할당으로 authorization 뽑아내서 https://kapi.kakao.com/v2/user/me로 인가 코드를 담아서 axios콜을 보내고 토큰에 해당하는 유저정보를 받아온다.
3.받아온 정보를 가지고 유저가 있는지 확인 한다. 유저가 없다면 회원가입을 시키고 kakaojwt함수에 userId를 담아서 실행 시킨다.
3-1. 등록된 회원이라면 바로 kakaojwt 함수에 userId를 담아서 실행 시킨다.
4. 프론트는 응답값에 있는 status로 구분해서 200이면 메인페이지 , 그 외 다른 코드면 로그인 페이지로 리다이렉트 시킨다.
프론트와 맞춰가면서 코드를 짜면 좋았을꺼 같지만 서로 처음 구현해보는 api이고 다양한 시도를 하다가 구현한 방식이 달라서 나는 문제였고 다시 보면 그렇게 어려웠던 문제는 아닌거 같다.