Spring/스프링 시큐리티 인 액션

[15장] OAuth 2: JWT와 암호화 서명 사용

꾸준함. 2025. 6. 1. 14:30

주의

이 책은 Spring Security 5 버전을 기준으로 작성되었으므로, Spring Boot 3.X 버전에서는 일부 클래스가 더 이상 사용되지(deprecated) 않을 수 있습니다.

 

서론

  • 암호화 서명으로 토큰을 검증하면 권한 부여 서버를 호출하거나 공유된 데이터베이스를 이용하지 않아도 리소스 서버가 토큰을 검증할 수 있다는 이점이 있음
    • 해당 토큰 검증 방식은 OAuth 2로 인증과 권한 부여를 구현하는 시스템에서 일반적으로 이용됨

 

1. JWT의 대칭 키로 서명된 토큰 이용

  • 토큰에 서명하는 가장 직관적인 방법은 대칭 키를 이용하는 것
    • 대칭 키 방식에서는 같은 키로 토큰에 서명하고 해당 서명을 검증할 수 있음
    • 이 방식은 속도가 빠르다는 장점이 있지만 인증 프로세스에 관여하는 모든 애플리케이션에 항상 키를 공유할 수 있는 것은 아니라는 단점이 존재

 

1.1 JWT 이용

  • JWT는 하나의 토큰 구현
    • 토큰은 헤더, 본문, 서명의 세 부분으로 구성됨
    • 헤더와 본문의 세부 정보는 JSON으로 표기되어 Base64로 인코딩 됨
    • 세 번째 부분인 서명은 헤더와 본문을 입력으로 이용하는 암호화 알고리즘으로 생성됨
      • 암호화 알고리즘을 이용한다는 것은 키가 필요하다는 뜻
      • 올바른 키가 있는 사람은 토큰에 서명하거나 서명이 진짜인지 검증 가능
      • 토큰의 서명이 진짜이면 토큰에 서명된 후 아무도 토큰을 변경하지 않았다는 증명이 됨

 

https://livebook.manning.com/book/spring-security-in-action/chapter-15/13

 

  • 서명된 JWT는 JWS(JSON Web Token Signed)라고 지칭함
  • 보통은 암호화 알고리즘으로 토큰에 서명하기만 해도 충분하지만 종종 토큰을 암호화하기도 하며 JWE (JSON Web Token Encrypted)라고 지칭함
    • 유효한 키가 없으면 암호화된 토큰의 내용을 볼 수 없음

 

  • 토큰이 서명되면 키나 암호 없이도 그 내용을 볼 수 있지만 이를 변경할 수는 없음
    • 내용을 변경하면 서명이 무효가 되기 때문
    • 서명에 대해 다음 조건이 성립해야 토큰이 유효하기 때문

 

https://livebook.manning.com/book/spring-security-in-action/chapter-15/18

 

1.2 JWT를 발행하는 권한 부여 서버 구현

  • 앞서 토큰을 관리하는 구성 요소가 TokenStore라고 배웠는데 이 절에서는 스프링 시큐리티에 있는 TokenStore의 다른 구현으로서 JWT를 관리하는 JwtTokenStore라는 구현을 이용
  • JWT로 토큰 검증을 구현하는 방법은 두 가지
    • 토큰에 서명하는 일과 서명을 검증하는 일에 같은 키를 이용하면 키가 대칭 (symmetric)
    • 토큰에 서명하는 일과 서명을 검증하는 일에 각기 다른 키를 이용하면 비대칭 키 (asymmetric key) 쌍을 이용하는 것

 

  • 이번 예제에서는 대칭 키로 서명을 구현하며 해당 방식에서는 권한 부여 서버와 리소스 서버가 같은 키를 공유하고 이용한다고 가정
    • 권한 부여 서버가 키로 토큰에 서명하면 리소스 서버가 같은 키로 서명을 검증

 

 

 

https://livebook.manning.com/book/spring-security-in-action/chapter-15/25

 

1.3 JWT를 이용하는 리소스 서버 구현

  • 이 절에서는 권한 부여 서버가 발행한 토큰을 대칭 키로 검증하는 리소스 서버를 구현
  • 보호할 엔드포인트 하나가 필요하므로 리소스 서버를 테스트할 간단한 엔드포인트를 노출하는 컨트롤러와 메서드를 정의

 

 

  • 이제 보호할 엔드포인트가 있으므로 구성 클래스를 선언하고 여기에서 TokenStore를 구성할 수 있음
    • 권한 부여 서버에서 했던 것과 같이 리소스 서버의 TokenStore를 구성하며 가장 중요한 것은 같은 키의 값을 이용해야 한다는 것
    • 대칭 암호화나 서명에 이용되는 키는 임의의 바이트 문자열이며 랜덤 알고리즘으로 생성할 수 있지만 실제 시나리오에서는 가급적 258 바이트 이상의 임의로 생성된 값을 이용하는 것을 권장

 

 

