[Spring] Spring Security에 대해 (2)
이번 프로젝트에서 JWT를 사용해보려고 하여 Spring Security에 대해 조금 더 자세히 알아보려고 한다..!!
기본적인 내용들은 1편에서
2021.11.27 - [Back-end/Spring & Spring Boot] - [Spring] Spring Security에 대해 (1)
[Spring] Spring Security에 대해 (1)
Spring Security란? Spring 기반의 어플리케이션의 인증과 권한, 인가 등 보안을 담당하는 스프링 하위에 있는 프레임워크이다. Spring Security는 보안과 관련해서 체계적으로 많은 옵션을 제공해주어 개
diddl.tistory.com
기본 용어 정리
1편에서 했지만 중요하니 한번 더..!
- 인증 (Authentication): 사용자 신원을 확인하는 행위 -> 회원가입하고 로그인 하는 것
- 인가 (Authentication): 사용자 권한을 확인하는 행위 -> 웹에서는 주로 역할에 따른 권한 관리
- Principal(접근 주체): 보호 받는 Resource에 접근하는 대상
- Credential(비밀번호): Resource에 접근하는 대상의 비밀번호
Spring Security는 기본적으로 인증 절차를 거친 후 인가 절차를 진행한다. 어떤 리소스를 요청했을 때 인가 과정에서 해당 리소스에 대한 접근 권한이 있는지 확인을 한다. Spring Security는 이러한 인증과 인가를 위해 Principle을 아이디, Credential를 비밀번호로 사용하는 Credential 기반의 인증 방식을 사용한다.
Credential 기반의 인증 방식을 사용 잘 기억해두기..!
로그인 과정 자세히 알아보기
이 그림이 이해가 잘되어 퍼왔다..! 출처는 이미지에 워터마크로..
그림 출처에서 말했는데 이 그림을 별도의 창에 띄워 같이 보면 이해가 더 잘된다..!
ApplicationFilter
클라이언트 요청(Http Request)이 오면, 요청은 위 그림에서 보이는 ApplicationFilter 객체들로 먼저 간다.
ApplicationFilter들을 거치다가 DelegatingFilterProxyRegistrationBean이라는 필터는 만나게 된다.
DelegatingFilterProxyRegistrationBean이라는 필터는 DelegatingFilterProxy라는 클래스로 만들어진 Bean을 등록시켜주는 역할을 한다.
DelegatingFilterProxy, AuthenticationFilter
→ 스프링 부트에서는 @EnableAutoConfiguration 어노테이션을 이용해서 SecurityFilterAutoConfiguration클래스를 로드하고 디폴트로 이름이 "springSecurityFilterChain"인 빈을 등록해준다.
스프링 시큐리티가 만든 DelegatingFilterProxy 클래스인 "springSecurityFilterChain"이라는 이름의 스프링 빈을 등록하고 이후에는 이 DelegatingFilterProxy(springSecurityFilterChain)가 필터로 동작하게 된다.
→ 구체적으로는 DelegatingFilterProxy가 처리를 위임하는 필터 클래스는 "FilterChainProxy"다. 이 클래스 내부에 체인으로 등록된 필터를 순서대로 수행하는 것이다.
DelegatingFilterProxy → FilterChainProxy → FilterList들 순서대로 수행
springSecurityFilterChain은 어떻게 동작할까..? -> 보안과 관련된 여러 필터 리스트를 갖고 있는 객체로 FilterList들을 순회하면서 필터링을 실시한다. 여기서의 필터 리스트가 AuthenticationFilter들이다!(스프링 시큐리티가 자동으로 생성)
List<AuthenticationFilter>가 있는 것이고 이 리스트 안의 AuthenticationFilter들을 하나씩 순차적으로 실행한다고 보면 된다. 스프링이 자동 생성해주는 것이기 때문에 앱을 실행하면 Run창에서 확인할 수 있다.
.WebAsyncManagerIntegrationFilter@16d41725, org.springframework.security.web.context.SecurityContextPersistenceFilter@664db2ca, org.springframework.security.web.header.HeaderWriterFilter@56b1e527, org.springframework.security.web.authentication.logout.LogoutFilter@3d0d120b, com.sparta.springcore.security.filter.FormLoginFilter@2017f6e6, com.sparta.springcore.security.filter.JwtAuthFilter@3b6c2be6, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@423f8a73, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@3f866f50, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@25533bba, org.springframework.security.web.session.SessionManagementFilter@7b95bdb0, org.springframework.security.web.access.ExceptionTranslationFilter@6c9a3661, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@36551e97]
이제 하나씩 살펴보자..
실행순서대로 표기함, 자세히 무엇인지는 몰라도 대충 의미를 알고 넘어가기 위해 정리..
WebAsyncManagerIntegrationFilter
- SpringSecurityContextHolder는 ThreadLocal기반(하나의 쓰레드에서 SecurityContext 공유하는 방식)으로 동작하는데, 비동기(Async)와 관련된 기능을 쓸 때에도 SecurityContext를 사용할 수 있도록 만들어주는 필터
< SecurityContextHolder >
보안 주체의 세부 정보를 포함하여 응용프로그램의 현재 보안 컨텍스트에 대한 세부 정보가 저장된다.
< SecurityContext >
Authentication을 보관하는 역할을 하며, SecurityContext를 통해 Authentication 객체를 꺼내올 수 있다.
< Authentication >
현재 접근하는 주체의 정보와 권한을 담는 인터페이스이다. Authentication 객체는 Security Context에 저장되며, SecurityContextHolder를 통해 SecurityContext에 접근하고, SecurityContext를 통해 Authentication에 접근할 수 있다.
SecurityContextPersistenceFilter
- SecurityContext가 없으면 만들어주는 필터
- SecurityContext는 인터페이스이기 때문에 구현체가 필요하다.
HeaderWriterFilter
- 응답(Response)에 Security와 관련된 헤더 값을 설정해주는 필터
CsrfFilter
- CSRF 공격을 방어하는 필터
LogoutFilter
- 로그아웃 요청을 처리하는 필터
- DefaultLogoutPageGeneratingFilter가 로그아웃 기본 페이지를 생성
UsernamePasswordAuthenticationFilter
- username, password를 쓰는 form 기반 인증을 처리하는 필터
- AuthenticationManager를 통한 인증 실행 --> 아래 자세한 예시참고
- 성공하면, Authentication 객체를 SecurityContext에 저장 후 AuthenticationSuccessHandler 실행
- 실패하면 AuthenticationFailureHandler 실행
유저가 인증을 요청할때 필터는 인증 메커니즘과 모델을 기반으로한 필터들을 통과한다.
예시)
- HTTP 기본 인증을 요청하면 BasicAuthenticationFilter를 통과한다.
- HTTP Digest 인증을 요청하면 DigestAuthenticationFilter를 통과한다.
- 로그인 폼에 의해 요청된 인증은 UseerPasswordAuthenticationFilter를 통과한다.
- x509 인증을 요청하면 X509AuthenticationFilter를 통과한다.
RequestCacheAwareFilter
- 인증 후, 원래 Request 정보로 재구성하는 필터
SecurityContextHolderAwareRequestFilter
AnonymousAuthenticationFilter
- 이 필터에 올 때까지 앞에서 사용자 정보가 인증되지 않았다면, 이 요청은 익명의 사용자가 보낸 것으로 판단하고 처리한다. (Authentication 객체를 새로 생성함(AnonymousAuthenticationToken))
SessionManagementFilter
- 세션 변조 공격 방지 (SessionId를 계속 다르게 변경해서 클라이언트에 내려준다)
- 유효하지 않은 세션으로 접근했을 때 URL 핸들링
- 하나의 세션 아이디로 접속하는 최대 세션 수(동시 접속) 설정
- 세션 생성 전략 설정
ExceptionTranslationFilter
- 앞선 필터 처리 과정에서 인증 예외(AuthenticationException) 또는 인가 예외(AccessDeniedException)가 발생한 경우, 해당 예외를 캐치하여 처리하는 필터 (모든 예외를 다 이 필터에서 처리하는 건 아님)
FilterSecurityInterceptor
- 인가(Authorization)를 결정하는 AccessDecisionManager에게 접근 권한이 있는지 확인하고 처리하는 필터
이렇게 Http Request 요청이 들어오면 많은 필터들을 거치게 된다. 이 필터들을 거치면서 앞 선 어떠한 필터에서 인증이 완료되면 해당 요청은 "인증된 요청"이 되는 것이고, 모든 필터들을 다 거쳤는데도 다 인증에 실패했다면 "인증되지 않은 요청"이 된다.
"인증되지 않은 요청"에 대해서는 접근 권한이 없으므로 그에 따른 처리를 한다.
Ex) 회원가입 페이지로 Redirect, Http Error Code : 403으로 반환 등의 처리를 할 수 있다.
UsernamePasswordAuthenticationFilter
username, password를 쓰는 form 기반 인증을 처리하는 필터라고 했다. 이 부분은 어떻게 동작하는지 좀 더 자세히 알아보자..
// 처음 로그인 정보 받는곳.. 로그인 성공 시만 JWT 생성
// Filter 는 Client 의 API 요청이 Controller 에 전달되기 전, 사전처리를 하는 영역
public class FormLoginFilter extends UsernamePasswordAuthenticationFilter {
final private ObjectMapper objectMapper;
public FormLoginFilter(final AuthenticationManager authenticationManager) {
super.setAuthenticationManager(authenticationManager);
objectMapper = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
UsernamePasswordAuthenticationToken authRequest;
try {
JsonNode requestBody = objectMapper.readTree(request.getInputStream());
String username = requestBody.get("username").asText();
String password = requestBody.get("password").asText();
authRequest = new UsernamePasswordAuthenticationToken(username, password); // 토큰 생성
} catch (Exception e) {
throw new RuntimeException("username, password 입력이 필요합니다. (JSON)");
}
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
요청으로부터 username과 password를 얻어오고 그 값으로 UsernamePasswordAuthenticationToken(Authentication)을 생성한다.
그 다음에 참조하고 있던 AuthenticationManager(구현체인 ProviderManager)에게 인증을 진행하도록 위임한다.
→ UsernamePasswordAuthenticationToken 은 Authentication 인터페이스의 구현체다.
< Authentication >
현재 접근하는 주체의 정보와 권한을 담는 인터페이스이다. Authentication 객체는 Security Context에 저장되며, SecurityContextHolder를 통해 SecurityContext에 접근하고, SecurityContext를 통해 Authentication에 접근할 수 있다.
< UsernamePasswordAuthenticationToken >
Authentication을 implements한 AbstractAuthenticationToken의 하위 클래스로, User의 ID가 Principal 역할을 하고, Password가 Credential의 역할을 한다.
< Authentication Manager >
인증에 대한 부분은 SpringSecurity의 AuthenticatonManager를 통해서 처리하게 되는데, 실질적으로는 AuthenticationManager에 등록된 AuthenticationProvider에 의해 처리된다.
< AuthenticationProvider >
AuthenticationProvider에서는 실제 인증에 대한 부분을 처리하는데, 인증 전의 Authentication객체를 받아서 인증이 완료된 객체를 반환하는 역할을 한다.
AuthenticationManager, ProviderManager, AuthenticationProvider
한번 더 정리...!
AuthenticationManager (Interface)
- authenticate(Authentication):Authentication → Authentication 객체를 받아 인증하고 인증되었다면 인증된 Authentication 객체를 돌려주는 메서드를 구현하도록 하는 인터페이스다.
- 이 메서드를 통해 인증되면 isAuthenticated(boolean)값을 TRUE로 바꿔준다.
- 인증에 대한 부분은 SpringSecurity의 AuthenticatonManager를 통해서 처리하게 되는데, 실질적으로는 AuthenticationManager에 등록된 AuthenticationProvider에 의해 처리된다.
ProviderManager (Class)
- AuthenticationManager의 구현체로 실제 인증에 대한 부분을 처리하는데, 인증 전의 Authentication객체를 받아서 인증이 완료된 객체를 반환하는 역할을 한다.
(스프링 시큐리티가 생성하고 등록하고 관리하는 스프링 빈이므로 직접 구현할 필요가 없음) - 직접 인증 과정을 진행하는게 아니라 멤버 변수로 가지고 있는 AuthenticationProvider들을에게 인증을 위임처리하고 그 중에 하나의 AuthenticationProvider(명확하게는 AuthenticationProvider를 구현한 클래스)객체가 인증 과정을 거쳐서 인증에 성공하면 요청에 대해서 ProviderManager가 인증이 되었다고 알려주는 방식이다.
- 인증이 되었다고 알려주는 건 AuthenticationManager 인터페이스의 메서드인 authenticate() 메서드의 리턴 값인 Authentication객체 안에 인증 값을 넣어주는 것으로 처리한다.
AuthenticationProvider (Interface)
- supports(Class<?>):boolean → 앞에서 필터에서 보내준 Authentication 객체를 이 AuthenticationProvider가 인증 가능한 클래스인지 확인하는 메서드다.
- UsernamePasswordAuthenticationToken이 ProviderManager에 도착한다면 ProviderManager는 자기가 갖고 있는 AuthenticationProvider 목록을 순회하면서 '너가 이거 해결해줄 수 있어?' 하고 물어보고(supports()) 해결 가능하다고 TRUE를 리턴해주는 AuthenticationProvider에게 authenticate() 메서드를 실행한다.
예시 코드..
// 로그인 정보와 DB의 유저 정보와 일치하는지 파악하는 클래스
@RequiredArgsConstructor
public class FormLoginAuthProvider implements AuthenticationProvider {
@Resource(name="userDetailsServiceImpl")
private UserDetailsService userDetailsService;
private final BCryptPasswordEncoder passwordEncoder;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication;
// FormLoginFilter 에서 생성된 토큰으로부터 아이디와 비밀번호를 조회함
String username = token.getName();
String password = (String) token.getCredentials();
// UserDetailsService 를 통해 DB에서 username 으로 사용자 조회
UserDetailsImpl userDetails = (UserDetailsImpl) userDetailsService.loadUserByUsername(username);
if (!passwordEncoder.matches(password, userDetails.getPassword())) {
throw new BadCredentialsException(userDetails.getUsername() + "Invalid password");
}
return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
로그인 정보 일치 여부를 파악하는 클래스이다.. supports함수를 보면 UsernamePassowordQuthenticationToken에 대해 처리를 할 수 있는 AuthenticationProvider이다. UsernamePassowordQuthenticationToken을 인증 완료하여 새 토큰을 생성해서 반환해준다..!!
핵심은 Authentication객체로부터 인증에 필요한 정보(username, password)를 받아오고, userDetailsService 인터페이스를 구현한 객체(CustomUserDetailsService)로 부터 DB에 저장된 유저 정보를 받아온 후, password를 비교하고 인증이 완료되면 인증이 완료된 Authentication 객체를 리턴해주는 것이다.
요약 하기
1. 클라이언트 요청(Http Request)이 오면, 요청은 위 그림에서 보이는 ApplicationFilter 객체들로 먼저 간 후 ApplicationFilter들을 거치다가 DelegatingFilterProxyRegistrationBean이라는 필터는 만나게 된다.
2. 스프링 시큐리티가 만든 DelegatingFilterProxy 클래스인 "springSecurityFilterChain"이라는 이름의 스프링 빈을 등록하고 이후에는 이 DelegatingFilterProxy(springSecurityFilterChain)가 필터로 동작하게 된다.
3. DelegatingFilterProxy → FilterChainProxy → FilterList들 순서대로 수행한다.
4. UsernamePasswordAuthenticationFilter에서 UsernamePasswordAuthenticationToken(Authentication) 객체를 AuthenticationManager에게 넘겼다.
5. AuthenticationManager인터페이스를 구현한 ProviderManager에게 넘어갔다.
6. ProviderManager는 여러 AuthenticationProvider들을 순회하면서 UsernamePasswordAuthenticationToken을 처리해줄 AuthenticationProvider를 찾는다.
7. 처리 가능한 AuthenticationProvider가 있다면 인증 후 반환..!
Reference : https://jeong-pro.tistory.com/205, https://diddl.tistory.com/89