인증 : 사용자의 신원을 확인하는 절차
인가 : 사용자의 권한을 확인하고 허가하는 절차
Stateless(무상태성), 말 그대로 상태를 유지하지 않는다는 말이다. 즉, 서버는 클라이언트의 상태를 저장하지 않는다. 때문에 서버측에서는 클라이언트의 이전의 상태에 대해서는 관리하고 있지 않기 때문에 해당 요청을 보낸 사용자가 누구인지 알 수 없다. 즉, 인증이 유지가 되지 않아 다시 인증을 해야하는 문제가 발생한다. 이러한 문제를 해결하기 위해 인증을 유지하는 방법으로 세션과 토큰 등이 있다.
토큰 기반 인증은 서버 내에 유저 정보 등을 저장하는 것이 아닌 클라이언트가 직접 자신에 해당하는 정보를 저장하는 방식이다.
JWT란 JSON Web Token의 줄임말로 JSON 포맷을 이용하여 사용자에 대한 속성(위에서의 유저 정보)을 저장하는 Claim 기반의 Web Token이다.
🍀 JWT = Header + Payload + Signature
JSON 형태인 각 부분은 Base64로 인코딩 되어 표현되고 구분자 '.'를 사용하여 구분된다.
이때 Base64로 인코딩된 문자열은 인코딩 전 같은 JSON 형태에 대해 항상 같은 인코딩 문자열을 반환(Header+Payload 부분)하므로(Signature 부분은 매번 변한다) 비밀번호와 같은 주요 정보를 인코딩시켜 보내게 된다면 보안에 매우 취약하다.
Header
토큰의 헤더는 alg과 typ 두 가지 정보로 구성된다
Payload
토큰의 페이로드에는 토큰에서 사용할 정보의 조각들인 클레임(Claim)이 담겨 있다. 클레임은 총 3가지로 나누어지며, Json(Key/Value) 형태로 다수의 정보를 넣을 수 있다.
Claim 1. 등록된 클레임(Registered Claim)
등록된 클레임은 토큰 정보를 표현하기 위해 이미 정해진 종류의 데이터들로, 모두 선택적으로 작성이 가능하며 사용할 것을 권장한다. 또한 JWT를 간결하게 하기 위해 key는 모두 길이 3의 String이다. 여기서 subject로는 unique한 값을 사용하는데, 사용자 이메일을 주로 사용한다.
iss: 토큰 발급자(issuer)
sub: 토큰 제목(subject)
aud: 토큰 대상자(audience)
exp: 토큰 만료 시간(expiration), NumericDate 형식으로 되어 있어야 함 ex) 1480849147370
nbf: 토큰 활성 날짜(not before), 이 날이 지나기 전의 토큰은 활성화되지 않음
iat: 토큰 발급 시간(issued at), 토큰 발급 이후의 경과 시간을 알 수 있음
jti: JWT 토큰 식별자(JWT ID), 중복 방지를 위해 사용하며, 일회용 토큰(Access Token) 등에 사용
Claim 2. 공개 클레임(Public Claim)
공개 클레임은 사용자 정의 클레임으로, 공개용 정보를 위해 사용된다. 충돌 방지를 위해 URI 포맷을 이용하며한다.
Claim 3. 비공개 클레임(Private Claim)
비공개 클레임은 사용자 정의 클레임으로, 서버와 클라이언트 사이에 임의로 지정한 정보를 저장한다. 아래의 예시와 같다.
{
"token_type": access
}
Signature
Signature는 토큰을 인코딩하거나 유효성 검증을 할 때 사용하는 고유한 암호화 코드이다.
💡 헤더 & 페이로드 인코딩 → 인코딩한 값을 해싱 → 해싱한 값을 다시 인코딩 → signature 완성
Signature는 위에서 만든 헤더(Header)와 페이로드(Payload)의 값을 각각 base64로 인코딩하고, 인코딩한 값을 비밀 키를 이용해 헤더(Header)에서 정의한 알고리즘으로 해싱을 하고, 이 값을 다시 Base64로 인코딩하여 생성한다.
Refresh Token은 Access Token의 보안 취약성을 보완하기 위해 등장한 개념이다.
일반적으로 AccessToken의 만료 기한보다 길게 설정하며, AccessToken이 만료되면 Refresh Token으로 요청을 보내 다시 AccessToken을 발급받는 방식으로 작동한다.
🌱 OAuth
웹, 모바일, 데스크 톱 어플리케이션에서의 간단하고 표준적인 방법으로 보안 인가를 허용하기 위한 개방형 표준 프로토콜
구글, 페이스북, 트위터와 같은 다양한 플랫폼의 특정한 사용자 데이터에 접근하기 위해 제3자 클라이언트(우리 서비스)가 사용자의 접근 권한을 위임(Delegated Authorization)받을 수 있는 표준 프로토콜이다.
쉽게 말하자면, 우리의 서비스가 우리 서비스를 이용하는 유저의 타사 플랫폼 정보에 접근하기 위해서 권한을 타사 플랫폼으로부터 위임 받는 것 이다.
OAuth에서는 동작에 참여하는 각각의 대상을 부르는 명칭이 존재한다.
Resource Owner
말 그대로 리소스 소유자. 우리 서비스의 사용자이자 구글, 카카오 등의 플랫폼에서 리소스를 소유하고 있는 사용자이다. 즉, 인증을 수행하는 주체이다.
Client
리소스 소유자 대신 위임 받은 권한으로 Resource Server의 자원을 이용하고자 하는 서비스. 보통 우리가 개발하려는 서비스로 권한을 위임받는 주체이다.
Authorization Server
리소스 소유자(Resource Owner)를 인증하고 클라이언트(Client)에 엑세스 토큰을 발급하는 서버이다. 즉, 인증을 검증하고 권한을 부여하는 주체이다.
Resource Server
엑세스 토큰을 사용하여 리소스 요청을 수락하고 응답할 수 있는 리소스를 가지고 있는 서버. 다시 말해, 애플, 카카오와 같이 리소스를 가지고 있는 서버로 인가를 수행하고 리소스를 제공하는 주체이다.
OAuth 2.0 서비스를 이용하기 위해서는 Client를 Resource Server에 등록해야하는 작업이 선행되어야 한다.
Redirect URI
Redirect URI는 사용자가 OAuth 2.0 서비스에서 인증을 마치고(ex. 카카오 로그인 페이지에서 로그인을 마쳤을 때) 사용자를 리디렉션시킬 위치이다. 즉, Redirect URI는 Authorization Code를 전달받을 주소이다. OAuth 2.0 서비스는 인증이 성공한 사용자를 사전에 등록된 Redirect URI로만 리디렉션 시킨다. 승인되지 않은 URI로 리디렉션 될 경우, Authorization Code를 중간에 탈취당할 위험성이 있기 때문이다. Redirect URI는 기본적으로 보안을 위해 https만 허용하지만 예외적으로 localhost는 http가 허용된다.
Authorization Code가 뭔가요?
Resource Server가 발급하는 임시 코드로 Client가 자신의 자원을 사용할 수 있는 Access Token을 발급받기 위해 사용한다.
Client ID, Client Secret
등록과정을 마치면, Client ID와 Client Secret를 얻을 수 있다. Client ID는 클라이언트 웹 어플리케이션을 구별할 수 있는 식별자이고 Clinet Secret은 Client ID에 대한 비밀키이다. 이때, Client ID는 공개되어도 상관없지만, Client Secret은 절대 유출되어서는 안된다. 발급된 Client ID와 Client Secret은 액세스 토큰을 획득하는데 사용된다.
1 ~ 2. 로그인 요청
Resource Owner가 로그인을 요청하면 Client는 OAuth 프로세스를 시작하기 위해 사용자의 브라우저를 Authorization Server로 보내야한다. 이때 Client는 Authorization Server가 제공하는 Authorization URL에 Response Type , Client Id , Redirect URI , Scope 등의 매개변수를 쿼리 스트링으로 포함하여 보낸다.
3 ~ 4. 로그인 페이지 제공, ID/PW 제공
Authorization URL로 이동된 Resource Owner는 제공된 로그인 페이지에서 ID와 PW를 입력하여 인증한다.
5 ~ 6. Authorization Code 발급, Redirect URI로 리디렉트
인증이 성공되었다면, Authorization Server는 제공된 Redirect URI로 사용자를 리디렉션시킨다. 이때, Redirect URI에 Authorization Code를 포함하여 사용자를 리디렉션 시킨다.
Authorization Code 사용 이점
Authorization Code는 access token을 직접 전달하는 대신 사용되므로, 중간에 탈취될 위험을 줄여 보안상의 이점이 존재한다.
7 ~ 8. Authorization Code와 Access Token 교환
Client는 Authorization Server에 Authorization Code를 전달하고, Access Token을 응답받는다. Client는 발급받은 Resource Owner의 Access Token을 저장하고, 이후 Resource Server에서 Resource Owner의 리소스에 접근하기 위해 Access Token(절대 유출되어서는 안된다)을 사용한다.
Authorization Code와 Access Token 교환은 token 엔드포인트에서 이루어지며 application/x-www-form-urlencoded
의 형식에 맞춰 전달해야한다.
application/x-www-form-urlencoded 타입이란?
html form을 통한 POST 전송 방식 중 가장 기본이 되는 Content-Type으로 보내는 데이터를 url 인코딩 후 웹 서버에 보내는 방식이다.
요청 데이터를 키(key)와 값(value)의 쌍으로 구성한다. 각 쌍은 '='로 키와 값이 연결되며, 여러 개의 key-value 쌍은 '&'로 구분함으로써 여러 개의 데이터를 한 번에 전송할 수 있다.
POST /oauth/token HTTP/1.1
Host: authorization-server.com # OAuth 토큰을 발급하는 서버의 도메인 이름
grant_type=authorization_code
&code=xxxxxxxxxxx
&redirect_uri=https://example-app.com/redirect
&client_id=xxxxxxxxxx
&client_secret=xxxxxxxxxx
9. 로그인 성공
위 과정을 성공적으로 마치면 Client는 Resource Owner에게 로그인이 성공하였음을 알린다.
10 ~ 13. Access Token으로 리소스 접근
이후 Resource Owner가 Resource Server의 리소스가 필요한 기능을 Client에 요청한다. Client는 위 과정에서 발급받고 저장해둔 Resource Owner의 Access Token을 사용하여 제한된 리소스에 접근하고, Resource Owner에게 자사의 서비스를 제공한다.
🌱 Spring Security
Spring 기반의 애플리케이션의 보안(인증과 권한, 인가 등)을 담당하는 스프링 하위 프레임워크
클라이언트 요청은 서버 컴퓨터의 WAS(톰캣)의 필터들을 통과한 뒤 스프링 컨테이너의 컨트롤러에 도달한다.
WAS의 필터단에서 요청을 가로챈 후 시큐리티의 역할을 수행한다.
SecurityContext 💾
SecurityFilterChain 내부에 존재하는 각각의 필터는 시큐리티 관련 작업을 진행한다. 이때, 모든 작업은 기능 단위로 분업하여 진행하기 때문에 앞에서 한 작업을 뒤의 필터가 알기 위한 저장소 개념이 필요하다. 예를 들어, 인가 필터가 작업을 하려면 유저의 ROLE 정보가 필요한데, 앞단의 필터에서 유저에게 ROLE 값을 부여한 결과를 인가 필터까지 공유해야만 인가 작업을 수행할 수 있다. 여기서 등장한 것이 SecurityContext 개념이다.
🌱 SecurityContext란, 필터를 거쳐 인증된 객체(Authentication)가 저장되는 저장소이다.
각 filter를 거치면서 도출된 인증 관련 정보는 Authentication 객체에 담긴다.
Authentication 객체
- Principal : 유저 정보를 담은 객체 (커스텀 가능)
- Credentials : Principal이 올바르다고 입증하는 객체 (비밀번호, 토큰 저장)
- Authorities : 인증된 유저의 권한(ROLE) 목록을 저장
Authentication 객체는 SecurityContext에 포함되어 관리되며 N개의 SecurityContext는 SecurityContextHolder에 의해서 관리된다. SecurityContextHolder는 SecurityContext를 감싸고 있는 wrapper 클래스며, 실제 Authentication객체에 접근할 땐 SecurityContext객체를 통해 꺼내올 수 있다.
그 예시로 SecurityContextHolder.getContext().getAuthentication().getAuthorities()
코드를 통해 Authentication객체의 Authorities에 접근할 수 있다.
FilterChain ⛓️
DelegatingFilterProxy
WAS의 filter단에서 스프링 Bean을 찾아 요청을 넘겨주는 서블릿 필터
⇒ Servlet Container와 Spring Container를 연결해주는 필터
FilterChainProxy
DelegatingFilterProxy에 의해 호출되는 SecurityFilterChain들을 들고 있는 Bean
SecurityContextHolderFilter
접근한 유저에 대해 SecurityContext를 관리
LogoutFilter
로그아웃에 대한 처리를 담당하는 필터로 사용자가 로그아웃 요청을 했을 경우에만 적용되는 필터
세션 무효화, 인증 토큰 삭제, SecurityContext에서 해당 토큰 삭제 등 로그아웃시 필요한 다양한 기능 제공
UsernamePasswordAuthenticationFilter
Form Based Authentication(폼 기반 인증)을 위한 인증을 진행할 때, 아이디와 패스워드 데이터를 파싱하여 인증 요청을 위임하는 필터
Form Based Authentication이란?
사용자가 입력한 인증 정보인 username과 password를 통해 인증을 하는 방식
현재에는 API 기반 인증 방식이 가장 많이 사용되고 있고 우리의 프로젝트에서도 Rest API로 토큰 기반 인증 방식을 사용할 것이기 때문에 폼 기반 인증을 비활성화해야 한다.
http.formLogin().disable()
은 폼 기반 로그인 방식을 비활성화 한다는 뜻으로, 다른 로그인 방식을 사용하겠다는 것을 의미한다.
ExceptionTranslationFilter
FilterChain을 거치면서 발생하는 인증 및 접근 예외에 대한 처리를 하기 위한 용도의 필터