2. JWT를 이용한 비대칭 키로 서명된 토큰 이용

  • 권한 부여 서버와 리소스 서버가 키를 공유할 수 없는 케이스가 존재할 수 있음
    • 보통 권한 부여 서버와 리소스 서버가 다른 조직에서 개발될 때 자주 볼 수 있는 케이스
    • 이때 권한 부여 서버는 리소스 서버를 신뢰할 수 없으므로 리소스 서버와 키를 공유하려고 하지 않으며 대칭 키를 가진 리소스 서버는 토큰을 검증하는 것은 물론 원한다면 토큰에 서명도 할 수 있는 너무 막강한 힘을 가짐
    • 따라서 이렇게 권한 부여 서버와 리소스 서버 간에 신뢰 관계가 성립되지 않는다면 비대칭 키 쌍을 이용함

 

https://livebook.manning.com/book/spring-security-in-action/chapter-15/71

 

  • 비대칭 키 쌍에는 비밀 키공개 키라는 각기 다른 두 개의 키가 포함됨
    • 권한 부여 서버는 비밀 키로 토큰에 서명하며 비밀 키가 있어야 토큰에 서명할 수 있음
    • 공개 키는 비밀 키와 연결되므로 이 두 키를 키 쌍이라고 부름
    • 하지만 공개 키는 서명을 검증하는데만 사용할 수 있고 토큰에 서명할 수는 없음

 

https://livebook.manning.com/book/spring-security-in-action/chapter-15/77

 

2.1 키 쌍 생성

  • 권한 부여 서버가 토큰을 서명하는 데 사용할 비밀 키와 리소스 서버가 서명을 검증하는 데 쓸 공개 키로 구성된 비대칭 키 쌍을 준비해야 함
    • 해당 키 쌍을 생성하는 데는 간단한 명령줄 툴인 keytool과 OpenSSL을 이용
    • keytool은 JDK를 설치할 때 함께 설치되므로 이미 컴퓨터에 있을 것이며 OpenSSL은 https://www.openssl.org/에서 다운로드할 수 있음

 

  • 위 두 툴이 준비되면 다음의 두 작업을 위한 명령을 실행해야 함
    • 비밀 키 생성
    • 생성한 비밀 키에 대한 공개 키 얻기

 

비밀 키 생성

  • 비밀 키를 생성하려면 다음 코드와 같이 keytool 명령을 실행
    • 해당 명령은 ssia.jks 파일에 비밀 키를 생성
    • 비밀 키를 보호할 암호는 'ssia123'으로 지정했고 키 이름의 별칭은 'ssia'로 지정함

 

keytool -genkeypair -alias ssia -keyalg RSA -keypass ssia123 -keystore ssia.jks -storepass ssia123

 

공개 키 얻기

  • 생성한 비밀 키에 맞는 공개 키를 얻으려면 다음과 같이 keytool 명령을 실행해야 함

 

keytool -list -rfc --keystore ssia.jks | openssl x509 -inform pem -pubkey

 

  • 생성하는 공개 키의 암호를 입력하라는 메시지가 나오면 암호로 정한 ssia123을 입력하면 되고 그러면 공개 키와 함께 인증서가 출력됨

 

 

2.2 비밀 키를 이용하는 권한 부여 서버 구성

  • 이 절에서는 비밀 키로 JWT에 서명하는 권한 부여 서버를 구성함
    • resources 폴더에 키를 추가하면 classpath에서 곧바로 읽을 수 있어 편하므로 애플리케이션의 resources 폴더로 비밀 키 파일 ssia.jks를 복사 (실무에서는 권장하지 않음)
    • application.properties 파일에는 JwtTokenStore를 구성하기 위한 세부 정보인 파일 이름, 키의 별칭 그리고 비밀 키를 생성할 때 보호하기 위해 지정된 암호를 저장

 

 

  • 대칭 키를 이용하는 권한 부여 서버의 구현과 다른 유일한 차이는 JwtAccessTokenConverter 객체의 정의를 변경한 것
    • 대칭 키를 구성하기 위해 JwtAccessTokenConverter를 이용했듯이 이번에도 같은 JwtAccessTokenConverter 객체로 비밀 키를 구성

 

 

  • 이제 권한 부여 서버를 시작하고 /oauth/token 엔드포인트를 호출해 액세스 토큰을 생성할 수 있음
    • 결과로 나오는 것은 보통의 JWT이지만, 해당 토큰의 서명을 검증하려면 키 쌍의 공개 키를 이용해야 하는 점이 다름
    • 하지만 아직 토큰은 서명됐을 뿐 암호화되지는 않았음

 

2.3 공개 키를 이용하는 리소스 서버 구현

  • 권한 부여 서버는 비밀 키로 토큰에 서명하고 리소스 서버는 공개 키로 서명을 검증함
    • 토큰에 서명하는 데만 키를 이용했고 암호화하지는 않았다는데 주의

 

  • 리소스 서버가 토큰의 서명을 검증하려면 키 쌍의 공개 키가 필요하므로 이를 application.properties 파일에 추가해야 함

 

 

  • 리소스 서버의 구성 클래스에서 키를 구성하고 간단한 엔드포인트를 구현

 

 

