article thumbnail image
Published 2024. 4. 2. 15:18
OAuth란?

 

타사 웹사이트나 웹이 리소스에 접근할 수 있게 허용해 주는 것이 주 목적이다
OAuth는 인증(Authentication)과 인가(Authorization)중 인가에 조금 더 초점을 맞추고 있따.

 

 

인증이란 ? 
인증은 사용자가 자신의 신원을 사용하는 프로세스이다.
예를 들어 시스템에 로그인하려면 직원 ID, 또는 사용자 ID를 확인한다.
인가란?
인증된 사용자에 대해 특정 자원 또는 서비스에 대한 엑세스 권한을 부여
즉, 누가 어떤 자원에 접근할 수 있는지 결정

 

 

구글 로그인을 예로 들자면, 우리는 사용자의 정보, 즉 해당 사용자가 구글에 가입이 되어있는지는 구글 서버측에서 확인을 하고, 그 후 해당 정보와 인증된 사용자에게 권한을 제공해준다.

 

 

 

웹 서버 애플리케이션에서의 Google Oauth2 인증 로직

1. 애플리케이션이 브라우저를 Google URL로 리다이렉션함

이 URL에는 요청중인 액세스 유형을 나타내는 쿼리 매개변수가 포함된다.

 

2.Google에서 사용자 인증, 세션 선택, 사용자 동의를 처리하고 그 결과로 애플리케이션이 Access Token, Refresh Token으로 교환할 수 있는 Authorization code 가 생성된다.

 

3. 애플리케이션은 나중에 사용할 수 있도록 Refresh Token을 저장하고 Access Token을 사용하여 Google API에 액세스해야 한다.Access Token이 만료되면 애플리케이션에서 Refresh Token을 사용하여 새 토큰을 받는다

 

 

 

 

1.

Google Cloud 접속하기

 

Google 클라우드 플랫폼

로그인 Google 클라우드 플랫폼으로 이동

accounts.google.com

 

 

 

2.새 프로젝트 생성 (이름은 원하는대로)

 

내가 만든 프로젝트 선택 후

 

3.Oauth2 동의화면 구성

API 및 서비스 -> Oauth 동의 화면 -> 외부 선택 -> 만들기

 

 

4. 사용자 인증 정보 만들기

API및 서비스 -> 사용자 인증 정보 -> 사용자 인증정보 만들기 -> Oauth 클라이언트 ID 클릭

 

웹 애플리케이션 선택,이름 입력

 

승인된 리다이렉션 url에는 자신의 프로젝트 google Oauth URI 입력

위 주소는 스프링 공식 문서 OAuth Login에 나와 있다.

https://docs.spring.io/spring-security/reference/servlet/oauth2/login/core.html

 

처음에는 로컬호스트만 추가했지만 배포 서버에서도 돌리기 위해서는 배포서버의 도메인 주소에 대한 리다이렉트 uri도 적어야 한다

 

만들기 후 발급된 클라이언트 아이디와 비밀번호 저장

 

 

 

application.yml 추가파일 추가

spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: ${GOOGLE_CLIENT_ID}
            client-secret: ${GOOGLE_CLIENT_SECRET}
            scope:
              - email
              - profile
            redirect-uri: "http://jandp-travel.kro.kr:8080/login/oauth2/code/google" #후에 배포 서버로 변경

 

 

구글 클라이언트 아이디, 시크릿은 인텔리제이, 배포환경에서  각각 환경변수로 등록해 주었다

https://develoyummer.tistory.com/135

 

 

build.gradle 에 의존성 추가

implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'

 

차례로 Oauth 관련 클래스들을 만들어주고 SecurityConfig에도 관련 설정들 추가해줄 것!

 

 

SecurityConfig에 다음 설정 추가

 

여기서 주의할 점은 기존의 and() 메서드는 spring 3.0 이상에서 사용하지 못한다

왜냐하면 Spring Security 6.1.0의 release note를 살펴보면 non-lamda DSL methods를 deprecating 했다고 한다

 

기존

                .and()
                .logout()
                .logoutSuccessUrl("/")
                .and()
                .oauth2Login()
                .userInfoEndpoint()
                .userService(customOAuth2UserService); //deprecated

 

