Auth (Authentication & Authorization)

Spring-security authorizeRequest 동적으로 설정하기

kellis 2020. 10. 12. 09:42

앞선 글에서의 코드는 몇 가지 문제점이 존재하며 이 글에서는 이 중 한 가지를 어떻게 개선할 수 있는지 살펴보도록 하겠습니다. 

 

[1] 문제점 파악

기존 코드의 Spring-security configuration에서 authorizeRequest()를 보면 요청 URL과 그 URL에 접근 가능한 권한을 설정하는 부분이 하드코딩되어있습니다. (6~7라인)

@Override   
protected void configure(HttpSecurity http) throws Exception {
    http
        .csrf().disable()
        .authorizeRequests()
            .antMatchers("/", "/main").permitAll()
            .antMatchers("/admin/*").hasAuthority("ADMIN")
            .anyRequest().authenticated()
            .and()
        .formLogin()
            .loginPage("/login")
            .usernameParameter("userId")
            .successHandler(new LoginSuccessHandler("/home"))
            .permitAll()
            .and()
        .logout()
            .invalidateHttpSession(true)
            .deleteCookies("JSESSIONID")
            .permitAll()
            .and()
        .authenticationProvider(authProvider)
        .exceptionHandling().accessDeniedHandler(accessDeniedHandler());
}

이는 권한을 변경하기 위해 어플리케이션의 코드를 변경해야 한다는 것을 의미합니다.

그래서 일반적인 어플리케이션에서는 요청 URL과 권한에 대한 매핑 정보를 DB에 저장하고, URL에 요청할 때마다 해당 DB에 접근하여 권한이 있는지 확인하는 작업을 수행합니다. (X2-Commerce도 이와 같은 방법으로 구현되어 있습니다.) 

이번 장에서는 Spring-security가 DB에 저장된 URL-권한 매핑 정보를 이용하여 권한 확인을 하도록 구현해보겠습니다. 

 

[2] Spring-security configuration에서 요청 URL-권한 매핑 정보 로딩.

요청 URL과 권한 정보가 매핑된 데이터를 로딩하기 위해 새롭게 DTO, Repository가 필요하며, 이를 이용하도록 Spring-security 설정이 변경되어야 합니다. 

 

 1) 다큐먼트 및 DTO생성

 이 글에서는 MongoDB를 사용하므로 컬렉션을 생성했으나, RDB를 사용한다면 Table을 생성합니다.

@Document("SecurityUrlMatcher")
public class SecurityUrlMatcher {
    private String url;
    private String authority;
     
    public String getUrl() {
        return url;
    }
    public void setUrl(String url) {
        this.url = url;
    }
    public String getAuthority() {
        return authority;
    }
    public void setAuthority(String authority) {
        this.authority = authority;
    }
}

2) Repository 생성

 이 프로젝트에서는 Spring-data를 이용했으므로 SecurityUrlMatcherRepository를 생성했습니다. RDB를 사용한다면 Mybatis를 이용해 위에서 생성한 Table의 모든 row를 조회하는 쿼리를 작성합니다.

@Repository
public interface SecurityUrlMatcherRepository extends CrudRepository<SecurityUrlMatcher, String> {
    List<SecurityUrlMatcher> findAll();
}

3) Spring-security Configuration 변경

 하드코딩했던 요청 URL과 권한에 대한 정보를 DB에서 조회하도록 변경합니다. 

@Override
protected void configure(HttpSecurity http) throws Exception {
     matchUrlAndAuthority(http);
      
     http
        .csrf().disable()
        .authorizeRequests()
            .antMatchers("/", "/main").permitAll()
            .anyRequest().authenticated()
            .and()
        .formLogin()
            .loginPage("/login")
            .usernameParameter("userId")
            .successHandler(new LoginSuccessHandler("/home"))
            .permitAll()
            .and()
        .logout()
            .invalidateHttpSession(true)
            .deleteCookies("JSESSIONID")
            .permitAll()
            .and()
        .authenticationProvider(authProvider)
        .exceptionHandling().accessDeniedHandler(accessDeniedHandler());
}
 
