[Spring Security] 권한

WOOK JONG KIM·2022년 12월 4일
0

패캠_java&Spring

목록 보기
83/103
post-thumbnail

Aspect Oriented Programming

의존성 주입(DI) -> Loosely Coupled 설계를 할 수 있고 모듈화를 완성할 수 있음

AOP -> 코드를 스파게티로 엮지 말고, 관심사에 따라 코드를 분리하는 개념

  • Aspect : 공통의 관심사(Pointcut + Advice)
    ex) 권한 처리, 로그, 트랜잭션 관리, 세션 관리, 기타...
    -> 이것들을 비즈니스 로직과 서로 분리해서 작업
    -> 어떤 포인트컷 메서드에 대해 어떤 어드바이스 메서드를 실행할 지 결정

  • Weaving : 빈과 빈을 Proxy로 감싸서 연결해주는 작업
    -> 빈과 빈 호출 사이에 Pointcut을 적용해서 JoinPoint를 판별한 다음 PointCut 을 요청한 Advice를 JoinPoint에 적용
    -> 쉽게 말해 포인트컷으로 지정한 핵심 관심 메서드가 호출될때, Advice에 해당하는 횡단 관심 메서드를 삽입하는 과정을 의미

  • JoinPoint : 클라이언트가 호출하는 모든 비즈니스 메서드(일종의 포인트컷 후보)

  • Pointcut : 특정 조건에 의해 필터링된 조인포인트
    -> 특정 메서드에서만 횡단 공통기능을 수행하기 위해 사용

  • Advice : 횡단 관심에 해당하는 공통 기능의 코드
    (@Before, @After-Returning ....)


Authorization

Spring Security에서 인증은 Securiy Config라는 필터 체인 상에 위치하게 됨

Authentication Manager(인증기관)이 Authentication Provider(인증 제공 주체 : 사람)들을 가지고 있고 Authentication이라는 통행증을 발급

권한의 경우 인증이 완료되어야 체크 가능
-> 어떤 리소스에 접근할려고 할 때 권한이 있는지

필터 위에 상주하는 Interceptor 를 FilterSecurityInterceptor라 하고 Method 위에 annotation의 형태로 상주하는 Interceptor 를 MethodSecurityInterceptor 라고 한다
-> @EnableGlobalMethodSecurity 를 설정해줘야 MethodSecurityInterceptor 가 동작합니다.

Filter Security Intercepter(필터 권한 위원회)는 Filter 단에서 설정

Access decision manager가 Filter Security Intercepter에 매핑이 되기 때문에 각 SecurityInterceptor당 한개의 AccessDecisionManager를 둘수 있다

스프링 애플리케이션에서는 어느 필터를 따라 들어올지 모르기 때문에 메소드 시큐리티에 대한 Access Decision Manager는 딱 한개가 존재

체크해야 할 내용(invocation) 에 있는 ConfigAttribute에 권한 관련 내용이 들어가 있음

Invocation을 직접 체크하는 클래스 -> VOTER

Voter 각각이 ConfigAttribute에 대해 평가를 했을때 Check or Deny 인지 결정

Access Decision Manager는 모든 평가 내용을 취합하여 결정을 내린다


권한 체크에 관여 하는 것

  • 접근하려고 하는 사람이 어떤 접근 권한을 가지고 있는가?

GrantedAuthority

  1. Role Based
  2. Scope Based
  3. User Defined
  • 접근하려고 하는 상황에서는 체크해야 할 내용은 무엇인가?
  1. SecurityMetadataSource, ConfigAttribute
  2. 정적인 경우와 동적인 경우
  3. AccessDecisionVoter 가 vote 해줌
  • 여러가지 판단 결과가 나왔을 때 취합은 어떤 방식으로 할 것인가?

AccessDecisionManager : 권한 위원회

  1. AffirmativeBased : 긍정 위원회(한명이라도 통과 시킬려고 하면 통과)
  2. ConsensusBased : 다수결 위원회
  3. UnanimouseBased : 만장일치 위원회

인증과 권한의 구조

인증은 Authentication Filter에서 Authentication을 인풋으로 Authentication Manager 에게 전달

이후 Authentication Manager는 Provider들을 소집해서 Authentication을 발급할 수 있는 Provider에게 인증을 맏김

supprt() 메서드는 매니저가 일을 맏길 적절한 Provider를 판단하는 기준

권한은 Security Intercepter에서 권한을 체크

Security Intercepter는 Invocation이라는 호출 시점의 환경을 Access Decision Manager에게 넘겨 줌
-> Manager가 Decide(평가)를 통해 통과시킬지 말지 결정

여기서 support()는 해당 ConfigAttribute를 너가 처리해줄수 있니?라고 물어봄