그래이 이제는 이 내용들을 그냥 메서드 체이닝으로 해결하지말고 앞으로는 람다식을 사용하여서 해결해야 한다.

 

변경

@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http,
//(생략)
			http.authorizeHttpRequests(
				authorize -> authorize
					.requestMatchers(HttpMethod.GET, "/api/users/**").authenticated()
					.requestMatchers("/api/users/**").permitAll()

					.requestMatchers("/h2/**").permitAll()
					.anyRequest().permitAll()
			)//여기부터 추가
			.logout(logout -> logout
				.logoutSuccessUrl("/")// 로그아웃 성공시 해당 주소로 이동
			)
			.oauth2Login(oauth2Login -> oauth2Login// OAuth2 로그인 기능에 대한 여러 설정의 진입점
					.userInfoEndpoint(userInfoEndpoint -> userInfoEndpoint  // OAuth2 로그인 성공 이후 사용자 정보를 가져올 때의 설정 담당
						.userService(customOauth2UserService) // 소셜 로그인 성공 시 후속 조치를 진행할 userService 인터페이스의 구현체 등록
					) // 리소스 서버(소셜 서비스들)에서 사용자 정보를 가져온 상태에서 추가로 진행하고자 하는 기능을 명시 가능.
					.successHandler(oAuth2AuthenticationSuccessHandler())
					.failureHandler(oAuth2AuthenticationFailureHandler())
				// 리소스 서버(소셜 서비스들)에서 사용자 정보를 가져온 상태에서 추가로 진행하고자 하는 기능을 명시 가능.
			);
            
            return http.build();
            }

            
            
  
  //success handler 와 failure handler 빈 추가
    @Bean
	public OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler() {
		return new OAuth2AuthenticationSuccessHandler(
			authTokenProvider,
			jwtConfig,
			oAuth2AuthorizationRequestBasedOnCookieRepository(),
			refreshService

		);
	}

	@Bean
	public OAuth2AuthenticationFailureHandler oAuth2AuthenticationFailureHandler() {
		return new OAuth2AuthenticationFailureHandler(oAuth2AuthorizationRequestBasedOnCookieRepository());
	}

 

 

 

User entity에 Role 추가 후 다음 메서드 추가

