나의 이전 글에서는 주로 Authentication
을 중점으로 다루었다.
Authentication
은 쉽게 말하면 로그인이다.
Authorization
은 권한 체크이다.
예를 들면, Authentication
은 사원증 발급이고, Authorization
은 경비 아저씨가 출입을 체크하는 것이다.
처음에는 나도 인증과 인가의 과정이 굉장히 헷갈렸는데, 쉽게 말하면, 인증(Authentication)은 로그인 정보를 받아서 인증된 Authenticaiton
을 만드는 과정이다. (인증되지 않은 Authentication
도 존재한다.)
이 Authentication
에는 Authorities
즉 역할이 존재한다.
이 역할을 이용해서 (또는 다른 정보도 같이 이용해서) 접근을 제어하는 것이 Authorization
이라고 생각 하면 된다.
//Authentication
Collection<? extends GrantedAuthority> getAuthorities();
이 역할은 GrantedAuthority
의 Collection으로 저장된다.
public interface GrantedAuthority extends Serializable {
String getAuthority();
}
GrantedAuthority
는 메서드가 하나뿐인 인터페이스이다. 이는 String
을 반환한다.
만약 역할이 String
으로 표현될 수 없으면 null
을 반환하고 이 역할을 표현할 수 있는 방법을 구현해야한다.
그리고 이를 사용하는 AccessDecisionManager
에 이를 읽어드릴 수 있는 방법을 구현해야한다.
이 과정에 대해서 아래에서 상술할 것이다.
이 시작지점은 크게 두가지가 있다. FilterSecurityInterceptor
, AuthorizationFilter
FilterSecurityInterceptor
의 경우 AuthorizationFilter
로 대체되었다.
https://docs.spring.io/spring-security/reference/servlet/authorization/authorize-requests.html
아래에서는 AuthorizationFilter
만 서술하겠다.
여기서 시작지점은 SecurityFilterChain
안에 있는 AuthorizationFilter
이다.
등록 방법은
@Bean
SecurityFilterChain web(HttpSecurity http) throws AuthenticationException {
http
// .authorizeRequests(authorize -> authorize 이건 대체된 된 FilterSecurityInterceptor 를 사용할 때 설정
.authorizeHttpRequests(authorize -> authorize // SecurityFilterChain 등록
.requestMatchers("/resources/**", "/signup", "/about").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/**").access(new TestAuthorize())
)
// ...
return http.build();
}
이렇게 등록하면 된다.
@FunctionalInterface
public interface AuthorizationManager<T> {
AuthorizationDecision check(Supplier<Authentication> authentication, Object secureObject);
default AuthorizationDecision verify(Supplier<Authentication> authentication, Object secureObject)
throws AccessDeniedException {
AuthorizationDecision decision = check(authentication, object);
if (decision != null && !decision.isGranted()) {
throw new AccessDeniedException("Access Denied");
}
}
AuthorizationManager
는 Authorization
처리를 담당한다. 마치 AuthenticationManager
과 비슷하다.
두가지 메서드가 있는데, verify
메서드가 내부적으로 check
를 호출하여 사용한다.
이 check
는 구현체에서 반드시 구현되어야 한다. 그리고 AuthorizationDecision
을 반환한다.
이 AuthorizationDecision
은 매우 간단하다.
public class AuthorizationDecision {
private final boolean granted;
public AuthorizationDecision(boolean granted) {
this.granted = granted;
}
public boolean isGranted() {
return this.granted;
}
@Override
public String toString() {
return getClass().getSimpleName() + " [granted=" + this.granted + "]";
}
}
생성자로 boolean granted
를 넣어서 생성하고, isGranted
로 인증 여부를 확인할 수 있다.
check
메서드를 통해 반환된 AuthorizationDecision
을 verify
메서드가 체크해서
실패하면 AccessEdniedException
을 발생시킨다.
Authorization
에는 많은 종류가 있다. 예를 들어 주소별로 다른 권한 부여가 필요하다면, 각각 이를 컨트롤하고 매칭시키기는 힘들것이다.
이를 가운데에서 중재하고 알맞은 AuthorizationManager
로 연결시켜주는 것이 바로 RequestMatcherDelegatingAuthorizationManager
이 존재하는 이유이다.
(AuthorizationManager
의 자손이다.)
이 클래스는 Request에 알맞은 AuthorizationManager
를 매칭시켜준다.
//RequestMatcherDelegatingAuthorizationManager extend AuthorizationManager
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, HttpServletRequest request) {
// for루프를 통해 하나씩 확인
for (RequestMatcherEntry<AuthorizationManager<RequestAuthorizationContext>> mapping : this.mappings) {
RequestMatcher matcher = mapping.getRequestMatcher();
MatchResult matchResult = matcher.matcher(request);
// request에 match 되는지 확인
if (matchResult.isMatch()) {
//적절한 AuthorizationManager를 받음
AuthorizationManager<RequestAuthorizationContext> manager = mapping.getEntry();
//그 Manager의 check메서드를 실행시키고 그 반환값을 반환함.
return manager.check(authentication,
new RequestAuthorizationContext(request, matchResult.getVariables()));
}
}
return DENY;
}
즉 아래와 같이 설정할때 내가 커스텀으로 만든TestAuthorize()
은 저 Matcher와 함께 RequestMatcherDelegatingAuthorizationManager
에 저장되게 된다.
@Bean
SecurityFilterChain web(HttpSecurity http) throws AuthenticationException {
http
// .authorizeRequests(authorize -> authorize 이건 대체된 된 FilterSecurityInterceptor 를 사용할 때 설정
.authorizeHttpRequests(authorize -> authorize // SecurityFilterChain 등록
.requestMatchers("/resources/**", "/signup", "/about").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/**").access(new TestAuthorize())
)
// ...
return http.build();
}
최종모습
위의 SecurityFilterChain
설정에 hasRole
과 같은 설정도 이제 각각의 Authorization
을 진행하는 AuthorizationManager
이다.
즉 각각의 Matchers에 AuthorizationManager
를 배정해주면 RequestMatcherDelegatingAuthorizationManager
가 요청에 따라 적절히 매칭해주어서 Authorization
이 일어나는 것이다.
아래는 내가 만든 커스텀 AuthorizationManager
이다. 이걸 등록한 것이다.
무조건 false를 줌으로써 무조건 거부한다.
public class TestAuthorize implements AuthorizationManager<RequestAuthorizationContext> {
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext object) {
Authentication a = authentication.get(); // 이걸 살펴보았다.
AuthorizationDecision authorizationDecision = new AuthorizationDecision(false);
return authorizationDecision;
}
}
그렇다면 인증을 거치지 않은 사용자가 이 필터를 타게된다면, authentication 은 null
이 될까?
정답은 그렇지 않다.
바로 annonymous 유저가 반환된다. 이는 ROLE_ANNONYMOUS 를 가진 유저로써 이에 대한 설명은 내용을 벗어나므로 자세히 설명하지는 않겠다.
나는 처음에 JWT Token을 인증 방식에서 Authorization 에 AuthorizationManager
을 쓰려고 했다.
하지만 이번 공부를 하면서 그건 좋은 방식이 아니라는 것을 느꼈다.
그 이유 첫번째는 AuthorizationManager에 올 때는 이미 Authentication
을 주입 받은 상태이다. 특히 JWT 토큰은 익명 사용자 Authentiaction을 주입 받겠지만, 이는 SpringSecurity
의 메커니즘에 위배된다.
즉 비정상적이다.
두번째는 이렇게 해서 그냥 isGranted
만 통과하게 한다면, 추후에 Principal을 뽑는다는가 하는 전체적인 메커니즘이 깨지게 된다.
그래서 JWT같은 비세션방식 을 이용할때는 이미 AuthorizeManager에 등록하기 전에 이미 SecurityContextHolder
에 Authentication
이 이미 존재하는 상태여야 한다.
그래야 자연스러운 흐름이 전개된다.
안녕하세요.
인가 관련해서 커스텀하려고 정보를 찾다가 작성하신 글을 봤는데
머리속에 정리가 너무 잘되었습니다. 감사합니다. :)