[개인 프로젝트(15)] Stateless OAuth2.0 Social Login (Spring Security + JWT + OAuth 2.0)

개발로그·2023년 12월 13일
1

개인 프로젝트

목록 보기
14/14
post-thumbnail


📌 서론


Spring Security OAuth2 Login은 따로 설정하지 않는다면,
인증 서버로 인가 코드 발급을 요청을 보내기 위해 필요한 정보를 캡슐화한 객체를 세션에 저장한다.


JWT 기반 인증을 구현하고, 세션을 사용하지 않도록 Security 설정도 했으나
OAuth2 Login을 도입하고 쿠키를 확인해보니 난데없이 JSESSIONID가 생겨있어서 당황했다면 (내 얘기 🙋🏻‍♀️)은 본 포스팅을 참고하여 해결해보자.


본 포스팅에서는 인증 서버로 인가 코드 발급을 요청을 보내기 위해 필요한 정보를 캡슐화한 객체 (OAuth2AuthorizationRequest 객체)를

세션에서 저장 및 관리하지 않고
객체를 직렬화하여 암호화한 후 쿠키에 저장하고,
쿠키에 저장된 값을 복호화한 후 역직렬화하여 관리하도록 설정하는 코드만을 다룬다.

[Spring Security + JWT + OAuth 2.0을 사용한 Stateless Social Login 전체 코드]





📌 OAuth(Open Authorization)란


💡
인터넷 사용자들이 비밀번호를 제공하지 않고 다른 웹사이트 상의
자신의 정보에 대해 웹사이트나 애플리케이션의 접근 권한을 부여할 수 있는
공통적인 수단으로서 사용되는, 접근 위임을 위한 개방형 표준
위키백과





📌 HttpCookieOAuth2AuthorizedClientRepository


Spring Security OAuth2 Login은 기본적으로,
AuthorizationRequestRepository 인터페이스의

구현체인 HttpSessionOauth2AuthorizationRequestRepository를 사용하여
OAuth2AuthorizationRequest를 세션에 저장한다.

이미지 출처




✅ OAuth2AuthorizationRequest

OAuth 2.0 Authorization Code Grant Flow는 일반적으로 다음의 단계를 따른다.

  1. 인가 코드 받기

    • 서비스 서버가 인증 서버로 인가 코드 발급을 요청한다.
      이때, client_id, redired_uri, response_type, scope 등의 정보 포함하여 인가 코드 발급 요청을 보낸다.
      - client_id
          : 인증 서버 식별자
      - redirect_uri
          : 인가 코드를 전달받을 URI
      -  response_type
          : 응답 타입, code로 고정된 값
      -  scope 
          : 인증 서버로부터 사용자의 리소스를 받기 위해 사용자에게 동의 요청할 동의 항목 목록)
    • 인증 서버가 인가 코드 발급 요청을 받으면, 사용자에게 로그인을 통한 인증을 요청한다.
    • 사용자가 인증 서버에 로그인 하면, 사용자에게 동의 화면을 출력하여 인가를 위한 사용자 동의를 요청한다.
    • 사용자가 동의 항목에 동의하면 인증 서버는 서비스 서버의 Redirect URI로 인가코드를 전달한다.
  2. 토큰 받기

    • 서비스 서버가 Redirect URI로 전달받은 인가 코드로 인증 서버에 토큰 발급을 요청한다.
    • 인증 서버가 토큰을 발급해 서비스 서버에 전달한다.
  3. 사용자 로그인 처리

    • 서비스 서버가 발급 받은 액세스 토큰으로 사용자 정보 가져오기를 요청해 사용자의 회원 번호 서비스 회원인지 확인한다.
    • 서비스 회원 정보 확인 결과에 따라 서비스 로그인 또는 회원 가입 과정을 진행한다.


OAuth2AuthorizationRequest는 인증 서버로 인가 코드 발급을 요청을 보내기 위해 필요한 다음과 같은 정보를 캡슐화한 객체이다.




✅ AuthorizationRequestRepository

인증 서버로 인가 코드 발급을 요청을 보내기 위해 필요한 정보(이하 OAuth2AuthorizationRequest)를 관리하기 위한 인터페이스로, 기본 구현체는 HttpSessionOauth2AuthorizationRequestRepository이다.

💡 AutorizationRequestRepository는 인가 코드 발급 요청을 시작한 시점부터,
인가 코드를 받는 시점까지 OAuth2AuthorizationRequest를 저장한다.



  1. loadAuthorizationRequest
    인증 서버는 사용자의 동의 항목에 대한 동의 여부에 따라 인가 코드 또는 에러 응답을 생성한다.

    loadAuthorizationRequest는 인가 코드 요청에 대한 응답을 받은 시점에 호출되어
    AuthorizationRequestRepository에 저장된 OAuth2AuthorizationRequest를 가져오는데,
    이는 인가 코드 발급 요청에 대한 응답을 처리하는데 사용된다.



  2. saveAuthorizationRequest
    인가 코드 발급 요청을 시작한 시점에 호출 되어, OAuth2AuthorizationRequest를 저장한다.



  3. removeAuthorizationRequest
    현재 인가 코드 발급 요청에 대한 응답을 받은 시점에 호출되어, AuthorizationRequestRepository에서 OAuth2AuthorizationRequest를 삭제한다.



