사전지식
- HTTP는 Stateless(무상태성) 하다!
→ 서버는 이전 요청과 현재 요청의 관계가 없다고 판단한다.
🔑인증/인가 을 하는 방법
- 요청이 들어온 url을 junsu:qwer1234@ 파싱 후 Base64라는 인코더를 이용해서 인코딩을 한 후에 http request를 전달 (이 부분은 클라이언트. 즉, 브라우저가 한다)
- url에서 로그인에 관한 부분을 파싱한 후에 인코더를 통해서 인코딩한 문자열을 가지고 있음(브라우저가)
- 이를 request header의 Authorization에 넣어서 보내주는 개념.
- 이렇게 브라우저가 http를 통해 서버로 날려주면 서버에서는 DB 체킹을 하고 일치여부를 판다.
- 문제점 : http는 stateless이기에 매번 인증해야 한다.(인증이 필요한 모든 요청에서는 1번 상황을 반복해서 사용해야 함)
- 문제를 해결하기 위해 브라우저의 Strorage를 사용해야 함 (로컬 스토리지, 세션 스토리지, 쿠키)
- 쿠키를 사용하면 쿠키에 사용자 id, pw를 집어 넣어서 사용자가 인증이 필요한 요청을 할 때 이 쿠키를 같이 넣어서 보내준다.
- 그러나 다시 문제점 발생 : 해커한테 쿠키에 사용자의 중요 데이터를 너무 쉽게 노출하게 됨, 클라이언트가 서버보다 보안에 취약하다는 단점!
2. Session 활용하기
- 세션은 인증된 사용자의 식별자(pk 등)와 랜덤한 문자열로 '세션 id'를 만들어서 이를 response header로 넘겨주고 이를 클라이언트가 저장할 수 있도록 한다.
- DB에서 체킹 -> 서버로 {session id : 사용자 식별자}를 응답 해더로 보냄. 클라이언트측에서는 {id: session id} 를 저장
- 장점 : 클라이언트가 사용자의 중요 데이터를 raw한 상태로 가지고 있지 않기 때문에 해커가 정보를 가져가더라도 크게 위험이 없음, 세션의 만료기간을 지정할 수 있다는 것이다. → 만료가 지난 세션은 해커가 가져가더라도 유효하지 않은 정보가 됨. 세션 관리를 서버 자체에서 하기 때문에 만일 탈취가 된 세션을 서버에서 삭제해버리면 세션 자체를 이용하지 못하게 되어서 보안이 향상!
- 문제점 : 로드밸런서를 이용해 서버를 여러 대 두는 상황에서는 처음 인증을 처리했던 한 대의 서버에서만 그 사용자에 대한 세션 정보를 가지고 있기 때문에 다음 요청에서 그 서버로 요청이 오지 않는다면 문제가 발생하게 됨
- 해결 방법
- 세션 스토리지를 사용하게 되면 로드밸런서가 어떤 서버로 요청을 보내더라도 결국에는 모두 하나의 세션 스트로지로 요청이 들어오기 때문에 문제를 피할 수 있다.
- 그러나 클라이언트가 많아진다면 세션 스토리지가 터진다는 또 다른 문제점이 발생한다.
- 각각 클라이언트, 서버, 세션 스토리지(세션 저장소)에서 모두 한 번씩 사용자의 정보를 관리할 수 있게 했더니 문제가 발생했다.
- 클라이언트, 서버, 세션스토리지가 통신할 때 사용하는 http와 서버 자체가 지향하는 rest api가 무상태성을 기초로 하는데, 인증/인가를 할 때는 사용자의 정보, 사용자의 상태를 가지고 있어야 한다. 무상태와 상태성 → 두 개념이 충돌.
3. JWT를 사용하여 인증/인가 하는 방식
- 클라이언트, 서버에 사용자의 정보를 저장하는게 아닌 http 요청과 응답 안에 사용자 상태를 담아보자 → 사용자의 인증/인가를 처리하는게 토큰을 활용한 인증 인가 방식이다.
- 간단한 JWT 소개 :
- JWT는 시크릿 키를 이용해서 만든다.
- JWT 자체는 해독하기 쉬우므로 민간한 정보(비밀번호 등)를 담지 않는다.
- 시크릿 키가 노출이 안되게 하는 것이 중요하기 때무에 서버 내부에서 잘 관리해야 한다.
- 토큰을 이용한 인증 프로세스
- 로그인 요청 → DB 체킹 후 일치하면 시크릿 키 이용해 토큰 생성 → 클라이언트가 엑세스 토큰을 저장해둠 → 이 토큰을 이용해서 요청/응답
- 서버는 토큰에 대한 유효성 검사를 시크릿 키로 진행 → 유효하지 않으면 버리고 유효하면 사용자 정보(사용자 이름 , 만료 시기, 권한 등)를 파악
- AccessToken 만료시간에 도달했으면 RefressToken을 통해 다시 토큰 재발급 → 사용자는 토큰이 만료된지 모른채 계속 서비스를 사용 가능
- 장점 : 로드밸런서를 이용한 서버에서도 문제가 없음. 각 서버에서 시크릿 키만 가지고 있으면 되기 때문. 서버가 확장되더라도(여러대가 추가되더라도) 문제가 없음. 각자 해독을 해서 인증을 하면 됨. 즉, 토큰으로 상태관리를 하기에 따로 세션을 둘 필요가 없다. 효율성이 좋아지고 DB를 찔러도 되지 않기 때문에 속도가 빠르다는 장점이 있다.
- 단점 : AccessToken을 탈취 당하면 해킹의 위험이 됨
- 따라서 AccessToken의 유효기간을 짧게 설정하는데, 정상적인 클라이언트는 유효기간이 끝난 Access Token에 대해 Refresh Token(새로운 Access Token을 발급하기 위한 토큰) 을 사용하여 새로운 Access Token을 발급받을 수 있다.
- 그런데 만약 Refresh Token이 유출되어서 다른 사용자가 이를 통해 새로운 Access Token을 발급받았다면? 이 경우, Access Token의 충돌이 발생하기 때문에, 서버측에서는 두 토큰을 모두 폐기시켜야 한다. 국제 인터넷 표준화 기구(IETF)에서는 이를 방지하기 위해 Refresh Token도 Access Token과 같은 유효 기간을 가지도록 하여, 사용자가 한 번 Refresh Token으로 Access Token을 발급 받았으면, Refresh Token도 다시 발급 받도록 하는 것을 권장하고 있다.
🤔JWT를 이용한 인증/인가 방식을 적용한 이유
- Request Header을 이용하는 방법은 보안상 너무 취약함으로 사용하면 안될 것 같다.
- 세션은 항상 인증 요청을 할 때마다 세션 ID를 세션 저장소에 있는 세션 ID에 비교를 해야 한다. 30분에 1만번의 요청을 한다고 했을 때 I/O는 1만 번 동작해야 한다.
- 즉, 쿠키와 세션을 이용한 방법은 서비스가 확장될 경우 세션 스토리지에 과부하가 올 수 있다.
- 대신, JWT를 이용하면 토큰이 상태를 가지고 있으므로 요청을 할 때마다 인증을 확인하는 추가적인 I/O 요청이 발생하지 않는다. Access Token의 유효시간을 짧게 하여 최소한의 보안성을 보장해준다. (Access Token의 만료 시간이 30분이면 30분당 1번씩 재발급 요청을 하면 된다.)
🤔JWT를 이용하여 로그아웃 하는 방법
- JWT를 통해 로그인을 하는 방법을 적용하였고, 로그아웃을 하는 방법을 찾아보아야 했다.
- 토큰을 만료시키면 로그인이 안 되는 것이니 어떻게 토큰을 만료시키면 좋을지 찾아보았다.
-
클라이언트 Storage에 저장된 토큰 제거
→ 로그아웃 할 경우 프론트엔드 단에서 Storage에 있는 JWT를 Clear 하면 된다. 그러나 이 방법은 만약 유저가 토큰을 미리 카피 했더라면 로그아웃을 성공 했더라도 계속해서 서버에 요청을 보낼 수 있게 된다. (참고 : 한 번 발급된 토큰은 수정을 할 수 없다. 즉, 만료시간을 수정하지 못한다.)
-
블랙리스트 생성
→ 로그아웃 하고 싶은 토큰들을 블랙리스트에 모은다. 그리고 블랙리스에 토큰이 들어오면 해당 토큰을 무효화하는 작업을 진행하는 방법이다.
🤔블랙리스트로 Redis를 사용해야 하는 이유
- 일단 클라이언트 Storage에 저장된 토큰을 제거하는 방식은 보안 상에 안 좋을 것 같아 안될 것 같다.
- 기존에 토큰이 만료되었을 시 JWT에서 Refresh Token을 이용하여 재발급을 해 주는 방식을 알아보자
- 클라이언트가 ID, PW로 서버에게 인증을 요청하고 서버는 이를 확인하여 Access Token과 Refresh Token을 발급합니다.
- 클라이언트는 이를 받아 Refresh Token를 본인이 잘 저장하고 Access Token을 가지고 서버에 자유롭게 요청합니다.
- 요청을 하던 도중 Access Token이 만료되어 더 이상 사용할 수 없다는 오류를 서버로부터 전달 받습니다.
- 클라이언트는 본인이 사용한 Access Token이 만료되었다는 사실을 인지하고 본인이 가지고 있던 Refresh Token를 서버로 전달하여 새로운 Access Token의 발급을 요청합니다.
- 서버는 Refresh Token을 받아 서버의 Refresh Token Storage에 해당 토큰이 있는지 확인하고, 있다면 Access Token을 생성하여 전달합니다.
- 이후 2로 돌아가서 동일한 작업을 진행합니다.
- Refresh Token Storage는 서버에서 Refresh Token을 저장하는 저장소이다.
- 사실상 세션과 별반 차이 없이 특정 Storage에 I/O작업이 발생하게 되기 때문에 Access Token이 지속되는 짧은 시간동안만 I/O작업이 일어나지 않는다이지, 세션의 단점을 하나 가져가는 셈이 됩니다. (물론 위에서 언급 했듯이 예를 들어 30분에 세션은 항상 인증 요청을 해야하지만 JWT는 1번만 인증 요청을 보네긴 함)
- 그렇다면 Refresh Token을 저장할 Refresh Token Storage 이 필요한데, RDBMS에 저장하면 Refresh Token의 만료 시간(Time To Live)를 통해 주기적으로 삭제해야 하는 번거로움이 생긴다.
- 해결 방법으로 Redis의 In-Memory DB를 사용하여 RDBMS보다 더 빠르게 조회가 가능하고, 만료시간을 설정하여 저장할 수 있다. 즉 Redis에 있는 데이터를 조회하여 로그아웃을 구현할 수 있다.
구현 방법
로그인/로그아웃
- 로그인 했을 때는 Key로 email, Value는 RefreshToken을 Redis에 저장. (만료시간은 RefreshToken 유효시간 만큼) → 클라이언트로부터 엑세스 토큰과 리프레쉬 토큰을 발급
- 로그아웃 했을 때는 AccessToken으로부터 Key(email)를 받아와서 Redis에 삭제 → 해당 AccessToken은 Key, Value “Logout”으로 Redis에 저장 (만료시간은 AccessToken의 남은 시간)
JwtAuthenticationFilter
에서 로그인 되어있으면 토큰으로부터 유저 정보를 받아와서
SecurityContext 에 Authentication 객체를 저장
한다.
재발급
- Redis에 RefreshToken이 저장 되어있을 때
- AccessToken이 만료되었을 시 401 UnauthorizedTime 예외가 발생 → Redis에 있는 RefreshToken으로 토큰 재발급
- Redis에 RefreshToken이 저장 안 되어있을 때
- AccessToken 만료 → 401 예외 → 재발급 → Redis에 RefreshToken이 없기에 403 예외 → 로그인 페이지로 이동 후 다시 로그인
→ 참고자료에서 spring boot로 구현하는 방법은 자세히 나와있다.
참고 자료
[10분 테코톡] 🎡토니의 인증과 인가
[Spring] Spring Security + JWT 토큰을 통한 로그인
JWT(Json Web Token) 인증방식
SpringBoot + JWT를 이용한 로그아웃