본문 바로가기

스프링

[Security] Authorization(Security Form Login 구현)

시작하기에 앞서

우리는 이전에 사용자의 인증(Authentication)에 대해서 다뤘었다.

이번에는 사용자의 인가(Authorization)에 대해 다뤄보려고 한다.

인가란 무엇일까?

우리는 Authentication에 Principal, Credentials뿐만 아니라 Authorities를 설정해줄 수 있었다.

Principal과 Credentials는 인증하려는 사용자의 ID, PW 정보를 담고 있으며 Authorities는 권한 정보를 담고 있다.

우리는 권한 정보를 활용해 해당 사용자가 해당 URL에 접근할 권한이 있는지 확인하는 것이다.

이를 처리하는 필터에 대해서 살펴보자.

FilterSecurityInterceptor

인가(Authorization)을 처리하는 필터는 바로 FilterSecurityInterceptor이다.

인증에서는 AuthenticationManager을 활용했듯이, 인가에서는 AccessDecisionManger를 활용하여 인가를 처리한다.

아래 그림에서 확인할 수 있듯이, FilterSecurityInterceptor에서 AbsractSecurityInterceptor에서 accessDecisionManger.decide()를 호출하여 처리하는 모습을 확인할 수 있다.

그렇다면, AccessDecisionManager에서 어떻게 인가 처리를 진행하는지 알아보자.

AccessDecisionManger는 인가 제어 결정을 내리는 인터페이스로 3가지의 구현체를 제공한다.

  • AffirmativeBased - 여러 Voter 중에 한 명이라도 허용하면 허용한다. (디폴트)
  • ConsensusBased - 각각의 Voter들의 결과를 다수결로 판단한다.
  • UnanimousBased - 모든 Voter들의 결과가 만장일치라면 허용한다.

여기서 말하는 Voter는 AccessDecisionVoter로, Authority를 심사하는 인터페이스이다. (구현체로는 WebExpressionVoter이다.)

Authentication이 어떤 URL에 접근할 때 필요한 ConfigAttributes를 만족하는지 확인한다.(permitAll, authenticated)

우리가 아래와 같이 어떤 URL에 대해 어떤 권한을 설정해주었을 때, 이 권한을 만족해야지만 해당 URL에 접근할 수 있다.

하지만 여기서 살펴보면, 여러가지 권한을 설정하는데 있어 ADMIN은 USER보다 높지만 따로 따로 설정한 모습을 확인할 수 있다.

Spring에서는 아래와 같이 각각의 권한을 여러 개 설정할 수도 있지만 계층적으로 권한을 설계하는 것을 제공한다.

public AccessDecisionManager accessDecisionManager() {
    RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
    roleHierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER");

    DefaultWebSecurityExpressionHandler defaultWebSecurityExpressionHandler = new DefaultWebSecurityExpressionHandler();
    defaultWebSecurityExpressionHandler.setRoleHierarchy(roleHierarchy);

    WebExpressionVoter webExpressionVoter = new WebExpressionVoter();
    webExpressionVoter.setExpressionHandler(defaultWebSecurityExpressionHandler);

    List<AccessDecisionVoter<? extends Object>> webExpressionVoters = Arrays.asList(webExpressionVoter);
    return new AffirmativeBased(webExpressionVoters);
}
http.authorizeRequests()
    .mvcMatchers("/login", "/register", "/logout").permitAll()
    .mvcMatchers("/user").hasAnyRole("USER", "ADMIN")
    .mvcMatchers("/admin").hasRole("ADMIN")
    .anyRequest().authenticated()
    .accessDecisionManager(accessDecisionManager())

위에서 볼 수 있듯이, RoleHierachy -> DefaultWebSecurityExpressionHandler -> WebExpressionVoter -> List<AccessDecision<? extends Object>> -> AccessDecisionManager 과 같은 순서로 주입하여 생성한다.

이렇게 AccessDecisionManger를 주입하는 방법은 상당히 길기에 Spring에서는 아래와 같이 ExpressionHandler를 등록하는 방법을 제공한다.

public DefaultWebSecurityExpressionHandler defaultWebSecurityExpressionHandler() {
    RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
    roleHierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER");

    DefaultWebSecurityExpressionHandler defaultWebSecurityExpressionHandler = new DefaultWebSecurityExpressionHandler();
    defaultWebSecurityExpressionHandler.setRoleHierarchy(roleHierarchy);

    return defaultWebSecurityExpressionHandler;
}
http.authorizeRequests()
    .mvcMatchers("/login", "/register", "/logout").permitAll()
    .mvcMatchers("/user").hasAnyRole("USER", "ADMIN")
    .mvcMatchers("/admin").hasRole("ADMIN")
    .anyRequest().authenticated()
    .expressionHandler(defaultWebSecurityExpressionHandler())

또한, 인증과 인가에 의해 정적 파일들이 로드되지 않을 수 있기에 Spring에서는 몇몇에 관해서는 Spring Security를 무시할 수 있게 Ignoring을 제공한다.

@Override
public void configure(WebSecurity web) throws Exception {
    web.ignoring().requestMatchers(PathRequest.toStaticResources().atCommonLocations())
            .antMatchers("/favicon.ico", "/resources/**", "/error");
}

위와 같이 configure(webSecurity web) throws Exception을 오버라이딩하여 제외할 부분을 매칭시켜 제외해준다.

결과

  1. 관리자로 회원가입

  1. 관리자 아이디로 로그인

  1. 관리자 권한 페이지 접근

  1. 성공