Role

	@Getter
	@RequiredArgsConstructor
	public enum UserRole {
		USER("ROLE_USER","일반유저"),
		ADMIN("ROLE_ADMIN","어드민유저");

		private final String key;
		private final String value;

 

 

 

User

	public String getRoleKey() {
		return this.role.getKey();
	}

	public User update(String name, String picture) {
		this.name = name;
		this.picture = picture;

		return this;
	}

 

session User

//세션에 사용자 정보를 저장하기 위한 DTO 클래스.
//SessionUser은 인증된 사용자 정보만 필요하고, 그 외 정보들은 필요가 없어 name, email, picture만 필드로 선언한다.
@Getter
public class SessionUser implements Serializable {
  // SessionUser는 인증된 사용자 정보만 필요하므로 아래 필드만 선언한다.
  private String name;
  private String email;
  private String picture;

  public SessionUser(User user) {
    this.name = user.getName();
    this.email = user.getEmail();
    this.picture = user.getPicture();
  }
}

 

OAuthProviderMissMatchException

public class OAuthProviderMissMatchException extends RuntimeException {

  public OAuthProviderMissMatchException(String message) {
   super(message);
  }
}

OAuth 제공자가 일치하지 않을 경우 발생하는 예외 생성

 

 

 

customOAuth2UserService

@RequiredArgsConstructor
@Service
public class CustomOauth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
	private final JpaUserRepository jpaUserRepository;
	private final HttpSession httpSession;

	@Override
	public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
		OAuth2UserService<OAuth2UserRequest, OAuth2User> delegate = new DefaultOAuth2UserService();
		OAuth2User oAuth2User = delegate.loadUser(userRequest);

		// 현재 로그인 진행 중인 서비스를 구분하는 코드 (네이버 로그인인지 구글 로그인인지 구분)
		String registrationId = userRequest.getClientRegistration().getRegistrationId();
		// OAuth2 로그인 진행 시 키가 되는 필드 값 (Primary Key와 같은 의미)을 의미
		// 구글의 기본 코드는 "sub", 후에 네이버 로그인과 구글 로그인을 동시 지원할 때 사용
		String userNameAttributeName = userRequest.
			getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();

		// OAuth2UserService를 통해 가져온 OAuthUser의 attribute를 담을 클래스 ( 네이버 등 다른 소셜 로그인도 이 클래스 사용)
		OAuthAttributes attributes = OAuthAttributes.of(registrationId, userNameAttributeName,
			oAuth2User.getAttributes());

		User user = saveOrUpdate(attributes);
		// User 클래스를 사용하지 않고 SessionUser클래스를 사용하는 이유는 오류 방지.
		httpSession.setAttribute("user", new SessionUser(user)); // SessionUser : 세션에 사용자 정보를 저장하기 위한 Dto 클래스

		return new DefaultOAuth2User(
			Collections.singleton(new SimpleGrantedAuthority(user.getRoleKey())),
			attributes.getAttributes(),
			attributes.getNameAttributeKey());
	}

	// 사용자 정보가 변경 될시 User 엔티티에도 반영
	private User saveOrUpdate(OAuthAttributes attributes) {
		User user = jpaUserRepository.findByEmail(attributes.getEmail())
			.map(entity -> entity.update(attributes.getName(), attributes.getPicture()))
			.orElse(attributes.toEntity());

		return jpaUserRepository.save(user);
	}

	private OAuth2User process(OAuth2UserRequest userRequest, OAuth2User user) {
		ProviderType providerType = ProviderType.valueOf(
			userRequest.getClientRegistration().getRegistrationId().toUpperCase());

		OAuthUserInfo userInfo = OAuthUserInfoFactory.getOAuthUserInfo(providerType, user.getAttributes());
		User savedUser = jpaUserRepository.findByEmail(userInfo.getEmail()).orElse(null);

		if (savedUser != null) {
			if (providerType != savedUser.getProviderType()) {
				throw new OAuthProviderMissMatchException(
					"Looks like you're signed up with " + providerType +
						" account. Please use your " + savedUser.getProviderType() + " account to login."
				);
			}
			updateUser(savedUser, userInfo);
		} else {
			savedUser = createUser(userInfo, providerType);
		}

		return UserPrincipal.create(savedUser, user.getAttributes());
	}

	public void updateUser(User user, OAuthUserInfo userInfo) {
		if (userInfo.getName() != null && !user.getName().equals(userInfo.getName())) {
			user.setName(userInfo.getName());
		}
	}

	public User createUser(OAuthUserInfo userInfo, ProviderType providerType) {
		User user = User.builder()
			.email(userInfo.getEmail())
			.password("oauth2")
			//.roles(AuthoritiesUtils.createRoles(userInfo.getEmail()))
			.providerType(providerType)
			.userStatus(User.UserStatus.MEMBER_ACTIVE)
			.name(userInfo.getName())
			.build();
		user.setRoles(AuthoritiesUtils.createAuthorities(user));
		return jpaUserRepository.saveAndFlush(user);
	}
}

Spring Security에서 OAuth2를 사용하여 사용자를 인증하고 사용자 정보를 가져오는 CustomOauth2UserService 클래스이다.

OAuth2UserService 인터페이스를 구현하고 있다.

이 클래스는 OAuth2 로그인 과정에서 사용자 정보를 가져오고 사용자 정보를 기반으로 사용자를 인증한다.

 

주요 메서드:

loadUser(OAuth2UserRequest userRequest):

OAuth2 로그인 요청을 처리하고 사용자 정보를 가져오는 메서드

먼저 DefaultOAuth2UserService를 사용하여 사용자 정보를 가져온다.

그런 다음 가져온 사용자 정보를 기반으로 OAuthAttributes 객체를 생성한다.

이후 saveOrUpdate() 메서드를 사용하여 사용자 정보를 데이터베이스에 저장하고 세션에 사용자 정보를 저장한 후 가져온 사용자 정보를 기반으로 DefaultOAuth2User 객체를 생성하여 반환한다.

 

saveOrUpdate(OAuthAttributes attributes):

가져온 OAuth2 사용자 정보를 기반으로 사용자를 데이터베이스에 저장하거나 업데이트하는 메서드.

