프로젝트에서
Spring Security
를 사용하여JWT 토큰 기반
인증/인가 검증을 하려고 한다.
JWT
토큰을 기반Spring Security Filter
를 커스텀하기전 공부한 개념을 정리하려고 한다.참고로 섬세한 구분이 필요한
api(로그인,비로그인 구분)
에 대해서는 Spring security를 사용하지 않고 뒤에 설명할커스텀 어노테이션
과Spring Intercepter
를 이용하여 인증,권한을 처리했다.
😂여러가지를 공부하면서 프로젝트에 적용해보고 구현중이라 어떤것이 더 현명한 방법인지는 잘모르겠다.ㅠ
스프링 시큐리티
의 구조와 동작원리를 알아보기전에 어플리케이션을 구성하는
두 가지 영역에는 인증(Authentication)과 인가(Authorization)이 있다.
인증은 해당 사용자가 본인임이 맞는지 확인하는 절차이다.
인가는 인증을 마친 사용자가 요청한 자원을 사용할 수 있는 권한을 가지고 있는지를 확인하는 과정이다.
스프링 시큐리티
는 인증 절차를 진행한 후, 인가 과정을 통해 요청 리소스에 접근 권한이 있는지를 확인해주는 역할을 수행하게 된다.
스프링 시큐리티
란 Spring 기반의 어플리케이션의 보안(인증,인가)을 담당하는 스프링 하위 프레임워크이다.
스프링 시큐리티
는 인증과 인가처리를 여러개의 Filter 흐름에 따라 처리한다.
스프링 시큐리티 필터
의 위치를 보게 되면,
Dispatcher Servlet
에 도달하기 전에 서블릿 Filter
를 거쳐가게 되는데 이 서블릿 Filter 이전에 Spring Security Filter
들이 위치하게 된다.
스프링 시큐리티
는 이러한 인증,인가 검증을 위해서 Pricipal를 아이디로, Credential를 패스워드로
사용하는 Credental 기반의 인증 방식을 사용한다.
Pricipal
: 보호 받는 리소스를 접근하는 대상Credential
: 보호 받는 리소스에 접근하는 비밀번호스프링 시큐리티
의 처리 과정의 예시를 통해 구조를 설명해보겠다.
즉, 인증(10번까지)처리가 완료되었다면 SecurityContextHolder는 세션 영역에 있는 SecurityContext에 Authentication 객체를 저장하게 된는 것이다.
(인가 처리는 11번과 같이 진행됨. 간단하게 설명하자면)
이를 통해 스프링 시큐리티
는 쿠키-세션 기반의 인증방식을 사용한다는 것을 알 수 있다.
리소스에 접근한 주체의 정보와 권한을 담는 인터페이스이다. Authentication은 객체이고 SecurityContext에 저장되고, SecurityContext를 통해 Authentication 객체에 접근할 수 있다.
public interface Authentication extends Principal, Serializable {
// 주체의 권한 목록을 가져온다.
Collection<? extends GrantedAuthority> getAuthorities();
//Credenticals를 가져온다. 주로 비밀번호가 사용되어진다.
Object getCredentials();
Object getDetails();
//Pricipal 객체를 가져온다.
Object getPrincipal();
//인증 여부를 확인 한다.
boolean isAuthenticated();
//인증 여부를 세팅한다.
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException; }
Authentication을 구현한 하위 클래스인 구현체이다. 사용자의 ID가 Pricipal의 역할을 하게되고, Password가 Credential의 역할을 하게된다.
두가지 생성자가 존재하는데 첫번째는 인증 전의 객체를 생성하고, 두번째는 인증 완료 객체를 위해 사용되어진다.
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
//주체에 ID 해당된다. 아이디나 엔티티 등을 넣으면 된다.
private final Object principal;
//주체에 비밀번호에 해당된다.
private Object credentials;
//인증 전의 객체 생성자
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false);
}
//인증 완료 객체 생성자(권한등이 추가로 요구된다.)
public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true); // must use super, as we override
}
@Override
public Object getCredentials() {
return this.credentials;
}
...
스프링 시큐리티 필터들이 수행하는 방식을 정의한 API라고 한다. 시큐리티 필터내에서 넘어온 인증정보를 가지고 Autentication
타입의 객체를 만드는 역할을 한다.
가장 많이 사용되는 AuthenticationManager의 구현체이다. 인증을 처리하는 부분이고 실제로는 ProviderManager 내부에도 AuthenticationProvider
리스트가 존재하고 인증처리를 위임하게 된다. 이 리스트들은 ProviderManager 생성시 주입된다.
각 AuthenticationProvider
들은 다른 인증 방식이 수행된다. 예를 들어 하나의 Provider가 이름, 비밀번호를 검증할수 있으면 다른 Provider는 다른 인증방식을 수행하게된다. 다운스트림 방식으로 요청한 인증 방식을 처리할수 있는 Provider를 찾게 되는 것이다.
UserDetails 객체를 반환하는 역할을 하는 인터페이스이다.
하나의 메서드를 가지고 있고 보통 이를 구현한 구현체에서 Repository를 사용하여 DB에 있는 사용자의 정보를 가지고 UserDetails를 생성하여 반환해준다.(보통 커스텀 하여 구현체를 만들어서 사용)
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
만약 사용자를 찾지 못하게 되면 UsernameNotFoundException
예외를 발생시키면 된다.
인증에 성공해서 Authentication 객체를 생성하기 위해 사용되어진다.
직접 커스텀 하여 원하는 객체를 넣어 처리할 수 있다.
참고로 인증/인가를 마친 후 Authentication객체를 SecurityContext에 저장하게 되는데, Controller에서 인증을 마친 사용자의 정보를 사용하기 위해서 UserDetails를 implements 하여 사용자 엔티티 객체를 넣어 커스텀 하였다.
자세한 코드는 뒤에서 살펴보겠다.
인증을 마친 Authentication을 보관하는 역할을 하며 저장하거나 꺼내올수 있따.
현재 주체(Pricipal)이 가지고 있는 권한을 의미한다.
주로 인증이 완료되어 Authentication를 생성할 때 인자로 넘기며 권한 체크에서 해당 객체의 권한을 판단할때 사용되어진다.
스프링 시큐리티
의 필터 구성은 아래와 같다.
스프링 시큐리티
를 통해 이번 프로젝트에서 크게 로그인 인증
, JWT 토큰 인증
, 권한검사
를 하기 위해 커스텀할 필터를 알아보겠다.
사용자가 로그인 인증
하기 위해서 Id,Password를 요청하게 되고, 인증이 완료되면 Authentication 객체가 생성되게 되고 실패한다면 생성되지 않는다. 그림을 보게 되면 위에서 설명했던 순서대로 모듈을 호출하는 것을 볼 수 있다.
인증이 완료되면 정상적으로 Authentication이 생성되고 AuthenticationSuccessHadler를 호출하게 된다.
프로젝트에서 JWT토큰을 사용할 예정이기 때문에 이 핸들러에서 토큰을 생성하여 응답할 예정이다.
인증이 실패할 경우에는 AuthenticationFailureHandler를 호출하게 된다.
참고로 UsernamePasswordAuthenticationFilter에 로그인 인증 성공 여부에 따라 호출되는 메서드가 존재한다.
이 메서드를 사용해도 되지만 나는 위에서 말한 두개의 핸들러를 호출하도록 할 것이다.
헤더에 토큰으로 "basic"으로 된 토큰을 사용하는 HTTPBasic 기반의 유저 인증을 담당하는 필터이다.
프로젝트에서는 JWT 토큰 인증
방식을 사용할 것이기 때문에, 이 부분을 커스텀 하여 인증이 필요한 리소스 접근시 JWT 토큰 검증 절차 로직을 구현할 것이다.
인증을 마치게 되면 SecurityContext에 Authentication객체를 담게 되는데, 만약 인증이 실패하여 Authentication 객체를 SecurityContext에 담지 못하게 되면 AuthenticationEntryPoint 핸들러를 호출하게 된다.
BasicAuthenticationFilter를 통해 인증이 완료되게 되면 SecurityContext에 Authentication 객체가 담아지게 되고 요청한 리소스의 권한을 가지고 있는지 검증하게 된다.
만약 올바른 권한을 가지고 있지 않는다면, AccessDeniedHandler 핸들러를 호출하게 된다.