0. 개요
- 서비스 운영에 있어서의 보안과 관련된 개념을 알아본다.
- 스프링에 보안을 적용할 때 사용하는 Spring Security에 대해 알아본다.
- 무상태(stateless) REST 애플리케이션 환경에 맞게 토큰값을 활용하는 보안 기법을 기본적으로 가정한다.
* stateless: 클라이언트와 서버 관계에서 서버가 클라이언트의 상태를 보존하지 않음
1. 인증과 인가는 무엇인가?
인증과 인가. 한국어로든 영어로든 서로 비슷해서 헷갈린다. 하지만 찬찬히 생각해보면 어감의 차이를 느낄 수 있다. 먼저 인가를 보면 '허락'의 뉘앙스가 풍긴다. 이를 중심으로 생각해보면 좀더 구분이 수월해지기도 한다.
놀이공원에 간 상황을 예로 들면, 놀이공원 입구에서 신분을 확인하여 입장하는 것이 인증이다. 그리고 입장 과정에서 내가 산 티켓이 자유이용권일 수도 있고 특정 구역만 이용할 수 있는 티켓일 수도 있다. 이때 놀이공원 내의 각각의 놀이기구에서 해당 놀이기구를 탈 수 있는 권한이 있는지 티켓의 속성을 확인하는 것이 인가이다.
인증(Authentication)
인증은 사용자가 누구인지 확인하는 단계이다.
'로그인'이 대표적인 예로, 로그인이 있다. 로그인에 성공하면 서버는 응답으로 토큰(token)을 전달한다. 사용자는 이 토큰을 통해 원하는 리소스에 접근할 수 있게 된다.
인가(Authorization)
인가는 사용자가 어떤 리소스에 접근할 권리가 있는지를 확인하는 단계이다.
일반적으로 사용자가 앞서 인증 단계에서 발급받은 토큰은 인가 내용을 포함하고 있다. 이를 통해 사용자가 접근하려 하는 리소스에 대한 권한 유무 등을 확인하여 인가를 수행한다.
2. Spring Security
스프링 시큐리티는 애플리케이션의 보안 기능(인증, 인가 등)을 제공하는 스프링 하위 프로젝트이다. 이를 활용하면 보안과 관련된 로직을 일일히 작성하지 않아도 된다는 효용이 있다.
스프링 시큐리티의 동작 구조
외부에서 클라이언트로부터 요청이 들어오면 Dispatcher Servlet을 확인하여 매핑된 Controller로 향하게 되는데, 스프링 시큐리티가 제공하는 보안 필터 체인(SecurityFilterChain)이 클라이언트와 Dispatcher Servlet 사이에 위치하여 인증 및 인가 처리를 한다.
이 보안 필터 체인에 포함된 필터는 십수가지가 있는데 지정된 우선순위를 따라 실행된다. 체인에 대한 설정은 WebSecurityConfigurerAdapter 클래스를 상속받아 진행할 수 있다.
별도의 설정이 없다면 스프링 시큐리티에서는 아래 그림과 같이 UsernamePasswordAuthenticationFilter를 통해 인증을 처리한다.
- 클라이언트로부터 요청을 받으면 ServletFilter에서 SecurityFilterChain으로 작업이 위임되고, 그 중 UsernamePasswordAuthenticationFilter에서 인증을 처리한다.
- AuthenticationFilter는 요청 객체에서 username과 password를 추출해서 토큰을 생성한다.
- AuthenticationManager(실제로는 구현체인 ProviderManager)에게 토큰을 전달한다.
- ProviderManager는 인증을 위해 AuthenticationProvider로 토큰을 전달한다.
- AuthenticationProvider는 토큰의 정보를 UserDetailsService로 전달한다.
- UserDetailsService는 전달받은 정보를 통해 DB에서 일치하는 사용자를 찾아 UserDetails 객체를 생성한다.
- 생성된 UserDetails 객체는 AuthenticationProvider로 전달되며, 해당 Provider에서 인증을 수행하고 성공하게 되면 ProviderManager로 권한을 담은 토큰을 전달한다.
- ProviderManager는 검증된 토큰을 AuthenticationFilter로 전달한다.
- AuthenticationFilter는 검증된 토큰을 SecurityContextHolder에 있는 SecurityContext에 저장한다.
3. JWT
JWT(JSON Web Token)은 당사자 간에 정보를 JSON 형태로 안전하게 전송하기 위한 토큰이다. 이는 주로 서버와의 통신에서 권한 인가를 위해 사용된다. 또한 문자열로만 구성돼있기 때문에 HTTP 구성요소 어디든 위치할 수 있다.
JWT의 구조
JWT는 아래와 같이 점으로 구분된 세 부분(header, payload, signature)으로 구성된다.
1. header
JWT의 헤더(header)에는 검증과 관련된 내용 두 가지 내용을 담고 있다.
- typ: 토큰의 타입 지정
- alg: 해싱 알고리즘 지정. 보통 SHA256 이나 RSA 방식을 사용하며, 토큰을 검증할 때 사용되는 서명 부분에 적용되는 방식이다.
{
"typ": "JWT",
"alg": "HS256"
}
위와 같은 정보가 base64로 인코딩된다.
2. payload
JWT의 내용(payload)에는 토큰에 담는 정보를 포함한다. 이곳에 포함된 각 속성들은 클레임(claim)이라 부르는데 토큰이나 사용자에 대한 정보를 key-value 형태로 담는다.
JWT 표준 스펙 상 정의된 7가지의 클레임은 아래와 같다.
- iss: JWT의 발급자(issuer)
- sub: JWT의 제목(subject)
- aud: JWT의 수신인(audience)
- exp: JWT의 만료시간(expiration). NumericDate 형식 사용.
- nbf: 'Not Before'를 의미
- iat: JWT가 발급된 시간(issued at)
- jti: JWT의 식별자(JWT ID). 주로 중복 처리 방지를 위해 사용됨.
위 7가지를 등록된 클레임이라고 부르며, 이외에도 개발자는 임의의 클레임을 추가할 수 있다. 다만, 노출되어서는 안될 민감한 정보를 담아서는 안된다. 본 payload 부분은 특별한 권한 없이도 누구나 디코딩하여 정보를 열람할 수 있기 때문이다.
{
"iss": "green",
"sub": "1",
"exp": "1602076490",
"userId": "greenId"
}
위와 같은 정보가 header와 마찬가지로 base64로 인코딩된다.
3. signature
JWT의 서명(signature) 부분은 인코딩된 헤더, 인코딩된 내용, 비밀키를 헤더에서 지정한 알고리즘을 통해 생성된다. 여기서 비밀키(secret key)는 서버가 가지고 있는 개인키이다. 즉, 개인키를 가지고 있는 서버만 해당 JWT을 복호화할 수 있는 것이다.
인증과정은 대략 다음과 같다.
- 클라이언트가 요청에 토큰을 포함하여 서버로 요청을 보낸다.
- 서버는 가지고 있는 개인키를 통해 토큰의 signature를 복호화(decode)한다.
- 위 결과로서 얻은 인코딩된 header과 인코딩된 payload가 JWT의 그것들과 일치하는지 확인한다.
만약 JWT가 변조되었다면 signature를 복호화하여 얻은 내용과 JWT에 담겨 있는 내용이 불일치하여 인증이 불가능할 것이다.
4. 스프링 시큐리티와 JWT
스프링 시큐리티는 기본적으로 UsernamePasswordAuthenticationFilter를 통해 인증을 수행하도록 구성되어 있다. 이 상황에서 JWT를 사용하기 위해서는 JWT를 사용하는 인증 필터를 구현하고 이를 UsernamePasswordAuthenticationFilter 앞에 배치하여 인증 주체를 변경하는 식으로 구성할 수 있다. 추가된 필터에서 인증이 정상적으로 처리되면 UsernamePasswordAuthenticationFilter는 자동으로 통과된다.
[참고 자료] 『스프링 부트 핵심 가이드』 장정우 지음
위 책을 읽고 정리한 내용입니다.
'Java > Spring' 카테고리의 다른 글
[Spring Security] 스프링 시큐리티의 구조와 인증 과정 (0) | 2024.11.18 |
---|---|
[Spring Boot] 서버 간 통신: RestTemplate과 WebClient (0) | 2023.04.22 |
[Spring Boot] 액추에이터 (0) | 2023.04.21 |
[Spring Boot] 유효성 검사와 예외처리 (0) | 2023.04.14 |
[Spring Boot] 연관관계 매핑 (0) | 2023.04.09 |