(파이널 프로잭트 때 사용한 인증 폼)
웹앱을 이용함에 있어 가장 기본이 되는 건 바로 회원가입, 로그인이다. 요즘은 로그인이 점점 간소화 되는 추세인 것 같다. 지금 내가 쓰는 벨로그를 예를 들면 비밀 번호를 입력할 필요도 없이 이메일 인증으로만 회원가입, 로그인을 진행한다. 이로인해 유저들의 편의성을 높이고 이것 또한 우리 페이지를 다시 찾아주는 데 영향을 주지 않을까 싶었다.
그래서 우린 이 방식을 도입하기로 결정하였다.
정말 매력적인 인증 방식이었지만, 처음엔 도대체 어떻게 해야할지 감이 오지 않았다. 혼자 백앤드를 맡고있는 상황이라 오롯이 혼자서 이 방법을 생각하고 구현해야 한다는게 힘들었다.
먼저 벨로그 로그인시 오는 이메일을 분석했다. 이메일 안 링크에 파라미터로 authCode가 담겨져 있었고 링크를 따라 들어가면 그대로 바로 로그인이 되었다. 과거 소셜로그인 스프린트를 할 때의 그 메커니즘과 비슷한것 같았다. 서버에 그랜트코드를 주면 토큰을 내어주는!그래서 정리를 해보기를
이메일을 입력하고 인증 버튼을 누른다 -> 서버 이메일 인증 API로 이메일을 전송 -> 서버에서 그랜트코드 생성 후 사용자 이메일로 그랜트 코드를 담은 리다이렉트 링크 전송 -> 클라이언트에서 코드를 받은 후 서버의 회원가입 또는 로그인 API 전송 -> 코드 비교 후 토큰 전달
위 방식대로 하면 될거라 단순하게 생각했는데 여러가지 변수들이 존재했다.
NodeMailer
일단 이메일을 보내기 위해 NodeMailer를 선택했다.
ejs
인증 이메일 폼을 만들기 위해 ejs를 선택했고 변수를 주어 회원가입/ 로그인 폼이 다르게 가도록 설정하였다.
구글을 선택했다. 이번에 구현할 때 사용한 방식은 보안수준이 낮은 앱의 액세스를 허용하는 방식으로 만약 보안이 신경쓰인다면 OAuth 방식을 추천한다.
먼저 내 구글 계정에 대한 액세스 허용을 해준다.
구글 계정이 만약 2단계 인증을 하고 있는 경우라면 내 구글 계정 -> 보안 탭으로 들어간다
앱 비밀번호를 눌러 생성해주고 나오는 비밀번호는 나중에 노드메일러에서 사용 될 예정이다
모두 연결되는 코드이지만 설명을 위해 떼어 놓겠다.
간단한 유효성 검사
import nodemailer from 'nodemailer';
import ejs from 'ejs';
import dotenv from 'dotenv';
import { Users } from '../../models/user';
dotenv.config();
const authEmail = async (req, res) => {
const { email } = req.body;
const vaildCheck = email.indexOf('@');
if (!email || email.length === 0 || vaildCheck === -1) {
return res.status(400).json({message: 'Need accurate informations'})
};
이메일 형식으로 보내지 않았을 때 400 응답을 보내 주었다.
그리고 앞서 얘기한 변수들을 고려하여 그에 알맞는 코드 작성.
let authCode = String(Math.random().toString(36).slice(2)) //? 랜덤 문자열 생성
let action = ''; //? 회원가입/ 로그인을 구분하기위한 변수
let endPoint = ''; //? 상황에 따른 리다이렉트 엔드포인트
let display = ''; //? 상황에 따른 이메일 인증 폼
//? 만약 이미 존재하는 유저라면 로그인 폼으로 아니라면 회원가입 폼으로.
const isUser = await Users.findOne({where:{ email }}).then(async (data) => {
if (data) {
//? 존재하지만 회원가입이 완료 되지 않았을 떄 status code는 0
const status = Number(data.getDataValue('status'));
//? 0일 때 다시한번 authCode를 갱신하여 회원가입 이메일을 보내고
if (status === 0) {
await Users.update({ authCode }, {where: { email }});
//? 1시간이 지나도 회원가입 완료하지 않을 시 자동으로 데이터 파괴
setTimeout(async () => {
await Users.findOne({where: { authCode }}).then( async (data) => {
if (data) {
const status = Number(data.getDataValue('status'));
const email = String(data.getDataValue('email'));
if (status === 0) {
await Users.destroy({where: { email }});
}
}
});
}, 60 * 60 * 1000);
action = '회원가입';
endPoint = 'signup';
return false;
} else {
await Users.update({ authCode }, {where: {email}});
//? 로그인 으로 진행할 때 1시간 후 자동으로 authCode -> null.
setTimeout(async () => {
await Users.update({ authCode: null }, {where: { email }})
}, 60 * 60 * 1000);
action = '로그인';
endPoint = 'login';
display= 'none'
return true;
}
} else {
//? 데이터베이스에 정보가 없을 때
const nickName = '시인' + Math.random().toString(36).slice(2);
//? 회원가입 전 임시 데이터를 만들어 준다.
//? 만약 링크를 누른다면 signUp 메소드에서 status -> 1(회원).
await Users.create({ email, nickName, introduction: null, authCode, status: 0, avatarUrl: null });
//? 1시간 안에 완료하지 않을 시 데이터 자체를 파괴.
setTimeout(async () => {
await Users.findOne({where: { authCode }}).then( async (data) => {
if (data) {
const status = Number(data.getDataValue('status'));
const email = String(data.getDataValue('email'));
if (status === 0) {
await Users.destroy({where: { email }});
}
}
});
}, 60 * 60 * 1000);
action = '회원가입';
endPoint = 'signup';
return false;
}
});
첫 구현이라 겹치는 코드가 많아서 복잡해 보인다. 다음은 인증 이메일을 보내는 코드이다.
//? ejs를 이용한 인증이메일 폼.
let authEmailForm;
//? 리다이렉선을 위한 코드
//?리다이렉선을 하고싶다면 .env 에서 수정
const clientAddr = process.env.CLIENT_ADDR || 'https://localhost:3000'
//? ejs 모듈을 이용해 ejs 파일을 불러온다.
//? ejs 에 담기는 변수들은 위 코드에서 경우에 따라 설정 된 상태로 올 것이다.
ejs.renderFile(__dirname + '/authForm/authMail.ejs', { clientAddr, authCode, action, endPoint, display }, (err, data) => {
if (err) console.log(err);
authEmailForm = data;
})
위에서 보내줄 폼을 설정 ejs에 대한 간략한 설명은 나중에.
//? 메일을 보내는 코드. 사용할 플렛폼에서 권한 설정 부터!
const transporter = nodemailer.createTransport({
//? 아래와같이 설정해준다
service: 'gmail',
host: 'smtp.gmail.com',
port: 587,
secure: false,
//? 여기엔 아까 생성한 앱 비밀번호와 이메일을 입력해준다.
auth: {
//? dotenv 환경변수를 이용하는 편이 보안에도 좋다
user: process.env.NODEMAILER_USER,
pass: process.env.NODEMAILER_PASSWD,
},
});
await transporter.sendMail({
from: `BBBA <tkdfo93@gmail.com>`, //? 보내는 사람 이메일 정보
to: email, //? 받는 사람 이메일 역시 변수로 설정 해둔 상태
//? 경우에 따른 메시지
subject: isUser ? 'NHB에 로그인을 완료해주세요!' : 'NHB의 회원이 되어주세요!',
html: authEmailForm,
}, (error, info) => {
if (error) {
console.log(error);
}
res.status(200).json({"message": action});
//? 전송을 끝내는 메소드
transporter.close();
});
};
여러 경우를 따지니 길고 복잡해지긴 했다.
다음은 ejs 폼을 설정해 볼 차례이다. ejs는 html에 <% %>등의 키워드를 사용하여 변수를 줄 수 있는게 장점이다. 이해가 어렵다면 위의 코드와 차근차근 비교하면서 보는게 좋다.
<html>
<body style="padding: 0; margin: 0; box-sizing: border-box; width: 920px;">
<div style="font-size: 50px; font-weight: bold; margin: 10px 10px;">NHB</div>
<div style="width: 920px;">
<% if(display !== 'none') {%>
<div style="overflow:scroll; width:auto; height:600px; padding:10px; border: 1px solid black; border-radius: 10px; margin: 10px 10px;">
서비스 이용약관
<br>
<br>
제 1 조 (목적)
<br>
이 약관은 BBBA(이하 '회사')이 제공하는 서비스의 이용과 관련하여 '회사'와 회원과의 권리, 의무 및 책임사항, 기타 필요한 사항을 규정함을 목적으로 합니다.
<br><br>
생략
(시행일) 이 약관은 서비스 화면에 게재한 후 즉시 시행합니다.<br>
</div>
<div style="margin: 5px 10px; font-size: 20px; font-weight: bold;">회원가입을 완료하면 자동으로 동의가 됩니다!</div>
<% } %>
<a href="<%= clientAddr %>/<%= endPoint %>?authCode=<%= authCode %>"" style="margin: 5px 10px; font-size: 16px;">이곳을 눌러 <%= action %>을 완료해주세요</a>
<p style="color:gray; margin: 5px 10px">위 링크는 1시간 후에 만료됩니다.</p>
</div>
</body>
</html>
먼저 if를 이용하여 회원가입시 동의서를 보이도록 하였다. 그리고 변수를 주어 리다이렉트 링크를 설정해주었다. 다른 건 그냥 html을 작성하는 것과 같다.
이런저런 사이트를 참고하면서 하니 생각보다 어렵진 않았다. 그러나 확실히 벨로그 처럼 링크를 줘서 리다이렉트 시키는 방식의 포스팅은 나오지 않았다. (내가 못 찾았을 수도) 그래서 조금 걱정되는 것은 '이 방식을 선호하지 않는 것인가?' 의문점을 가졌다. 자료가 없다면 그럴 만한 이유가 있을 텐데 말이다. 벨로그의 로그인 방식엔 내가 놓치고 있는 조금 더 특별한 무엇이 있나 싶기도 하다. 끊임 없이 고민을 해봐야 할 것 같다.