사용자 정보가 이미 존재하는 경우에는 업데이트하고, 존재하지 않는 경우에는 새로운 사용자로 등록한다.

 

process(OAuth2UserRequest userRequest, OAuth2User user):

OAuth2 로그인 프로세스를 처리하는 메서드

ProviderType을 확인하여 사용자 정보를 업데이트하거나 새로운 사용자를 생성

 

 

 

 

 

 

OAuthAttributes

//구글 사용자 정보를 전달하는 DTO
@Getter
public class OAuthAttributes {
	private Map<String, Object> attributes;
	private String nameAttributeKey;
	private String name;
	private String email;
	private String picture;

	@Builder
	public OAuthAttributes(Map<String, Object> attributes, String nameAttributeKey, String name, String email,
		String picture) {
		this.attributes = attributes;
		this.nameAttributeKey = nameAttributeKey;
		this.name = name;
		this.email = email;
		this.picture = picture;
	}

	public static OAuthAttributes of(String registrationId, String userNameAttributeName,
		Map<String, Object> attributes) {
		return ofGoogle(userNameAttributeName, attributes);
	}

	// OAuth2User에서 반환하는 사용자 정보는 Map이기 때문에 값 하나하나를 변환해야한다.
	private static OAuthAttributes ofGoogle(String userNameAttributeName, Map<String, Object> attributes) {
		return OAuthAttributes.builder()
			.name((String)attributes.get("name"))
			.email((String)attributes.get("email"))
			.picture((String)attributes.get("picture"))
			.attributes(attributes)
			.nameAttributeKey(userNameAttributeName)
			.build();
	}

	// User 엔티티 생성 (생성 시점은 처음 가입할 때)
	public User toEntity() {
		return User.builder()
			.name(name)
			.email(email)
			.picture(picture)
			.providerType(ProviderType.GOOGLE)
			.userStatus(User.UserStatus.MEMBER_ACTIVE)
			.role(User.UserRole.USER) // 가입할 때 기본 권한
			.build();
	}
}

위의 코드는 구글 OAuth2를 통해 인증된 사용자의 정보를 담는 DTO(Data Transfer Object)인 OAuthAttributes를 정의하고 있다.
이 DTO는 사용자의 이름, 이메일, 프로필 사진 등을 저장한다.
주요 구성 요소는 다음과 같다:


attributes:

OAuth2 인증 서비스(여기서는 구글)에서 반환된 사용자 정보를 담는 Map.
이 맵에는 사용자의 이름(name), 이메일(email), 프로필 사진(picture) 등의 정보가 포함된다.

nameAttributeKey:

사용자의 이름 속성 키. OAuth2 서비스에서 사용자 이름을 가져오는 데 사용.

name,email, picture: 

사용자 이름, 이메일주소, 프로필 사진 URL

ofGoogle 메서드:

OAuthAttributes 객체를 생성하는 정적 팩토리 메서드.

여기서는 구글에서 제공하는 사용자 정보를 이용하여 OAuthAttributes 객체를 생성한다.

toEntity 메서드:

OAuthAttributes 객체를 기반으로 User 엔티티를 생성

이를 통해 사용자 정보를 기반으로 실제 데이터베이스에 저장되는 User 엔티티를 생성한다.

 

 

OAuthUserInfo

public abstract class OAuthUserInfo {
  protected Map<String, Object> attributes;

  public OAuthUserInfo(Map<String, Object> attributes) {
   this.attributes = attributes;
  }

  public Map<String, Object> getAttributes() {
   return attributes;
  }

  public abstract String getId();

  public abstract String getName();

  public abstract String getEmail();

  public abstract String getImageUrl();

  public abstract ProviderType getProvider();
}


OAuth2를 통해 가져온 사용자 정보를 표현
attributes: OAuth2를 통해 가져온 사용자 정보를 담는 맵.

 

추상 메서드들: getId(), getName(), getEmail(), getImageUrl(), getProvider()는 사용자의 ID, 이름, 이메일, 이미지 URL, 제공자 유형(ProviderType) 등을 반환하는 메서드

이러한 메서드들은 각각의 하위 클래스에서 구현되어야 한다.

 

