이 연재글은 SpringBoot2로 Oauth2 서버 만들기의 4번째 글입니다.

이번 시간에는 인증서버와 리소스 서버간에 토큰 인증시 서명 방식을 변경해 보겠습니다. 서명이란 서로간에 신뢰성있는 통신을 하기 위한 절차라고 생각하면 됩니다.

최신 소스는 아래 GitHub 주소를 참고해 주세요.

https://github.com/codej99/SpringOauth2AuthorizationServer.git
https://github.com/codej99/SpringOauth2ResourceServer.git

대칭키 서명

A(리소스서버)가 B(인증서버)에게 데이터를 보낼때 B의 sign으로 암호화 해서 보내면 B는 자신의 sign으로 데이터를 복호화해서 볼 수 있습니다. B는 데이터를 전달받기 전에 A에게 자신의 sign정보(비밀키)를 공유해주기만 하면 됩니다. 그런데 이 방식은 B의 sign이 노출되면 다른 C가 B에게 보내진 데이터를 가로채서 볼 수 있는 단점이 있습니다. 위의 방식은 양쪽에 같은 sign키를 공유하기 때문에 문제가 됩니다. 이전 장에서 인증서버와 리소스서버의 application.yml 파일을 보면 아래와 같이 동일한 signKey를 공유하고 있음을 확인 할 수 있습니다.

security:
  oauth2:
    jwt:
      signkey: 123@#$

비대칭키를 이용한 서명(asymmetric keys to do the signing process)

비대칭키 방식은 B(인증서버)가 비밀키와 공개키 1쌍을 만듭니다. 데이터는 공개키로 암호화 되는데 복호화는 비밀키로만 가능합니다. B(인증서버)는 A(리소스서버)에게 공개키를 공유하고 해당 키로 암호화하여 데이터를 전달해 달라고 합니다. 이렇게 되면 공개키와 암호화된 데이터가 다른곳에 노출 되더라도, 다른 누군가가 데이터를 복호화해 볼 수 없기 때문에 결과적으로 높은 보안의 데이터 통신이 가능하게 됩니다.

Java KeyStore (JKS) – 자바 키 저장소

java에서는 keytool을 제공하여 비대칭키를 생성할 수 있는 방법을 제공합니다. 보통 비대칭키 암호화는 RSA 알고리즘을 많이 사용하며 다음과 같이 생성할 수 있습니다.

$ keytool -genkeypair -alias Key의 별칭
                    -keyalg RSA 
                    -keypass Key 암호
                    -keystore 저장될 key의 파일명 
                    -storepass 저장될 파일의 암호

keytool 명령을 실행하면 실행한 디렉토리에 *.jks 파일이 생성됩니다. 해당 파일은 비밀키와 공개키 정보를 담고 있습니다.

$ keytool -genkeypair -alias oauth2jwt -keyalg RSA -keypass oauth2jwtpass -keystore oauth2jwt.jks -storepass oauth2jwtpass
이름과 성을 입력하십시오.
  [Unknown]:
조직 단위 이름을 입력하십시오.
  [Unknown]:
조직 이름을 입력하십시오.
  [Unknown]:
구/군/시 이름을 입력하십시오?
  [Unknown]:
시/도 이름을 입력하십시오.
  [Unknown]:
이 조직의 두 자리 국가 코드를 입력하십시오.
  [Unknown]:
CN=Unknown, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, C=Unknown이(가) 맞습니까?
  [아니오]:  y

공개키 정보 확인

생성한 jks 파일에서 다음과 같이 공개키 정보를 확인 할 수 있습니다.
클라이언트에는 —–BEGIN PUBLIC KEY—– 공개키 —–END PUBLIC KEY—– 내용을 전달하면 됩니다.

