Spring Boot + Keycloak ์ ์ฉ๊ธฐ
์ง๊ธ ๋ค๋๊ณ ์๋ ์ง์ฅ์์ ์
์ฌ ํ์ ๋ง์ Keycloak์ด๋ผ๋ OAuth2 ์ธ์ฆ ํ๋กํ ์ฝ์ ์ ์ฉ์ํค๋ผ๋ ๋ง์คํ ์๋ฌด๋ฅผ ๋ฐ์๊ธฐ์,
์ฐ์ฌ๊ณก์ ๋์ ์ ์ฉ์ํจ ์ผ์ง๋ฅผ ์ ์ด๋ณด๊ณ ์ ํ๋ค.
์ฌ๋ด์์ ์ง์ ๊ตฌ์ถํ OAuth ํด๋ผ์ด์ธํธ์ ์ธ๊ฐ ์๋ฒ๋ฅผ ๊ฑท์ด๋ด๋ฉด์ ์์
ํ๊ธฐ์ ๋ ํ๋ค์๋๊ฒ ๊ฐ๋ค.
Keycloak
์คํ ์์ค ๊ธฐ๋ฐ์ ์ฑ๊ธ ์ฌ์ธ์จ(SSO) ๋ฐ ID ๋ฐ ์ก์ธ์ค ๊ด๋ฆฌ ์๋ฃจ์
Java๋ก ์์ฑ๋ ํ๋ซํผ์ผ๋ก, OAuth 2.0 ๋ฐ OpenID Connect๊ณผ ๊ฐ์ ํ์ค ๊ธฐ๋ฐ ์ธ์ฆ ๋ฐ ๊ถํ ๋ถ์ฌ ํ๋กํ ์ฝ์ ๊ตฌํํ๊ณ ์๋ค.
Keycloak์ ์ฌ์ฉ์ ์ธ์ฆ, ๊ถํ ๋ถ์ฌ, ์ธ์
๊ด๋ฆฌ ๋ฑ ๋ค์ํ ๋ณด์ ๊ธฐ๋ฅ์ ์ ๊ณตํ์ฌ ๊ฐ๋ฐ์๋ค์ด ๋ณด์ ๊ด๋ จ ์์
์ ๊ฐ์ํํ๊ณ ์์ ์ ์ธ ์ธ์ฆ ์์คํ
์ ๊ตฌ์ถํ ์ ์๋๋ก ์ง์ํ๋ค.
Keycloak์ ์ค์ ๊ธฐ๋ฅ๋ค์ ๋ค์๊ณผ ๊ฐ๋ค.
- SSO
์ฌ์ฉ์๊ฐ ํ ๋ฒ ์ธ์ฆํ๋ฉด ์ฌ๋ฌ ์ ํ๋ฆฌ์ผ์ด์ ๋๋ ์๋น์ค์ ๋ํด ์ฌ์ธ์ฆ ์์ด ์ ๊ทผํ ์ ์๋ SSO ๊ธฐ๋ฅ์ ์ ๊ณต
-> ์ฐ๋ฆฌ ํ์ฌ์์ ์ ์ฉํ๋ ค๊ณ ํ๋ ๊ถ๊ทน์ ์ธ ์ด์ - ์ฌ์ฉ์ ์ธ์ฆ ๋ฐ ๊ด๋ฆฌ
์ฌ์ฉ์ ๊ณ์ ์์ฑ, ๋น๋ฐ๋ฒํธ ์ฌ์ค์ , ์ด๋ฉ์ผ ํ์ธ ๋ฑ์ ๊ธฐ๋ฅ์ ํฌํจํ์ฌ ์ฌ์ฉ์ ๊ด๋ฆฌ ๊ธฐ๋ฅ ์ง์ - ํด๋ผ์ด์ธํธ ์ ํ๋ฆฌ์ผ์ด์ ์ง์ ์น ์ ํ๋ฆฌ์ผ์ด์ , ๋ชจ๋ฐ์ผ ์ฑ, ๋ฐฑ์๋ ์ดํ๋ฆฌ์ผ์ด์ ๋ฑ ๋ค์ํ ํ๋ซํผ์์ Keycloak์ ํตํด ์ธ์ฆ ๋ฐ ๊ถํ ๋ถ์ฌ๋ฅผ ์ฒ๋ฆฌ ๊ฐ๋ฅ
- ๋ค์ํ ์ธ์ฆ ๋ฐฉ์
์ฌ์ฉ์ ์์ด๋/๋น๋ฐ๋ฒํธ ์ธ์ฆ, ์์ ๋ฏธ๋์ด ๊ณ์ ์ ํตํ ์ธ์ฆ(Google, Facebook ๋ฑ), SAML, LDAP, OpenID Connect ๋ฑ์ ์ธ์ฆ ํ๋กํ ์ฝ์ ์ง์ - ์ก์ธ์ค ์ ์ด ๋ฐ ๊ถํ ๋ถ์ฌ
์ฌ์ฉ์์ ์ญํ ๊ณผ ๊ถํ์ ๊ด๋ฆฌํ๊ณ , ๋ฆฌ์์ค์ ๋ํ ์ ๊ทผ ๊ถํ์ ์ ์ด - ํด๋ผ์ฐ๋ ๋ฐ ์ปจํ
์ด๋ ํ๊ฒฝ ์ง์
ํด๋ผ์ฐ๋ ํ๊ฒฝ๊ณผ ์ปจํ ์ด๋ ๊ธฐ๋ฐ ์ํคํ ์ฒ ์ง์. Kubernetes, Docker ๋ฑ๊ณผ ํตํฉํ์ฌ ํ์ฅ์ฑ๊ณผ ์ ์ฐ์ฑ์ ์ ๊ณต
Step 1. ์ค์น
์ค์น๊ฐ ๋์ง ์์ผ๋ฉด ์ฃฝ๋๋ฐฅ๋ ์๋๋ ์ผ๋จ Keycloak์ ์ค์นํด๋ณด๋๋ก ํ์.
์ค๋น๋ฌผ : docker, terminal
- Image Pull
docker pull jboss/keycloak
๋ช ๋ น์ด๋ฅผ ํตํด ์ด๋ฏธ์ง๋ฅผ ๋ฐ๋๋ค.Last login: Tue May 23 15:57:27 on ttys002 jaeho@mac ~ % docker pull jboss/keycloak Using default tag: latest latest: Pulling from jboss/keycloak ac10f00499d5: Pull complete 96d53117c12e: Pull complete 1d929376eb7f: Pull complete 93e1e1b6d192: Pull complete f353ba0db29e: Pull complet Digest: sha256:abdb1aea6c671f61a594af599f63fbe78c9631767886d9030bc774d908422d0a Status: Downloaded newer image for jboss/keycloak:latest docker.io/jboss/keycloak:latest
- Docker run
docker run -p 9000:8080 -e KEYCLOAK_USER=jaeho -e KEYCLOAK_PASSWORD=1234 jboss/keycloak
์ฝ์ ๊ด๋ฆฌ์ ๊ณ์ ์ ์ง์ ํด์ฃผ๊ณ ์คํ์์ผ์ฃผ์.========================================================================= Using Embedded H2 database ========================================================================= ========================================================================= JBoss Bootstrap Environment JBOSS_HOME: /opt/jboss/keycloak ... 07:15:47,439 INFO [org.jboss.as.server] (Controller Boot Thread) WFLYSRV0212: Resuming server 07:15:47,442 INFO [org.jboss.as] (Controller Boot Thread) WFLYSRV0025: Keycloak 16.1.0 (WildFly Core 18.0.0.Final) started in 10777ms - Started 674 of 975 services (696 services are lazy, passive or on-demand) 07:15:47,443 INFO [org.jboss.as] (Controller Boot Thread) WFLYSRV0060: Http management interface listening on http://127.0.0.1:9990/management 07:15:47,443 INFO [org.jboss.as] (Controller Boot Thread) WFLYSRV0051: Admin console listening on http://127.0.0.1:9990
Admin console listening ์ด์ฉ๊ตฌ๊ฐ ๋์ค๋ฉด ์ฑ๊ณต์ ์ผ๋ก ์คํ๋๊ฒ์ด๋ค.
http://127.0.0.1:9000 ์ผ๋ก ์ ์ํด์ ์ฐ์ปด ํ๋ฉด์ด ์ ๋ํ๋๋์ง ํ์ธํด๋ณด์.- Welcome ํ์ด์ง
Administration Console ํญ์ ๋๋ฅด๊ณ , ๋ช ๋ น์ด์ ์ ๋ ฅํ ๊ณ์ ์ ๋ณด๋ฅผ ์ ๋ ฅํ๋ฉด ๊ด๋ฆฌ์๋ก์จ ๊ด๋ฆฌ ์ฝ์ ํ์ด์ง์ ์ ๊ทผ ํ ์ ์๋ค. - Login ํ์ด์ง
- Console ํ์ด์ง
- Welcome ํ์ด์ง
Step 2. ์ค์
- ๊ณ์ ์์ฑ & ์ธํ
์ค์ ์๋น์ค์ ์ ๊ทผ ํ๊ธฐ ์ํ ๊ณ์ ์ ์์ฑํด์ผํ๋ค. (์ฝ์์ ๋ก๊ทธ์ธ ํ ๊ณ์ ์ Admin ๊ณ์ )
์ ์ ๊ด๋ฆฌ ํญ
๊ณ์ ์ ๋ณด ์ ๋ ฅ
๋น๋ฐ๋ฒํธ ์ค์ - ํด๋ผ์ด์ธํธ ์ธํ
์ดํ๋ฆฌ์ผ์ด์ ์์ ์ ๊ทผํ ํด๋ผ์ด์ธํธ๋ฅผ ์ธํ ํด ์ฃผ์ด์ผ ํ๋ค.
Access type ๋ณ๊ฒฝ
Credentials ํญ์ Secret ์์ฑ ํ์ธ - Realm ์ธํ
Access Token์ Life Cycle ์ธํ ์ ํด ์ค๋ค(ํ ์คํธ ํธ์์ฑ)
ํ๋จ์ save ๋ฒํผ ๋๋ฅด๋๊ฒ์ ์์ง ๋ง์
Step 3. ๋ก๊ทธ์ธ ํ ์คํธ
์ง๊ธ๊น์ง ์ค์ ํ ํด๋ผ์ด์ธํธ ์ ๋ณด, ์ ์ ์ ๋ณด๋ฅผ ํตํด Access Token์ ๋ฐ๊ธํ๋ ํ ์คํธ๋ฅผ ์งํํด๋ณด์.
curl --location 'http://localhost:9000/auth/realms/master/protocol/openid-connect/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=jaeho-client' \
--data-urlencode 'client_secret=sCMQZdkQO2SJMmjRk5zW22y3EMFmO5j9' \
--data-urlencode 'username=jaeho.choi' \
--data-urlencode 'password=1234' \
--data-urlencode 'grant_type=password' \
--data-urlencode 'scope=openid'
Response
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJ4UDFyUERZQUgtaHJOVmVVd2NjSDBGN3RlTmdaNGhCWFRLem9vcE42MVVvIn0.eyJleHAiOjE2ODQ4ODk0ODcsImlhdCI6MTY4NDg4NzY4NywianRpIjoiOTRhMTYwMWYtMTBmOS00ZjkwLTg3NjMtMmUyYzhiOTkwY2RkIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo5MDAwL2F1dGgvcmVhbG1zL21hc3RlciIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiJlNjg0MzU5Ni04NzNkLTQyMGEtYjc1My04MGVhMjgzOTlmMTgiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJqYWVoby1jbGllbnQiLCJzZXNzaW9uX3N0YXRlIjoiOGI2YjdhYjItMmIzNy00YzEwLWFlZjEtMWJjODAzYjUxMThmIiwiYWNyIjoiMSIsImFsbG93ZWQtb3JpZ2lucyI6WyJodHRwOi8vbG9jYWxob3N0OjgwODAiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImRlZmF1bHQtcm9sZXMtbWFzdGVyIiwib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoib3BlbmlkIHByb2ZpbGUgZW1haWwiLCJzaWQiOiI4YjZiN2FiMi0yYjM3LTRjMTAtYWVmMS0xYmM4MDNiNTExOGYiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIm5hbWUiOiJKYWVobyBDaG9pIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiamFlaG8uY2hvaSIsImdpdmVuX25hbWUiOiJKYWVobyIsImZhbWlseV9uYW1lIjoiQ2hvaSIsImVtYWlsIjoianVseXNldmVuMTk5NUBnbWFpbC5jb20ifQ.TwkMwHLMJSXhg1yyRU9_dPu5EiOQA5wqOf_Kjgxja541mXow1KImjaNS7K7F89CnWWtkaZqUbDIE9lc0Auyunavhg_4vw615Xb1yjmn8c3DARj6zrIjgGOrHvU_LDg2irKfeH8uTS7lzqhgV01--6ehBT_OAJBjFAXnQ0TEGxcg1fAyIO032u6gKJpUmZlqyR2z0m2OvKCgjRCMe9cUCgKOyUL7jgFBrWQc5SXzgroE5ju7u0MFlcpVCw4xf6uBWb9sFLm6k04qgIRZ8ycW7eT9K8YXHbeBDMt5MotssQzACXRgD8loLwwihgf22goj5aOOLHiRkDn6l4J5nMYE3mg",
"expires_in": 1800,
"refresh_expires_in": 3600,
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI4ZjMzMjJkOC1kMTMyLTRmOWYtYTJkOS00NDU3YjI0MjliNjAifQ.eyJleHAiOjE2ODQ4OTEyODcsImlhdCI6MTY4NDg4NzY4NywianRpIjoiNjY1ZTFhZWUtZWQ5MS00ZThiLTk2ODMtNzE4YmM3ZmEzNjBhIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo5MDAwL2F1dGgvcmVhbG1zL21hc3RlciIsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6OTAwMC9hdXRoL3JlYWxtcy9tYXN0ZXIiLCJzdWIiOiJlNjg0MzU5Ni04NzNkLTQyMGEtYjc1My04MGVhMjgzOTlmMTgiLCJ0eXAiOiJSZWZyZXNoIiwiYXpwIjoiamFlaG8tY2xpZW50Iiwic2Vzc2lvbl9zdGF0ZSI6IjhiNmI3YWIyLTJiMzctNGMxMC1hZWYxLTFiYzgwM2I1MTE4ZiIsInNjb3BlIjoib3BlbmlkIHByb2ZpbGUgZW1haWwiLCJzaWQiOiI4YjZiN2FiMi0yYjM3LTRjMTAtYWVmMS0xYmM4MDNiNTExOGYifQ.Zt3X13xaP-40tHpNhKrTOQIkP8IIzw4HoGnYNwgxm24",
"token_type": "Bearer",
"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJ4UDFyUERZQUgtaHJOVmVVd2NjSDBGN3RlTmdaNGhCWFRLem9vcE42MVVvIn0.eyJleHAiOjE2ODQ4ODk0ODcsImlhdCI6MTY4NDg4NzY4NywiYXV0aF90aW1lIjowLCJqdGkiOiJiNTYzZTYzMC1lOWM5LTRkMTgtYWQxYi1iMjZlNjA0ZGI5YzkiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjkwMDAvYXV0aC9yZWFsbXMvbWFzdGVyIiwiYXVkIjoiamFlaG8tY2xpZW50Iiwic3ViIjoiZTY4NDM1OTYtODczZC00MjBhLWI3NTMtODBlYTI4Mzk5ZjE4IiwidHlwIjoiSUQiLCJhenAiOiJqYWVoby1jbGllbnQiLCJzZXNzaW9uX3N0YXRlIjoiOGI2YjdhYjItMmIzNy00YzEwLWFlZjEtMWJjODAzYjUxMThmIiwiYXRfaGFzaCI6InlhMS1CdU5HSHE1T0VfdlQyeC1kNFEiLCJhY3IiOiIxIiwic2lkIjoiOGI2YjdhYjItMmIzNy00YzEwLWFlZjEtMWJjODAzYjUxMThmIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoiSmFlaG8gQ2hvaSIsInByZWZlcnJlZF91c2VybmFtZSI6ImphZWhvLmNob2kiLCJnaXZlbl9uYW1lIjoiSmFlaG8iLCJmYW1pbHlfbmFtZSI6IkNob2kiLCJlbWFpbCI6Imp1bHlzZXZlbjE5OTVAZ21haWwuY29tIn0.WtfpE3TUT8Y3sSYfI6GKKWxpfVNI7OdmjwAraPmaXmBPHEkt5nCbXwtQsg6YKotNLLHJdzOg_GtKPzg67tuR8eJDBbf2ei_UvnSTcWVMY3okI_X2pwGlPH_JS6rXH-ADPUKIxVvhgKrXbNdksLaO99mnyQJgKrB8foISgHDrtl9Y2Mdp6yBLPewQKt3ekmpcQYKTRuDNICp7vSUi_h72LA08ySh5_82rHVKTSp6NDrLRl1D569vU2G8ekm1XuiF_jh9bsV7cQBT9NtVL0XE28z2h30nEmv3a-U-nwMd0G70xzZC9SWaPRPZo5BJWAS11dQu10bZG1zfzvoGYBzfXBA",
"not-before-policy": 0,
"session_state": "8b6b7ab2-2b37-4c10-aef1-1bc803b5118f",
"scope": "openid profile email"
}
JWT ํ ํฐ์ ๋ฐ๊ธ๋ฐ์ ์ ์๋๊ฒ์ ํ์ธ ํ ์ ์๋ค.
Spring Boot + Keycloak
์ด์ Keycloak์ ๊ธฐ๋ณธ์ ์ธ ์ธํ ๋ ๋ง์ณค๊ณ , Access Token ๋ฐ๊ธ๋ ํด๋์ผ๋, Spring Boot Application๊ณผ ์ฐ๋ํด ๋ณด๋๋ก ํ๊ฒ ๋ค.
-
ํ๋ก์ ํธ ์ธํ
Spring initalizr์์ Spring Boot ํ๋ก์ ํธ ์์ฑ์ ํด์ฃผ๋ฉด ๋๋ค.
์ถ๊ฐ ํ๊ณ ์ถ์ ์์กด์ฑ์ด ์๋ค๋ฉด ์ถ๊ฐํด๋ ๋ฌด๋ฐฉํ๋ค.ํ๋ก์ ํธ๋ฅผ ์ด์๋ค๋ฉด, application.yml์ ๋ค์๊ณผ ๊ฐ์ด ์ธํ ํด์ค๋ค.
spring: application: name: jaeho-project security: oauth2: client: provider: oidc: issuer-uri: 'http://localhost:9000/auth/realms/master' registration: oidc: client-id: jaeho-client client-secret: sCMQZdkQO2SJMmjRk5zW22y3EMFmO5j9 scope: - openid - profile - email
- Jwt Validator ํด๋์ค ์์ฑ
Spring Security ์ค์ ์ ์์ฒญ๋ฐ์ ํ ํฐ์ ์ธ์ฆํ๊ธฐ ์ํ Custom Validator๋ฅผ ๋ง๋ค์ด์ค๋ค.@Slf4j public class JwtTokenValidator implements OAuth2TokenValidator<Jwt> { private final String userInfoEndpoint; private final OAuth2Error error; private final RestTemplate restTemplate = new RestTemplate(); public JwtTokenValidator(String userInfoEndpoint) { this.userInfoEndpoint = userInfoEndpoint; this.error = new OAuth2Error("invalid_token", "User information fetching failed", userInfoEndpoint); } @Override public OAuth2TokenValidatorResult validate(Jwt token) { HttpHeaders headers = new HttpHeaders(); headers.set("Authorization", "Bearer " + token.getTokenValue()); try { // ์ ์ ์ ๋ณด Fetching ์ ํตํด token ์ ํจ์ฑ ์ฒดํฌ restTemplate.exchange( userInfoEndpoint, HttpMethod.GET, new HttpEntity<String>(headers), Object.class ); // Context holder์ ์์ฒญ๋ฐ์ ํ ํฐ์ผ๋ก Authentication์ ๋ง๋ค์ด ๋ฃ์ด์ค๋ค. SecurityContextHolder.getContext().setAuthentication(new JwtAuthenticationToken(token)); return OAuth2TokenValidatorResult.success(); } catch (Exception e) { log.error("Token Validation Failed. ->", e); return OAuth2TokenValidatorResult.failure(error); } } }
-
Security Configuration ์ค์
http ์ ๊ทผ ์ ์ด๋ฅผ ์ํ Security Configuration์ ์ค์ ํด์ค๋ค.@EnableWebSecurity public class SecurityConfiguration { private final ClientRegistration clientRegistration; public SecurityConfiguration(ClientRegistrationRepository clientRegistrationRepository) { this.clientRegistration = clientRegistrationRepository.findByRegistrationId("oidc"); } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf() .disable() .authorizeHttpRequests() .mvcMatchers("/api/**").authenticated() .and() .oauth2ResourceServer() .jwt() .jwtAuthenticationConverter(this.jwtAuthenticationConverter()); return http.build(); } @Bean public JwtDecoder jwtDecoder() { NimbusJwtDecoder jwtDecoder = JwtDecoders.fromIssuerLocation(clientRegistration.getProviderDetails().getIssuerUri()); jwtDecoder.setJwtValidator(new JwtTokenValidator(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUri())); return jwtDecoder; } private Converter<Jwt, AbstractAuthenticationToken> jwtAuthenticationConverter() { JwtAuthenticationConverter converter = new JwtAuthenticationConverter(); converter.setJwtGrantedAuthoritiesConverter(new JwtGrantedAuthoritiesConverter()); return converter; } }
- Endpoint ์์ฑ
ํ ํฐ์ ์ ๋ฌด์ ๋ฐ๋ผ ์ ์์ฒญ์ ์ ๊ฑฐ๋ฅด๋์ง ํ์ธํ๊ธฐ ์ํด ๊ฐ๋จํ ์๋ํฌ์ธํธ๋ฅผ ๊ตฌํํด๋ณด์.@RestController @RequestMapping("/api") public class JaehoController { @GetMapping(path = "/hello") public ResponseEntity<String> hello() { return ResponseEntity.ok("Hello!"); } }
- ํ
์คํธ
๋ชจ๋ ์ธํ ์ด ๋๋ฌ์ผ๋ ํ๋ก์ ํธ๋ฅผ ์คํ์ํค๊ณ , ํ ํฐ ์์ด hello api๋ฅผ ํธ์ถํด๋ณด์.curl -L -k -s -o /dev/null -w "%{http_code}\n" http://localhost:8080/api/hello
๊ถํ์ด ์์ด 401์ด ์๋ต๊ฐ์ผ๋ก ๋ํ๋๋๊ฒ์ ์ ์ ์๋ค. ๊ทธ๋ ๋ค๋ฉด Access token์ ๋ฐ๊ธ๋ฐ์์ ํธ์ถํ๋ฉด ์ ๋ ๊น
curl --location 'http://localhost:8080/api/hello' \ --header 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJ4UDFyUERZQUgtaHJOVmVVd2NjSDBGN3RlTmdaNGhCWFRLem9vcE42MVVvIn0.eyJleHAiOjE2ODQ4OTUxODcsImlhdCI6MTY4NDg5MzM4NywianRpIjoiYWNmYjU0ODAtYjczMS00YmFkLTkzYTEtZGY0MTYxMGQyNzVjIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo5MDAwL2F1dGgvcmVhbG1zL21hc3RlciIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiJlNjg0MzU5Ni04NzNkLTQyMGEtYjc1My04MGVhMjgzOTlmMTgiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJqYWVoby1jbGllbnQiLCJzZXNzaW9uX3N0YXRlIjoiNTA3ZTE3YWMtYmM5Mi00M2Q5LTgxNjUtZGVkZjE4MGJkNmZhIiwiYWNyIjoiMSIsImFsbG93ZWQtb3JpZ2lucyI6WyJodHRwOi8vbG9jYWxob3N0OjgwODAiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImRlZmF1bHQtcm9sZXMtbWFzdGVyIiwib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoib3BlbmlkIHByb2ZpbGUgZW1haWwiLCJzaWQiOiI1MDdlMTdhYy1iYzkyLTQzZDktODE2NS1kZWRmMTgwYmQ2ZmEiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIm5hbWUiOiJKYWVobyBDaG9pIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiamFlaG8uY2hvaSIsImdpdmVuX25hbWUiOiJKYWVobyIsImZhbWlseV9uYW1lIjoiQ2hvaSIsImVtYWlsIjoianVseXNldmVuMTk5NUBnbWFpbC5jb20ifQ.PjEHSyGLt95aJhOJM35eJ06vNNngjXe-urdn9c7lYwEhd4882E8zKCwBQe4Cl8xDwnkV04Xlv2NW2uA9uNOQI-HQLusKzqRtfpndc5JefPqh60gL5g1Qvgh3mhWHo2wz2y3r6YPQaJvlP2bADj-zzN7VU8vUARlVgfm4JvLpO27O_UzuF72NMt98ICT6XbHGdQqbCBunwwXwa0X-NQNBZTOxvh4mVXwAcmVJ0aTrT-v5XnCNZcc_JYKyVYLYiQfEkyVgStdpetCGyDMkO03I_yPETEs7-9bQJ3AJEQgWqiUVmtVE6EWIMWNlxm_EBhSryS12Bjq9v4znHYg2gkrqZg'
์ ์์ ์ผ๋ก ์ ์ด์์๋ ํ ํฐ์ด๋ผ๋ฉด, Hello๋ผ๋ Response Body๋ฅผ ํ์ธ ํ ์ ์๋ค!
๋๊ธ๋จ๊ธฐ๊ธฐ