일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- Today
- Total
- DI컨테이너
- beandefinition
- RequiredArgsConstructor
- 스프링 Configuration
- 스프링 컨테이너
- Spring
- 객체지향
- Servlet Filter
- HandlerMethodArgumentResolver
- autowired
- 롬복 Qualifier
- Autowired 옵션
- springsecurity
- docker
- 스프링
- qualifier
- 스프링 싱글톤
- 싱글톤 컨테이너
- 라즈베리파이4
- Spring interceptor
- DI
- 스프링 빈 조회
- 생성자 주입
- 빈 중복 오류
- 의존관계 주입
- 도커
- 라즈베리파이
- 스프링 빈
- ComponentScan
- UsernamePasswordAuthenticationFilter
그날그날 공부기록
스프링 시큐리티 로그인 동작 따라가보기 본문
스프링 시큐리티 설정
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.formLogin().disable()
.httpBasic().disable()
.csrf().disable()
.headers().frameOptions().disable()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeHttpRequests()
.requestMatchers("/", "/css/**", "/images/**", "/js/**", "/favicon.ico", "/h2-console/**").permitAll()
.requestMatchers("/sign-up").permitAll()
.anyRequest().authenticated()
.and()
// oauth 관련 설정
.oauth2Login()
.successHandler(oAuth2LoginSuccessHandler)
.failureHandler(oAuth2LoginFailureHandler)
.userInfoEndpoint().userService(customOAuth2UserService);
http.addFilterAfter(jwtAuthenticationProcessingFilter(), LogoutFilter.class);
http.addFilterAfter(customJsonUsernamePasswordAuthenticationFilter(), JwtAuthenticationProcessingFilter.class);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setPasswordEncoder(passwordEncoder());
provider.setUserDetailsService(loginService);
return new ProviderManager(provider);
}
@Bean
public CustomJsonUsernamePasswordAuthenticationFilter customJsonUsernamePasswordAuthenticationFilter () {
CustomJsonUsernamePasswordAuthenticationFilter customLoginFilter = new CustomJsonUsernamePasswordAuthenticationFilter(objectMapper);
customLoginFilter.setAuthenticationManager(authenticationManager());
customLoginFilter.setAuthenticationSuccessHandler(loginSuccessHandler);
customLoginFilter.setAuthenticationFailureHandler(loginFailureHandler);
return customLoginFilter;
}
@Bean
public JwtAuthenticationProcessingFilter jwtAuthenticationProcessingFilter() {
JwtAuthenticationProcessingFilter jwtAuthFilter = new JwtAuthenticationProcessingFilter(jwtService, userRepository);
return jwtAuthFilter;
}
로그인 정보를 json으로 받음 → form login, http basic 기능 비활성화
서버에 인증정보 저장 안 함 → csrf공격 방지기능, session 비활성화(Stateless)
authorizeHttpRequests()로 요청에 대한 인가를 구성.
requestMatchers().permitAll()로 설정 경로에 대해서는 인증 없이 접근 가능
anyRequest().authenticated()로 나머지 요청들에 대해서 인증된 사용자만 접근 가능하도록 설정
http.addFilterAfter, http.addFilterBefore를 통해
LogoutFilter → JwtAuthenticationProcessingFilter → CustomJwonUserNamePasswordAuthentiactionFilter 순으로 필터가 작동되도록 설정.
- 필터 실행순서가 왜이럴까?
http.addFilterAfter(customJsonUsernamePasswordAuthenticationFilter(), LogoutFilter.class);
http.addFilterBefore(jwtAuthenticationProcessingFilter(), CustomJsonUsernamePasswordAuthenticationFilter.class);
LogoutFilter
"이후"에 CustomJsonUsernamePasswordAuthenticationFilter
실행.CustomJsonUsernamePasswordAuthenticationFilter
"이전"에 JwtAuthenticationProcessingFilter
실행.
예상 = LogoutFilter
→ JwtAuthenticationProcessingFilter
→ CustomJsonUsernamePasswordAuthenticationFilter
하지만 실행결과를 보면 순서가 꼬인다. 왜 그럴까..
http.addFilterAfter(jwtAuthenticationProcessingFilter(), LogoutFilter.class);
http.addFilterAfter(customJsonUsernamePasswordAuthenticationFilter(), JwtAuthenticationProcessingFilter.class);
이렇게 적어주니 순서가 잘 적용되는 걸 확인 가능했다.
로그인 과정
POST /login으로 로그인 요청을 보냄.
LogoutFilter
이후 JWT 인증 과정을 위한 JwtAuthenticationProcessingFilter
호출.
JWT를 받기 위한 로그인 과정이므로 JwtAuthenticationProcessingFilter
를 패스.
// JwtAuthenticationProcessingFilter의 doFilterInternal 일부
// NO_CHECK_URL = "/login"
if (request.getRequestURI().equals(NO_CHECK_URL)) {
filterChain.doFilter(request, response);
return;
}
filterChain.doFilter
로 다음 필터인 AbstractAuthentiactionProcessingFilter
를 호출하여 인증과정 진행.
해당 필터에서는 우선 requiresAuthentication
메서드를 통해 인증이 필요한지 체크한다.
로직은 다음과 같다.
matcher를 통해 request가 사전에 설정했던 url과 http method와 일치하는지 확인한다.
일치하는 request라면 attemptAuthentication 메서드가 실행된다.
이 메서드는 추상 메서드이다.
따라서 AbstractAuthentiactionProcessingFilter
를 구현한 클래스에서 원하는 조건에 따라 구현하면 된다.
formLogin에서는 AbstractAuthentiactionProcessingFilter
를 구현한 UsernamePasswordAuthenticationFilter
가 사용된다.
이번 프로젝트에서는 json을 통해 로그인을 하므로 UsernamePasswordAuthenticationFilter
를 참고하여 CustomJsonUsernamePasswordAuthenticationFilter
를 만들었다.
사용자가 보낸 로그인 정보를 form에서 가져오냐, messageBody에서 가져오냐의 차이만 존재한다.
코드는 다음과 같다.
public class CustomJsonUsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private static final String DEFAULT_LOGIN_REQUEST_URL = "/login";
public static final String HTTP_METHOD = "POST";
public static final String CONTENT_TYPE = "application/json";
public static final String USERNAME_KEY = "email";
public static final String PASSWORD_KEY = "password";
public static final AntPathRequestMatcher DEFAULT_LOGIN_PATH_REQUEST_MATCHER = new AntPathRequestMatcher(DEFAULT_LOGIN_REQUEST_URL, HTTP_METHOD);
public final ObjectMapper objectMapper;
private boolean postOnly = true;
public CustomJsonUsernamePasswordAuthenticationFilter(ObjectMapper objectMapper) {
super(DEFAULT_LOGIN_PATH_REQUEST_MATCHER);
this.objectMapper = objectMapper;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException {
if (postOnly && !request.getMethod().equals(HTTP_METHOD)) {
log.error("로그인 요청의 HTTP_MOTHOD가 잘못되었습니다. : {}", request.getMethod());
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
if (request.getContentType()==null || !request.getContentType().equals(CONTENT_TYPE)) {
log.error("로그인 요청의 ContentType이 잘못되었습니다. : {}", request.getContentType());
throw new AuthenticationServiceException("Authentication Cont-Type not supported: " + request.getContentType());
}
ServletInputStream inputStream = request.getInputStream();
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
Map<String, String> loginDataMap = objectMapper.readValue(messageBody, new TypeReference<>() {});
String email = loginDataMap.get(USERNAME_KEY);
String password = loginDataMap.get(PASSWORD_KEY);
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(email, password);
return this.getAuthenticationManager().authenticate(authRequest);
}
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
}
(코드가 길어 나눠서 확인)
앞서 설명한 AbstAbstractAuthentiactionProcessingFilter
에
이 필터를 적용할 url과 http method를 설정한다.
attemptAuthentication
과정은 다음과 같음.
- http method와 content type을 체크
- request에서 message body를 추출
- ObjectMapper를 통해 json데이터를 Map형식으로 변환
- username(email)과 password를 통해
UsernamePasswordAuthenticationToken
생성 getAuthentication()
메서드를 통해AuthenticationManager
를 불러온 뒤authenticate()
메서드에UsernamePasswordAuthenticationToken
을 넘겨주고Authentication
객체 반환(객체는 아래 정보와 같이 되어있음)
그렇다면 AuthenticationManager.authentication
은 어떻게 동작할까?
우선 해당 객체는 SecurityConfig
에서 setAuthenticationManager()
메서드를 통해ProviderManager
를 주입해 줌.
해당 ProviderManager
에는 DaoAuthenticationProvider
가 들어가 있음.
이제 getAuthenticationManager()
로 ProviderManager
가 불려 오는 걸 알았으니ProviderManager
의 authentication()
메서드를 확인해 보자.
파라미터로 들어온 Authentication 객체를 처리할 수 있는 provider를 리스트에 찾고, 해당 provider의 authenticate()
메서드를 실행한다.
여기서 사용한 provider는 DaoAuthenticationProvider
이다.
DaoAuthenticationProvider
는 AbstractUserDetailsAuthenticationProvider
를 구현한 클래스이다.
authenticate()
메서드는 AbstractUserDetailsAuthenticationProvider
에 있고,
그 안에 들어가는 추상 메서드인 retrieveUser()
를 DaoAuthenticationProvider
에서 구현한다.
AbstractUserDetailsAuthenticationProvider
를 먼저 확인해 보자.
이 전에 Authentication으로 넘겨주었던 UsernamePasswordAuthenticationToken
을 지원하는 걸 확인할 수 있다.
retrieveUser()를 통해 DB에서 유저 정보인 UserDetails를 가져오고,
additionalAuthenticationChecks()를 통해 사용자가 입력한 정보가 일치하는지 확인한다.
최종적으로 createSuccessAuthentication()를 통해 Authentication객체를 반환한다.
앞서 말했듯, 추상 메서드로 작성되어 있고 AbstractUserDetailsAuthenticationProvider의 구현체인 DaoAuthenticationProvider에서 이를 구현한다.
구현 내용을 확인하기 위해 잠깐 DaoAuthenticationProvider를 확인.
UserDetailService의 loadUserByUsername()메서드로 DB에서 유저 정보를 가져와 반환한다.
다시 AbstractUserDetailsAuthenticationProvider로 돌아와 createSuccessAuthentication()을 확인.
이 메서드에서는 UsernamePasswordAuthenricationToken을 생성해서 반환한다.
UsernamePasswordAuthenricationToken에 들어가는 값은 다음과 같음.
1. principal
2. authentication.getCredentials()
3. user.getAuthorities()
반환되면 다시 AbstractAuthenticationProcessingFilter로 돌아와 인증의 성공/실패에 따른 분기를 처리한다.
미리 등록해 둔 성공/실패 핸들러가 실행된다.
그림으로 정리하며 다시 한번 확인하자…
좀 더 보기 쉽게 다시 그렸다.
'Spring 공부' 카테고리의 다른 글
필터, 인터셉터 (0) | 2023.12.19 |
---|---|
쿠키, 세션 사용해보기 (0) | 2023.11.28 |
빈 생명주기, 콜백 이용하기 (0) | 2022.08.16 |
Map, List에 빈 주입 & 사용하기 (0) | 2022.08.09 |
Qualifier을 대체할 수 있는 커스텀 애노테이션 만들기 (0) | 2022.08.09 |