네이버 로그인 API를 활용한 소셜로그인 구현 과정(with. Spring Security, JWT) - 6. ConnectionBasedVoter class 생성

Sia Hwang·2022년 11월 30일
0

인가(Authorization)를 요청받은 사용자에 대해서 적절한 권한을 가지고 있는지 확인 후 인가 허용을 결정하는 컴포넌트인 ConnectionBasedVoter를 만들 것이다.

Location

package com.august.soil.api.security;
  • security 패키지 아래에 만들어 준다.

ConnectionBasedVoter class

import com.august.soil.api.model.commons.Id;
import com.august.soil.api.model.user.User;
import com.august.soil.api.service.user.UserService;
import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.util.matcher.RequestMatcher;

import javax.servlet.http.HttpServletRequest;
import java.util.Collection;
import java.util.function.Function;

import static com.google.common.base.Preconditions.*;
import static org.apache.commons.lang3.ClassUtils.isAssignable;

public class ConnectionBasedVoter implements AccessDecisionVoter<FilterInvocation> {
  
  private final RequestMatcher requiresAuthorizationRequestMatcher;
  
  private final Function<String, Id<User, Long>> idExtractor;
  
  private UserService userService;
  
  public ConnectionBasedVoter(RequestMatcher requiresAuthorizationRequestMatcher, Function<String, Id<User, Long>> idExtractor) {
    checkArgument(requiresAuthorizationRequestMatcher != null, "requiresAuthorizationRequestMatcher must be provided.");
    checkArgument(idExtractor != null, "idExtractor must be provided.");
    
    this.requiresAuthorizationRequestMatcher = requiresAuthorizationRequestMatcher;
    this.idExtractor = idExtractor;
  }
}
  • AccessDecisionVoter<FilterInvocation> 인터페이스를 구현한 클래스를 만든다.
  • 생성자 작성 시 final 키워드가 붙은 멤버 변수들의 값 유효성 검사를 한 뒤 대입되도록 한다. 이번 프로그래머스 백엔드 스터디 강의를 들으면서 com.google.common.base 패키지를 알게 되었는데 이것 외에도 유용한 클래스가 많아서 좀 더 공부 해봐야겠다는 생각을 많이 했다.
import com.august.soil.api.model.commons.Id;
import com.august.soil.api.model.user.User;
import com.august.soil.api.service.user.UserService;
import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.util.matcher.RequestMatcher;

import javax.servlet.http.HttpServletRequest;
import java.util.Collection;
import java.util.function.Function;

import static com.google.common.base.Preconditions.*;
import static org.apache.commons.lang3.ClassUtils.isAssignable;

public class ConnectionBasedVoter implements AccessDecisionVoter<FilterInvocation> {
  
  ...
  
  @Override
  public int vote(Authentication authentication, FilterInvocation fi, Collection<ConfigAttribute> attributes) {
    HttpServletRequest request = fi.getRequest();
    
    // 인가 가능한 사용자면 접근 권한을 부여한다.
    if (!requiresAuthorization(request))
      return ACCESS_GRANTED;
    
    // JwtAuthenticationToken 생성이 불가능하다면 접근 거부한다.
    if (!isAssignable(JwtAuthenticationToken.class, authentication.getClass()))
      return ACCESS_ABSTAIN;
      
    /* 필요없는 부분
    JwtAuthentication jwtAuth = (JwtAuthentication) authentication.getPrincipal();
    // 요청받은 URI에서 ID를 얻는다.
    Id<User, Long> targetId = obtainTargetId(request);
    
    // jwt 인증정보와 비교해서 URI에서 얻은 ID가 본인이라면(현재 인증되어 토큰이 발급된 사용자) 접근 권한 부여
    if (jwtAuth.id.equals(targetId))
      return ACCESS_GRANTED; */
    
    return ACCESS_DENIED;
  }
  
  private Id<User, Long> obtainTargetId(HttpServletRequest request) {
    return idExtractor.apply(request.getRequestURI());
  }
  
