[Spring Security] Voter(WebExpressionVoter, PreInvocationAuthorizationAdviceVoter)

WOOK JONG KIM·2022년 12월 4일
0

패캠_java&Spring

목록 보기
84/103
post-thumbnail

FilterSecurityInterceptor는 Default로 WebExpressionVoter, Method에서는 Default로 RoleVoter, AuthenticatedVoter, PIAAVoter 사용

RoleVoter, AuthenticatedVoter는 올드하다

@Secured 어노테이션 기반 Voter

RoleVoter

Role 기반의 권한은 리눅스부터 아파치, 톰켓등 IT 초기부터 전통적으로 구현해서 사용하던 가장 직관적인 권한 체계
-> 하지만, Role 을 기반으로 권한을 판단하기엔, 상황이 너무 다양

-> Role 을 확장한 Authority 기반의 권한 체계를 사용하고 있다

-> 그렇지만, 기존의 Role 기반이 가지고 있는 직관적이고 계층적인 사용성을 그대로 사용할 수 있도록 해주기 위해 RoleVoter를 사용하기도 함

  • @Secured("ROLE_USER") -> ROLE 언더바 형태로 권한 지정
  • ROLE_xxx : GrantedAuthority
  • 권한 계층 선언 : RoleHierarchyVoter

AuthenticatedVoter

인증(통행증)을 받았다면 그 인증의 종류가 어떤 종류인지를 판단

이제 막 인증을 받고 들어온 사용자 VS RememberMe 토큰을 통해서 들어온 사용자 VS 익명 사용자를 구분하기 위해 사용

Remember me는 fully authenticated로 보진 않음
-> Fully Authenticated는 토큰 만으론 믿을 수 없다!, 재 로그인하라는 의미

  • authenticated
  1. fully authenticated
  2. remember me
  • anonymous

SPEL(Spring Expression Language)

표현식을 통해 객체의 값을 가져오거나 동작을 시켜주는 기능

사실상 컴파일언어라고 할 수 있는 자바를 스크립트언어처럼 동작하게 해주는 기능
ex) 웹에서 타이핑 해서 스프링 빈이나 비즈니스에 반영되도록 하기

SPEL 예시

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
class Person{
    private String name;
    private int height;

    public boolean over(int pivot){
        return height >= pivot;
    }
}

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
class Horse{
    private String name;
    private int height;

    public boolean over(int pivot){
        return height >= pivot;
    }
}


public class SpELTest {

    ExpressionParser parser = new SpelExpressionParser();

    Person person = Person.builder().name("홍길동").height(178).build();

    Horse nancy = Horse.builder().name("nancy")
            .height(160).build();

    @Test
    void test_1(){

        assertEquals("홍길동", parser.parseExpression("name")
                .getValue(person));
    }

    @Test
    void test_2(){

        //이러한 방식으로 빈에 있는 값을 스크립트로 수정이 가능함
        parser.parseExpression("name").setValue(person, "호나우두");
        assertEquals("호나우두", parser.parseExpression("name")
                .getValue(person, String.class));

    }

    @Test
    void test_3(){

        // 빈에 있는 객체의 메서드 호출 테스트
        assertTrue(parser.parseExpression("over(170)").getValue(person
        , Boolean.class));

        assertFalse(parser.parseExpression("over(170)").getValue(nancy
                , Boolean.class));
    }

    @Test
    void beanTest(){
        // Context 테스트

        StandardEvaluationContext ctx = new StandardEvaluationContext();
        ctx.setBeanResolver(new BeanResolver() {
            // BeanResolver : 빈 네임으로 빈을 찾아 주는 것
            @Override
            public Object resolve(EvaluationContext context, String beanName) throws AccessException {
                return beanName.equals("person") ? person : nancy;
            }
        });

        // 컨텍스트는 빈만 가져오는게 아니라 루트 객체 설정 가능
        ctx.setRootObject(person);

        // 밑에 코드와 동작 같음
        assertTrue(parser.parseExpression("over(170)").getValue(ctx
                , Boolean.class));

        // person이라는 빈에서 이 사람이 170이 넘는지 보는 것
        assertTrue(parser.parseExpression("@person.over(170)").getValue(ctx
                , Boolean.class));

        assertFalse(parser.parseExpression("@nancy.over(170)").getValue(ctx
                , Boolean.class));

        ctx.setVariable("horse", nancy); // 변수 접근 가능

        assertFalse(parser.parseExpression("#horse.over(170)").getValue(ctx
                , Boolean.class));

    }
}

