Spring Boot + Keycloak ์ ์šฉ๊ธฐ

4 ๋ถ„ ์†Œ์š”

์ง€๊ธˆ ๋‹ค๋‹ˆ๊ณ  ์žˆ๋Š” ์ง์žฅ์—์„œ ์ž…์‚ฌ ํ•˜์ž ๋งˆ์ž Keycloak์ด๋ผ๋Š” OAuth2 ์ธ์ฆ ํ”„๋กœํ† ์ฝœ์„ ์ ์šฉ์‹œํ‚ค๋ผ๋Š” ๋ง‰์ค‘ํ•œ ์ž„๋ฌด๋ฅผ ๋ฐ›์•˜๊ธฐ์—,
์šฐ์—ฌ๊ณก์ ˆ ๋์— ์ ์šฉ์‹œํ‚จ ์ผ์ง€๋ฅผ ์ ์–ด๋ณด๊ณ ์ž ํ•œ๋‹ค.
์‚ฌ๋‚ด์—์„œ ์ง์ ‘ ๊ตฌ์ถ•ํ•œ OAuth ํด๋ผ์ด์–ธํŠธ์™€ ์ธ๊ฐ€ ์„œ๋ฒ„๋ฅผ ๊ฑท์–ด๋‚ด๋ฉด์„œ ์ž‘์—…ํ–ˆ๊ธฐ์— ๋” ํž˜๋“ค์—ˆ๋˜๊ฒƒ ๊ฐ™๋‹ค.

Keycloak

์˜คํ”ˆ ์†Œ์Šค ๊ธฐ๋ฐ˜์˜ ์‹ฑ๊ธ€ ์‚ฌ์ธ์˜จ(SSO) ๋ฐ ID ๋ฐ ์•ก์„ธ์Šค ๊ด€๋ฆฌ ์†”๋ฃจ์…˜

Java๋กœ ์ž‘์„ฑ๋œ ํ”Œ๋žซํผ์œผ๋กœ, OAuth 2.0 ๋ฐ OpenID Connect๊ณผ ๊ฐ™์€ ํ‘œ์ค€ ๊ธฐ๋ฐ˜ ์ธ์ฆ ๋ฐ ๊ถŒํ•œ ๋ถ€์—ฌ ํ”„๋กœํ† ์ฝœ์„ ๊ตฌํ˜„ํ•˜๊ณ  ์žˆ๋‹ค.
Keycloak์€ ์‚ฌ์šฉ์ž ์ธ์ฆ, ๊ถŒํ•œ ๋ถ€์—ฌ, ์„ธ์…˜ ๊ด€๋ฆฌ ๋“ฑ ๋‹ค์–‘ํ•œ ๋ณด์•ˆ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜์—ฌ ๊ฐœ๋ฐœ์ž๋“ค์ด ๋ณด์•ˆ ๊ด€๋ จ ์ž‘์—…์„ ๊ฐ„์†Œํ™”ํ•˜๊ณ  ์•ˆ์ •์ ์ธ ์ธ์ฆ ์‹œ์Šคํ…œ์„ ๊ตฌ์ถ•ํ•  ์ˆ˜ ์žˆ๋„๋ก ์ง€์›ํ•œ๋‹ค.

Keycloak์˜ ์ค‘์š” ๊ธฐ๋Šฅ๋“ค์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

  1. SSO
    ์‚ฌ์šฉ์ž๊ฐ€ ํ•œ ๋ฒˆ ์ธ์ฆํ•˜๋ฉด ์—ฌ๋Ÿฌ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋˜๋Š” ์„œ๋น„์Šค์— ๋Œ€ํ•ด ์žฌ์ธ์ฆ ์—†์ด ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋Š” SSO ๊ธฐ๋Šฅ์„ ์ œ๊ณต
    -> ์šฐ๋ฆฌ ํšŒ์‚ฌ์—์„œ ์ ์šฉํ•˜๋ ค๊ณ  ํ–ˆ๋˜ ๊ถ๊ทน์ ์ธ ์ด์œ 
  2. ์‚ฌ์šฉ์ž ์ธ์ฆ ๋ฐ ๊ด€๋ฆฌ
    ์‚ฌ์šฉ์ž ๊ณ„์ • ์ƒ์„ฑ, ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ •, ์ด๋ฉ”์ผ ํ™•์ธ ๋“ฑ์˜ ๊ธฐ๋Šฅ์„ ํฌํ•จํ•˜์—ฌ ์‚ฌ์šฉ์ž ๊ด€๋ฆฌ ๊ธฐ๋Šฅ ์ง€์›
  3. ํด๋ผ์ด์–ธํŠธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ง€์› ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜, ๋ชจ๋ฐ”์ผ ์•ฑ, ๋ฐฑ์—”๋“œ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋“ฑ ๋‹ค์–‘ํ•œ ํ”Œ๋žซํผ์—์„œ Keycloak์„ ํ†ตํ•ด ์ธ์ฆ ๋ฐ ๊ถŒํ•œ ๋ถ€์—ฌ๋ฅผ ์ฒ˜๋ฆฌ ๊ฐ€๋Šฅ
  4. ๋‹ค์–‘ํ•œ ์ธ์ฆ ๋ฐฉ์‹
    ์‚ฌ์šฉ์ž ์•„์ด๋””/๋น„๋ฐ€๋ฒˆํ˜ธ ์ธ์ฆ, ์†Œ์…œ ๋ฏธ๋””์–ด ๊ณ„์ •์„ ํ†ตํ•œ ์ธ์ฆ(Google, Facebook ๋“ฑ), SAML, LDAP, OpenID Connect ๋“ฑ์˜ ์ธ์ฆ ํ”„๋กœํ† ์ฝœ์„ ์ง€์›
  5. ์•ก์„ธ์Šค ์ œ์–ด ๋ฐ ๊ถŒํ•œ ๋ถ€์—ฌ
    ์‚ฌ์šฉ์ž์˜ ์—ญํ• ๊ณผ ๊ถŒํ•œ์„ ๊ด€๋ฆฌํ•˜๊ณ , ๋ฆฌ์†Œ์Šค์— ๋Œ€ํ•œ ์ ‘๊ทผ ๊ถŒํ•œ์„ ์ œ์–ด
  6. ํด๋ผ์šฐ๋“œ ๋ฐ ์ปจํ…Œ์ด๋„ˆ ํ™˜๊ฒฝ ์ง€์›
    ํด๋ผ์šฐ๋“œ ํ™˜๊ฒฝ๊ณผ ์ปจํ…Œ์ด๋„ˆ ๊ธฐ๋ฐ˜ ์•„ํ‚คํ…์ฒ˜ ์ง€์›. Kubernetes, Docker ๋“ฑ๊ณผ ํ†ตํ•ฉํ•˜์—ฌ ํ™•์žฅ์„ฑ๊ณผ ์œ ์—ฐ์„ฑ์„ ์ œ๊ณต

