...이 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
인터페이스의 구현체로 등록한다.DelegatingOAuthUserService
OAuth2UserService
인터페이스의 구현체로 인증을 위임(delegate)하는 역할만 한다.OidcUserService
id
값을 활용했다(이것도 식별자이긴 하다).DefaultOAuth2UserService
✅org.springframework.security.oauth2.client.userinfo.OAuth2UserService
package 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
클래스를 사용했다.OAuth2UserRequest
loadUser
OAuth2UserRequest
로 받아진 인증 정보를 필요에 따라 처리한 후 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
등) 기본적인 인증 과정은 비슷하다.
다만 성공 핸들러, 실패 핸들러는 같은 인터페이스를 구현한다.
다음 글에서는 설정을 다룬다.