스프링 시큐리티 인가

allen·2020년 12월 2일
1
post-thumbnail

이번에는 스프링시큐리티에서 어떻게 인가가 이뤄지고, 어떤 라이프사이클을 갖는지 알아보려고 한다.

시큐리티를 모르고 읽으면 어려울 것 같다. 이전 포스팅을 읽고보면 좋을 것 같다. - 스프링 시큐리티와 인증

인가(Authorization)란?

인가란 리소스에 대한 접근 권한 및 정책을 지정하는 기능이다

유저의 Request가 우리의 리소스에 접근할 수 있는지 확인하는 절차다.

ex) 관리자 페이지 접근, 게시판 글쓰기 등에서 보통 인가절차를 밟는다. 우리가 로그인 이후, 해당 리소스를 접근할 수 있는 건. 보통 로그인 되어있음을 Session에 저장하거나, JWT 토큰을 이용한다. 그래서 사용자는 로그인이되어있음을 느낄 수 있다. 인증이 선행되는 걸 알 수 있다.

권한(Authority)이란?

권리나 권력 또는 직권이 미치는 범위.

Request한 유저의 권한을 검사한다. 인가절차에서 요소로 사용된다.

ex) 관리자 페이지를 접근할 때, Request한 유저가 관리자인지 확인한다.

유튜브에 잘 나와 있다. - 토니의 인증과 인가


WebSecurityConfigurerAdapter로 Configure 만들기

  1. URL별 권한을 설정을 등록할 수 있다.
  2. accessDecisionManager를 통해 인가 작업을 할 수 있다.
@Configuration
@EnableWebSecurity
public class WebSecurityConfigure extends WebSecurityConfigurerAdapter {

  @Override
    protected void configure(HttpSecurity http) throws Exception {
      http
        .csrf()
          .disable()
        .headers()
          .disable()
        .exceptionHandling()
          .accessDeniedHandler(accessDeniedHandler)
          .authenticationEntryPoint(unauthorizedHandler)
          .and()
        .sessionManagement()
          .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
          .and()
        .authorizeRequests()
          .antMatchers("/api/auth").permitAll()
          .antMatchers("/api/user/join").permitAll()
          .antMatchers("/api/**").hasRole(Role.USER.name())
          .accessDecisionManager(accessDecisionManager())
          .anyRequest().permitAll()
          .and()
        .formLogin()
          .disable();
      http
        .addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
	}
}

위 클래스에서 WebSecurityConfigurerAdapter를 상속하면서 configure()를 구현하고 있다. 이 부분이 시큐리티의 메인설정이라고 볼 수 있다. (어렵다. 필요한 부분만 살펴보자)

우리가 눈 여겨봐야 할 곳은 authorizeRequests()부분과 accessDecisionManager(accessDecisionManager())이 부분이다.

한 눈에 알수있는부분은 authorizeRequests()을 통해 URL에 따른 권한승인을 할 수 있는 것으로 보인다.

.antMatchers("/api/auth").permitAll()
.antMatchers("/api/user/join").permitAll()

위와 같이 로그인/회원가입 같은 부분은 모든 유저들에게 허용할 수 있도록 해야 한다. 로그인/회원가입할 때 권한이필요하면 아무도 서비스를 이용할 수 없을 것 같다😭

.antMatchers("/api/**").hasRole(Role.USER.name())

코드를보면 URL패턴(ANT 문법)에 맞게 적절한 권한을 줄 수 있는 걸로 보인다.
보통 위처럼 모든 권한을 주고 나서, 권한이 불필요한 URL을 따로 제외하는 게 방어적인 프로그래밍이라고 생각한다.

그럼 지금 서비스는 "/api/auth", "/api/user/join"를 제외하고 모두 Role.User의 권한을 갖고 있어야 접근할 수 있다.


.accessDecisionManager(accessDecisionManager())를 보면 accessDecisionManager()를 통해 만들어 둔 빈을 주입하는 걸 볼 수 있다. 그럼 구현체를 봐 보자.

@Bean
public AccessDecisionManager accessDecisionManager() {
  List<AccessDecisionVoter<?>> decisionVoters = new ArrayList<>();
  decisionVoters.add(new WebExpressionVoter());
  decisionVoters.add(connectionBasedVoter());
  return new UnanimousBased(decisionVoters);
}

UnanimousBased라는 AccessDecisionManager의 구현체를 리턴하는 메서드고, AccessDecisionManager는 Voter들의 리스트를 참조하고 있다.

Voter는 투표자라는 뜻으로, N명의 투표자의 투표를 통해 인가가 이뤄진다.
투표의 결과는 AccessDecisionManager의 전략에 따라서 달라지며 AccessDecisionManager를 자세히 알아보겠다.

Voter와 AccessDisicionManager

인가를 추가로 하는 요소를 알아보자.


먼저 Voter를 알아보면, 투표자라는 뜻으로 멤버변수로 GRANTED, ABSTAIN, DENINED이 있다.

각 Voter객체는 로직에 따라 요청에 따라 권한을 승인, 무효, 거절로 투표할 수 있다.

왠지 추측으로는 AccessDisicionManager는 여러 개의 Voter들의 vote()를 통해서 ACCESS의 결과들의 합을 구할 수 있을 것 같다. '그 합의 값이 양수이면 인증된 유저라고 생각할 수 있지 않을까'? 라는 생각이 들었다.

