보통 웹서비스를 이용하다보면 다음과 같은 상황을 자주 마주한다.
로그인하지 않고 이용하다가 특정 기능을 이용하면 로그인이 필요해서 로그인창으로 이동되고 로그인을 마치면 원래 다시 작업하던 페이지로 돌아간다.
어떻게 이 기능을 구현할 수 있을까?
나 다시 돌아갈래!!!
두 가지를 활용해서 생각보다 쉽게 구현할 수 있었다.
리소스가 요청된 주소 정보를 담고 있는 헤더
이 헤더 정보를 사용하면 서버에서는 이 위치로의 요청이 어디서 발생했는지 알 수 있어서 데이터 분석, 로깅, 캐싱에 활용할 수 있다고 한다.
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referer
클라이언트와 서버의 연결 정보를 저장하는 방법
HTTP 프로토콜을 비연결을 지향하는 무상태 프로토콜로 기본적으로 한 번의 요청 사이클이 끝나면 서버에서 클라이언트의 정보를 알 수 없다.
그렇지만 클라이언트의 상태를 서버에서 저장할 필요가 있는데 이때 세션을 활용할 수 있다.
개발 중인 서비스는 모든 로그인은 소셜로그인으로 구현하고 있다.
OAuth로 로그인 로직을 구현하는 경우는 웹 서비스가 별도의 로그인 페이지를 가지는 것과 다르게 인증서버, 리소스서버로 리다이렉션이 여러 번 이루어진다.
로그인 페이지에서 모든 로그인 로직이 끝나는 경우라면 Referer 헤더만 가지고도 구현하고자하는 기능을 만들 수 있지만,
여러 번 리다이렉션 요청이 있다면 모든 로그인 로직이 끝나고도 처음 요청이 들어왔던 위치를 기억하고 있어야하기 때문에 세션을 이용하였다.
처음 github 로그인 요청이 들어오면 아래 컨트롤러에서 처리하게 된다.
@Get('github')
async githubProxy(@Res() res: Response, @Req() req: Request) {
req.session['returnTo'] = req.headers.referer || '/';
const redirectUrl = await this.githubService.authProxy();
return res.redirect(redirectUrl);
}
Request 헤더에서 referer 정보를 꺼내서 세션에 저장해준다.
그리고 브라우저에서 github 로그인이 끝나면 아래 컨트롤러로 리다이렉트 된다.
@Get('github-callback')
async githubCallback(
@Req() req: Request,
@Res() res: Response,
@Query('code') code: string,
) {
/*
*
******** 로그인 로직 ********
*
*/
const returnTo = req.session['returnTo'];
delete req.session['returnTo'];
res.redirect(HttpStatus.NOT_MODIFIED, returnTo);
}
이때 세션에 저장해두었던 referer 정보를 꺼내서 그 위치로 리다이렉트 응답을 보내면 완성된다.
한 번 사용한 referer 정보는 나중에 재사용될 수 없게 삭제처리했다.
참고
Documentation | NestJS - A progressive Node.js framework
위와 같이 기능을 개발하고 배포를 했다.
배포 환경은 pm2로 작동시키고 있었다.
원래 서버가 1코어 cpu였으므로 문제가 발생하지 않았는데, 2코어 서버로 업그레이드 하고 난 후 문제가 발생했다 😱
서로 다른 프로세스간 메모리가 공유되지 않는다.
현재 옵션으로 세션은 서버의 메모리에 저장되는데 2개의 프로세스에서 앱을 작동시키면 처음 referer 정보를 저장한 서버와 리다이렉션 된 서버가 다르면 referer 정보가 없어 undefind가 발생하고... undefind로 리다이렉트가 되니 당연히! 404 에러가 났다.
전에 세션 방식을 사용한 로그인을 구현할 때는 이런 상황때문에 세션정보를 저장하는 레디스를 두었다.
다른 프로세스간 IPC로 정보를 교환할 수 있지만 잘 사용되지 않는 방식이기도 하고 또 다중 프로세스가 아니라 다중 서버 환경이라면 적용할 수 없다.
session store에 redis를 넣어주어 세션정보를 redis에 저장했다.
const redisStore = new RedisStore({
client: new Redis({
port: configService.get<number>('REDIS_PORT'),
host: configService.get<string>('REDIS_HOST'),
password: configService.get<string>('REDIS_PASSWORD'),
}),
prefix: 'sess:',
});
app.use(cookieParser());
app.use(
session({
store: redisStore,
secret: configService.get<string>('SESSION_SECRET'),
resave: false,
saveUninitialized: false,
}),
);
http referer 정보 없음…
어디로 돌아갈지 http referer 헤더에서 찾아 세션에 저장해주고 있는데,
http referer 헤더가 들어오지 않는 경우가 있다. 그러면 돌아갈 수 없다.
에러가 나지 않게 default 값 처리를 해두었지만 원하는 동작이 아니다.
@Get('github')
async githubProxy(@Res() res: Response, @Req() req: Request) {
req.session['returnTo'] = req.headers.referer || '/';
req.session.save((err) => {
if (err) this.logger.log(err);
});
const redirectUrl = this.githubService.getAuthUrl();
return res.redirect(redirectUrl);
}
생각난 해결 방법
클라이언트에서 요청시 referer 정보를 항상 넣어서 보내준다.
document.getElementById("local").addEventListener("click", () => {
fetch("http://localhost:4000/auth/github", {
method: 'GET',
headers: {'Referer': '"http://localhost:3000/'},
})
.then(response => response.json())
.then(data => {
console.log(data)
})
.catch(error => console.error('Error:', error));
})
하지만 이렇게 요청을 보내면 CORS 에러가 발생한다.
이것을 해결하는 방법은 서버 프록시를 거처 클라이언트에게 로그인페이지를 보내줄 수 있고
서버코드의 변경이 필요하고, 클라이언트도 매번 보낼때마다 fetch 요청을 보내야한다는 번거로움이 있다
괜찮은 방법인 것 같다!
velog는 어떻게 하고 있을까? 찾아보니 쿼리파라미터로 현재 보고있는 페이지의 경로를 적어주고 있었다!
velog의 방식대로 쿼리파라미터로 돌아갈 위치를 입력받기로 했다.
next로 위치를 받고, 그 정보를 세션에 저장해 주었다.
dev는 로컬에서 개발할 때 편의성을 위해 추가한 파라미터이다. next 파라미터 값으로 demo가 들어온다면, false일때algoitni.site/next
로 리다이렉트하고 true 값을 주게되면 localhost:3000/next
로 리다이렉트하게 된다. default는 false이다.
@Get('github')
async githubProxy(
@Res() res: Response,
@Req() req: Request,
@Query('next') next: string = '/',
@Query('dev') dev: boolean = false,
) {
req.session['returnTo'] = this.getRedirectionPath(dev, next);
req.session.save((err) => {
if (err) this.logger.log(err);
});
const redirectUrl = this.githubService.getAuthUrl();
return res.redirect(redirectUrl);
}
getRedirectionPath(dev: boolean, next: string) {
return dev
? `http://${path.join('localhost:3000', next)}`
: `https://${path.join('algoitni.site', next)}`;
}