μλ νμΈμ, κ°λ°μ μ¬λ¬λΆ! μ€λμ μ κ° μ΅κ·Όμ μμ±ν Spring Boot κΈ°λ°μ μμ λ‘κ·ΈμΈ νλ‘μ νΈμ λν΄ κ³΅μ ν΄λ³΄λ €κ³ ν©λλ€. μμ¦ λλΆλΆμ μλΉμ€μμ μμ λ‘κ·ΈμΈμ νμμ μΈ κΈ°λ₯μ΄ λμμ£ . μ¬μ©μ κ²½νμ κ°μ νκ³ λ³΄μμ κ°ννλ μμ λ‘κ·ΈμΈ μμ€ν μ μ΄λ»κ² ꡬννλμ§ ν¨κ» μ΄ν΄λ³΄μμ!

μ°λ¦¬ νμ μ¬μ©μ μΈμ¦ μμ€ν μ κ°λ°νλ©΄μ 보μμ±κ³Ό μ¬μ©μ κ²½ν λ λ§λ¦¬ ν λΌλ₯Ό λͺ¨λ μ‘κ³ μΆμμ΄μ. π°π° κ·Έλμ κ°μ₯ λ§μ΄ μ¬μ©λλ μμ λ‘κ·ΈμΈ(Google, Kakao, Naver)μ λͺ¨λ μ§μνκ³ , JWT κΈ°λ°μ μΈμ¦ μμ€ν μ λμ νλ΅λλ€.
μ΄ νλ‘μ νΈμ μ£Όμ κΈ°λ₯μ λ€μκ³Ό κ°μμ:
νλ‘μ νΈλ₯Ό μμνκΈ° μ , μ΅μ κΈ°μ μ€νμ μ νν΄ μμ μ μ΄κ³ νμ₯ κ°λ₯ν μμ€ν μ ꡬμΆνλ €κ³ λ Έλ ₯νμ΄μ. μ¬μ©ν κΈ°μ μ λ€μκ³Ό κ°μ΅λλ€:
νΉν Java 21κ³Ό Spring Boot 3.4.5λ₯Ό μ νν μ΄μ λ μ΅μ κΈ°λ₯λ€κ³Ό μ±λ₯ κ°μ μ νμ©νκΈ° μν΄μμμ΄μ. π λν Redisλ₯Ό μ¬μ©ν΄ ν ν° κ΄λ¦¬μ ν¨μ¨μ±μ λμμ΅λλ€.
OAuth2 μμ λ‘κ·ΈμΈ κ΅¬νμ ν¬κ² μΈ λ¨κ³λ‘ λλμ΄μ μ§ννμ΄μ.
λ¨Όμ , Spring Security OAuth2 Clientλ₯Ό μ¬μ©ν΄ κ° μμ λ‘κ·ΈμΈ μ 곡μ(Google, Kakao, Naver)λ₯Ό μ€μ νμ΄μ. κ° μ 곡μλ³λ‘ νμν scopeμ user-info-uriλ₯Ό μ μ ν ꡬμ±νλ κ²μ΄ μ€μνμ΅λλ€.
μμ λ‘κ·ΈμΈ μΈμ¦ νλ‘μΈμ€λ λ€μκ³Ό κ°μ μ»΄ν¬λνΈλ‘ ꡬμ±λμ΄ μμ΄μ:
νΉν λ‘κ·ΈμΈ μ±κ³΅ νΈλ€λ¬μμλ μ¬μ©μ μ 보λ₯Ό κΈ°λ°μΌλ‘ JWT ν ν°μ μμ±νκ³ , μ΄λ₯Ό μΏ ν€μ μμ νκ² μ μ₯νλ λ‘μ§μ ꡬννμ΄μ. π
Spring Security μ€μ μ 보μκ³Ό νμ₯μ±μ κ³ λ €ν΄ λ€μκ³Ό κ°μ΄ ꡬννμ΅λλ€:
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) {
return http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/public/**").permitAll()
.requestMatchers("/private/**").authenticated()
.anyRequest().denyAll())
.oauth2Login(oauth2 -> oauth2
.authorizationEndpoint(endpoint -> endpoint
.authorizationRequestResolver(customAuthorizationRequestResolver))
.userInfoEndpoint(userInfo -> userInfo
.userService(customOAuth2UserService))
.successHandler(oAuth2AuthenticationSuccessHandler)
.failureHandler(oAuth2AuthenticationFailureHandler))
.build();
}
}
μ΄ μ€μ μ REST API κΈ°λ° μλΉμ€μ λ§κ² μ΅μ νλμ΄ μμ΄μ. CSRF 보νΈλ λΉνμ±ννμ§λ§, λμ JWT ν ν°κ³Ό HttpOnly μΏ ν€λ‘ 보μμ κ°ννμ΅λλ€.
λν μΈμ μ STATELESSλ‘ μ€μ ν΄ μλ²μ λΆνλ₯Ό μ€μ΄κ³ νμ₯μ±μ λμμ΄μ. π
μ¬μ©μ μΈμ¦ κ³Όμ μ΄ μ΄λ»κ² μ΄λ£¨μ΄μ§λμ§ μ’ λ μμΈν μ΄ν΄λ³ΌκΉμ?
/api/public/oauth2/google μλν¬μΈνΈλ‘ μμ²μ΄ μ μ‘λ©λλ€.μ΄ κ³Όμ μ Kakaoμ Naver λ‘κ·ΈμΈμλ λμΌνκ² μ μ©λ©λλ€. λ¨μ§ μ 곡μλ³ API μλν¬μΈνΈμ μ¬μ©μ μ 보 νμλ§ λ€λ₯Ό λΏμ΄μμ. π
μμ λ‘κ·ΈμΈμ μ²μ μλνλ μ¬μ©μλ μλμΌλ‘ νμκ°μ νλ‘μΈμ€λ‘ μλ΄λ©λλ€:
μ΄λ κ² μ¬μ©μλ λ³λμ νμκ°μ νΌμ μμ±ν νμ μμ΄ μμ κ³μ λ§μΌλ‘ λΉ λ₯΄κ² μλΉμ€μ κ°μ ν μ μμ΄μ. π
JWT ν ν° κ΄λ¦¬λ 보μμ ν΅μ¬μ΄μ£ ! π μ°λ¦¬ νλ‘μ νΈμμλ Access Tokenκ³Ό Refresh Tokenμ ν¨κ» μ¬μ©ν΄ 보μμ κ°ννμ΅λλ€.
μ΄λ° λ°©μμΌλ‘ μ¬μ©μλ 2μΌ λμ λ‘κ·ΈμΈ μνλ₯Ό μ μ§ν μ μμΌλ©΄μλ, μ€μ API μ κ·Ό κΆν(Access Token)μ 30λΆλ§λ€ κ°±μ λμ΄ λ³΄μμ΄ κ°νλ©λλ€.
μ΄ νλ‘μ νΈμμλ λ€μν λ°©λ²μΌλ‘ 보μμ κ°ννμ΄μ:
νΉν λ‘κ·Έμμ μμλ λ€μκ³Ό κ°μ νλ‘μΈμ€λ‘ 보μμ κ°ννμ΅λλ€:
// λ‘κ·Έμμ μ²λ¦¬ μ½λ μΌλΆ
Cookie deletedAccessCookie = cookieProvider.generateDeletedAccessTokenCookie();
Cookie deletedRefreshCookie = cookieProvider.generateDeletedRefreshTokenCookie();
response.addCookie(deletedAccessCookie);
response.addCookie(deletedRefreshCookie);
// Redisμμ Refresh Token μμ
// SecurityContext μ΄κΈ°ν
μ΄λ κ² ν΄λΌμ΄μΈνΈ μΏ ν€μ μλ² Redis μ μ₯μ λͺ¨λμμ ν ν°μ μ κ±°ν΄ μλ²½ν λ‘κ·Έμμμ ꡬννμ΄μ. π‘οΈ
μ΄ νλ‘μ νΈλ₯Ό μ§μ μ€νν΄λ³΄κ³ μΆμΌμ κ°μ? λ€μ λ¨κ³λ₯Ό λ°λΌν΄λ³΄μΈμ:
# OAuth2 Client Credentials
GOOGLE_CLIENT_ID=${YOUR_GOOGLE_CLIENT_ID}
GOOGLE_CLIENT_SECRET=${YOUR_GOOGLE_CLIENT_SECRET}
KAKAO_CLIENT_ID=${YOUR_KAKAO_CLIENT_ID}
KAKAO_CLIENT_SECRET=${YOUR_KAKAO_CLIENT_SECRET}
NAVER_CLIENT_ID=${YOUR_NAVER_CLIENT_ID}
NAVER_CLIENT_SECRET=${YOUR_NAVER_CLIENT_SECRET}
# JWT Keys
ACCESS_TOKEN_KEY=${YOUR_ACCESS_TOKEN_KEY}
REFRESH_TOKEN_KEY=${YOUR_REFRESH_TOKEN_KEY}
# Database
MYSQL_URL=jdbc:mysql://localhost:3306/oauth2_demo
MYSQL_USER_NAME=${YOUR_MYSQL_USERNAME}
MYSQL_PASSWORD=${YOUR_MYSQL_PASSWORD}
κ° μμ λ‘κ·ΈμΈ μ 곡μμ κ°λ°μ μ½μμμ ν΄λΌμ΄μΈνΈ IDμ μν¬λ¦Ώμ λ°κΈλ°μ μ€μ ν΄μ£ΌμΈμ.
./gradlew bootRun
http://localhost:8080μ μ μν΄ μμ
λ‘κ·ΈμΈ κΈ°λ₯μ ν
μ€νΈν΄λ³΄μΈμ!API λ¬Έμλ Swagger UIλ₯Ό ν΅ν΄ νμΈν μ μμ΄μ: http://localhost:8080/api/swagger-ui/index.html
μμ λ‘κ·ΈμΈκ³Ό JWT μΈμ¦μ ꡬννλ©΄μ λ°°μ΄ μ μ΄ μ λ§ λ§μμ΄μ. μ¬μ©μ κ²½νκ³Ό 보μ μ¬μ΄μ κ· νμ λ§μΆλ κ²μ΄ κ°μ₯ μ΄λ €μ μ§λ§, Spring Securityμ OAuth2 Clientμ κ°λ ₯ν κΈ°λ₯ λλΆμ μμ μ μΈ μμ€ν μ ꡬμΆν μ μμμ΅λλ€.
λ€μ λ¨κ³λ‘λ μμ λ‘κ·ΈμΈ μ 보λ₯Ό νμ©ν μ¬μ©μ νλ‘ν κ΄λ¦¬μ, κΆν κΈ°λ°μ μΈλ°ν μ κ·Ό μ μ΄ μμ€ν μ ꡬνν κ³νμ΄μμ. π
μ¬λ¬λΆλ μ΄ κΈμ΄ μμ λ‘κ·ΈμΈ κ΅¬νμ λμμ΄ λμμΌλ©΄ μ’κ² λ€μ! κΆκΈν μ μ΄λ κ°μ ν λΆλΆμ΄ μλ€λ©΄ λκΈλ‘ μλ €μ£ΌμΈμ. ν¨κ» μ±μ₯ν΄μ! π
μ°Έκ³ : μ΄ κΈμ μ¬μ©λ μ½λλ λ°λͺ¨ μ ν리μΌμ΄μ μ μν κ²μ΄λ©°, μ€μ νλ‘λμ νκ²½μμλ μΆκ°μ μΈ λ³΄μ μ€μ μ΄ νμν μ μμ΅λλ€.