  private boolean requiresAuthorization(HttpServletRequest request) {
    return requiresAuthorizationRequestMatcher.matches(request);
  }
}
  • 주어진 정보를 바탕으로 인가 요청중인 사용자를 허용하거나 거부하는 메서드인 vote()를 오버라이딩한다. 자세한 설명은 주석을 참고하자.
  • 이 클래스에서 처리하는 핵심 기능이다.
import com.august.soil.api.model.commons.Id;
import com.august.soil.api.model.user.User;
import com.august.soil.api.service.user.UserService;
import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.util.matcher.RequestMatcher;

import javax.servlet.http.HttpServletRequest;
import java.util.Collection;
import java.util.function.Function;

import static com.google.common.base.Preconditions.*;
import static org.apache.commons.lang3.ClassUtils.isAssignable;

public class ConnectionBasedVoter implements AccessDecisionVoter<FilterInvocation> {
  
  ...
  
  @Override
  public boolean supports(ConfigAttribute attribute) {
    return true;
  }
  
  @Override
  public boolean supports(Class<?> clazz) {
    return isAssignable(FilterInvocation.class, clazz);
  }
  
  @Autowired
  public void setUserService(UserService userService) {
    this.userService = userService;
  }
}
  • 마지막으로 인터페이스에 남은 메서드들을 오버라이딩 해 주었는데 저번 포스트에서 만들었던 메서드들과 기능이 비슷하기 때문에 설명은 생략하겠다.
  • userService는 만들어져 있는 객체를 자동으로 주입받도록 했다.

Entry Bean

  • 이제 WebSecurityConfigure에 빈으로 등록해 주자.
@Component
@EnableWebSecurity
@RequiredArgsConstructor
@Slf4j
public class WebSecurityConfigure {
  
  ...
  
  @Bean
  public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration)
    throws Exception {
    return authenticationConfiguration.getAuthenticationManager();
  }
  
  @Bean
  public ConnectionBasedVoter connectionBasedVoter() {
    final String regex = "^api/diaries/(\\d+)/.*$";
    final Pattern pattern = Pattern.compile(regex);
    RequestMatcher requiresAuthorizationRequestMatcher = new RegexRequestMatcher(pattern.pattern(), null);
    return new ConnectionBasedVoter(
      requiresAuthorizationRequestMatcher,
      (String url) -> {
        // url에서 targetId를 추출하기 위해 정규식 처리
        Matcher matcher = pattern.matcher(url);
        long id = matcher.matches() ? NumberUtils.toLong(matcher.group(1), -1) : -1;
        log.info("@@@@@ id: {}", id);
        return Id.of(User.class, id);
      }
    );
  }
}
  • 하는 김에 AuthenticationManager도 빈으로 등록해 주자!
  • API URL에서 사용자 PK 값을 받을 경우 connectionBasedVoter()에서 사용자 PK만 추출해 유효한 사용자라면 접근 권한을 부여하려 했는데 이 포스트를 쓰면서 보니까 지금은 쓰고 있지 않은 기능이었다. 왜냐면 JwtAuthenticaion에서 사용자 PK 정보를 얻어서 처리하기 때문에... 여기까지 올 일이 없다.
  • 이렇게 작성해 놓은 이유는 강의에서 얻은 샘플코드를 바탕으로 만들다 보니 처음에 틀만 만들어 놓자 하고 뒀다가, 까먹고 있었다가, 지금 발견하게 된 것으로 추측된다.
  • 그래서 정규식을 통해 ID를 얻는 부분은 지워도 되는데 나중에 다른 프로젝트를 진행할 때 참고할 작성법이 필요해질 수도 있으니까 여기에 남겨놓고 실제 비즈니스 코드에서는 삭제 해야겠다.

코드 전문 : https://github.com/miro7923/soil/blob/main/soil/src/main/java/com/august/soil/api/security/ConnectionBasedVoter.java

다음 포스트에서 계속...

profile
시키는 거 다 하는 개발 잡부입니다.

0개의 댓글

관련 채용 정보