본문 바로가기

Spring

스프링 시큐리티 코어

 

 

스프링 시큐리티 코어 동작 방식 정리

 

AuthenticationManager: AuthenticationProvider를 들고있는 주머니 역할

  • Builder 패턴으로 구현
  • 등록된 Authentication Provider들에 접근하는 유일한 객체
  • 단순 인터페이스에 불과, 내장 구현체: Provider Manager
  • AuthenticationManager를 구현해서 쓰지말자(Pivotal사 스프링 개발자보다 더 잘 만들 자신이 없다면..)
  • 직접 구현해서 만들어진 인터페이스가 아니다.

 

ProviderManager.class(Spring Security's DeCompiled ProviderManager Class)

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        Class<? extends Authentication> toTest = authentication.getClass();
        AuthenticationException lastException = null;
        AuthenticationException parentException = null;
        Authentication result = null;
        Authentication parentResult = null;
        boolean debug = logger.isDebugEnabled();

        // (1)
        Iterator var8 = this.getProviders().iterator();

        while(var8.hasNext()) {
            AuthenticationProvider provider = (AuthenticationProvider)var8.next();
            // (2)
            if (provider.supports(toTest)) {
                if (debug) {
                    logger.debug("Authentication attempt using " 
                    + provider.getClass().getName());
                }

                try {
                    // (3)
                    result = provider.authenticate(authentication);
                    if (result != null) {
                        this.copyDetails(authentication, result);
                        break;
                    }
                } catch (AccountStatusException var13) {
                    this.prepareException(var13, authentication);
                    throw var13;
                } catch (InternalAuthenticationServiceException var14) {
                    this.prepareException(var14, authentication);
                    throw var14;
                } catch (AuthenticationException var15) {
                    lastException = var15;
                }
            }
        }

        if (result == null && this.parent != null) {
            try {
                result = parentResult = this.parent.authenticate(authentication);
            } catch (ProviderNotFoundException var11) {
            } catch (AuthenticationException var12) {
                parentException = var12;
                lastException = var12;
            }
        }

        if (result != null) {
            if (this.eraseCredentialsAfterAuthentication 
            && result instanceof CredentialsContainer) {
                ((CredentialsContainer)result).eraseCredentials();
            }

            if (parentResult == null) {
                this.eventPublisher.publishAuthenticationSuccess(result);
            }

            return result;
        } else {
            if (lastException == null) {
                lastException = 
                new ProviderNotFoundException(
                this.messages.getMessage("ProviderManager.providerNotFound", 
                new Object[]{toTest.getName()}, 
                "No AuthenticationProvider found for {0}")
                );
            }

            if (parentException == null) {
                this.prepareException((AuthenticationException)lastException, authentication);
            }

            throw lastException;
        }
    }

 

  1. authentication 객체를 입력받으면, 1번의 provider를 이터레이션으로 조회한다.(this.getProviders()는 List<AuthenticationProvider>를 리턴받는다.)
  2. 현재 들어온 인증객체가 어떤 provider에서 구현되어있는지 조회를하고 적합한 인증 객체를 찾을 때 까지 루프를 돈다.
  3. 적합한 provider를 찾으면  result에 인증된 객체를 받고 리턴한다.

 

AuthenticationProvider: 진짜 인증이 일어나는 인터페이스

  • 인증전 객체를 받아 인증가능 여부를 체크한 후, 예외를 던지거나 인증 후 객체로 돌려준다.
  • 구현하라고 만들어진 인터페이스이다.
  • 필요에 맞게 정교하게 구현하고 인증 관리자에 등록시키자.

 

UsernamePasswordAuthenticationToken (Spring Security 4.0.4.RELEASE API)

UsernamePasswordAuthenticationToken 생성자

UsernamePasswordAuthenticationToken(Object principal, Object credentials)

This constructor can be safely used by any code that wishes to create a UsernamePasswordAuthenticationToken, as the AbstractAuthenticationToken.isAuthenticated() will return false.

UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities)

This constructor should only be used by AuthenticationManager or AuthenticationProvider implementations that are satisfied with producing a trusted (i.e.

 

두개의 생성자의 차이는 파라미터를 입력받는 갯수가 다르기 때문에 반드시 주의해야한다.

첫번째 생성자 같은 경우, 인증의 주체가되는 principal 객체와 credentails 객체를 기본 파라미터로 받는다.

반면에. 두번째 생성자인 경우 GrantedAuthority를 상속받는 컬렉션 형태의 authorities 를 받는다.

첫번째 생성자는 인증전에 호출되는 생성자이며, 두번째 생성자는 인증 후 호출되는 생성자이기 때문에 개발할 때 반드시 주의하자.

 

여기서 실수를 하지 않기 위해 인증전 객체를 사용할 클래스에서는 두개의 파라미터를 받는 생성자를 super로 호출하고, 인증 후 객체를 호출할 때 클래스는 인증 후 클래스로 super로 호출하는 방식이 좀 더 명확하고 실수를 줄 일 수 있다.

 

 

인증전 클래스(PreAuthorizationToken)

public class PreAuthorizationToken extends UsernamePasswordAuthenticationToken {

    public PreAuthorizationToken(String username, String password) {
        super(username, password);
    }
}

 

인증후 클래스(PostAuthorizationToken)

    private PostAuthorizationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
        super(principal, credentials, authorities);
    }

 

 

유저 객체 설계

  • 유저 인증을 위해 필요한 정보, 서비스 제공을 위한 정보를 필요한 만큼 저장한다.
  • 비밀번호를 비롯한 민감한 정보는 암호화 하는 것이 원칙(BcryptPasswordEncoder)
  • 소셜 회원도 저장 할 수 있게 확장성 있는 구현
  • @ElementCollection, Enum 등을 활용하자.

 

Account Entity @Enumerated 주의사항

 

Account

@Enumerated(value = EnumType.ORDINAL) // (1)
// (2) @Enumerated(value = EnumType.STRING)
private UserRole userRole;

 

UserRole

@Getter
public enum UserRole {
    ADMIN("ROLE_ADMIN"), USER("ROLE_USER");

    private String roleName;

    UserRole(String roleName) {
        this.roleName = roleName;
    }
}

 

  1. @Enumerated 어노테이션의 value 값을 EnumType이 ORDINAL이면, Integer 타입 형태로 인덱스 값이 DB에 저장 된다.
  2. 만약 UserRole이라는 Enum의 선언 순서가 뒤바뀌면, USER가 1번 인덱스인데 ADMIN보다 앞에 선언되었을 시, 0번 인덱스가 되어 서비스에서 관리자 권한을 얻을 수 있기 때문에 심각한 문제가 발생할 수 있다.
  3. ORIDINAL를 사용말고, 명시적인 EnumType을 String으로 관리하자.