청양고추마켓은 중고거래 플랫폼입니다.
게시물을 올리고, 채팅을 진행하기 위해서는 인증과 인가를 거친 로그인 기능
이 구현되어야합니다.
로그인을 구현하기에 앞서 로그인을 구현하는 방법에 대해 종류별로 알아보고 어떤 방식으로 구현할지 정한 뒤, 구현 단계로 넘어가도록 하겠습니다.
HTTP는 기본적으로 무상태(stateless) 프로토콜
이기 때문에 클라이언트와 서버가 요청을 한번 주고받으면 연결이 끊어지고 상태 정보가 유지되지 않습니다. 이러한 특성은 많은 사용자가 동시에 요청을 보낼때 과부하를 방지하는데 유리하지만, 로그인 기능과 같이 상태를 유지해야 하는 경우에는 단점으로 작용합니다.
이 문제를 해결하기 위해, 로그인 상태와 같은 사용자 정보를 유지할 방법이 필요했고, 쿠키와 세션 등의 기술이 등장했습니다.
[ 간단 용어 정리 ]
- 인증(Authentication): 해당 사용자가 본인이 맞는지 확인
- 인가(Authorization): 인증된 사용자가 요청한 자원에 접근 가능한지를 결정합니다.
그럼 지금부터 본격적으로 로그인 방식에 대해서 알아보겠습니다.
웹 접속시 브라우저에 저장되는 텍스트 파일, 브라우저마다 쿠키를 저장하는 전용 공간(브라우저 내 쿠키저장소)이 존재(자동 관리)
쿠키는 만료 기간에 따라 영속쿠키와 세션쿠키로 분류됩니다.
쿠키 로그인 진행 방식
- Client → Server: 로그인 폼에 id & pw를 서버에 보냄
- Server: 받은 id와 pw를 DB의 정보와 비교해서 인증을 진행 / 인증 성공 시 서버는 쿠키를 생성→ 쿠키는 사용자를 식별할 수 있는 값이 들어감
- Server → Client: HTTP 응답의 Set-Cookie 헤더를 통해 쿠키 전체를 Client에게 보냄
- Client: 받은 쿠키를 브라우저의 쿠키 저장소에 자동으로 저장(쿠키의 Key-value)
.
이후 요청 시 Client는 저장된 쿠키를 해당 도메인에 요청할때마다 HTTP 요청 헤더에 자동으로 포함해서 서버로 보냄
Server는 요청에 포함된 쿠키의 값을 확인해서 사용자가 로그인한 상태임을 판별하고 관련 인증/인가를 진행
⇒ Server는 Client로부터 전달된 쿠키의 값을 별도의 Session이나 JWT Token 같은 별도의 메커니즘으로 쿠키 상태를 저장해야함, 해당 메커니즘이 없다면 해당 쿠키가 본인 Server에서 발급된 것인지확인 불가능
하지만 쿠키는 클라이언트 측에서 관리되는 자원이기 때문에 보안적에 있어 취약한 부분이 있습니다.
• 쿠키 값은 클라이언트가 임의로 변경할 수 있으므로, 전송되는 쿠키 값은 위조될 위험이 있습니다.
• 쿠키는 웹 브라우저에 저장되며, 매 요청 시 서버로 전송되기 때문에, 로컬 PC에서 탈취되거나 네트워크 전송 과정에서 가로챌 수 있습니다.
• 만약 쿠키 값이 탈취되면, 해당 쿠키를 이용해 사용자의 인증 상태를 계속 유지할 수 있어 보안상 큰 위협이 됩니다.
웹 어플리케이션에서 한 사용자의 여러 요청을 하나의 연속된 상태로 보고, 그 상태 정보를 일정 시간동안 유지하는 기술
쿠키의 보안 문제를 해결하기위해 중요한 정보는 서버에만 저장하고 클라이언트와 서버는 그 자체로는 의미가 없으며 추정이 불가능한 임의의 식별자로만 통신을 진행합니다. 이렇게 서버에만 중요한 정보를 보관하고 연결을 유지하는 방식을 세션 방식이라 합니다.
세션 로그인 진행 방식
- Client → Server: 로그인 폼으로 Id, pw를 Server에 전달
- Server: 로그인 로직을 처리하고 올바른 로그인이면 Session ID를 생성
- UUID는 식별이 불가능해 Session ID로 사용된다.
- Java의 UUID를 사용하면 확실한 랜덤값 생성 가능
- Server: Server의 세션 저장소에 Session ID와 멤버(로그인한 사용자 정보- id, 권한 등등)를 묶어 보관한다.
.
이후 요청 시, Server는 Client에게 JSESSIONID라는 이름으로 Session ID만 쿠키에 담아 전달한다. Client는 쿠키 저장소에 JSESSIONID를 저장한 뒤 이후 요청시 헤더에 포함해 보낸다.
Server는 쿠키를 통해 전달된 Session ID를 통해 세션 저장소에서 해당 key가 존재하는지 확인하고, 존재한다면 Value 내에 저장된 값들로 인증, 인가를 진행한다.
→ 세션 저장소: 서버 메모리, Redis, DB 등 다양한 방법으로 저장 가능
→ Session ID와 멤버를 묶어 보관한다
⇒ 쿠키와 달리 세션의 Key 값이 예측 불가능하기 때문에 다른 사용자인 것처럼 보내는 것이 불가능하다
session은 서버에 연결 정보를 저장하는 것이기 때문에 사용자가 많아질수록 저장해야하는 session이 늘어나 비효율적 처리로 이어집니다.
결국 우리는 API가 실행될 때마다 사용자를 인증해야하는데 Spring Security에서는 그 인증을 구현해 놓았습니다. Spring Security는 인증과 인가를 담당하고 보안에 관련하여 많은 옵션을 제공하는 프레임워크입니다.
Spring Security의 경우 그 양이 굉장히 방대하기 때문에 간단하게 특징과 구조에 대해서만 알아보고 넘어가도록 하겠습니다.
Spring에서는 Client가 요청을 보내게 되면 DispatcherServlet이 요청을 처리하게 되는데 DispatcherSevlet이 요청을 받기 전에 다양한 필터들이 있을 수 있습니다.
여기서 필터는 Client와 Server 사이에서 요청과 응답 정보를 전처리, 후처리하는데 목적이 있습니다. Spring Security는 다양한 기능을 가진 필터들을 10개 이상 기본적으로 제공하며 이렇게 제공되는 필터들을 Security Filter Chain
이라고 말합니다.
SecurityContextPersistenceFilter
: SecurityContextRepository에서 SecurityContext를 가져오거나 저장하는 역할LogoutFilter
: 설정된 로그아웃 URL로 오는 요청을 감시하며 해당 유저를 로그아웃 처리(UsernamePassword)AuthenticationFilter
: id, pw 사용하는 form 기반 인증, 설정된 로그인 URL로 오는 요청을 감시하며 유저 인증 처리DefaultLoginPageGeneratingFilter
: form 기반 또는 OpenId 기반 인증에 사용하는 가상 URL에 대한 요청을 감시하고 로그인 폼 기능을 수행하는데 필요한 HTML 생성BasicAuthenticationFilter
: HTTP 기본 인증 헤더를 감시하여 처리RequestCacheAwareFilter
: 로그인 성공 이후, 원래 인증 요청에 의해 가로채진 사용자의 원래 요청을 재구성하는데 사용SecurityContextHolderAwareRequestFilter
: HttpServletRequest 정보를 감싼다. 다음 필터들에게 부가정보를 제공AnonymousAuthenticationFilter
: 이 필터가 호출되는 시점까지 사용자가 인증되지 않았다면 인증토큰에 사용자가 익명 사용자로 나타남SessionManagementFilter
: 인증된 주체를 바탕으로 세션 트래킹을 처리해 다닝ㄹ 주체와 관련한 모든 세션들이 트래킹되도록 도움ExceptionTranslationFilter
: 보호된 요청을 처리하던 중에 발생할 수 있는 예외의 기본 라우팅과 위임, 전달하는 역할FilterSecurityInterceptor
: AccessDecisionManager로 권한 부여 처리를 위임함으로써 접근 제어 결정을 쉽게해줌
- 로그인 성공시 Spring Security는 Authentication 객체를 만듦
→ 사용자가 누구인지, 어떤 권한을 가지고 있는지 정보가 저장- Authentication 내에 UserDetails는 사용자의 구체적인 정보(사용자 이름, 비밀번호, 권한정보)를 저장
- Authentication 객체를 Spring Security의 Security Session에 저장
→ 사용자가 이후에 다른 요청을 할 때, 이 Session 정보를 기반으로 인증 인가 진행
로그인 이후 Server가 만들어서 사용자에게 넘겨주는 문자열로
해당 문자열은 사용자 정보가 암호화 되어있고, 이 토큰을 이용해서 사용자가 인증됐는지 확인할 수 있습니다.
Header, Payload, Signature로 구성 → xxxxxx.yyyyyy.zzzzzz 로 표현(구분자: ‘ . ‘)
Header
: 토큰 종류와 해싱 알고리즘 정보가 담김Payload
: 토큰의 내용물이 인코딩된 부분, 토큰에 담을 정보가 들어있는 부분Signature
: 토큰을 인코딩하거나 유효성을 검증할때 사용하는 고유한 암호화 코드JWT 동작 과정
- Client → Server: ID, PW를 Server에게 전달
- Server: 인증 성공 시 JWT 토큰 생성
- Access Token: 유효기간이 짧아 요청마다 인증에 사용
- Refresh Token: 유효기간이 길어 Access Token이 만료됐을때 새 토큰을 발급받기 위해 사용
- Server → Client: 생성한 토큰을 전달
- Client: 전달받은 토큰을 로컬 스토리지 혹은 쿠키에 저장
.
이후 요청 시, Client는 HTTP 헤더에 Authorization: Bearer {accessToken} 형식으로 토큰을 같이 보냄
Server는 매 요청마다 토큰의 유효성을 검증, 유효하면 해당 사용자의 요청을 인증/인가하고 문제가 있으면 요청을 거부
→ RefreshToken: 만약 Access Token이 만료되면, 클라이언트는 저장된 Refresh Token을 이용해서 새로운 Access Token을 만들 수 있음
→ Access Token의 유효기간을 짧게 잡아 탈취되더라도 문제가 생기지 않게 빠르게 만료
Access Token은 암호화된 토큰이라 하더라도, 탈취된다면 마치 열쇠처럼 사용되어 로그인이 가능해집니다. 따라서 탈취를 방지하기 위해 토큰을 안전하게 관리하는 것이 매우 중요합니다.
JWT 토큰을 SessionStorage나 localStorage 대신 쿠키에 저장할 경우, 서버에서 쿠키를 저장할 때 HttpOnly와 Secure 옵션을 적용하여 보안을 강화할 수 있습니다.
HttpOnly
의 경우 자바 스크립트에서 접근할 수 없으므로 XSS 공격에 의한 탈취 위험이 줄어듭니다.Secure
옵션의 경우 Https 접속에서만 쿠키가 전송되도록 하여, 네트워크 상에서 탈취될 위험을 낮춥니다.이렇게 여러가지 로그인 구현 방식에 대해서 공부해 보았고, 청양고추마켓은 Spring Security 기반으로 인증 인가를 진행하되, 인가의 과정에서 JWT 토큰 방식을 사용할 예정입니다.
Spring Security + JWT Token