private void matchUrlAndAuthority(HttpSecurity http) throws Exception {
    List<SecurityUrlMatcher> urlMatchers = repository.findAll();
     for (SecurityUrlMatcher matcher : urlMatchers) {
         http
             .authorizeRequests()
             .antMatchers(matcher.getUrl()).hasAuthority(matcher.getAuthority());
     }
}

이와 같이 구현하면, 애플리케이션 최초 구동 시에 DB의 매핑 정보를 읽어 들여 Spring-security의 authorizationRequest 정보를 설정합니다. 즉 권한 정보가 변경된 경우, 애플리케이션을 재기동해야 변경된 정보가 적용된다는 문제가 있습니다. 

 

[3] URL로 요청이 올 때마다 권한 체크

Spring-security에는 URL로 요청이 올 때마다 해당 URL에 대한 접근 가능 여부를 동적으로 확인할 수 있도록 해주는 설비가 있습니다. Spring-seucrity의 configuration을 다시 살펴보도록 하겠습니다. 

.authorizeRequests()
                .antMatchers("/", "/main","/accessDenied").permitAll()
                .anyRequest().access("@authorizationChecker.check(request, authentication)")
                .and()

3라인에서 AuthorizedUrl.access API가 사용된 것을 확인할 수 있습니다. 이 메소드는 입력된 SpEL을 런타임 시에 평가하여 현재 사용자가 해당 Url에 대한 접근 권한이 있는지 동적으로 확인합니다. 즉, 위의 코드는 특정 URL로의 요청이 들어왔을 때 authorizationChecker 빈의 check 메서드를 호출하여 그것이 반환하는 값(True/False)에 따라 접근 가능 여부를 설정하겠다는 의미입니다.

 

AuthorizationCheck.check 메소드는 요청한 USER의 권한이 해당 URL에 접근 가능한 권한을 가지고 있는지 판단하는 역할을 합니다. 구현은 다음과 같습니다.

@Component
public class AuthorizationChecker {
    @Autowired
    private SecurityUrlMatcherRepository urlMatcherRepository;
  
    @Autowired
    private UserRepository userRepository;
  
    public boolean check(HttpServletRequest request, Authentication authentication) {
        Object principalObj = authentication.getPrincipal();
  
        if (!(principalObj instanceof User)) {
            return false;
        }
  
        String authority = null;
        for (SecurityUrlMatcher matcher : urlMatcherRepository.findAll()) {
            if (new AntPathMatcher().match(matcher.getUrl(), request.getRequestURI())) {
                authority = matcher.getAuthority();
                break;
            }
        }
  
        String userId = ((User) authentication.getPrincipal()).getUserId();
        User loggedUser = userRepository.findByUserId(userId);
  
        List<String> authorities = loggedUser.getAuthority();
  
        if (authority == null || !authorities.contains(authority)) {
            return false;
        }
        return true;
    }
}

 

[4] 개선 필요 사항

- 현재 Anonymous User에 대한 고려가 없습니다. 그래서 모든 요청에 대해 접근 가능한 URL은 Configuration에 하드코딩되어 있습니다.

- SecurityUrlMatcher의 document에 sorting 기준 필드를 추가해야 합니다. 

  ["/home/*", "/home/test"] 이 두개의 URL 각각에 대해 권한이 다르다면, 읽는 순서가 중요할 것입니다. 따라서 구체적인 URL을 보다 먼저 읽도록 해야 합니다. 

- Url에 대한 권한 체크를 위해 매번 데이터베이스의 정보를 확인하는 것은 매우 비효율적입니다. 이 문제를 해결하기 위해 Spring Cache Abstraction을 이용할 수 있습니다.