Spring/Spring Security

[Spring Security] 인가 구조(Authorization Architecture)

공대키메라 2022. 8. 6. 12:36

이번시간에는 Spring Security의 인가 구조(Authentication Architecture)에 대해 알아볼 것이다. 

 

Spring Security 를 공부하기 전에, 인증과 인가라는 개념에 대해 알아야 하는데,

 

인증은 어떤 특정 사용자의 신원을 확인하는 것이고, 인가는 그 사용자에게 부여된 권한이 무엇인지 확인하는 것 이라고 보면 된다. 

 

참고한 사이트는 다음과 같다. 

 

출처:

https://docs.spring.io/spring-security/reference/servlet/authorization/architecture.html

https://docs.spring.io/spring-security/reference/servlet/authentication/architecture.html#servlet-authentication-granted-authority

https://www.inflearn.com/course/%EC%BD%94%EC%96%B4-%EC%8A%A4%ED%94%84%EB%A7%81-%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0/unit/31606?tab=curriculum 

 

Authorization Architecture

 

1. 인가(Authorization)

인증(Authentication) section에서 전에 모든 Authentication 구현체들을 GrantedAuthority 객체의 하나의 리스트로 저장할 수 있는지 이야기했다.

 


 

띠용! 사실 필자는 이게 공부한다고 봤지만 기억이 안나서 다시 찾아보았다...

 

https://tech-monster.tistory.com/203

어머나 세상에... 정말이잖아...? 내가 이걸 공부했엇구나 ㅠㅠㅠ

 

객체의 Collection을 제공한다고 한것이 하나의 리스트로 저장할 수 있다는 것과 동의어인것 같다. 

 

이것을 다시 보면서 다시 읽어보도록 하자.


이것들은 principal(본인, 사용자)에게 허가받아온 권한들을 나타낸다.

 

GrantedAuthority객체의 리스트가 곧 허가받아온 권한들을 나타낸다고 한다. 

 

GrantedAuthority 객체들은 AuthenticationManger에 의해 Authentication 객체 안에 삽입되고, 나중에 권한 결정을 내릴 때 AuthorizationManager에 의해 읽힌다. 

 

GrantedAuthority는 오직 하나의 메소드만 가지고 있다.(띠용?)

 

String getAuthority();

 

이 메소드는 AuthorizationManager들이 GrantedAuthority의 정확한 String representation을 얻도록 해준다. String 으로 representation을 반환함으로서, GrantedAuthority는 대부분 AuthorizationManager나 AccessDecisionManager에 의해 쉽게 읽힐 수 있다. 

 

만약 GrantedAuthority가 정확하게 String으로 나타내지지 않는다면, GrantedAuthority는 복잡하다고 여겨질 것이고 getAuthority는 반드시 null을 return한다.

 

편의성을 위해서 getAuthority()는 GrantedAuthroity(이걸 해석해보면 허가받은 권한 혹은 허가받은 권한의 이름 정도? 로 보면 될 것 같다.)를 string의 형식으로 무조건 반환해서 읽도록 하는 것 같다. 

Spring Security는 하나의 GrantedAuthority 구현체인 SimpleGrantedAuthority를 제공한다. 이것은 어떤 특정한 String이GrantedAuthority로 변환되도록 한다. 시큐리티의 구조에 포함된모든 AuthenticationProvider들은 Authentication 객체를 공유하기 위해 SimpleGrantedAuthority를 사용한다. 

 

2. 선 호출 관리(Pre-Invocation Handling)

Spring Security는 method invocation이나 web request 같은 객체들을 보호하기 위해 접근을 제어하는 interceptor들을 제공한다. 

 

호출이 처리되도록 허가됫는지 안됬던지에 대한 선 호출(pre-invocation) 결정(decision)은 AccessDecisionManager에 의해 처리된다. 

 

3. The AuthorizationManager

AuthorizationManager는 AccessDecisionManager와 AccessDecisionVoter를 둘 다 대체한다.

 