$ keytool -list -rfc --keystore oauth2jwt.jks | openssl x509 -inform pem -pubkey
키 저장소 비밀번호 입력:  oauth2jwtpass
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArJerQDKo7Vwl2an9UEZD
9npx8vXoB+setdeZn5OKEntdpdXMdc1KE07Q8aejXnEdzTHqeWxfdctyJ0FZzphX
PSfw8IzjjElmi4GoM5Aqh0ecPUjRrSHrE3EXwxagfoRy1igfD5ALCH7VIOvf3erU
QKhAY+ARdQzNn2+d1V9y6atnPPwbjflm2Ke+S9K+Q+dvIpIbotRG7rJqO9RSn89Q
E6CcNP0LhaL6FFt9mtCEtOAlZyJHLO/CJ51XQzfGP4cYXCUQT5TC0yRKmPlRuurE
gfgmiAYl6XD1o3iui3vEJzNLN9nrbte4rjc+Rqv3aE23hu3f8QImNzdSZo6HJhQC
NwIDAQAB
-----END PUBLIC KEY-----
-----BEGIN CERTIFICATE-----
MIIDdzCCAl+gAwIBAgIEJg3+SjANBgkqhkiG9w0BAQsFADBsMRAwDgYDVQQGEwdV
bmtub3duMRAwDgYDVQQIEwdVbmtub3duMRAwDgYDVQQHEwdVbmtub3duMRAwDgYD
VQQKEwdVbmtub3duMRAwDgYDVQQLEwdVbmtub3duMRAwDgYDVQQDEwdVbmtub3du
MB4XDTE5MDQyNjE1MjAzMloXDTE5MDcyNTE1MjAzMlowbDEQMA4GA1UEBhMHVW5r
bm93bjEQMA4GA1UECBMHVW5rbm93bjEQMA4GA1UEBxMHVW5rbm93bjEQMA4GA1UE
ChMHVW5rbm93bjEQMA4GA1UECxMHVW5rbm93bjEQMA4GA1UEAxMHVW5rbm93bjCC
ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKyXq0AyqO1cJdmp/VBGQ/Z6
cfL16AfrHrXXmZ+TihJ7XaXVzHXNShNO0PGno15xHc0x6nlsX3XLcidBWc6YVz0n
8PCM44xJZouBqDOQKodHnD1I0a0h6xNxF8MWoH6EctYoHw+QCwh+1SDr393q1ECo
QGPgEXUMzZ9vndVfcumrZzz8G435ZtinvkvSvkPnbyKSG6LURu6yajvUUp/PUBOg
nDT9C4Wi+hRbfZrQhLTgJWciRyzvwiedV0M3xj+HGFwlEE+UwtMkSpj5UbrqxIH4
JogGJelw9aN4rot7xCczSzfZ627XuK43Pkar92hNt4bt3/ECJjc3UmaOhyYUAjcC
AwEAAaMhMB8wHQYDVR0OBBYEFGwU4l85+ogUly2Gv+cuzzRUvOs1MA0GCSqGSIb3
DQEBCwUAA4IBAQBaL58pMhDpSebvZFQeQkyDfc95lo/9b/+e109ryORIoX3zzbNO
O1v8By2qhjj11QS3skTv2jNE4h1wDxlF+EK6uom3Vc+yHH85/cRP6Ec9bK3+Vab7
BxE/r713L3I7rGbM0DMnTeDEDaCytN6TmwxeqJiUMePfWdKAZUV/WZeKTZxePfUf
ZK9J+FFEaua3hay2iY7s+eGPZ93rcNfrudARARdn34/coWx4lD8kh6At/K1qJrM2
7/QGMJeFP2zwBhhEhAwAN2ogad7cXOoy6OwcM0sMuZ+y5Vt2WKFLbHtRPj1V5mhk
Nq/ACKliqWctpd8P5M/4Oat+zdyR3YjnU3+r
-----END CERTIFICATE-----

인증서버에 jks 설정 추가

keytool로 만든 jks 파일을 인증서버의 resources 아래에 복사합니다.

Oauth2AuthorizationConfig의 TokenConverter 수정

sign키 공유방식은 주석처리하고 oauth2jwt.jks를 읽어 처리하는 방식을 추가합니다.

/**
* jwt converter - signKey 공유 방식
*/
//    @Bean
//    public JwtAccessTokenConverter jwtAccessTokenConverter() {
//        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
//        converter.setSigningKey(signKey);
//        return converter;
//    }

/**
* jwt converter - 비대칭 키 sign
*/
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
    KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new FileSystemResource("src/main/resources/oauth2jwt.jks"), "oauth2jwtpass".toCharArray());
    JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
    converter.setKeyPair(keyStoreKeyFactory.getKeyPair("oauth2jwt"));
    return converter;
}

리소스서버(api)에 공개키 등록