권한 처리 클래스

Security Intercepter라는 것은 Aop개념에서 Advice를 삽입하는 것

Authentication Manager는 재검증이 필요할 경우 사용

권한판정을 위한 Config Attribute를 모아 놓은 Map이 SecurityMetaDataSource
-> 두 인터셉터는 서로 각자 다른 SecurityMetaDataSource를 가지고 있음


코드 예시

Security Config

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {


    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .inMemoryAuthentication()
                .withUser(
                        User.withDefaultPasswordEncoder()
                                .username("user1")
                                .password("1111")
                                .roles("USER")
                );
    }

    FilterSecurityInterceptor filterSecurityInterceptor;

    AccessDecisionManager filterAccessDecisionManager(){
        return new AccessDecisionManager() {
            @Override
            public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {

                // throw new AccessDeniedException("접근 금지"); 이 경우 403
                // 그냥 return 만 할시 다 통과
                return;
            }

            @Override
            public boolean supports(ConfigAttribute attribute) {
                // 어떤것이 오든 다 트루
                return true;
            }

            @Override
            public boolean supports(Class<?> clazz) {
                // filter invocation에서 이 보터가 쓰일 것
                return FilterInvocation.class.isAssignableFrom(clazz);
            }
        };
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .httpBasic().and()
                .authorizeRequests(
                        authority->authority
                                // test1() Filter Security Intercepter에서 Access Denied(User 권한으로 get url 보낼 시)
                                .mvcMatchers("/greeting").hasRole("USER")
                                .anyRequest().authenticated()
//                                .accessDecisionManager(filterAccessDecisionManager()) // 위에 구현한 매니저 사용, 이 경우 다 통과(위에 코드 두줄의 의미가 없겠지?)
                );
                ;
    }
}

Controller

@RestController
public class HomeController {

    MethodSecurityInterceptor methodSecurityInterceptor;

    // 이를 동작시킬려면 Global Method 권한 위원회를 소집해야 함(@EnableGlobalMethodSecurity)
    // 직접 만들수 도
    @PreAuthorize("hasRole('ADMIN')") // Filter Security Intercepter 후 2차 방어선
    @GetMapping("/greeting")
    public String greeting(){
        return "hello";
    }
}

Custom Voter

public class CustomVoter implements AccessDecisionVoter<MethodInvocation> {

    // 이 Voter 혼자서 통과시켜주는 방식으로 동작

    @Override
    public boolean supports(ConfigAttribute attribute) {
        return true;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return MethodInvocation.class.isAssignableFrom(clazz);
    }

    @Override
    public int vote(Authentication authentication, MethodInvocation object, Collection<ConfigAttribute> attributes) {

        return ACCESS_GRANTED;
    }
}

Method Security Configuration

// 이는 반드시 Configuration에 선언해야함
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfiguration extends GlobalMethodSecurityConfiguration {
    @Override
    protected AccessDecisionManager accessDecisionManager() {
        List<AccessDecisionVoter<?>> decisionVoters = new ArrayList<>();
        ExpressionBasedPreInvocationAdvice expressionAdvice = new ExpressionBasedPreInvocationAdvice();
        expressionAdvice.setExpressionHandler(getExpressionHandler());

        decisionVoters.add(new PreInvocationAuthorizationAdviceVoter(expressionAdvice));
        decisionVoters.add(new RoleVoter());
        decisionVoters.add(new AuthenticatedVoter());
        decisionVoters.add(new CustomVoter()); // 이 권한 위원회는 컨트롤러가 아닌 서비스 단에서도 적용 됨

        return new AffirmativeBased(decisionVoters); // 한명이라도 동의할 시 통과(긍정 위원회)
//        return new UnanimousBased(decisionVoters); // 이 경우는 만장일치
//         return new ConsensusBased(decisionVoters); // 다수결 경우 위경우에는 찬성:1 반대:1 -> 이 경우 allowIfEqualGrantedDeniedDecision 옵션 설정 해야 함
    }
}

테스트 코드

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class WebIntegrationTest {

    @LocalServerPort
    int port;

    public URI uri(String path) {
        try{
            return new URI(format("http://localhost:%d%s", port, path));
        } catch(Exception ex) {
            throw new IllegalArgumentException();
        }
    }

}
public class AuthorityBasicTest extends WebIntegrationTest {

    TestRestTemplate client;

    @DisplayName("greeting 메세지 불러오기")
    @Test
    void test_1(){
        client = new TestRestTemplate("user1", "1111");
        ResponseEntity<String> response = client.getForEntity(uri("/greeting"), String.class);

        System.out.println(response.getBody());
    }
}
profile
Journey for Backend Developer

0개의 댓글