...이 SSAFY에서의 마지막 프로젝트를 하며 알게 된 내용이다.
이번 시리즈로 이 내용을 공유(겸 미래의 나를 위한 기록)하려고 했는데, 몰라서 못 쓰는 내용이 좀 있다.
sendRedirect()를 해줘야 한다.물론 빠진 내용까지 한 번에 쓰면 좋겠지만, 이걸 다 학습하다가는 정작 본 내용을 잊어버릴 것 같아, 그 전에 기록해두려고 한다.
그래서 일단 이 글에 포함되는 내용은 '사용법'에 가깝다.
그렇다고 내부적인 동작 시퀀스를 아예 배제하지는 않겠지만 약간의 디테일은 생략될 수 있다.
인증과 인가를 혼용할 겁니다. 별 이유는 없고 그냥 헷갈려서 의도치 않게 그렇게 될 것 같습니다.
어 이건 인가인데 왜 인증이라고 썼지? 이건 인증인데 왜 인가로 썼지? 싶으시다면 제발 지적해주세요
SecurityConfig.filterChain)@Configuration
@EnableWebSecurity(debug = false)
@RequiredArgsConstructor
public class SecurityConfig {
public final static String ACCESS_TOKEN_HEADER = "Authorization";
public final static String REFRESH_TOKEN_HEADER = "RefreshToken";
private final AuthTokenProvider authTokenProvider;
private final AuthTokenService authTokenService;
private final MemberService memberService;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
...
// OAuth 설정
http
.oauth2Login( // OAuth2.0 로그인 활성화
httpSecurityOAuth2LoginConfigurer ->
httpSecurityOAuth2LoginConfigurer
.userInfoEndpoint(
userInfoEndpointConfig ->
userInfoEndpointConfig.userService(customOAuth2MemberService())
)
.successHandler(oAuth2SuccessHandler())
.failureHandler(oAuth2FailureHandler())
);
return http.build();
}
@Bean
public CustomOAuth2MemberService customOAuth2MemberService() {
return new CustomOAuth2MemberService(memberService);
}
@Bean
public CustomOAuth2SuccessHandler oAuth2SuccessHandler() {
return new CustomOAuth2SuccessHandler(authTokenProvider);
}
@Bean
public CustomOAuth2FailureHandler oAuth2FailureHandler() {
return new CustomOAuth2FailureHandler();
}
}
exceptionHandling()) 등 인증과 관련없는 부분은 생략했다(원래 있어야 함).CustomXXX는 직접 작성한 부분들이다. 다른 패키지에 작성되어 있고, 여기서는 @Bean으로 등록하기만 한다.oauth2Login()).userInfoEndpoint()).oAuth2SuccessHandler(), oAuth2FailureHandler())을 작성한다.사실 이 모든 것 이전에 Client ID니 뭐니 하는 다른 부가적인(그리고 많은) 설정이 필요하다.
또 구글, 카카오 등 Resource server에서 내 서비스 등록도 해야 한다.
그런데 이것들은 스프링 시큐리티의 구조와는 별 상관이 없으므로 다음 글에서 기술한다.
org.springframework.security.oauth2.client.userinfo.OAuth2UserService 인터페이스의 구현체로 등록한다.DelegatingOAuthUserServiceOAuth2UserService 인터페이스의 구현체로 인증을 위임(delegate)하는 역할만 한다.OidcUserServiceid 값을 활용했다(이것도 식별자이긴 하다).DefaultOAuth2UserService ✅org.springframework.security.oauth2.client.userinfo.OAuth2UserServicepackage org.springframework.security.oauth2.client.userinfo;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
@FunctionalInterface
public interface OAuth2UserService<R extends OAuth2UserRequest, U extends OAuth2User> {
U loadUser(R userRequest) throws OAuth2AuthenticationException;
}
위와 같이 선언되어 있다.
여기서 알아볼 것은 OAuth2User 인터페이스, OAuth2UserRequest 클래스, loadUser 메소드다.
OAuth2User 인터페이스 org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal을 상속받는다.OAuth2User는 Resource server(구글 등 인증을 해주는 서버)로부터 인증 결과로 받아오는 인증 정보를 표현하는 인터페이스다.OAuth2User 자체는 사실 빈 껍데기고, 실질적으로 사용하는 것은 구현 클래스다.DefaultOAuth2User 클래스를 사용했다.OAuth2UserRequestloadUserOAuth2UserRequest로 받아진 인증 정보를 필요에 따라 처리한 후 OAuth2User 타입으로 반환한다.OAuth2User 기반으로 Authentication 객체가 만들어지고, 이것이 SecurityContext에 등록됨으로써 인증이 완료된다.인증의 성공 여부는 OAuth2UserService가 수행하는 일련의 로직의 성공 여부와 같다.
즉 다음이 모두 성공해야 한다.
oauthUserRequest에 있다).loadUser()를 통해 SecurityContext에 Authentication 객체가 등록된다.실패 요인은 다양하다.
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import org.springframework.security.core.Authentication;
public interface AuthenticationSuccessHandler {
default void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException {
this.onAuthenticationSuccess(request, response, authentication);
chain.doFilter(request, response);
}
void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException;
}
org.springframework.security.web.authentication.AuthenticationSuccessHandler(위 인터페이스)의 구현 객체SecurityContext에 Authentication이 등록된 상태에 수행할 작업을onAuthenticationSuccess() 내부에 작성한다.Authentication은 Authentication authentication parameter로 받아온다.import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import org.springframework.security.core.AuthenticationException;
public interface AuthenticationFailureHandler {
void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException;
}
org.springframework.security.web.authentication.AuthenticationSuccessHandler(위 인터페이스)의 구현 객체onAuthenticationSuccess() 내부에 작성한다.AuthenticationException exception parameter로 받아온다.개략적으로 쓰려고 헀지만 쓰다 보니 좀 주절주절 복잡하게 서술한 것 같다.
동작 과정을 요약하면 다음과 같다.
SecurityContext에 인증 객체가 등록된다).OAuth2UserService가 Resource server로 인증 요청을 하고 인증 정보를 받아온다.OAuth2UserService.loadUser()의 파라미터(OAuth2UserRequest 객체)를 통해 받아와지고,OAuth2User의 구현 타입으로 반환한다.SecurityContext에 인증 객체가 등록된다).oauth2Login()에서 설정된 AuthenticationSuccessHandler의 구현체가,AuthenticationFailureHandler의 구현체가 동작한다.사실 이 흐름은 그냥 Spring security의 흐름이라고 봐도 무방하다. 즉 일반적인 ID + PW 로그인도 똑같은 흐름이다.
그냥 구현하는 인터페이스 이름만 다를 뿐(UserDetailsService, UserDetails 등) 기본적인 인증 과정은 비슷하다.
다만 성공 핸들러, 실패 핸들러는 같은 인터페이스를 구현한다.
다음 글에서는 설정을 다룬다.