예를 들어, 구글 사용자 정보를 표현하는 클래스가 있을 경우, 이 클래스는 OAuthUserInfo를 상속받은 후 각 추상 메서드들을 구현하여 구글 사용자 정보에 맞게 반환한다.

 

이렇게 하면 이다른 클래스들이 특정 제공자의 사용자 정보에 의존하지 않고도 일반적인 방식으로 사용자 정보를 처리할 수 있어

코드의 유연성을 높이고, 다른 제공자의 사용자 정보를 추가하거나 변경할 때 코드 수정을 최소화할 수 있다

 

 

GoogleOauthUserInfo - OauthUserInfo 상속

public class GoogleOAuthUserInfo extends OAuthUserInfo {
  private static final ProviderType provider = ProviderType.GOOGLE;

  public GoogleOAuthUserInfo(Map<String, Object> attributes) {
   super(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");
  }

  @Override
  public String getImageUrl() {
   return (String)attributes.get("picture");
  }

  public ProviderType getProvider() {
   return provider;
  }
}

 

구글의 사용자 정보를 표현하는 클래스를 만들어준다. 

OAuthUserInfo 클래스를 상속받아 사용자 정보를 처리한다.

 

생성자로는 Map<String, Object> attributes를 매개변수로 받아 부모 클래스인 OAuthUserInfo의 생성자를 호출한다.

 

페이스북, 네이버 등 다른 Oauth2 에 대한 사용자 정보 클래스를 구현할 때도 비슷한 방식으로 구현할 수 있다.

이렇게 함으로써 다양한 제공자의 사용자 정보를 일관된 방식으로 처리할 수 있다.

 

 

OAuthUserInfoFactory

public class OAuthUserInfoFactory {
  public static OAuthUserInfo getOAuthUserInfo(ProviderType providerType, Map<String, Object> attributes) {
   switch (providerType) {
    case GOOGLE:
     return new GoogleOAuthUserInfo(attributes);
    // case KAKAO: return new KakaoOAuth2UserInfo(attributes);
    // case FACEBOOK: return new FacebookOAuth2UserInfo(attributes);
    default:
     throw new IllegalArgumentException("Invalid Provider Type.");
   }
  }
}

OAuth 2.0 권한 부여 요청을 쿠키를 사용하여 저장하고 관리하기 위한 Repository를 만들어준다.

 

클라이언트가 인증 코드를 요청하면, 해당 요청이 쿠키에 저장되고, 인증 과정이 완료되면 해당 쿠키가 삭제된다. 이를 통해 사용자가 OAuth2 권한 부여 프로세스를 완료하기 위해 리다이렉트되는 동안 필요한 정보를 보존할 수 있다.

 

CoockieUtils

@Slf4j
public class CookieUtils {
  public static Optional<Cookie> getCookie(HttpServletRequest request, String name) {
   Cookie[] cookies = request.getCookies();

   if (cookies != null && cookies.length > 0) {
    for (Cookie cookie : cookies) {
     if (name.equals(cookie.getName())) {
      return Optional.of(cookie);
     }
    }
   }
   return Optional.empty();
  }

  public static void addCookie(HttpServletResponse response, String name, String value, int maxAge) {
       /* Cookie cookie = new Cookie(name, value);
        cookie.setPath("/");
        cookie.setSecure(true);
        cookie.setHttpOnly(true);
        cookie.setMaxAge(10000000);
        response.addCookie(cookie);*/
   ResponseCookie cookie = ResponseCookie.from(name, value)
    .sameSite("None")
    .secure(true)
    .path("/")
    .maxAge(maxAge)
    .httpOnly(true)
    .build();
   response.addHeader("Set-Cookie", cookie.toString());
  }

  public static void deleteCookie(HttpServletRequest request, HttpServletResponse response, String name) {
   Cookie[] cookies = request.getCookies();

   if (cookies != null && cookies.length > 0) {
    for (Cookie cookie : cookies) {
     if (name.equals(cookie.getName())) {
      cookie.setValue("");
      cookie.setPath("/");
      cookie.setMaxAge(0);
      response.addCookie(cookie);
     }
    }
   }
  }

  public static String serialize(Object obj) {
   return Base64.getUrlEncoder()
    .encodeToString(SerializationUtils.serialize(obj));
  }

