💻 뚝딱뚝딱/북북클럽
[개발일지 #011] Oauth 로그인 구현 (구글)
뚜루리
2025. 4. 24. 11:09
728x90
320x100
🎯 오늘의 목표
- Oauth 로그인 구현 (구글)
⚙️ 진행한 작업
- Oauth 로그인 구현 (구글)
🛠️ 개발내용
✅ Oauth 로그인을 도입하는 이유
장점 | 설명 |
🔑 로그인 개발/유지보수 부담 감소 | 비밀번호 저장, 해싱, 변경, 분실 처리 로직이 없어도 됨 |
🪪 신뢰성 있는 사용자 정보 제공 | 구글, 네이버 등에서 직접 검증된 정보(email 등)를 제공 |
📱 소셜 연동 쉽게 구현 가능 | 로그인 + 프로필 불러오기 + 인증 절차까지 한 번에 가능 |
👌 사용자 진입 장벽 낮음 | 사용자는 회원가입 없이 "구글로 로그인"으로 빠르게 진입 |
🔒 보안적으로 우수함 | 인증/인가를 제공업체(Google 등)에게 위임 → 우리가 직접 비밀번호를 다루지 않음 |
🌍 다양한 플랫폼과 연동 | 하나의 인증 방식으로 웹, 모바일, 외부 API까지 모두 대응 가능 |
위와 같은 장점들로 인해 Oauth를 도입하려고 함. 그러나 단점도 존재한다.
단점 | 설명 |
🔁 추가 정보가 부족함 | 기본 프로필 정보만 제공되므로 닉네임/약관 동의 등은 별도 처리 필요 |
🌐 외부 서비스 의존도 증가 | 구글/네이버 API 장애 시 로그인 불가 가능성 있음 |
🧩 프로바이더별 파싱 방식이 다름 | Google, Naver, Kakao 등 API 응답 포맷이 다 달라서 provider별 분기 필요 |
🧷 추가적인 보안 검토 필요 | redirect_uri 위변조, CSRF, 토큰 탈취 등 보안 고려사항 있음 |
🔁 동일 이메일로 다른 소셜 로그인 가능성 | 같은 이메일로 구글/네이버 둘 다 가입하면 충돌 가능 → providerId로 구분 필요 |
🧠 개념이 많고 복잡하다 | OAuth2 흐름, Security 필터 체인, 토큰 처리까지 전반적으로 학습 난이도 높음 |
즉, 주도권이 외부서비스에 있다보니 API 장애시 로그인이 불가능하고, 아무래도 설정하는 것이 다소복잡하다는 것이 단점임.
Oauth를 적용하게 되면 처음엔 로그인/회원가입이 이뤄지고 그 다음부터는 로그인만 이뤄지도록 흐름 구성
구글, 네이버, 카카오, 애플 총 4개의 로그인을 구현하려 했으나 애플은 돈이 든다는 소리에.....
일단 구글, 네이버, 카카오 3곳만 연결해보려함.
📌 User 엔티티 컬럼 추가
- 적용하기 전에 User엔티티에 구글, 네이버, 카카오 등 provider를 구분할 수 있는 컬럼을 추가함.
- 소셜 로그인 제공자 구분 컬럼 (provider) : 유지보수, 분기처리, 통계 등 다양하게 사용함
- 제공자별 ID 저장 컬럼 (providerId) : 동일 이메일로 여러 소셜 가입 방지하기 위해
why?
- 사용자 식별 : 어떤 소셜(Google, Kakao, Naver 등)로 가입했는지 확인 가능
- 기능 분기 처리 : 예: 구글 로그인 사용자만 이메일 인증 생략 등
- 일반 로그인 사용자와 구분
- 중복 사용자 관리 : 같은 이메일로 다른 소셜 로그인 가입 방지할 수 있음
User.java
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@EntityListeners(AuditingEntityListener.class)
public class User {
//생략//
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private AuthProvider provider;
@Column(nullable = false)
private String providerId;
//생략//
public static User create(String email, String encodedPassword, String nickname) {
User user = new User();
user.email = email;
user.password = encodedPassword;
user.nickname = nickname;
user.role = Role.USER;
user.status = UserStatus.ACTIVE;
user.provider = AuthProvider.LOCAL; // ✅ 추가!
user.providerId = "LOCAL"; // ✅ 구분용 더미 값
return user;
}
public static User createSocialUser(String email, String nickname, AuthProvider provider, String providerId) {
User user = new User();
user.email = email;
user.password = "oauth2"; // 의미 없는 값
user.nickname = nickname;
user.role = Role.USER;
user.status = UserStatus.ACTIVE;
user.provider = provider;
user.providerId = providerId;
return user;
}
}
📌 AuthProvider
package ddururi.bookbookclub.domain.user.enums;
import lombok.Getter;
@Getter
public enum AuthProvider {
LOCAL("일반 가입"),
GOOGLE("구글"),
KAKAO("카카오"),
NAVER("네이버"),
APPLE("애플");
private final String description;
AuthProvider(String description) {
this.description = description;
}
}
📌 Google Developer Console 설정
당연히 구글 아이디가 있어야함.
- https://console.cloud.google.com 접속하여 상단에 [프로젝트 생성]클릭함.
- 이미지 상으로는 프로젝트 생성이 보이지 않는데, 난 이미 생성해놔서 bookbookclub 이라는 프로젝트 명만 뜨는거임.
작성하라는대로 쭉쭉 작성하여 프로젝트를 일단 생성해줌.
그리고 Oauth를 설정할 프로젝트를 선택해줌.
좌측 메뉴에 [API 및 서비스] - [사용자 인증 정보] 이동.
그럼 위와 같은 화면이 뜰꺼임.
상단에 [사용자 인증 정보 만들기] - [Oauth 클라이언트 ID] 클릭
폼을 입력하고 만들기!
그후(사진은 없지만ㅠㅠ) 팝업창에 나오는 클라이언트 ID와 클라이언트 시크릿 복사해놓기
📌 application.yml에 구글 OAuth2 설정 추가
spring:
security:
oauth2:
client:
registration:
google:
client-id: YOUR_GOOGLE_CLIENT_ID
client-secret: YOUR_GOOGLE_CLIENT_SECRET
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
scope:
- profile
- email
provider:
google:
authorization-uri: https://accounts.google.com/o/oauth2/v2/auth
token-uri: https://oauth2.googleapis.com/token
user-info-uri: https://www.googleapis.com/oauth2/v3/userinfo
user-name-attribute: sub
clinet-id, client-secret은 아까 발급받은 값으로 변경해줘야 함.
📌 SecurityFilterChain 설정 (OAuth2 + JWT + 필터)
- 소셜 로그인 URL을 허용하고, 로그인 성공 시 커스텀 핸들러로 넘겨주는 설정을 담당
@Configuration
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final OAuth2UserProviderRouter oAuth2UserProviderRouter;
private final OAuth2SuccessHandler oAuth2SuccessHandler;
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers(
"/api/users/login",
"/api/users",
"/api/users/check-email",
"/api/users/check-nickname",
"/api/email/**",
"/api/users/token/refresh",
"/api/users/logout",
"/oauth2/**", // 소셜 로그인 관련 요청 허용
"/login/**" // OAuth2 redirect 관련 요청 허용
).permitAll()
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
.userInfoEndpoint(userInfo -> userInfo
.userService(oAuth2UserProviderRouter) // 사용자 정보 받아오기
)
.successHandler(oAuth2SuccessHandler) // JWT 발급 및 응답
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
}
📌 OAuth2UserProviderRouter.java
- provider 마다 따로 서비스를 분리하기로 했음.
- 핵심 라우터 역할.provider가 "google"이면 GoogleOAuth2UserService에 위임하는 방식
[패키지 구조]
ddururi.bookbookclub
└── global
└── security
└── oauth
├── OAuth2UserProviderRouter.java // 등록된 provider 별 서비스 라우팅
├── userinfo
│ ├── GoogleOAuth2UserService.java ✅ 구글 전용
│ ├── NaverOAuth2UserService.java ✅ 네이버 전용 (나중에 추가)
│ └── KakaoOAuth2UserService.java ✅ 카카오 전용 (나중에 추가)
package ddururi.bookbookclub.global.security.oauth;
import ddururi.bookbookclub.global.security.oauth.userinfo.GoogleOAuth2UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
@RequiredArgsConstructor
@Service
public class OAuth2UserProviderRouter implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
private final GoogleOAuth2UserService googleOAuth2UserService;
// 추후 NaverOAuth2UserService, KakaoOAuth2UserService 등 추가 가능
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
String provider = userRequest.getClientRegistration().getRegistrationId(); // "google"
return switch (provider) {
case "google" -> googleOAuth2UserService.loadUser(userRequest);
// case "naver" -> naverOAuth2UserService.loadUser(userRequest);
// case "kakao" -> kakaoOAuth2UserService.loadUser(userRequest);
default -> throw new OAuth2AuthenticationException("지원하지 않는 소셜 로그인입니다: " + provider);
};
}
}
📌 GoogleOAuth2UserService.java
- 로그인한 사용자의 **구글 정보(email, name 등)**을 받아와서 우리 서비스의 User 엔티티랑 연결해주는 로직
[✅ 구글 OAuth2 사용자 정보 응답 예시]
{
"sub": "109103204190219023923", // 구글 고유 ID (providerId로 사용)
"name": "홍길동", // 사용자 이름 (nickname 후보)
"given_name": "길동",
"family_name": "홍",
"picture": "https://lh3.googleusercontent.com/a/abc123...", // 프로필 이미지 URL
"email": "honggildong@gmail.com", // 사용자 이메일
"email_verified": true, // 이메일 인증 여부
"locale": "ko" // 언어/지역
}
@RequiredArgsConstructor
@Service
public class GoogleOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
private final UserRepository userRepository;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = new DefaultOAuth2UserService().loadUser(userRequest);
String email = oAuth2User.getAttribute("email");
String name = oAuth2User.getAttribute("name");
String providerId = oAuth2User.getAttribute("sub"); // 구글 ID
AuthProvider provider = AuthProvider.GOOGLE;
return userRepository.findByEmail(email)
.map(CustomUserDetails::new)
.orElseGet(() -> {
// 닉네임 중복 검사
if (userRepository.existsByNickname(name)) {
throw new OAuth2AuthenticationException("이미 사용 중인 닉네임입니다.");
}
User newUser = userRepository.save(User.createSocialUser(email, name, provider, providerId));
return new CustomUserDetails(newUser);
});
}
}
📌 OAuth2SuccessHandler.java
- 소셜 로그인 성공 시 JWT 발급해서 클라이언트에 응답하는 역할
@RequiredArgsConstructor
@Component
public class OAuth2SuccessHandler implements AuthenticationSuccessHandler {
private final JwtUtil jwtUtil;
private final JwtRefreshTokenService refreshTokenService;
private final ObjectMapper objectMapper;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException {
CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();
// JWT 발급
String accessToken = jwtUtil.createToken(userDetails.getEmail());
String refreshToken = jwtUtil.createRefreshToken();
// Redis 저장
refreshTokenService.save(userDetails.getId(), refreshToken, REFRESH_EXPIRATION_DAYS, TimeUnit.DAYS);
// 응답
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(
ApiResponse.success(new TokenResponse(accessToken, refreshToken))
));
}
}
📌 TokenResponse.java
@Builder
@Getter
@AllArgsConstructor
public class TokenResponse {
private String accessToken;
private String refreshToken;
}
[테스트]
- Postman에서는 구글 로그인 팝업 처리가 안 되기 때문에 브라우저로 직접 로그인 URL을 호출해야 함.
http://localhost:8080/oauth2/authorization/google 입력하면 위와 같은 이미지가 뜸.
계정을 선택하면 로그인 할수 있는 화면이 뜨고 계속을 누르면 아래와 같은 결과값이 나옴.
728x90
320x100