package com.shineidle.tripf.oauth2;
import com.shineidle.tripf.oauth2.util.CookieUtils;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.util.WebUtils;
@Slf4j
@Component
@RequiredArgsConstructor
public class HttpCookieOAuth2AuthorizationRequestRepository implements AuthorizationRequestRepository<OAuth2AuthorizationRequest> {
public static final String OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME = "oauth2_auth_request";
private static final int COOKIE_EXPIRE_SECONDS = 18000;
@Override
public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) {
Cookie cookie = WebUtils.getCookie(request, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME);
return CookieUtils.deserialize(cookie, OAuth2AuthorizationRequest.class);
}
@Override
public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest, HttpServletRequest request, HttpServletResponse response) {
if (authorizationRequest == null) {
removeAuthorizationRequestCookies(request, response);
return;
}
CookieUtils.addCookie(response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME, CookieUtils.serialize(authorizationRequest), COOKIE_EXPIRE_SECONDS);
}
@Override
public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request, HttpServletResponse response) {
return this.loadAuthorizationRequest(request);
}
public void removeAuthorizationRequestCookies(HttpServletRequest request, HttpServletResponse response) {
CookieUtils.deleteCookie(request, response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME);
}
}
이 클래스는 OAuth2 인증 요청에 대한 쿠키를 관리한다.
package com.shineidle.tripf.oauth2.service;
import com.shineidle.tripf.oauth2.exception.OAuth2AuthenticationProcessingException;
import com.shineidle.tripf.oauth2.user.OAuth2Provider;
import com.shineidle.tripf.oauth2.user.OAuth2UserInfo;
import com.shineidle.tripf.oauth2.user.OAuth2UserInfoFactory;
import com.shineidle.tripf.user.entity.User;
import com.shineidle.tripf.user.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.Optional;
@Slf4j
@Service
@RequiredArgsConstructor
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
private final UserRepository userRepository;
@Override
public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(oAuth2UserRequest);
return processOAuth2User(oAuth2UserRequest, oAuth2User);
}
private OAuth2User processOAuth2User(OAuth2UserRequest userRequest, OAuth2User oAuth2User) {
String registrationId = userRequest.getClientRegistration().getRegistrationId(); // Google, Kakao, Naver
String accessToken = userRequest.getAccessToken().getTokenValue();
log.info("소셜 로그인 플랫폼: {}", registrationId);
log.info("소셜 로그인 엑세스 토큰: {}", accessToken);
// OAuth2UserInfo 객체 생성
OAuth2UserInfo oAuth2UserInfo = OAuth2UserInfoFactory.getOAuth2UserInfo(
registrationId,
accessToken,
oAuth2User.getAttributes()
);
// 이메일 검증
if (!StringUtils.hasText(oAuth2UserInfo.getEmail()) && !OAuth2Provider.KAKAO.getRegistrationId().equals(registrationId)) {
throw new OAuth2AuthenticationProcessingException("이메일을 찾을 수 없습니다.");
}
// 유저 정보 저장 또는 업데이트
saveOrUpdate(oAuth2UserInfo);
// OAuth2UserPrincipal 반환
return new OAuth2UserPrincipal(oAuth2UserInfo);
}
private void saveOrUpdate(OAuth2UserInfo oAuth2UserInfo) {
Optional<User> existUser = userRepository.findByEmail(oAuth2UserInfo.getEmail());
if (existUser.isEmpty() && OAuth2Provider.KAKAO.equals(oAuth2UserInfo.getProvider())) {
existUser = userRepository.findByProviderId(oAuth2UserInfo.getId());
}
if (existUser.isPresent()) {
// 기존 유저 정보 업데이트
User user = existUser.get();
user.update(oAuth2UserInfo);
} else {
User newUser;
if (OAuth2Provider.KAKAO.equals(oAuth2UserInfo.getProvider())) {
newUser = new User(null,
oAuth2UserInfo.getNickname(),
oAuth2UserInfo.getProvider().getRegistrationId(),
oAuth2UserInfo.getId()
);
} else {
newUser = new User(oAuth2UserInfo.getEmail(),
oAuth2UserInfo.getName(),
oAuth2UserInfo.getProvider().getRegistrationId(),
oAuth2UserInfo.getId()
);
}
// 새로운 유저 생성
userRepository.save(newUser);
}
}
}
이 클래스는 소셜 로그인 후 사용자 정보를 처리한다.
주요 메서드:
package com.shineidle.tripf.oauth2.handler;
import com.shineidle.tripf.common.util.JwtProvider;
import com.shineidle.tripf.common.util.TokenType;
import com.shineidle.tripf.oauth2.HttpCookieOAuth2AuthorizationRequestRepository;
import com.shineidle.tripf.oauth2.service.OAuth2UserPrincipal;
import com.shineidle.tripf.oauth2.user.OAuth2Provider;
import com.shineidle.tripf.oauth2.util.CookieUtils;
import com.shineidle.tripf.user.entity.RefreshToken;
import com.shineidle.tripf.user.entity.User;
import com.shineidle.tripf.user.repository.UserRepository;
import com.shineidle.tripf.user.service.RefreshTokenService;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.util.UriComponentsBuilder;
import java.io.IOException;
@Slf4j
@Component
@RequiredArgsConstructor
public class OAuth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
public static final String REDIRECT_URL = "/";
private final HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository;
private final UserRepository userRepository;
private final JwtProvider jwtProvider;
private final RefreshTokenService refreshTokenService;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
OAuth2UserPrincipal oAuth2UserPrincipal = getOAuth2UserPrincipal(authentication);
User user = oAuth2UserPrincipal.getUserInfo().getProvider().equals(OAuth2Provider.KAKAO) ?
userRepository.findByProviderId(oAuth2UserPrincipal.getUserInfo().getId()).orElseThrow(() ->
new ResponseStatusException(HttpStatus.NOT_FOUND)) :
userRepository.findByEmail(oAuth2UserPrincipal.getName()).orElseThrow(() ->
new ResponseStatusException(HttpStatus.NOT_FOUND));
String accessToken = jwtProvider.generateToken(authentication, true, TokenType.ACCESS);
RefreshToken refreshToken = refreshTokenService.generateToken(user.getId(), authentication, true);
// 쿠키에 사용
addRefreshTokenToCookie(request, response, refreshToken);
//Path 뒤에 accessToken 붙이기
String targetUrl = getTargetUrl(accessToken);
clearAuthenticationAttributes(request, response);
// 리다이렉트
getRedirectStrategy().sendRedirect(request, response, targetUrl);
}
protected String getTargetUrl(String accessToken) {
return UriComponentsBuilder.fromUriString(REDIRECT_URL)
.queryParam("accessToken", accessToken)
.build().toUriString();
}
private OAuth2UserPrincipal getOAuth2UserPrincipal(Authentication authentication) {
Object principal = authentication.getPrincipal();
if (principal instanceof OAuth2UserPrincipal) {
return (OAuth2UserPrincipal) principal;
}
return null;
}
private void addRefreshTokenToCookie(HttpServletRequest request, HttpServletResponse response, RefreshToken refreshToken) {
int cookieMaxAge = jwtProvider.getRefreshExpiryMillis().intValue();
CookieUtils.deleteCookie(request, response, "refresh_token");
CookieUtils.addCookie(response, "refresh_token", refreshToken.getToken(), cookieMaxAge);
}
protected void clearAuthenticationAttributes(HttpServletRequest request, HttpServletResponse response) {
super.clearAuthenticationAttributes(request);
httpCookieOAuth2AuthorizationRequestRepository.removeAuthorizationRequestCookies(request, response);
}
}
소셜 로그인 성공 후 최종적으로 호출되는 클래스
주요 메서드:
사용자 요청 -> 인증 요청 저장
사용자가 소셜 로그인 버튼을 클릭하면 서버는 HttpCookieOAuth2AuthorizationRequestRepository의 saveAuthorizationRequest 메서드를 호출하여 인증 요청 정보를 쿠키에 저장한다.
소셜 플랫폼 인증 -> 사용자 정보 로드
소셜 플랫폼 인증이 완료되면, CustomOAuth2UserService의 loadUser가 호출되어 사용자의 정보를 로드한다.
processOAuth2User 메서드에서 어플리케이션에 맞는 사용자 정보를 생성하거나 업데이트한다.
로그인 성공 -> 토큰 발급
인증이 성공하면, OAuth2AuthenticationSuccessHandler의 onAuthenticationSuccess가 호출된다.
이 메서드에서 액세스 토큰과 리프레시 토큰을 생성하고, 최종 리다이렉션 URL을 결정한다.
리다이렉션
getTargetUrl 메서드는 리다이렉션 Path와 함께 이어서 accessToken을 표시한다.