오늘은 JWT를 이용한 모바일 인증을 구현하기로 해서 관련된 레퍼런스를 찾아보고 상세히 설명해주는 영상을 보면서 구현했다. 그리고 인증하는 부분을 위한 서버를 따로 분리하기로 해 그것도 실행했다.
먼저 모바일 인증을 하는 방법을 여러가지 인데 우리 팀은 우리가 이미 한번 구현해보고 더 익숙한 JWT를 사용해 인증을 구현하기로 했다.
JWT의 핵심은 토큰을 발급해 그 토큰을 가지고 있는지 없는지, 있다면 토큰의 정보가 일치하는지 안 하는지로 인증을 해준다. 일단 인증 부분이 필요한 로그인 부분에서 이 파트를 구현하기로 했다.
토큰을 구현하는 부분에 있어 액세스와 리프레쉬 토큰을 사용해주기로 했는데 액세스 토큰은 말그대로 접속을 할 때 접근 권한을 주는 토큰이고 리프레쉬 토큰은 이 액세스 토큰이 만료되었을 때 요청이 오면 새로 액세스 토큰을 보내주는 역할을 한다. 그리고 이 리프레쉬 토큰이 만료되면 이제 로그아웃이 되어 다시 로그인이 필요한 것이었다. 각 토큰에는 maxAge라는, 만료 기한을 설정해줄 수 있다.
res.cookie("accessToken", accessToken, { maxAge: 1000 * 60 * 60 * 24, signed: true });
api.use(cookieParser(process.env.TOKEN_KEY))
이런식으로 설정을 해주는 것인데 cookie-parser라는 모듈을 사용해서 쿠키에 담아줄 수 도 있다. signed 부분은 토큰을 쿠키에 담을 때 외부에서 볼 수 없게 암호화 해주는 것인데 cookieParser를 적용할 때 괄호 안에 넣어주는 키를 기반으로 서명 (암호화)을 해주는 것이다. 저걸 읽으려면 res.cookie 가 아닌 res.signedCookies로 봐야 한다.
https://www.youtube.com/watch?v=mbsmsi7l3r4
이 강좌가 많은 도움이 되었다!
흐름을 따라서 알맞게 토큰을 만들어 주었다. 중요한 정보를 담아주는 곳으로 쿠키에 담아두기로 한 이유는 만약에 토큰이 아닌 정말로 중요한 금융정보 같은, 매우 귀중한 정보였다면 키스토어나 키체인 같이 체계적인 보안이 된 저장소에 저장을 했을 텐데 우리의 경우 로그인 상태 유지를 위한 정보 저장이었기 때문에 로컬 쿠키에 저장해도 괜찮을 것이라 판단했다.
const router:express.Router = express.Router();
const refresKey:any = process.env.JWT_SECRET_REFRESH;
const generateAccessToken = (payload:{id:number, nickname:string, email:string}) => {
const accessKey:any = process.env.JWT_SECRET_ACCESS;
const options:{expiresIn:string} = { expiresIn: "1d" };
return jwt.sign(payload, accessKey, options);
};
router.post("/signin", async (req:express.Request, res:express.Response) => {
const { email, password }:{email:string, password:string} = req.body;
if (!email) {
res.status(409).send("Email is required");
return;
}
if (!password) {
res.status(409).send("Password is required");
return;
}
try {
const user:User|undefined = await getConnection()
.createQueryBuilder()
.select("user")
.from(User, "user")
.where("user.email = :email", { email })
.getOne();
// ! 유효하지 않은 이메일
if (!user) {
res.status(401).send("Invalid Email");
return;
}
// ? 암호화 후 비교
const pdkdf2Promise:Function = util.promisify(crypto.pbkdf2);
const key:Buffer = await pdkdf2Promise(password, user.salt, 105123, 64, "sha512");
const encryPassword:string = key.toString("base64");
if (encryPassword !== user.password) {
res.status(401).send("Incorrect Password.");
return;
}
// ! 토큰 발급
const payload:{id:number, nickname:string, email:string} = {
id: user.id,
nickname: user.nickname,
email: user.email,
};
// ? accessToken
const accessToken = generateAccessToken(payload);
// ? refresh Token
// const refresKey:any = process.env.JWT_SECRET_REFRESH;
const refreshToken = jwt.sign({ id: user.id }, refresKey, { expiresIn: "30d" });
const result:UpdateResult = await getConnection().createQueryBuilder()
.update(User).set({ refreshToken })
.where({ id: user.id })
.execute();
if (result.raw.affectedRows === 0) {
res.status(409).send("Failed to insert Token");
return;
}
// * token을 어디에 저장할것인가?
// * -> 일단 cookie
res.clearCookie("refreshToken");
res.cookie("accessToken", accessToken, { maxAge: 1000 * 60 * 60 * 24, signed: true });
res.cookie("refreshToken", refreshToken, { maxAge: 1000 * 60 * 60 * 24 * 30, signed: true });
res.status(201).send("User signed in");
} catch (e) {
console.log(e);
res.status(400).send(e);
}
});
서버 분리는 포트를 새로 파서 인증을 포함한 부분만 따로 옮겨놓았다. 조금 번거로운 작업이었지만 그래도 수월하게 분리할 수 있었다. 프런트 분들은 꾸준히 열심히 코드를 짜고 계셨고 이제 에러들을 잡고 마무리 하면 프런트로 넘어갈 수 있을 것 같다.