  public static <T> T deserialize(Cookie cookie, Class<T> cls) {
   return cls.cast(
    SerializationUtils.deserialize(
     Base64.getUrlDecoder().decode(cookie.getValue())
    )
   );
  }

}

 

 

 

HeaderUtils

public class HeaderUtils {
  private final static String HEADER_AUTHORIZATION = "Authorization";
  private final static String TOKEN_PREFIX = "Bearer ";
  private final static String HEADER_REFRESH_TOKEN = "RefreshToken";

  public static String getAccessToken(HttpServletRequest request) {
   String headerValue = request.getHeader(HEADER_AUTHORIZATION);

   if (headerValue == null) {
    return null;
   }

   if (headerValue.startsWith(TOKEN_PREFIX)) {
    return headerValue.substring(TOKEN_PREFIX.length());
   }

   return null;
  }

  public static String getHeaderRefreshToken(HttpServletRequest request) {
   String headerValue = request.getHeader(HEADER_REFRESH_TOKEN);

   if (headerValue == null) {
    return null;
   }

   if (headerValue.startsWith(TOKEN_PREFIX)) {
    return headerValue.substring(TOKEN_PREFIX.length());
   }

   return null;
  }
}

 

쿠키와 헤더를 관리하기 위한 유틸 추가

 

OAuth2AuthenticationSuccessHandler

@Component
@RequiredArgsConstructor
@Slf4j
public class OAuth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

  private final AuthTokenProvider tokenProvider;
  private final JwtConfig jwtConfig;
  private final OAuth2AuthorizationRequestBasedOnCookieRepository authorizationRequestRepository;
  private final RefreshService refreshService;
  private static final String AUTHORIZATION = "token";

