[Spring Security, JWT, Redis] Spring Security의 인증과 인가

3Beom's 개발 블로그·2023년 8월 6일
0

프로젝트에서 Spring Security + JWT + Redis 방식으로 인증/인가 로직을 구현하였고, 전체 과정을 기록으로 남겨두려 한다.


1. 인증(Authentication)과 인가(Authorization)

  • 인증(Authentication)
    • 사용자가 본인이 맞는지 확인하는 절차
  • 인가(Authorization)
    • 인증된 사용자가 요청한 자원에 접근 가능한지를 결정하는 절차

⇒ 즉, 먼저 사용자가 맞는지 인증한 후, 인증된 사용자에 한해 인가 절차를 수행한다.

Ex) 로그인을 통해 인증되면 url 별로 설정된 접근 권한에 따라 인가 여부가 결정된다.

2. Spring Security

  • Spring 기반의 애플리케이션의 보안(인증/인가)을 담당하는 Spring 하위 프레임워크이다.
  • Filter 계층에서 인증/인가 로직을 처리한다.
  • 세션-쿠키 방식으로 인증한다.
  • 보안과 관련된 체계적인 옵션을 제공해주기 때문에 개발자 입장에서 보완 관련 로직을 작성하지 않아도 된다는 장점이 있다.

3. Spring Security의 인증/인가

  • Spring Security는 기본적으로 인증 → 인가 순서로 진행되며, 인가 과정에서 해당 리소스에 대한 접근 권한이 있는지 확인한다.
  • Spring Security의 인증 과정은 여러 Filter를 거쳐 수행될 수 있으며, Credential 기반의 인증 방식을 활용한다.
    • 기본적으로 UsernamePasswordAuthenticationFilter가 활용되며, 해당 Filter의 인증 방식이 Credential 방식이다.
    • Credential 기반 인증
      • Principal(이름)과 Credential(비밀번호)을 통해 인증 과정을 수행한다.
      • Principal : 접근 주체(보호받는 리소스에 접근하는 대상). Ex) 사용자의 아이디.
      • Credential : 비밀번호(접근 주체의 비밀번호).
  • Spring SEcurity의 인가 과정은 FilterSecurityInterceptor 라는 Filter에서 담당하며, SecurityMetadataSource와 AccessDecisionManager에 의해 수행된다.
    • SecurityMetadataSource, AccessDecisionManager
      • SecurityMetadataSource
        • 접근 주체가 요청한 URL에 특정 권한이 필요한지에 대한 권한 정보를 반환한다.
        • 권한 정보가 없을 경우 인가되고, 있을 경우 AccessDecisionManager로 넘어간다.
        • Spring Security 설정파일 SecurityConfig에서 특정 URL에 특정 권한을 가진 사용자만이 허용할 수 있도록 hasRole() 과 같은 메서드로 설정할 수 있는데, 해당 정보가 SecurityMetadataSource에 의해 반환된다.
      • AccessDecisionManager
        • AccessDecisionVoter를 활용하여 최종 승인 여부를 결정한다.

4. Spring Security의 인증에 활용되는 모듈

  • Spring Security의 인증 과정에는 위 사진의 모듈들이 활용된다.

4-1. SecurityContext

  • Authentication 객체를 보관하며 꺼내올 수도 있다.

4-2. SecurityContextHolder

  • 보안 관련 세부 정보가 포함되어 있다.
  • MODE_THREADLOCAL, MODE_INHERITABLETHREADLOCAL, MODE_GLOBAL 방식을 제공한다.
    • MODE_??
      • MODE_THREADLOCAL
        • ThreadLocalSecurityContextHolderStrategy 클래스를 구현체로 활용한다.
        • 같은 Thread 내에서만 자원을 공유한다.
      • MODE_INHERITABLETHREADLOCAL
        • InheritableThreadLocalSecurityContextHolderStrategy 클래스를 구현체로 활용한다.
        • 새로운 Thread가 생성될 경우, 부모 Thread의 SecurityContext가 공유된다.
      • MODE_GLOBAL
        • GlobalSecurityContextHolderStrategy 클래스를 구현체로 활용한다.
        • 응용 프로그램 내에서 단 하나의 SecurityContext를 저장하며, 해당 JVM 내 모든 인스턴스들과 자원을 공유할 수 있다.
  • SecurityContextHolder를 통해 SecurityContext에 접근하여 Authentication 객체를 가져올 수 있다.
    • Authentication 객체 가져오는 코드 예시
      		try {
                  Authentication authentication = Objects.requireNonNull(SecurityContextHolder
                      .getContext()
                      .getAuthentication());
      
                  if (authentication instanceof AnonymousAuthenticationToken) {
                      authentication = null;
                  }
      
                  return authentication.getName();
              } catch (NullPointerException e) {
                  throw new RuntimeException();
              }