AccesssDecisionManager 혹은 AccessDecisionVoter를 customize한 어플리케이션은 AuthoizationManager를 사용해서 변경하는것을 권장한다. 

 

AuthenticationManager들은 AuthorizationFilter에 의해 호출되고, 최종 접근 제어 결정을 내리는데 책임이 있다. 

 

AuthorizationManager interface는 두 개의 메소드를 포함한다. 

 

AuthorizationDecision check(Supplier<Authentication> authentication, Object secureObject);

default AuthorizationDecision verify(Supplier<Authentication> authentication, Object secureObject)
        throws AccessDeniedException {
    // ...
}

 

위에서 check 메소드는 인가 결정을 내리기 위해 필요한 모든 관련있는 정보를 넘긴다. 

 

특히, 안전한(secure) object를 넘기는 것은 이러한 인자들이 관찰된 실제 secure 객체 호출을 포함하는것을 가능하게 한다. 

 

예를 들어, secure object가 MethodInvocation이라고 가정하자.

 

모든 Customer 인자에게 MethodInvocation을 요청하는 것은 간단할 것이며 , principal(사용자)가 그 고객으로 역할을 하도록 허락을 받는것을 확실히 하려고 AuthorizationManager에서 security logic의 몇개를 구현할 수 있다. 

 

verify 메소드는 check을 호출하고, 연속적으로 부정적인 AuthorizationDecision의 경우에 AccessDeninedException을 던진다. 

 

4. Delegate-based AuthorizationManager Implementations

사용자들은 모든 권한들을 관리하는 자신만의 AuthorizationManager를 구현할 수 있다. Spring Security는 개인의 AuthorizationManager랑 잘 작동할 수 있는 delegating하는 AuthroizationManager를 제공한다. 

 

RequestMatcherDelegatingAuthorizationManager 는 가장 적절한 delegate AuthroizationManager와 요청을 맞출것이다.

method 보안을 위해, AuthorizationManagerBeforeMethodInterceptor와  AuthorizationManagerAfterMethodInterceptor를 사용할 수 있다.

 

아니 이름도 참 정직하니 직관적이다. 

AuthorizationManager에서 인가(authroization)를 관리(manage)하고, 여기서 말하는 것은 AuthorizationManager의 구현체들을 호출하기 전에 Interceptor를 제공한다는 것이다.

interceptor와 filter의 차이를 다시 떠올리지 못하겠으면 찾아보고오자(나도 헐레벌떡...)

Method를 사용할 때 인가(authroization)를 관리(manage)하기 전에(Before) 보안 일을 처리할 수 있는 Interceptor인 AuthorizationManagerBeforeMethodInterceptor랑

Method를 사용할 때 인가(authroization)를 관리(manage)한 후에 (After) 보안 일을 처리할 수 있는 Interceptor인 
AuthorizationManagerBeforeMethodInterceptor라니,

이름이 길어도 무조건 직관적으로 하는것이 이게 한눈에 보기에 너무 이해가 잘되니 참 좋다. 



여담이지만 아직도 회사에서 정말 형편없게 이름을 짓는 개발자분들이 많다. 

진짜 다가가서 꿀밤 한대 매기고 싶다...

 

https://docs.spring.io/spring-security/reference/_images/servlet/authorization/authorizationhierarchy.png

 

이러한 접근을 가지고, AuthroizationManager 구현체들의 구성은 인가 결정에서 고를 수 있다.(poll - 선택될 수 있다고 봐도 무방 : 여기서 여러개의 선택지가 있어서 원하는 것을 선택한다는 것에서 poll이라는 단어를 선택한 것 같다.)

 

 

5. AuthorityAuthroizationManager

Spring Security에서 제공되는 가장 흔한 AuthorizationManager는 AuthorityAuthorizationManager다.

(이름 자체가 인가 중 권한에 대한 처리를 담당하는 것 같다.)

 

현재 Authenticiation에서 찾고 있는 주어진 권한 세트를 설정한다. 

 

Authentication이 어떠한 설정된 권한을 포함한다면 positive AuthroizationDecision을 반환한다.

 

