Spring Security : 선언적 권한 설정과 표현식

    이 글은 인프런 정수원님의 강의를 복습하며 작성한 글입니다.

     

    Spring Security 권한 설정

    Spring Security는 크게 인증과 인가로 나누어진다. '인가'라는 개념에 필수적인 것이 권한이다. 인증을 받은 객체가 있는데, 이 객체가 가진 권한으로 이 자원에 접근 가능한지를 확인하는 과정이 인가다. 그렇다면 권한 설정이란 무엇일까? 바로 특정한 자원에 어떤 권한이 있어야만 접근할 수 있다는 것을 설정해주는 것이다. 

     

    Spring Security는 크게 두 가지 권한 설정 방식을 제공하고, 각 권한 설정 방식을 통해 URL, Method에 권한을 설정해줄 수 있다. 

    선언적 권한 설정 방식 

    • URL
      • http.antMatchers("/user/**).hasRole("USER)
    • Method
      • @PreAuthorize("hasRole('USER')") public void user(){}

    URL과 Method에 선언적 권한 설정 방식으로 권한을 설정할 수 있다. URL은 HttpSecurity 객체를 이용해서 설정한다. 메서드는 @PreAuthorize라는 어노테이션을 이용해 권한을 설정할 수 있다.

    동적 권한 설정 방식 : DB 연동 프로그래밍 

    • URL
    • Method 

    동적방식은 DB 연동 프로그래밍으로 하는 것이다.

     

    선언적 권한 설정 API : WebSecurityConfigurerAdataper의 Configurer()

    • http.authorizeRequest() : HttpSecurity 객체를 통해 권한 설정을 시작.
    • antMatcher("/user/**").permitAll() : User 및 하위 경로에 대한 권한 설정.
    • antMatcher(*/user/"").hasRole("USER") : USER 권한을 가진 사람만 /user 하위 경로에 접근 가능.
    • anyRequest().authenticated() : 나머지 모든 요청에 대해 인증을 받아야만 접근 가능함. 

    위는 권한 설정을 하는 한 예시를 의미한다. 

    • authenticated() : 인증된 사용자의 접근 허용. 권한 무관
    • fullyAuthenticated() : 인증된 사용자 접근 허용. Remember Me 인증 제외
    • permitAll() : 무조건 접근 허용
    • denyAll() : 무조건 접근 허용하지 않음
    • anonymous() : 익명사용자의 접근 허용
    • rememberMe() : Remember Me 인증 사용자의 접근 허용
    • access(String) : 주어진 SpEL 표현식의 평가 결과가 True면 접근 허용
    • hasRole(String) : 사용자가 주어진 역할이 있다면 접근 허용 
    • hasAuthority(String) : 사용자가 주어진 권한이 있다면 접근 허용
    • hasAnyRole(String...,) : 사용자가 주어진 권한이 있다면 접근 허용. (하나라도 있으면)
    • hasAnyAuthority(String...) : 사용자가 주어진 권한 중 어떤 것이라도 있따면 접근 허용
    • hasIpAddress(String) : 주어진 IP로부터 요청이 온 경우 접근 허용

    antMatchers()에 위의 메서드를 자유롭게 조합해서 어떤 자원에 어떤 방식의 접근이 가능할지를 설정해줄 수 있다. 

    http
            .authorizeRequests() 
            .antMatchers("/user").hasRole("USER") 
            .antMatchers("/admin/pay").hasRole("ADMIN")
            .antMatchers("/admin/**").access("hasRole('ADMIN') or hasRole('SYS')")
            .anyRequest().authenticated();
    
    http
            .formLogin();

    위의 사용 예시를 참고할 수 있다. 

     

    Spring Security 권한 설정 시 주의사항

    1. antMatchers()를 생략하는 경우

    antMatchers()를 생략하는 경우, 어떠한 URL에 대한 설정도 되지 않는 것을 의미한다. 모든 요청에 대한 인증 처리를 설정하지 않았기 때문에, 보수적으로 생각해보면 Spring Security는 모든 요청에 대해 인증 검사를 하는 것으로 이해할 수 있다. 즉, antMatchers()를 하나도 설정하지 않으면 모든 요청은 인증을 받아야지만 접근할 수 있다. 

     

    2. 구체적인 경로가 위에 와야함. 

    Spring Security는 HttpSecurity의 authorizeRequests()로부터 시작되어서 각 자원에 어떤 권한이 설정될지를 결정한다. 이 때 중요한 부분은 Spring Security는 AuthorizeRequest()부터 차례대로 인가처리를 한다. 

    http
            .authorizeRequests()
            .antMatchers("/user/**").permitAll()
            .antMatchers("/user/user").hasRole("USER");

    예컨데 위와 같은 경우가 있을 것이다. Spring Security는 위에서부터 차근히 내려오기 때문에 더 넓은 경로인 /user/**에 대해 permitAll()처리를 한다. 따라서 "/user/user"에 있는 "USER" 권한에 관한 인가처리는 무시된다. 

    http
            .authorizeRequests()
            .antMatchers("/user/user").hasRole("USER")
            .antMatchers("/user/**").permitAll();

    따라서, 다음과 같이 구체적인 부분이 항상 위에 있도록 권한 인가 설정을 처리해야한다. 

     

    3. 하위 경로를 표현할 때

    http
            .authorizeRequests()
            .antMatchers("/user/**").permitAll();

    하위 경로를 표현할 때는 "**"으로 표현해주면 된다. 예컨데 다음과 같이 표현할 수 있다. 

     

     

    코드 실습

    이번 코드 실습은 각 계정에 권한을 설정하고, 각 자원들에 다른 권한을 설정해서 정상적으로 동작하는지를 확인하는 것이다. 그리고 실제 Spring Security가 어떻게 권한을 설정하는지도 함께 살펴볼 수 있으면 좋겠다.

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    
        // 메모리 방식으로 사용자 생성
        // {noop}는 prefix가 붙음. 이 prefix가 붙으면, 암호가 암호화됨.
        // 만약에 id에 해당하는 prefix를 적어주지 않으면 Null Exceptino 발생함.
        // {noop}는 아무 암호화도 하지 않음.
        auth.inMemoryAuthentication().withUser("user").password("{noop}1111").roles("USER");
        auth.inMemoryAuthentication().withUser("sys").password("{noop}1111").roles("SYS");
        auth.inMemoryAuthentication().withUser("admin").password("{noop}1111").roles("ADMIN", "USER", "SYS);
    
    
    }
    http
            .authorizeRequests()
            .antMatchers("/user/**").permitAll()
            .antMatchers("/user/user").hasRole("USER")
            .anyRequest().authenticated();

     

    예를 들어 권한 설정을 위와 같이 설정했을 때, /user/user로 접근한다고 가정해보자. 이 때 문제는 /user/**의 permitAll() API에 의해서 인증이 되지 않은 객체도 자유자재로 /user/user에 접근할 수 있다는 것이다. 왜 그럴까?

    FilterSecurityInterceptor

    Spring Security의 최종 인가처리는 Spring Security의 마지막 단계인 FilterSecurityInterceptor에서 처리를 해준다. FilterSecurityInterceptor는 beforeInvocation에서 인가 처리를 해준다.

    AbstractSecurityInterceptor
    AbstractSecurityInterceptor

    super.beforeInvocation을 통해 AbstractSecurityInterceptor로 타고 올라온다. 이 때, AbstractSecurityInterceptor는 내부적으로 MetaDataSource를 가지고 있는데, MetaDataSource에는 RequestMap이라는 객체가 있다. 그리고 Request Map 객체에는 권한 인가를 위한 requestMap이 HashMap 형태로 가지고 있다. 딱 봐도, 순서대로 등록된것이 아주 잘 보인다.

    DefaultFilterInvocationSecurityMetadataSource

    getAttribute()를 통해서 위 클래스로 넘어온다. 여기서 앞서 이야기 한 RequestMap의 entrySet()을 얻은 후 For문을 돌면서 순서대로 사용자 요청 Request와 실제 Entry가 매칭되는지를 확인한다. 순서대로 확인할 것이기 때문에 위에 있는 URL이 먼저 매칭되어서 그 권한에 따라 인가 처리가 될 것이다. 

    결국 "/USER/**".permitAll()에 따라서 얻어지는 값은 permitAll이 된다. 즉, attributes에 permitAll이 들어간다는 소리다.

    인가 정보를 얻은 후 쭈욱 내려가서 attemptAuthorization에 현재 객체와 attributes, 인증 객체를 넘겨준다. 여기서 Object는 요청온 Http 메서드 + URL을 의미하고, attributes는 인가정보, authenticated는 인증 객체다. 따라서 첫번째 URL을 바탕으로 인가 정보가 결정되고, 이 값이 PermiAll이기 때문에 어떤 권한을 가지든, 인증 객체가 없든 간에 접근할 수 있도록 된다는 것을 알 수 있다. 

    http
            .authorizeRequests()
            .antMatchers("/user/user").hasRole("USER")
            .antMatchers("/user/**").permitAll()
            .anyRequest().authenticated();

     

    반대로 설정을 위와 같이 한 후, /user/user로 접근한다고 가정해보자.

    이 때, 얻어져서 attributes에 저장되는 인가 정보는 "hasRole('ROLE_USER')"라는 값이다. 즉, attempAuth 메서드로 넘어갈 때 이 인가 정보가 넘어가기 때문에 원하는대로 정상적으로 인가처리가 되는 것을 이해할 수 있다. 

     

    정리

    • HttpSecurity 객체를 통해 각 URL별 인가 / 인증 정보를 설정할 수 있다.
    • @PreAuthorize 어노테이션을 이용해 메서드별 인가/ 인증 정보를 설정할 수 있다.
    • URL 인증/인가 정보를 선언할 때, 반드시 좀 더 세밀한 부분부터 URL을 설정하고, 포괄적인 부분으로 확대해나간다. 

    댓글

    Designed by JB FACTORY