인가(Authorization)를 요청받은 사용자에 대해서 적절한 권한을 가지고 있는지 확인 후 인가 허용을 결정하는 컴포넌트인
ConnectionBasedVoter
를 만들 것이다.
package com.august.soil.api.security;
security
패키지 아래에 만들어 준다.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
는 만들어져 있는 객체를 자동으로 주입받도록 했다. 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
도 빈으로 등록해 주자!connectionBasedVoter()
에서 사용자 PK만 추출해 유효한 사용자라면 접근 권한을 부여하려 했는데 이 포스트를 쓰면서 보니까 지금은 쓰고 있지 않은 기능이었다. 왜냐면 JwtAuthenticaion
에서 사용자 PK 정보를 얻어서 처리하기 때문에... 여기까지 올 일이 없다. 다음 포스트에서 계속...