resources아래에 공키키 파일을 하나 만들어 공개키를 저장합니다.

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArJerQDKo7Vwl2an9UEZD
9npx8vXoB+setdeZn5OKEntdpdXMdc1KE07Q8aejXnEdzTHqeWxfdctyJ0FZzphX
PSfw8IzjjElmi4GoM5Aqh0ecPUjRrSHrE3EXwxagfoRy1igfD5ALCH7VIOvf3erU
QKhAY+ARdQzNn2+d1V9y6atnPPwbjflm2Ke+S9K+Q+dvIpIbotRG7rJqO9RSn89Q
E6CcNP0LhaL6FFt9mtCEtOAlZyJHLO/CJ51XQzfGP4cYXCUQT5TC0yRKmPlRuurE
gfgmiAYl6XD1o3iui3vEJzNLN9nrbte4rjc+Rqv3aE23hu3f8QImNzdSZo6HJhQC
NwIDAQAB
-----END PUBLIC KEY-----

Oauth2ResourceServerConfig의 TokenConverter 수정

signKey공유방식은 주석처리하고 publicKey.txt를 읽어 처리하도록 Converer를 추가합니다.

//    @Bean
//    public JwtAccessTokenConverter accessTokenConverter() {
//        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
//        converter.setSigningKey(signKey);
//        return converter;
//    }

@Bean
public JwtAccessTokenConverter accessTokenConverter() {
    JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
    Resource resource = new ClassPathResource("publicKey.txt");
    String publicKey = null;
    try {
        publicKey = IOUtils.toString(resource.getInputStream());
    } catch (final IOException e) {
        throw new RuntimeException(e);
    }
    converter.setVerifierKey(publicKey);
    return converter;
}

테스트

인증서버(8081), 리소스서버(8080)을 스타트합니다. 인증서버에서 로그인하여 토큰을 생성합니다.

http://localhost:8081/oauth/authorize?client_id=testClientId&redirect_uri=http://localhost:8081/oauth2/callback&response_type=code&scope=read

{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NTYzMzQ0MDMsInVzZXJfbmFtZSI6ImhhcHB5ZGFkZHlAZ21haWwuY29tIiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9VU0VSIl0sImp0aSI6IjNjZGIwY2NhLWU2MzItNDhmMC1hNzJlLWJiYWYzZjhmNDI0ZiIsImNsaWVudF9pZCI6InRlc3RDbGllbnRJZCIsInNjb3BlIjpbInJlYWQiXX0.jZG2IOrKCyJkq8F-j_vr-0Kenurq8YgA9Z95Y4N_DMk3AvzxLMfq1GuyT2HmW6pCQTbQbFqFu1oG-QanihKLBCSD-u3BYvtbGheql-gFC0SjUwhk1JXYSpeOWIo-giS6wPaiB4ahkcLoAgVhg4tAw9nNiIySDBC8dwKnn8jIPxRPRNpp64C-cPncixeos5T3vX-AIZz48VTpotTR6zXqmM12D9nhVJw2UTaMrKK9uQ2z5WNuruiopSF-BCgWMlb1Iq6k9TnMXi0tG7dHyImw-CsMsWjLKBhciI8N2IH2MCyHufkxK8Vx-qg0yWT83VzWpktOl9P_sT2b9R-bNgPaRA",
"token_type": "bearer",
"refresh_token": null,
"expires_in": 35999,
"scope": "read"
}

리소스서버에서 JWT로 users api 호출

비대칭키 서명을 통해 정상적으로 JWT인증이 되는것을 확인합니다.

리소스 서버의 공개키를 다른 공개키로 변경할경우

이런 경우는 없겠지만 테스트 결과는 JWT를 복호화하지 못하여 invalid_token 오류가 발생하게 됩니다.

비대칭키 서명을 통하여 인증서버와 리소스서버간의 통신을 고도화해 보았습니다. HTTPS(SSL)도 기본적으로는 이러한 방식으로 데이터를 주고 받고 있습니다. 인터넷에서는 데이터 탈취가 가능한 요소가 많기 때문에 보호되어야 할 정보를 전달할때 서명은 신뢰있는 데이터 통신을 위해 매우 중요한 요소입니다.

연재글 이동[이전글] Spring Boot Oauth2 – ResourceServer