그렇지 않으면, negative를 반환한다. 

 

6. AuthentizatedAuthorizationManger

다른 매니저는 AuthenticationManager이다. 이것은 익명사용자, 완전이 인증받은 사용자, 그리고 remember-me로 인증된 사용자들을 구별하기 위해 사용된다. 

 

7. Custom Authorization Managers

명확하게, custom AuthorizationgManager를 구현할 수 있고, 원하는 접근 제어 로직을 넣을 수 있다. 

 

당신 application에서 이것은 구체적일것수 있고 혹은 몇몇 security 관리 로직을 구현할 수 있다. 

 

예를 들어, Open Policy Agent를 요청하거나 당신 자신의 인가 데이터베이스에 요청을 할 수있는 구현체를 생성할 수 있다.

 

8. Adapting AccessDecisionManager and AccessDecisionVoters

이전의 AuthroizationManager에서, Spring Security는 AccessDecisionManager와 AccessDecisionVoter를 공개했다.(publish)

 

몇몇 경우에, 더 오래된 application으로 migrating하는 것 처럼 AccessDecisionManager와 AccessDecisionVoter를 호출하는 AuthorizationManger를  도입하는건 바람직할 수 있다.

 

기존에 존재하는 AccessDecisionManager를 부르기 위해, 당신은 다음처럼 할 수 있다.

 

Example 1. Adapting an AccessDecisionManager

@Component
public class AccessDecisionManagerAuthorizationManagerAdapter implements AuthorizationManager {
    private final AccessDecisionManager accessDecisionManager;
    private final SecurityMetadataSource securityMetadataSource;

    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication, Object object) {
        try {
            Collection<ConfigAttributes> attributes = this.securityMetadataSource.getAttributes(object);
            this.accessDecisionManager.decide(authentication.get(), object, attributes);
            return new AuthorizationDecision(true);
        } catch (AccessDeniedException ex) {
            return new AuthorizationDecision(false);
        }
    }

    @Override
    public void verify(Supplier<Authentication> authentication, Object object) {
        Collection<ConfigAttributes> attributes = this.securityMetadataSource.getAttributes(object);
        this.accessDecisionManager.decide(authentication.get(), object, attributes);
    }
}

 

그리고 나서, Security FilterChain에 등록하라.

 

혹은, AccessDecisionVoter를 다음처럼 호출 할 수 있다.

 

@Component
public class AccessDecisionVoterAuthorizationManagerAdapter implements AuthorizationManager {
    private final AccessDecisionVoter accessDecisionVoter;
    private final SecurityMetadataSource securityMetadataSource;

    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication, Object object) {
        Collection<ConfigAttributes> attributes = this.securityMetadataSource.getAttributes(object);
        int decision = this.accessDecisionVoter.vote(authentication.get(), object, attributes);
        switch (decision) {
        case ACCESS_GRANTED:
            return new AuthorizationDecision(true);
        case ACCESS_DENIED:
            return new AuthorizationDecision(false);
        }
        return null;
    }
}

 

그리고 나서, Security FilterChain에 등록하라.

 

9. Hierarchical Roles

application에서 특정한 역할이 자동적으로 다른 역할을 포함하는것은 흔한 요구사항이다. 

 

예를 들어, admin과 user 역할의 개념을 가진 application에서, 당신은 admin이 일반 유저가 할 수 있는 모든것을 할 수 있으도록 하고 싶을 수 있다. 

 

이를 위해서, 모든 admin 사용자가 user 역할을 부여받도록 할 수 있다. 

 

대안으로, 또한 admin role들이 user role을 포함하도록 제한을 걸 수 있다. 

 

이것은 applicatiojn에서 역할이 많이 다양하다면 복잡할 수 있다. 

 

 

role-hierarchy는 역할들이 다른 역할을 포함할 수 있도록 한다. Spring Security의 RoleVoter를 상속한 RoleHierarchyVoter는 RoleHierarchy로 설정되는데, RoleHierarchy는 사용자가 할당받은 모든 다을 수 있는 권한들을 포함하고 있다.

 