Step 1. ์„ค์น˜

์„ค์น˜๊ฐ€ ๋˜์ง€ ์•Š์œผ๋ฉด ์ฃฝ๋„๋ฐฅ๋„ ์•ˆ๋˜๋‹ˆ ์ผ๋‹จ Keycloak์„ ์„ค์น˜ํ•ด๋ณด๋„๋ก ํ•˜์ž.

์ค€๋น„๋ฌผ : docker, terminal

  1. 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
    
  2. 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 ์œผ๋กœ ์ ‘์†ํ•ด์„œ ์›ฐ์ปด ํ™”๋ฉด์ด ์ž˜ ๋‚˜ํƒ€๋‚˜๋Š”์ง€ ํ™•์ธํ•ด๋ณด์ž.

    1. Welcome ํŽ˜์ด์ง€

      Administration Console ํƒญ์„ ๋ˆ„๋ฅด๊ณ , ๋ช…๋ น์–ด์— ์ž…๋ ฅํ•œ ๊ณ„์ • ์ •๋ณด๋ฅผ ์ž…๋ ฅํ•˜๋ฉด ๊ด€๋ฆฌ์ž๋กœ์จ ๊ด€๋ฆฌ ์ฝ˜์†” ํŽ˜์ด์ง€์— ์ ‘๊ทผ ํ•  ์ˆ˜ ์žˆ๋‹ค.
    2. Login ํŽ˜์ด์ง€
    3. Console ํŽ˜์ด์ง€

Step 2. ์„ค์ •

  1. ๊ณ„์ • ์ƒ์„ฑ & ์„ธํŒ…
    ์‹ค์ œ ์„œ๋น„์Šค์— ์ ‘๊ทผ ํ•˜๊ธฐ ์œ„ํ•œ ๊ณ„์ •์„ ์ƒ์„ฑํ•ด์•ผํ•œ๋‹ค. (์ฝ˜์†”์— ๋กœ๊ทธ์ธ ํ•œ ๊ณ„์ •์€ Admin ๊ณ„์ •)
    ์œ ์ € ๊ด€๋ฆฌ ํƒญ
    ๊ณ„์ •์ƒ์„ฑ
    ๊ณ„์ • ์ •๋ณด ์ž…๋ ฅ
    ๋น„๋ฐ€๋ฒˆํ˜ธ ์„ค์ •
    ๋น„๋ฐ€๋ฒˆํ˜ธ ์„ค์ •
  2. ํด๋ผ์ด์–ธํŠธ ์„ธํŒ…
    ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ์ ‘๊ทผํ•  ํด๋ผ์ด์–ธํŠธ๋ฅผ ์„ธํŒ…ํ•ด ์ฃผ์–ด์•ผ ํ•œ๋‹ค.



    Access type ๋ณ€๊ฒฝ

    Credentials ํƒญ์˜ Secret ์ƒ์„ฑ ํ™•์ธ
  3. 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๊ณผ ์—ฐ๋™ํ•ด ๋ณด๋„๋ก ํ•˜๊ฒ ๋‹ค.

  1. ํ”„๋กœ์ ํŠธ ์„ธํŒ…
    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
    
  2. 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);
             }
         }
     }
    
    
  3. 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;
         }
    
     }
    
  4. Endpoint ์ƒ์„ฑ
    ํ† ํฐ์˜ ์œ ๋ฌด์— ๋”ฐ๋ผ ์ž˜ ์š”์ฒญ์„ ์ž˜ ๊ฑฐ๋ฅด๋Š”์ง€ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด ๊ฐ„๋‹จํ•œ ์—”๋“œํฌ์ธํŠธ๋ฅผ ๊ตฌํ˜„ํ•ด๋ณด์ž.
     @RestController
     @RequestMapping("/api")
     public class JaehoController {
    
         @GetMapping(path = "/hello")
         public ResponseEntity<String> hello() {
    
             return ResponseEntity.ok("Hello!");
         }
     }
    
  5. ํ…Œ์ŠคํŠธ
    ๋ชจ๋“  ์„ธํŒ…์ด ๋๋‚ฌ์œผ๋‹ˆ ํ”„๋กœ์ ํŠธ๋ฅผ ์‹คํ–‰์‹œํ‚ค๊ณ , ํ† ํฐ ์—†์ด 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๋ฅผ ํ™•์ธ ํ•  ์ˆ˜ ์žˆ๋‹ค!

๋Œ“๊ธ€๋‚จ๊ธฐ๊ธฐ