2.4 공개 키를 노출하는 엔드포인트 이용

  • 공개 키는 리소스 서버 쪽에서 구성하고 해당 공개 키로 JWT를 검증함
  • 하지만 같은 키 쌍을 계속 유지하는 것은 좋은 생각이 아님
    • 따라서 시간이 지남에 따라 키를 순환해야 하며 이렇게 하면 시스템의 키 도난에 견디는 능력을 높일 수 있음

 

  • 지금까지는 권한 부여 서버에서 비밀 키를 구성하고 리소스 서버에서 공개 키를 구성했음
    • 이렇게 두 곳에서 키를 설정하면 관리하기 어렵기 때문에 한 곳에서만 키를 구성하는 것이 좋음
    • 해결책은 권한 부여 서버에 전체 키 쌍을 관리하는 책임을 옮기고 공개 키를 노출하는 엔드포인트를 추가하는 것

 

 

 

 

https://livebook.manning.com/book/spring-security-in-action/chapter-15/122

 

 

  • 리소스 서버가 권한 부여 서버의 엔드포인트에서 공개 키를 얻을 수 있도록 엔드포인트와 properties 파일의 자격 증명을 구성해야 함

 

 

  • 이제 리소스 서버는 권한 부여 서버의 /oauth/token_key 엔드포인트에서 공개 키를 가져올 수 있으므로 더는 리소스 서버의 구성 클래스에서 공개 키로 구성할 필요가 없음
    • 이제 리소스 서버의 구성 클래스를 비워도 무방함

 

 

3. JWT에 맞춤형 세부 정보 추가

  • 스프링 시큐리티가 토큰에 추가하는 것 이상의 정보를 추가할 일은 많지 않지만 실제 시나리오에서는 토큰에 맞춤형 세부 정보를 추가해야 하는 요구 사항이 있을 수 있음
  • 이 절에서는 JWT에 맞춤형 세부 정보를 추가하도록 권한 부여 서버를 변경하고 이러한 세부 정보를 읽도록 리소스 서버를 변경한 예제를 구현함
  • 아래는 권한 부여 서버가 발행한 JWT의 본문에 포함된 기본 세부 정보

 

 

  • 토큰은 일반적으로 기본 권한 부여에 필요한 모든 세부 정보를 저장하지만 실제 시나리오에는 다음과 같은 추가 요구 사항이 있을 수 있음
    • 독자가 책의 평점을 등록하는 애플리케이션에서 권한 부여 서버를 이용하는데 일부 엔드포인트는 평점 건수가 일정 수 잇아인 사용자만 접근할 수 있어야 함
    • 특정 표준 시간대 지역에서 이증한 사용자만 엔드포인트를 호출할 수 있어야 함
    • 권한 부여 서버가 소셜 네트워크이고 일부 엔드포인트는 최소 팔로워를 보유한 사용자만 접근할 수 있어야 함

 

  • 위 요구 사항 모두 JWT를 맞춤 구성함으로써 해결 가능
    • 첫 번째 예에서는 토큰의 평점 건수를 추가해야 함
    • 두 번째 예에서는 클라이언트가 연결한 표준 시간대를 추가해야 함
    • 세 번째 예에서는 사용자의 팔로워를 추가해야 함

 

3.1 토큰에 맞춤형 세부 정보를 추가하도록 권한 부여 서버 구성

  • 토큰에 세부 정보를 추가하려면 토큰 인핸서라는 TokenEnhancer 형식의 객체를 만들어야 함

 

 

 

3.2 JWT의 맞춤형 세부 정보를 읽을 수 있게 리소스 서버 구성

  • 이 절에서는 JWT에 추가한 세부 정보를 읽을 수 있게 리소스 서버를 변경하는 과정을 알아봄
  • JWT에 맞춤형 세부 정보를 추가하도록 권한 부여 서버를 변경한 뒤 당연히 이러한 정보를 읽을 수 있게 리소스 서버 또한 변경해야 함
  • AccessTokenConverter 객체가 토큰을 Authentication으로 변환하는데 이제 토큰에 들어 있는 맞춤형 세부 정보도 고려하도록 해당 객체를 수정해야 함
  • 해당 빈으로 리소스 서버가 토큰을 검증할 때 쓰는 키를 설정했으며 이제 토큰에 있는 추가 세부 정보도 고려하도록 JwtAccessTokenConverter의 맞춤형 구현을 생성해야 함
    • 가장 간단한 방법은 이 클래스를 확장하고 extractAuthentication() 메서드를 재정의하는 것
    • extractAuthentication() 메서드는 토큰을 Authentication 객체로 변환해 줌



 

참고

스프링 시큐리티 인 액션

반응형