@Bean
AccessDecisionVoter hierarchyVoter() {
    RoleHierarchy hierarchy = new RoleHierarchyImpl();
    hierarchy.setHierarchy("ROLE_ADMIN > ROLE_STAFF\n" +
            "ROLE_STAFF > ROLE_USER\n" +
            "ROLE_USER > ROLE_GUEST");
    return new RoleHierarchyVoter(hierarchy);
}

 

여기서 우리는 ROLE_ADMIN ⇒ ROLE_STAFF ⇒ ROLE_USER ⇒ ROLE_GUEST 계층으로 4개의 역할을 가지고 있다. 

(기존에는 DB에서 직접 부모 계층을 찾아서 설정해야 하고 그런것 같은데 이방식을 사용하면 훨씬 간편할 것 같다!)

 

여기서 또 설명이 조금 더 더해지면 좋을 것 같아서 필자가 자주 참고하는 정수원 강사님의 인프런 코어 스프링 시큐리티의 설명을 조금 덧붙인다.

출처 : 인프런 코어 스프링 시큐리티 Authorization, FilterSeucirtyInterceptor 중


위 그림처럼 웹 계층, 서비스 계층, 도메인 계층에서 권한 계층을 다룰 수 있다고 한다. 

 

10. The AccessDecisionManager

AccessDecisionManager는 AbstractSecurityInterceptor에 의해 호출되고, 최종 접근 제어 결정을 하는데 책임이 있다. 

 

AbstractSecurityInterceptor를 또 FilterSecurityInterceptor가 상속한다. 

 

AccessDecisionManager 인터페이스는 세개의 메소드를 포함한다. 

 

void decide(Authentication authentication, Object secureObject,
	Collection<ConfigAttribute> attrs) throws AccessDeniedException;

boolean supports(ConfigAttribute attribute);

boolean supports(Class clazz);

 

AccessDecisionManager의 decide method는 인가 결정을 하기 위해 필요한 모든 관련된 정보를 전달한다. 특히, secure object를 전달하는 것은 실제 secure object 호출에 포함된 그러한 인자들이 점검될 수 있도록 한다.

 

접근이 제한되면 AccessDeniedExcetion을 던진다.

 

support(ConfigAttribute attribute) 메소드는 AccessDecisionManager가 전달받은 ConfigAttribute을 처리할 수 있는지를 시작 시점에 결정하기 위해 AbstractSecurityInterceptor에 의해 호출된다.

 

supports(Class clazz) 메소드는 security interceptor 구현체에 의해 호출되는데, 설정된 AccessDecisionManager가 secure object의 종류를 지원하는지 알아본다.

 

11. Voting-Based AccessDecisionManager Implementations

 

사용자들이 자신만의 AccessDecisionManager로 권한을 제오할 수 있는 반면, Spring Security는 voting 에 기반을 둔 몇개의 AccessDecisionManger 구현체들을 포함한다. 

 

Voting Decision Manager는 관련있는 클래스들을 다음처럼 그려낸다.

 

https://docs.spring.io/spring-security/reference/_images/servlet/authorization/access-decision-voting.png

 

이 접근을 사용하는  일련의 AccessDecisionVoter의 구현체들은 인가 결정에서 투표받는다. AccessDecisionManager는 그리고 할당받은 투표에 따라서 AccessDeniedException을 던질지 말지 결정한다.(던질까말까 던질까말까~ 던던던던~)

 

AccessDecisionManager interface는 3개의 메소드를 가진다. 

 

int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attrs);

boolean supports(ConfigAttribute attribute);

boolean supports(Class clazz);

 


이게 큰 흐름에 대한 설명이 부족한 것 같아서 자주 보는 우리 인프런 코어 스프링 시큐리티 중 AccessDecisionManager, AccessDecisionVoter 중 내용을 참고했다.

출처 : 인프런 코어 스프링 시큐리티 중 AccessDecisionManager, AccessDecisionVoter