AuthorizationRequestRepository 인터페이스에 대해 알아보았으니,
이제 Stateless한 소셜 로그인을 구현하기 위해 OAuth2AuthorizationRequest를 쿠키에 저장하는 구현 클래스를 만들어보자.

이미지 출처








✅ HttpCookieOAuth2AuthorizedClientRepository

@Slf4j
@Component
public class HttpCookieOAuth2AuthorizedClientRepository
	implements AuthorizationRequestRepository<OAuth2AuthorizationRequest> {

	public static final String OAUTH2_COOKIE_NAME = "OAUTH2_AUTHORIZATION_REQUEST";
	public static final Duration OAUTH_COOKIE_EXPIRY = Duration.ofMinutes(5);

	@Override
	public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) {
		return getCookie(request);
	}

	@Override
	public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest, HttpServletRequest request,
		HttpServletResponse response) {
		if (isNull(authorizationRequest)) {
			removeAuthorizationRequest(request, response);
			return;
		}

		CookieUtil.addCookie(response, OAUTH2_COOKIE_NAME, encrypt(authorizationRequest), OAUTH_COOKIE_EXPIRY);
	}

	@Override
	public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request,
		HttpServletResponse response) {
		OAuth2AuthorizationRequest oAuth2AuthorizationRequest = getCookie(request);
		CookieUtil.removeCookie(request, response, OAUTH2_COOKIE_NAME);
		return oAuth2AuthorizationRequest;
	}

	private OAuth2AuthorizationRequest getCookie(HttpServletRequest request) {
		return CookieUtil.getCookie(request, OAUTH2_COOKIE_NAME).map(this::decrypt).orElse(null);
	}

	private String encrypt(OAuth2AuthorizationRequest authorizationRequest) {
		byte[] bytes = SerializationUtils.serialize(authorizationRequest);
		return Aes256.encrypt(bytes);
	}

	private OAuth2AuthorizationRequest decrypt(Cookie cookie) {
		byte[] bytes = Aes256.decrypt(cookie.getValue().getBytes(StandardCharsets.UTF_8));
		return (OAuth2AuthorizationRequest)SerializationUtils.deserialize(
			bytes);
	}

}







✅ CookieUtil

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class CookieUtil {

	public static Optional<Cookie> getCookie(HttpServletRequest request, String cookieName) {
		return Optional.ofNullable(request.getCookies())
			.flatMap(cookies -> Arrays.stream(cookies)
				.filter(cookie -> cookie.getName().equals(cookieName))
				.findFirst());
	}

	public static void addCookie(HttpServletResponse response, String cookieName, String cookieValue,
		Duration maxAge) {
		Cookie cookie = new Cookie(cookieName, cookieValue);
		cookie.setPath("/");
		cookie.setHttpOnly(Boolean.TRUE);
		cookie.setSecure(Boolean.TRUE);
		cookie.setMaxAge((int)maxAge.toSeconds());

		response.addCookie(cookie);
	}

	public static void removeCookie(HttpServletRequest request, HttpServletResponse response, String cookieName) {
		Optional.of(request.getCookies())
			.ifPresent(cookies ->
				Arrays.stream(cookies)
					.filter(cookie -> cookie.getName().equals(cookieName))
					.forEach(cookie -> {
						cookie.setValue(EMPTY);
						cookie.setPath("/");
						cookie.setMaxAge(ZERO);
						response.addCookie(cookie);
					})
			);
	}
}



이처럼 인가 코드 발급을 요청을 보내기 위해 필요한 다음과 같은 정보를 캡슐화한 객체를 쿠키에서 저장하고 관리하도록 AuthorizationRequestRepository를 구현한 HttpCookieOAuth2AuthorizedClientRepository 클래스를 작성했다면,

이제 남은 것은 Spring Security 설정 파일에서 AuthorizationRequestRepository의 기본 구현체인 HttpSessionOauth2AuthorizationRequestRepository가 아닌,
HttpCookieOAuth2AuthorizedClientRepository를 사용하도록 설정해주는 것이다.




✅ SecurityConfiguration

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfiguration {

	// ...

	private final HttpCookieOAuth2AuthorizedClientRepository httpCookieOAuth2AuthorizedClientRepository;

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
		return httpSecurity
			// ...

			.oauth2Login(oauth2 -> oauth2
				.authorizationEndpoint(config ->
					config.authorizationRequestRepository(httpCookieOAuth2AuthorizedClientRepository))
				// ...other config...
  
			.build();
	}
}



0개의 댓글