프로퍼티, 메서드, 필드를 처리하고 타입변환을 수행하는 표현식을 평가할 때 EvaluationContext 인터페이스를 사용하는데 이 구현체 중 하나인 StandardEvaluationContext는 객체를 조작하려고 리플렉션을 사용하고 성능을 향상시키기 위해서 java.lang.reflect의 Method, Field, Constructor를 캐싱


SpEL을 사용하는 Voter

RoleVoter 는 SpEL을 사용하는 WebExpressionVoterPIAAVoter (PreInvocationAuthorizationAdviceVoter)
가 사용성을 대체하는 추세

스프링 Security 에서 SpEL을 사용해 권한 검증을 하나의 Voter로 통일하기 시작
-> 의미있게 남은 Voter 는 WebExpressionVoterPIAAVoter 밖에 남지 않았다고 볼 수 있다


SecurityExpressionRoot

WebExpressionVoter와 PreInvocationAuthorizationAdviceVoter가 SPEL 사용

WebExpressionVoter는 WebSecurityExpressionRoot를 Context의 루트로 보고 있음
-> Bean resolve는 Application Context가 될 것

PIAA voter는 MethodSecurityExpressionRoot를 컨텍스트의 루트로 봄

각각은 SecurityExpressionRoot라는 공통객체를 상속받아 구현 하고 있음

사용 가능 기능

  • permitAll, denyAll
  • hasRole, hasAnyRole, hasAuthority, hasAnyAuthority : method 에서 Role 로 판단을 할지 Authority로 판단을 할지 명확하게 구분
  • isAnonymous, isAuthenticated, isFullyAuthenticated : AuthenticatedVoter 의 기능을 대체
  • hasPermission
    target : 대상 객체를 보고 permission을 검사
    targetId, type : type을 보고 targetId인 대상 객체를 조회해와서 permission을 검사

PermissionEvaluator

기존의 AuthenticatedVoter나 Roler Voter는 Authentication Token을 검사하였음

그런데 권한 검사를 제대로 하려면 , 통행증만 아니라 통행증으로 가져가려고 하는 물건이 뭔지도 검사를 해야 함
-> hasPermission

이해를 위해 기말고사 시험지를 작성중이라 가정 해 보자

  • 시험지를 작성하는 선생님은 시험지를 볼 수 있음.
  • 교장 선생님은 모든 시험지를 사전에 볼 수 있음.
  • 교과 선생님들 중에 허락된 선생님은 시험지를 볼 수 있음.
  • 그 밖의 선생님과 학생은 시험지를 볼 수 없음.

이러한 케이스는 AuthenticatedVoter나 Roler Voter로 다룰수 없다

이런 경우라면 당연히 어떤 시험지인지, 그리고 그 시험지에 대한 열람 권한이 누구에게 있는지, 즉 여러가지 데이터들이 모여야 해당 권한을 판단할 수가 있다
-> 이런 경우에는 PermissionEvaluator 를 사용하거나 객체 별로 접근 권한을 DB로 관리(권한 테이블) 해주는 ACL 처럼 권한을 체크하기 위한 별도의 설계가 들어가야 합


WebExpressionVoter와 PIIAVoter (@PreAuthorize 어노테이션 기반)

ebExpressionVoter와 PIIAVoter 는 모두 아래와 같은 방식으로 SpEL 을 사용

WebExpressionVoter, PIAA Voter는 SPEL을 사용해야 하기에, parser가 Context를 가지고 핸들링 할 수 있어야 함
-> parser가 Context를 핸들링기에 각 Voter들은 ExpressionHandler 객체를 가지고 있어야 함

루트 객체 또한 필요하기에 createEvaluationContext, createSecurityExpressionRoot가 필요

web에는 hasipAddress라는 메서드가 추가로 제공되는데 이는 루트 객체이기에 #붙일 필요 X

Method 에는 filterObject, return Object에 #을 붙이기 않고도 접근 가능

웹에서 만약 url에 PathVariable을 넘기면 , 이때 PathVariable은 컨텍스트에서 Variable로 취급하기에 #을 붙여야 함

Bean을 가지고 검사한다면 @를 붙여 빈을 가져와 해당 메서드를 검사하는 작업을 수행해야 한다

profile
Journey for Backend Developer

0개의 댓글