
지금까지 수동으로 코드를 짜며 배웠던 Authorization Code 교환, JWKS 조회, ID Token 검증 등의 복잡한 과정을 Spring Security는 단 몇 줄의 설정으로 자동화 해준다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
}
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
spring:
security:
oauth2:
client:
registration:
google:
client-id: ${GOOGLE_CLIENT_ID}
client-secret: ${GOOGLE_CLIENT_SECRET}
scope:
- openid
- profile
- email
provider:
google:
# Tip: issuer-uri만 적으면 스프링이 /.well-known/...을 자동으로 호출해 모든 정보를 가져옴
issuer-uri: https://accounts.google.com
# Google OAuth2 Registration
spring.security.oauth2.client.registration.google.client-id=${GOOGLE_CLIENT_ID}
spring.security.oauth2.client.registration.google.client-secret=${GOOGLE_CLIENT_SECRET}
spring.security.oauth2.client.registration.google.scope=openid,profile,email
# Google OAuth2 Provider
spring.security.oauth2.client.provider.google.issuer-uri=https://accounts.google.com
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/login/**").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
// 로그인 성공 후 후처리 로직을 담당할 서비스 등록
.userInfoEndpoint(userInfo -> userInfo.userService(customOAuth2UserService()))
);
return http.build();
}
}
스프링이 인증을 다 해주더라도, 인증된 사용자 정보를 나의 서비스의 DB에 저장하거나 추가 권한을 부여하는 작업을 해야함
@Service
public class CustomOAuth2UserService extends OidcUserService {
@Override
public OidcUser loadUser(OidcUserRequest userRequest)
throws OAuth2AuthenticationException
{
// 1. 스프링이 이미 검증 완료한 OIDC 사용자 정보를 가져옴
OidcUser oidcUser = super.loadUser(userRequest);
// 2. ID Token의 클레임들을 확인 (sub, email 등)
String email = oidcUser.getEmail();
String name = oidcUser.getFullName();
String registrationId = userRequest.getClientRegistration()
.getRegistrationId(); // "google"
// 3. DB 저장 또는 업데이트 로직 (나의 서비스 회원가입 처리)
// User user = userRepository.findByEmail(email)
// .orElseGet(() -> userRepository.save(new User(name, email, registrationId)));
return oidcUser;
}
}
내가 news-summary 프로젝트에서 썼던 OAuth2.0과 OIDC의 다른 점은 무엇일까?
"&scope=profile email"
-> OIDC를 사용하려면 반드시 scope에 openid라는 값이 들어가야 한다. 나의 요청문에는 profile과 email만 포함되어 있으므로, 구글은 이를 "사용자 정보에 접근할 권한을 얻으려는 OAuth 2.0 요청"으로 인식
Map<String, Object> tokenResponse = resp.getBody();
// ...
return (String) tokenResponse.get("access_token");
-> OIDC 방식이라면 구글은 access_token과 함께 id_token을 준다. 하지만 내 코드는 오직 access_token만 꺼내서 사용하고 있다.
private Map<String, Object> requestGoogleUserInfo(String accessToken) {
// ...
headers.setBearerAuth(accessToken);
ResponseEntity<Map<String, Object>> resp = restTemplate.exchange(GOOGLE_USERINFO_URL, ...);
}
-> 이 부분이 가장 결정적인 차이
OAuth 2.0 방식: Access Token을 들고 다시 구글의 API 서버(UserInfo API)에 "이 열쇠 줄 테니까 프로필 정보 좀 줘"라고 한 번 더 물어본다 (현재 나의 방식)
OIDC 방식: 앞 단계에서 이미 ID Token이라는 신분증을 받았기 때문에, 이 단계 자체가 필요 없음. 토큰만 뜯으면 그 안에 이름과 이메일이 다 들어있다.