실제 AccessDisicionManager는 얼추 비슷했다. 다음은 그들의 구현체들을 알아보겠다.


위 사진에서 AccessDecisionManager과 Voter의 관계를 볼 수 있다.

  • 디시전매니저가 configure에 설정되어 있으면FilterSecurityInterceptor 필터가 호출해준다.

    • 디시전 매니저를 따로 설정하지 않았다면, 디폴트설정으로 AffirmativeBased기반 Manager와 WebExpressionVoter의 Voter 1개로 인가처리가 진행된다.
    • WebExpressionVoter가 아까 configure()에 등록한 권한을 비교하여 투표한다.
  • 디시전 매니저는 N개의 Voter를 들고 있으며, 요청에 따른 Voter들의 투표결과에 따라 승인이 이뤄진다.

    • 투표결과는 디지전매니저의 구현체에 따라 달라진다.

디시전매니저는 시큐리티에 제공하는 건 3가지가 있다. 어떤 게 있는지 알아놓고, 적재적소 사용할 수 있어야 한다.
또, 단순 decide 로직은 어렵지 않아서 커스터마이징하여 사용할 수도 있을 것 같다.

AccessDisicionManager의 구현체

위 처럼 AccessDisicionManager은 다양한 전략이 존재하는데, 어떤 구현체들이 있는지 알아보자.

AffirmativeBased

  • 승인이 하나라도 있으면 종료한다. 위 사진처럼 case가 1인 경우 검증로직을 종료한다.

ConsensusBased

  • 승인이 과반수 이상일 때 종료되는 디시전매니저다.
  • grant와 deny의 수를 기반으로 검증하게 되는데 grant가 승인의 수, deny가 거절의 수로 볼 수 있다.
  • if문의 로직이 핵심이다. allowIfEqualGrantedDeniedDecisions을 통해서 동점일 때 처리를 어떻게 할지 전략을 정할 수 있다. 거기에 따른 동점전략 setter가 존재한다.

UnanimousBased

  • 권한이 하나라도 없으면 실패한다.
  • ACCESS_ABSTAIN(기권)은 신경 쓰지 않지만, 1개 이상의 승인은 필요한 특징이 있다.

즉 우리는 상황에 맞는 AccessDisicionManager를 사용하여 투표전략을 어떻게 정할지 선택한다. 도메인 상황에 따라 Voter로 인증에 구체적인 로직을 넣을 수 있다. 그 Voter들을 Manager에 주입하여 사용하면 된다.

Voter 구현체


(권한을 등록하는 사진)

디폴트 Voter인 WebExpressionVoter를 알아보자.
현재 인증된 사용자가 가지고 있는 권한이 ROLE_XXXX와 매치되는지 확인한다. 즉 실질적으로 WebExpressionVoter을 통해서 권한을 확인하는 걸 알 수 있다.
(위 사진에 등록된 권한이 Request User의 권한이 일치하는지 확인한다.)


WebExpressionVoter의 구현체다. weca 객체를 통해 권한을 확인하고, 권한이 일치하면 1, 아니면 -1을 주는 모습을 볼 수 있었다.
(@Override를 안 쓰고, 인터페이스의 static변수 활용하지 않는 부분이 코드에서 아쉽다..ㅎㅎ)

정리하자면

  1. configure() 메서드를 오버라이딩 했을 때, URL별 권한을 설정할 수 있다. 알고 보면 디폴트 디시전매니저인 AccessDicisionManager가 디폴트 Voter인 WebExpressionVoter에서 실질적으로 Request의 권한을 확인하는걸 볼 수 있었다. 또 이런 디시전매니저는 FilterSecurityInterceptor 필터가 호출해준다.
  2. 디시전매니저는 다양한 전략이 있고, 프로젝트에서 어떤 전략으로 인기처리를 할지 고민하여 사용하면 될 것 같다.
  3. Voter는 요청과 인증자의 정보를 가지고, 통과시켜줄지 투표시켜주는 객체다. 개발자가 적절히 판단하여 로직(필요하다면 비즈니스 로직까지)을 넣어 요청에 대해 투표를 할 수 있다.

근데 Voter에서 어느정도 비즈니스로직이 들어 갈 것 같다. '너무 많은 Voter를 사용하여 인가부분을 위임하면 프로그램의 흐름을 읽기 어렵지 않을까?' 라는 생각이 든다.

스프링 AOP에서도 중복코드와 비즈니스코드를 잘 분리할 수 있지만, 로직의 순서나 코드의 분리 측면에서 이해하기 힘든 단점이 있다고 생각했는데, Voter로 비슷함을 느꼈다.

인가를 다루려니 생각보다 '내가 제대로 알고 있는 게 맞을까?'라고 생각하며 더 공부하여 포스팅할 수 있었다. 아직도 두루뭉술한 부분이 많은 것 같고, 부족한 내용은 더 퇴고하여 글을 더 성장시키고 싶다.

시큐리티 쉽지 않다. 너

profile
쉽게쉽게

1개의 댓글

comment-user-thumbnail
2020년 12월 2일

오. 추가 자료 링크들 까지!

답글 달기