728x90
1. JsonUsernamePasswordAuthenticationFilter.java
Spring Security의 기본 폼 로그인 방식 대신 AbstractAuthenticationProcessingFilter를 상속받아 JSON 데이터를 처리하여 인증을 수행하는 JSON 기반 로그인 요청을 처리 커스텀 필터이다. (RESTFUL API 기반의 로그인을 구현할것이 때문에, Json을 처리할 수 있는 필터를 구현)
[작동 흐름]
- 클라이언트가 /login 경로로 POST 요청을 보낸다
- 요청 본문에 JSON 형태로 email과 password를 포함한다.
- 필터가 요청을 인터셉트한다.
- Content-Type이 application/json인지 확인한다.
- JSON 본문에서 아이디와 비밀번호를 추출한다.
- 추출된 정보로 UsernamePasswordAuthenticationToken을 생성한다.
- AuthenticationManager를 사용하여 인증을 수행한다.
- 성공 시 인증 객체를 반환하고, 실패 시 예외를 던진다.
1) 필드
private static final String DEFAULT_LOGIN_REQUEST_URL = "/login";
private static final String HTTP_METHOD = "POST";
private static final String CONTENT_TYPE = "application/json";
private final ObjectMapper objectMapper;
private static final String USERNAME_KEY = "email";
private static final String PASSWORD_KEY = "password";
private static final AntPathRequestMatcher DEFAULT_LOGIN_PATH_REQUEST_MATCHER = new AntPathRequestMatcher(
DEFAULT_LOGIN_REQUEST_URL, HTTP_METHOD);
- DEFAULT_LOGIN_REQUEST_URL = "/login" : 필터가 처리할 로그인 URL이며 "/login/*"으로 오는 요청 처리
- HTTP_METHOD = "POST" : HTTP 메서드 방식 중 POST 메서드만 허용
- CONTENT_TYPE = "application/json" : 요청의 Content-Type이 application/json이어야 하며, json 타입의 데이터로만 로그인 진행
- ObjectMapper : JSON 데이터를 읽고 Java 객체로 변환하기 위해 Jackson 라이브러리의 ObjectMapper를 사용
- USERNAME_KEY, PASSWORD_KEY : JSON 요청 본문에서 사용할 키(email과 password)를 정의
ObjectMapper : Jackson 라이브러리에서 제공하는 클래스로, JSON과 Java 객체 간의 직렬화(Serialization) 및 역직렬화(Deserialization)를 처리한다.
1) JSON → Java 객체 변환: JSON 문자열을 Java 객체로 변환.
2) Java 객체 → JSON 변환: Java 객체를 JSON 문자열로 변환.
2) 요청 매처 설정
private static final AntPathRequestMatcher DEFAULT_LOGIN_PATH_REQUEST_MATCHER =
new AntPathRequestMatcher(DEFAULT_LOGIN_REQUEST_URL, HTTP_METHOD);
- AntPathRequestMatcher: 특정 URL과 HTTP 메서드를 매칭
- 이 필터는 /login 경로로 들어오는 POST 요청만 처리하도록 제한된다.
3) 생성자
public JsonUsernamePasswordAuthenticationFilter(ObjectMapper objectMapper) {
super(DEFAULT_LOGIN_PATH_REQUEST_MATCHER);
this.objectMapper = objectMapper;
}
- super(DEFAULT_LOGIN_PATH_REQUEST_MATCHER) : 부모 클래스(AbstractAuthenticationProcessingFilter)에 요청 매처를 전달하여 필터를 초기화
- this.objectMapper = objectMapper : ObjectMapper를 주입받아 JSON 데이터 처리 할 준비
4) 메서드 단계별 설명
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
// 1. 요청의 Content-Type 확인
if (request.getContentType() == null || request.getContentType().equals((CONTENT_TYPE))) {
throw new AuthenticationServiceException(
"Authentication Content-Type not supported" + request.getContentType());
}
// 2. JSON 본문 읽기
String messageBody = StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8);
// 3. JSON 데이터 파싱
Map<String, String> usernamePasswordMap = objectMapper.readValue(messageBody, Map.class);
// 4. 사용자 아이디와 비밀번호 추출
String username = usernamePasswordMap.get(USERNAME_KEY);
String password = usernamePasswordMap.get(PASSWORD_KEY);
// 5. 인증 토큰 생성
UsernamePasswordAuthenticationToken authRequest =
new UsernamePasswordAuthenticationToken(username, password);
// 6. 인증 시도
return this.getAuthenticationManager().authenticate(authRequest);
}
- 요청 Content-Type 확인 : 요청의 Content-Type이 application/json이 아니면 인증 실패 예외를 던져 JSON 기반 요청만 처리하도록 필터 동작을 제한한다.
- JSON 본문 읽기 : StreamUtils.copyToString() 을 사용해 HTTP 요청 본문(InputStream)을 문자열로 변환하고, UTF_8로 인코딩을 지정해준다.
- JSON 데이터 파싱 : ObjectMapper 사용하여 JSON 문자열을 Java Map 객체로 변환한다.
public <T> T readValue(String content, Class<T> valueType) throws IOException, JsonProcessingException
1) 파라미터
- content : JSON 문자열 (여기서는 massageBody)
- valueType : 변환할 Java 객체의 클래스 (여기서는 Map.class로 이렇게 전달하면 JSON 데이터를 Map<String, Object> 형태로 변환해준다.)
2) 반환값
- JSON 데이터를 변환한 Java 객체 (T 타입)
- 사용자 아이디와 비밀번호 추출 : Map에서 email(아이디)와 password를 추출한다.
- 인증 토큰 생성 : UsernamePasswordAuthenticationToken 객체를 생성하여 인증 요청을 준비하며, 생성된 토큰은 principal(사용자 ID) 과 credentials(비밀번호)를 포함한다.
- 인증 시도 : 부모 클래스의 getAuthenticationManager() 를 호출하여 인증 매니저를 가져오고, 생성된 authRequest를 authenticate() 메서드에 전달하여 인증을 시도한다. 인증 성공 시 인증 객체가 반환되며, 실패 시 예외가 발생한다.
5) 전체 로직
public class JsonUsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private static final String DEFAULT_LOGIN_REQUEST_URL = "/login";
private static final String HTTP_METHOD = "POST";
private static final String CONTENT_TYPE = "application/json";
private final ObjectMapper objectMapper;
private static final String USERNAME_KEY = "email";
private static final String PASSWORD_KEY = "password";
private static final AntPathRequestMatcher DEFAULT_LOGIN_PATH_REQUEST_MATCHER = new AntPathRequestMatcher(
DEFAULT_LOGIN_REQUEST_URL, HTTP_METHOD);
public JsonUsernamePasswordAuthenticationFilter(ObjectMapper objectMapper) {
super(DEFAULT_LOGIN_PATH_REQUEST_MATCHER);
this.objectMapper = objectMapper;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
if (request.getContentType() == null || request.getContentType().equals((CONTENT_TYPE))) {
throw new AuthenticationServiceException(
"Authentication Content-Type not supported" + request.getContentType());
}
String messageBody = StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8);
Map<String, String> usernamePasswordMap = objectMapper.readValue(messageBody, Map.class);
String username = usernamePasswordMap.get(USERNAME_KEY);
String password = usernamePasswordMap.get(PASSWORD_KEY);
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
2. UserDetailsServiceImpl.java 구현
Spring Security의 사용자 인증을 처리하는 핵심 컴포넌트로, Spring Security의 UserDetailsService 인터페이스의 메서드를 구현한다. 사용자 정보를 로드하고, 이를 Spring Security가 이해할 수 있는 형식으로 반환하는 역할을 한다.
[동작 과정]
- 클라이언트가 인증 요청을 보낸다.
- Spring Security는 사용자 정보를 로드하기 위해 loadUserByUsername 메서드를 호출한다.
- email을 기반으로 userRepository에서 사용자를 조회한다.
- 조회한 사용자 정보를 UserDetailsImpl 객체로 변환한다.
- 반환된 UserDetailsImpl 객체는 Spring Security의 인증 프로세스에서 사용된다.
@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
UserEntity users = userRepository.findByUserEmail(email)
.orElseThrow(() -> new IllegalArgumentException());
return new UserDetailsImpl(users);
}
}
- 매개변수 email은 클라이언트에서 전달된 사용자 ID(이 경우 이메일)이며, 반환값은 Spring Security에서 인증 과정을 처리하기 위해 필요한 UserDetails 객체이다.
- 이메일을 기반으로 사용자 정보를 데이터베이스에서 조회하고, 해당 이메일에 대한 사용자가 없는 경우 예외를 던진다.
- 조회한 UserEntity 객체를 Spring Security에서 사용하는 UserDetails 인터페이스로 변환한다.
3. WebSecurityConfig.java 수정
1) SecurityFliterChain 추가
addFilterAfter(jsonUsernamePasswordAuthenticationFilter(), LogoutFilter.class)
커스텀 인증 필터인 JsonUsernamePasswordAuthenticationFilter를 추가
2) 메서드 추가
@Bean
public DaoAuthenticationProvider daoAuthenticationProvider() throws Exception {
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setUserDetailsService(userDetailsService);
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
return daoAuthenticationProvider;
}
@Bean
public AuthenticationManager authenticationManager() throws Exception {
DaoAuthenticationProvider provider = daoAuthenticationProvider();
provider.setPasswordEncoder(passwordEncoder());
return new ProviderManager(provider);
}
@Bean
public JsonUsernamePasswordAuthenticationFilter jsonUsernamePasswordAuthenticationFilter() throws Exception {
JsonUsernamePasswordAuthenticationFilter jsonUsernamePasswordAuthenticationFilter = new JsonUsernamePasswordAuthenticationFilter(
objectMapper);
jsonUsernamePasswordAuthenticationFilter.setAuthenticationManager(authenticationManager());
return jsonUsernamePasswordAuthenticationFilter;
}
- DaoAuthenticationProvider 설정
- AbstractUserDetailsAuthenticationProvider를 상속받아, 실제 인증 로직을 구현한 클래스로 Spring Security에서 데이터베이스에서 사용자 정보를 조회하고, 비밀번호를 비교하여 인증을 처리한다.
- setUserDetailsService(userDetailsService)를 통해 사용자 정보를 로드할 UserDetailsService를 설정한다. 여기서는 userDetailsService가 UserDetailsServiceImpl 인스턴스이다.
- setPasswordEncoder(passwordEncoder())를 통해 비밀번호 인코딩 방식을 설정하여 비밀번호를 암호화하고 안전하게 비교할 수 있도록 한다.
- AuthenticationManager 설정
- 인증을 관리하는 핵심 인터페이스로 DaoAuthenticationProvider를 사용하여 ProviderManager 인스턴스를 반환한다. ProviderManager는 여러 인증 제공자를 사용할 수 있게 해준다.
- JsonUsernamePasswordAuthenticationFilter 설정
- 사용자가 JSON 형식으로 로그인 정보를 제출할 때 처리한다. 일반적인 폼 기반 로그인과 달리 JSON을 처리하는 커스텀 필터JSON 기반 인증 요청을 처리하는 커스텀 필터를 생성한다.
- setAuthenticationManager(authenticationManager())를 통해 앞서 설정한 AuthenticationManager를 필터에 연결한다. 이 필터는 사용자가 입력한 JSON 데이터를 기반으로 인증을 시도한다.
AbstractUserDetailsAuthenticationProvider
[동작 과정]
- 사용자가 로그인 폼에 이메일과 비밀번호를 입력하고 제출한다.
- JsonUsernamePasswordAuthenticationFilter가 요청을 처리하며, 이메일과 비밀번호를 받아 AuthenticationManager를 통해 인증을 시도한다.
- AuthenticationManager는 DaoAuthenticationProvider를 사용하여 사용자 정보를 UserDetailsService에서 로드한다.
- UserDetailsServiceImpl은 이메일을 사용해 사용자 정보를 데이터베이스에서 조회하고, 사용자가 존재하면 UserDetailsImpl 객체를 반환한다.
- 반환된 UserDetailsImpl을 사용하여 인증을 완료하고, 로그인 성공 여부를 결정한다.
3) 전체 로직
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final UserDetailsServiceImpl userDetailsService;
private final ObjectMapper objectMapper;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
httpSecurity
.csrf(AbstractHttpConfigurer::disable)
.cors((cors) -> cors.configurationSource(corsConfigurationSource()))
.httpBasic(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.addFilterAfter(jsonUsernamePasswordAuthenticationFilter(), LogoutFilter.class) // 필터 추가
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/", "/movie/**")
.permitAll()
.anyRequest().authenticated())
.exceptionHandling((exception) -> exception
.authenticationEntryPoint(unauthorizedEntryPoint).accessDeniedHandler(accessDeniedHandler))
.logout((logout) -> logout
.logoutSuccessUrl("/movie/login")
.invalidateHttpSession(true))
.sessionManagement((session) -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return httpSecurity.build();
}
// Cors 설정
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration(); // 허용할 origin 설정
config.addAllowedOrigin("http://localhost:3000");
config.setAllowCredentials(true); // 쿠키 허용
config.addAllowedHeader("*"); // 모든 헤더 허용
config.addAllowedMethod("*"); // 모든 http 메소드 허용
config.setExposedHeaders(Arrays.asList("Access-Control-Allow-Headers",
"Authorization, x-xsrf-token, Access-Control-Allow-Headers, Origin, Accept, X-Requested-With, ",
"Content-Type, Access-Control-Request-Method, Access-Control-Request-Headers")); // 헤더 요청 열어둠
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config); // 모든 경로에 대해 위 설정 적용
return source;
}
@Bean
public static PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
// 인증 관리자 관련 설정
@Bean
public DaoAuthenticationProvider daoAuthenticationProvider() throws Exception {
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setUserDetailsService(userDetailsService);
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
return daoAuthenticationProvider;
}
@Bean
public AuthenticationManager authenticationManager() throws Exception {
DaoAuthenticationProvider provider = daoAuthenticationProvider();
provider.setPasswordEncoder(passwordEncoder());
return new ProviderManager(provider);
}
@Bean
public JsonUsernamePasswordAuthenticationFilter jsonUsernamePasswordAuthenticationFilter() throws Exception {
JsonUsernamePasswordAuthenticationFilter jsonUsernamePasswordAuthenticationFilter = new JsonUsernamePasswordAuthenticationFilter(
objectMapper);
jsonUsernamePasswordAuthenticationFilter.setAuthenticationManager(authenticationManager());
return jsonUsernamePasswordAuthenticationFilter;
}
}
728x90
반응형
'Framework > Spring' 카테고리의 다른 글
[Spring / ToyProject] Spring Security 설정 - 3_1 (1) | 2025.01.16 |
---|---|
[Spring / ToyProject] Spring Security 설정 - 2_2 (0) | 2025.01.16 |
[Spring / ToyProject] 에러일지: Request failed with status code 403 (0) | 2025.01.08 |
[Spring / ToyProject] Spring Security 설정 - 1 (0) | 2025.01.08 |
[Spring / ToyProject] Spring-boot/React 초기 세팅 (0) | 2025.01.03 |