  //인증이 성공적으로 처리될 때 호출
  //사용자를 성공적으로 인증한 후 사용자를 리디렉션할 대상 url 결정
  //응답이 이미 커밋되었을 때 (응답에 헤더가 이미 기록되었을 때) 디버그 메세지를 로그로 기록하고 반환,
  //인증 속성을 지우고, 결정된 대상 url로 사용자를 리디렉션
  @Override
  public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
   Authentication authentication) throws IOException, ServletException {
   String targetUrl = determineTargetUrl(request, response, authentication);

   if (response.isCommitted()) {
    logger.debug("Response has already been committed. Unable to redirect");
    return;
   }

   clearAuthenticationAttributes(request, response);
   getRedirectStrategy().sendRedirect(request, response, targetUrl);
  }

  //인증이 성공한 후의 대상 url 결정
  @Override
  protected String determineTargetUrl(HttpServletRequest request, HttpServletResponse response,
   Authentication authentication) {
   //요청 쿠키에서 리디렉션 url 매개변수가 있는지 확인
   Optional<String> redirectUri = CookieUtils.getCookie(request, REDIRECT_URI_PARAM_COOKIE_NAME)
    .map(Cookie::getValue);

   //매개변수가 있다면, 인가된 리디렉션 url인지 확인, 인가되지 않은 경우 예외
   if (redirectUri.isPresent() && !isAuthorizedRedirectUri(redirectUri.get())) {
    throw new IllegalArgumentException(
     "Sorry! We've got an Unauthorized Redirect URI and can't proceed with the authentication");
   }

   String targetUrl = redirectUri.orElse(getDefaultTargetUrl());

   //인증 토큰에서 사용자의 이메일, 역할 등의 정보를 추출, 액세스 토큰과 리프레시 토큰 생성
   OAuth2AuthenticationToken authToken = (OAuth2AuthenticationToken)authentication;
   ProviderType providerType = ProviderType.valueOf(authToken.getAuthorizedClientRegistrationId().toUpperCase());

   System.out.println("유저타입 : " + authentication.getPrincipal().getClass());
   DefaultOAuth2User user = (DefaultOAuth2User)authentication.getPrincipal();
   OAuthUserInfo userInfo = OAuthUserInfoFactory.getOAuthUserInfo(providerType, user.getAttributes());
   Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();

   List<String> roles = authorities.stream().map(GrantedAuthority::getAuthority).toList();
   AuthToken accessToken = tokenProvider.createAccessToken(
    userInfo.getEmail(),
    roles
   );
   // refresh 토큰 설정

   AuthToken refreshToken = tokenProvider.createRefreshToken(
    userInfo.getEmail()
   );
   // DB 저장
   //refresh 토큰 데이터베이스에 저장
   refreshService.saveRefreshToken(userInfo.getEmail(), refreshToken);

   //응답에 쿠키로 추가!
   CookieUtils.addCookie(response, "RefreshToken", refreshToken.getToken(),
    (int)(System.currentTimeMillis() + jwtConfig.getRefreshTokenValidTime()));

   HttpHeaders headers = new HttpHeaders();
   headers.set(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken.getToken());

   // 응답 헤더에 토큰을 설정하여 클라이언트에게 반환
   // 이 부분에서 targetUrl 대신에 response header에 직접 토큰을 설정해줍니다.
   response.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken.getToken());

   //엑세스 토큰과 리프레시 토큰을 쿼리 매개변수로 하는 대상 url을 구성하여 반환
   return UriComponentsBuilder.fromUriString(targetUrl)
    .build().toUriString();
  }

  //요청에 저장된 인증 속성을 지우고 권한 부여 요청 쿠키를 리포지토리에서 제거
  protected void clearAuthenticationAttributes(HttpServletRequest request, HttpServletResponse response) {
   super.clearAuthenticationAttributes(request);
   authorizationRequestRepository.removeAuthorizationRequestCookies(request, response);
  }

  private boolean hasAuthority(Collection<? extends GrantedAuthority> authorities, String authority) {
   if (authorities == null) {
    return false;
   }

   for (GrantedAuthority grantedAuthority : authorities) {
    if (authority.equals(grantedAuthority.getAuthority())) {
     return true;
    }
   }
   return false;
  }

  //리디렉션 URI가 인가된 것인지 확인.
  //제공된 URI의 호스트 및 포트를JWT구성에서 구성된 인가된 리디렉션 URI와 비교
  private boolean isAuthorizedRedirectUri(String uri) {
   URI clientRedirectUri = URI.create(uri);
   return jwtConfig.getOauth2().getAuthorizedRedirectUris()
    .stream()
    .anyMatch(authorizedRedirectUri -> {
     // Only validate host and port. Let the clients use different paths if they want to
     URI authorizedURI = URI.create(authorizedRedirectUri);
     return authorizedURI.getHost().equalsIgnoreCase(clientRedirectUri.getHost())
      && authorizedURI.getPort() == clientRedirectUri.getPort();
    });
  }
}

 

OAuth2 인증 성공시 처리해 줄 로직 

 

대상 URL을 결정하고 응답이 이미 커밋되었는지 확인한 후 사용자를 리디렉션한다.

determineTargetUrl 메서드로 대상 URL을 결정하고,

OAuth2 토큰을 생성하여 응답에 추가한 후 대상 URL을 반환한다.

clearAuthenticationAttributes 메서드로 인증 속성을 지우고 권한 부여 요청 쿠키를 리포지토리에서 제거한다.

 

@Component
@RequiredArgsConstructor
public class OAuth2AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {

  private final OAuth2AuthorizationRequestBasedOnCookieRepository authorizationRequestRepository;

  @Override
  public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
   AuthenticationException exception) throws
   IOException, ServletException {
   String targetUrl = CookieUtils.getCookie(request, REDIRECT_URI_PARAM_COOKIE_NAME)
    .map(Cookie::getValue)
    .orElse(("/"));

   exception.printStackTrace();

   targetUrl = UriComponentsBuilder.fromUriString(targetUrl)
    .queryParam("error", exception.getLocalizedMessage())
    .build().toUriString();

   authorizationRequestRepository.removeAuthorizationRequestCookies(request, response);

   getRedirectStrategy().sendRedirect(request, response, targetUrl);
  }
}

 

 

로그인 실패 시 처리를 담당

실패한 URI로 리다이렉션하고 인증 요청 쿠키를 제거한다. 실패한 메시지도 쿼리 매개변수에 추가된다.

 

 

 

 

 

 

 

참고 자료

https://developers.google.com/identity/protocols/oauth2?hl=ko

복사했습니다!