4-3. Authentication

  • 접근 주체의 정보와 권한을 담는 Interface이다.
    • 주로 활용되는 구현체로 UsernamePasswordAuthenticationToken 클래스가 있다.
    • UsernamePasswordAuthenticationToken
      • AbstractAuthenticationToken 추상 클래스를 상속받아 구현된다.
        • AbstractAuthenticationToken
          • 해당 클래스 내부에 boolean 타입의 authenticated 변수를 갖고 있으며, setAuthenticated() 메서드를 통해 인증되면 true, 아니면 false가 저장된다.
      • 사용자의 아이디가 Principal 역할을, 비밀번호가 Credential 역할을 수행한다.
  • SecurityContext에 저장된다.

4-4. AuthenticationProvider

  • 실제 인증을 처리하는 authenticate() 메서드가 있는 Interface이다.
    • authenticate()
      Authentication authenticate(Authentication var1) throws AuthenticationException;
      • 인증 전의 객체를 파라미터로 받아 인증 로직을 수행한다.
        • 인증 성공 : authenticated가 true로 설정된 Authentication 객체를 반환한다.
        • 인증 실패 : AuthenticationException을 던진다.

4-5. AuthenticationManager

  • 인증을 처리하며, AuthenticationManager에 등록된 AuthenticationProvider에 의해 처리된다.
    • AuthenticationProvider 활용 과정
      • AuthenticationManager를 implements한 ProviderManager는 AuthenticationProvider를 List로 갖고 있다.
        public class ProviderManager implements AuthenticationManager, MessageSourceAware,
        InitializingBean {
            public List<AuthenticationProvider> getProviders() {
        		return providers;
        	}
        
        ...
      • ProviderManager는 for문을 통해 모든 AuthenticationProvider 객체의 authenticate 메서드를 호출한다.
        public Authentication authenticate(Authentication authentication)
        			throws AuthenticationException {
        
        ...
        
        		for (AuthenticationProvider provider : getProviders()) {
                    ....
        			try {
        				result = provider.authenticate(authentication);
        
        				if (result != null) {
        					copyDetails(authentication, result);
        					break;
        				}
        			}
        			catch (AccountStatusException e) {
        				prepareException(e, authentication);
        				// SEC-546: Avoid polling additional providers if auth failure is due to
        				// invalid account status
        				throw e;
        			}
                    ....
        		}
                ...
        }
    • AuthenticationProvider의 구현체를 직접 구현하여 AuthenticationManager에 등록할 수도 있다.
  • 개발 과정에서 활용하는 인증 메서드는 AuthenticationManager의 authenticate() 이다.
    • 활용 예
      		// 1. id, pw 기반 Authentication 객체 생성, 해당 객체는 인증 여부를 확인하는 authenticated 값이 false.
              UsernamePasswordAuthenticationToken authenticationToken =
                  new UsernamePasswordAuthenticationToken(id, password);
      
              // 2. 검증 진행 - 내부적으로 CustomUserDetailsService.loadUserByUsername 메서드가 실행됨
              Authentication authentication = authenticationManagerBuilder.getObject()
                  .authenticate(authenticationToken);
  • 인증에 성공할 경우, 인증된 Authentication 객체를 SecurityContext에 저장하고 인증 상태를 유지하기 위해 Session에 보관한다.
  • 인증에 실패할 경우, AuthenticationException을 던진다.

4-6. UserDetails

  • username, password, 권한 정보 등 접근 주체에 대한 정보를 반환하는 메서드로 이루어진 Interface이다.
  • authenticate() 메서드를 통해 인증 로직이 수행될 때, 성공한 Authentication 객체를 생성하는 과정 중 UserDetails를 구현한 클래스의 객체가 필요하다.
  • UserDetailsService의 loadUserByUsername() 메서드를 통해 반환된다.

4-7. UserDetailsService

  • UserDetails 객체를 반환하는 메서드 loadUserByUsername() 만을 갖고 있는 Interface이다.
    public interface UserDetailsService {
        UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
    }
  • 인증 과정(authenticate()) 중 해당 메서드가 호출되며, 접근 주체의 정보에 해당하는 사용자 정보를 담은 UserDetails 객체를 반환하는 역할을 수행한다.
    • 보통 UserRepository를 주입받고 파라미터로 전달된 username(아이디)을 기준으로 db를 조회하여 사용자 정보를 가져온 후, UserDetails 객체를 생성하여 반환한다.
    • 만약 db에서 조회되지 않을 경우 UsernameNotFoundException을 던지도록 설정한다.

4-8. GrantedAuthority

  • 사용자가 갖고 있는 권한 정보를 받아올 수 있는 메서드 getAuthority() 를 갖고 있는 Interface이다.
    public interface GrantedAuthority extends Serializable {
    	String getAuthority();
    }
  • 권한은 보통 다음과 같이 ROLE_? 형태로 활용된다.
    • ROLE_USER
    • ROLE_ADMIN
  • UserDetails Interface 에서 getAuthorities() 메서드를 갖고 있으며, 해당 사용자가 갖고 있는 권한들을 GrantedAuthority 객체의 List로 반환해야 한다.
    public interface UserDetails extends Serializable {
    
    	Collection<? extends GrantedAuthority> getAuthorities();
    
    ...

5. Spring Security 인증 과정

  1. Http Request가 전달된다.
  2. AuthenticationFilter에서 인증 과정이 이루어진다. 해당 Filter에서 인증되지 않은(authenticated가 false) Authentication 객체 UsernamePasswordAuthenticationToken 객체가 생성된다.
    • UsernamePasswordAuthenticationToken 객체에 Http Request를 통해 전달된 접근 주체의 정보가 담긴다.
  3. 생성한 UsernamePasswordAuthenticationToken 객체를 파라미터로 전달하여 AuthenticationManager를 통해 인증 과정(authenticate())이 수행된다.
  4. AuthenticationManager에 등록된 AuthenticationProvider를 통해 인증 과정이 수행된다.(authenticate())
  5. authenticate() 과정 중 UserDetailsService의 loadUserByUsername() 메서드가 호출된다.
  6. loadUserByUsername() 메서드를 통해 UserDetails 객체가 생성된다.
    • UserDetails 객체는 Http Request를 통해 전달된 접근 주체의 정보에 해당하는 실제 사용자 정보(db를 통해 조회된)를 담고 있다.
    • 만약 실제 사용자 정보가 없을 경우 UsernameNotFoundException 이 발생한다.
  7. 생성된 UserDetails 객체가 AuthenticationProvider에게 반환된다. 해당 UserDetails 객체를 통해 인증된(authenticated가 true) Authentication 객체가 생성된다.
  8. 생성된 Authentication 객체가 AuthenticationManager에게 반환된다. AuthenticationManager는 해당 객체를 반환하여 인증을 성공한다.
  9. 반환된 Authentication 객체가 AuthenticationFilter에게 전달된다.
  10. AuthenticationFilter는 전달받은 Authentication 객체를 SecurityContext 및 Session에 보관한다.

6. Spring Security 인가 과정

  1. Spring Security는 인증 → 인가 순서로 진행되므로 가장 먼저 전달받은 요청이 인증되었는지 여부를 확인한다.
    • 인증 Filter들을 거쳐 인증되었을 경우, SecurityContext에 Authentication 객체가 있어야 한다.
    • 따라서 SecurityContext에 Authentication 객체가 존재하는지 여부를 통해 인증 여부를 확인한다.
    • 만약 인증되지 않았을 경우 AuthenticationException을 발생시킨다.
  2. 인증된 요청인 경우, SecurityMetadataSource를 통해 권한 정보를 확인한다.
    • 이 때 권한 정보는 요청이 가진 권한 정보(Authentication 객체의 권한 정보)가 아닌, 요청 URL에 부여된 권한 정보이다.
    • 즉, 특정 권한이 필요한 URL인지 확인하는 것이다.
    • SecurityConfig에서 hasRole(), hasAuthority() 등으로 설정하는 권한 정보가 해당 과정에서 반환된다.
    • 권한 정보가 없을 경우, 해당 URL에 특정 권한이 필요하지 않다는 의미이므로 해당 요청은 인가된다.
  3. 권한 정보가 존재할 경우, AccessDecisionManager로 전달된다.
    • AccessDecisionManager에서 최종 승인을 결정한다.
    • AccessDecisionManager에는 여러 개의 AccessDecisionVoter가 등록될 수 있으며, 각 Voter들의 투표를 통해 인가 여부를 결정한다.
      • 최근 SpEL이 많이 사용됨에 따라 WebExpressionVoter 등을 통해 SecurityConfig에서 설정한 권한을 토대로 투표가 수행된다.
      • SpEL
        • Spring Expression Language
        • 컴파일 언어인 Java를 스크립트 언어처럼 쓸 수 있도록 해주는 기능이다.
        • Spring Security에서 활용되는 예로는 다음과 같은 것들이 있다.
          • hasRole(), hasAnyRole(), hasAuthority(), permitAll(), denyAll(), …
    • 최종 승인될 경우 최종적으로 인가되며, 승인되지 않을 경우 AccessDeniedException을 발생시킨다.
  4. 인가 과정에서 예외가 발생할 경우 ExceptionTranslationFilter로 전달된다.
    • AuthenticationException의 경우 AuthenticationEntryPoint로 전달된다.
    • AccessDeniedException의 경우 AccessDeniedHandler로 전달된다.

참고 자료

profile
경험과 기록으로 성장하기

0개의 댓글