또한 AccessDecisionManager와  AccessDecisionVoter들은 인터페이스로 이것을들을 구현한 것을 사용한다. 

AccessDecisionManager의 구현체들

AccessDesicionManger의 경우 구현체가 보이는 거와 같이 

AffirmativeBased, ConsensusBased, UnanimousBased가 존재한다.

    - AffirmativeBased : 여러개의 Voter 클래스 중 하나라도 접근 허가로 결론내리면 접근 허가로 판단

   - ConsensusBased : 다수표(승인 및 거부)에 의해 최종 결정을 판단

   - UnanimousBased : 모든 voter가 만장일치로 접근을 승인해야 하며 그렇지 않은 경우 접근 거부


AccessDecisionVoter의 구현체들

AccessDecisionVoter는 판단을 심사하는 역할을 한다.

Voter가 권한 부여 과정에서 판단하는 자료로는 Authentication(인증 정보), FilterInvocation(요청 정보), ConfigAttributes(권한 정보)가 있다.

결정 방식으로는 ACCESS_GRANTED, ACCESS_DENIED, ACCESS_ABSTAIN이  있는데, 순서대로
접근허용(1), 접근 거부(-1), 접근 보류(0)로 나뉜다.

출처 : 인프런 코어 스프링 시큐리티 중 AccessDecisionManager, AccessDecisionVoter


이것은 다음을 고려했을 때 볼 수 있는 흐름이다.



 

12. RoleVoter

 Spring Security에서 제공되는 가장 흔히 사용되는 AccessDecisionVoter는 간단한 RoleVoter인데, 이것은 사용자가 그 역할을 받아왔다면 접근을 허가하는 간단한 역할이름과 투표들로 설정 속성을 처리한다.

 

어떤 ConfigAttribute이 접두사 "ROLE_"로 시작하면 투표할 것이다.만약 String representation을 반환하는 GrantedAuthority가 있다면 접근을 허용할 것이다. 

 

접두사 "ROLE_"로 시작하는 어떤 ConfigAttribute과도 매치하지 않으면, RoleVoter는 접근을 거부할 것이다. 

 

ConfigAttribute이 ROLE_로 시작하지 않는다면, voter는 기권할 것이다. 

 

13.  AuthenticatedVoter

우리가 함축적으로 봐온 또다른 voter는 AuthenticatedVoter인데, 이것은 익명, 완전히 인증받은, 그리고 remember-me로 인증된 사용자들 사이에서 구별하기 위해 사용된다. 

 

많은 사이트들은 remember-me authentication에서 특정 제한된 접근을 허용하지만, 전체적인 접근을 위해 로그인 함으로 그들의 인원을 확인하기를 요구한다.

 

14.  Custom Voters

명확하게, 당신은 custom AccessDecisionVoter를 구현할 수 있고 원하는 어떤 접근 제어 로직에 넣을 수 있다. 

 

이것은 당신의 application에 구체적일 수 있거나 몇몇 보안 관리 로직을 구현할 수 있다.

 

https://docs.spring.io/spring-security/reference/_images/servlet/authorization/after-invocation.png

 

Spring Security의 다른 많은 부분들처럼, AfterInvocationManager가 하나의 concrete 구현체인 AfterInvocationProviderManager를 가진다. AfterInvocationProviderManager는 AfterInvocationProvider들의 리스트를 투표한다. 

 

각각의 AfterInvocationProvider는 반환 객체를 수정하거나 AccessDeniedException을 던지는 것을 허가받는다.

 

실제 다수의 provider들이 object를 수정할 수 있고, 그 결과 이전의 provider가 그 리스트의 다음으로 넘겨진다.


이번 시간에 인가 구조에 대해서 알아보았다. 이맘 때 쯤 되니 기존에 알아보았던 Authentication 관련 설명들이 기억이 잘 안았다.

 

다시 봐야한는것은 맞겠지만, 인가에 대한 설명을 보면서도 인증 관련 설명이 중간 중간에 있으니 큰 흐름을 보는것이 좋을 것 같다는 생각이 들었다.