java
CHAPTER 72 / 99
읽기 약 2분
FUNCTION
OAuth2 소셜 로그인
핵심 개념
OAuth2 흐름·Google/Kakao 연동·Spring Security OAuth2 Client + JWT 발급.
본문
OAuth2 Authorization Code Flow
1. 사용자 → /oauth2/authorization/google → Google 로그인
2. Google → 인증 코드 → /login/oauth2/code/google
3. 서버가 코드를 access token으로 교환
4. Google API → 사용자 정보 (email, name)
5. 자체 JWT 발급 → 클라이언트application.yml
spring:
security:
oauth2:
client:
registration:
google:
client-id: ${GOOGLE_CLIENT_ID}
client-secret: ${GOOGLE_CLIENT_SECRET}
scope:
- email
- profile
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
kakao:
client-id: ${KAKAO_CLIENT_ID}
client-secret: ${KAKAO_CLIENT_SECRET}
client-authentication-method: client_secret_post
authorization-grant-type: authorization_code
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
scope:
- profile_nickname
- account_email
client-name: Kakao
provider:
kakao:
authorization-uri: https://kauth.kakao.com/oauth/authorize
token-uri: https://kauth.kakao.com/oauth/token
user-info-uri: https://kapi.kakao.com/v2/user/me
user-name-attribute: idSecurity 설정
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class OAuth2SecurityConfig {
private final CustomOAuth2UserService oAuth2UserService;
private final OAuth2SuccessHandler successHandler;
@Bean
public SecurityFilterChain filter(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/login/**", "/oauth2/**").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(oauth -> oauth
.userInfoEndpoint(userInfo -> userInfo
.userService(oAuth2UserService)
)
.successHandler(successHandler)
);
return http.build();
}
}CustomOAuth2UserService
@Service
@RequiredArgsConstructor
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
private final UserRepository userRepository;
@Override
public OAuth2User loadUser(OAuth2UserRequest request) {
OAuth2User oAuth2User = super.loadUser(request);
String registrationId = request.getClientRegistration().getRegistrationId();
OAuth2UserInfo userInfo = OAuth2UserInfoFactory.getOAuth2UserInfo(
registrationId,
oAuth2User.getAttributes()
);
// DB에 저장 또는 갱신
User user = userRepository.findByEmail(userInfo.getEmail())
.map(existing -> updateUser(existing, userInfo))
.orElseGet(() -> createUser(userInfo, registrationId));
return new CustomOAuth2User(user, oAuth2User.getAttributes());
}
private User createUser(OAuth2UserInfo info, String provider) {
User user = User.builder()
.email(info.getEmail())
.name(info.getName())
.provider(provider)
.role(Role.USER)
.build();
return userRepository.save(user);
}
}OAuth2 Provider별 사용자 정보 추출
public abstract class OAuth2UserInfo {
protected Map<String, Object> attributes;
public abstract String getId();
public abstract String getName();
public abstract String getEmail();
}
public class GoogleUserInfo extends OAuth2UserInfo {
public GoogleUserInfo(Map<String, Object> attributes) {
this.attributes = attributes;
}
@Override public String getId() { return (String) attributes.get("sub"); }
@Override public String getName() { return (String) attributes.get("name"); }
@Override public String getEmail() { return (String) attributes.get("email"); }
}
public class KakaoUserInfo extends OAuth2UserInfo {
public KakaoUserInfo(Map<String, Object> attributes) {
this.attributes = attributes;
}
@Override public String getId() {
return String.valueOf(attributes.get("id"));
}
@Override public String getName() {
Map<String, Object> properties = (Map<String, Object>) attributes.get("properties");
return (String) properties.get("nickname");
}
@Override public String getEmail() {
Map<String, Object> account = (Map<String, Object>) attributes.get("kakao_account");
return account != null ? (String) account.get("email") : null;
}
}
public class OAuth2UserInfoFactory {
public static OAuth2UserInfo getOAuth2UserInfo(String provider, Map<String, Object> attrs) {
return switch (provider.toLowerCase()) {
case "google" -> new GoogleUserInfo(attrs);
case "kakao" -> new KakaoUserInfo(attrs);
default -> throw new IllegalArgumentException("지원하지 않는 provider: " + provider);
};
}
}로그인 성공 핸들러 — JWT 발급
@Component
@RequiredArgsConstructor
public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
private final JwtUtil jwtUtil;
@Override
public void onAuthenticationSuccess(HttpServletRequest req,
HttpServletResponse res,
Authentication auth) throws IOException {
CustomOAuth2User oAuth2User = (CustomOAuth2User) auth.getPrincipal();
User user = oAuth2User.getUser();
String token = jwtUtil.generate(user.getEmail(), user.getRole().name());
// 프론트엔드로 리다이렉트 + 토큰 전달
String redirectUrl = UriComponentsBuilder
.fromUriString("https://openhyperstep.com/auth/callback")
.queryParam("token", token)
.build()
.toUriString();
res.sendRedirect(redirectUrl);
}
}다음 챕터
CH.4 "Spring Data JPA 심화" — N+1 문제 해결.
AI 프롬프트
🤖 AI에게 잘 물어보는 법 — 모델·전략별 프롬프트
Claude
무료: Sonnet 4.6 / Pro $20/mo: Opus 4.6
내 Spring 코드의 OAuth2 소셜 로그인 부분을 분석해서 보안 헤더·토큰 처리와 개선 우선순위를 알려줘.
ChatGPT
무료: GPT-5.5 / Plus $20/mo: GPT-5.5 Pro
OAuth2 소셜 로그인 vs 다른 패턴 비교를 실전 사례 5개로 보여주고 Google vs Kakao vs Naver 비교를 알려줘.
Gemini
무료: 2.5 Flash / Pro $19.99/mo: 3.1 Pro
내 코드베이스 전체를 분석해서 OAuth2 소셜 로그인 관련 인증 흐름의 약점를 보고해줘.
Grok
무료: Grok 4.1 / SuperGrok $30/mo
2026년 한국 기업의 OAuth2 소셜 로그인 채택률과 한국 시장 소셜 로그인 트렌드를 솔직히 알려줘.
⭐ 이것만 기억하세요
OAuth2 소셜 로그인은 이 3가지만 확실히 잡으세요
1.OAuth2는 표준 — Google/Kakao/Naver 모두 같은 흐름 + provider별 attribute 추출만 분기
2.OAuth2 로그인 후 자체 JWT 발급 — 클라이언트가 모든 API에 같은 토큰 사용
3.다음 챕터 CH.4에서 JPA 심화 — N+1 문제와 페이지네이션
공유하기
진행도 72 / 99