0. 소스 코드 수정
강의에서 DB 연동 처리를 하기 위해서 JPA를 사용하였다. 그런데 2년 전 강의라 그런지, Entity 구조상 @ManyToMany 등 JPA 안티패턴이 있고 잘못된 부분들이 좀 있어서 Entity 및 전체 구조를 적절히 수정했다.
아래 주소의 레포지토리 - master branch에 수정된 파일들을 담았다. (원본은 keep 브랜치)
https://github.com/CJ0823/corespringsecurity
복습도 할겸, thymeleaf와 controller 코드를 잠시 살펴보자.
강의 소스코드에는 권한을 등록하는 admin/role/register 주소의 view 파일이 존재하지 않았다. 그래서 detail.html 파일을 복사해서 조금 바꿔주었다.
register.html 파일의 일부
여기서 th:object = "${role}" 이라고 표시하고 하위의 th:value="*{roleName}"이라고 표기했다. 이것은 controller에서 전달받은 Model 객체 내부에 있는 role 속성과 role.roleName의 값을 설정하여 <form> 태그 형태로 서버에 제출한다는 의미가 된다.
<div class="tbl_wrp">
<form class="form-horizontal" th:action="@{/admin/roles/register}" method="post" th:object="${role}">
<div class="form-group">
<label for="roleName" class="col-sm-2 control-label">권한명</label>
<div class="col-sm-10">
<input type="text" class="form-control input-large" name="roleName" id="roleName" placeholder="roleName" th:value="*{roleName}">
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-1 col-sm-10">
<button type="Submit" class="btn btn-dark btn-lg">등록</button>
<a class="btn btn-dark btn-lg" style="margin:10;" th:href="@{/admin/roles}">목록 </a>
</div>
</div>
</form>
</div>
그래서, Controller에서는 model 객체를 인자로해서 아래와 같이 작성해주었다.
RoleController.java의 일부
"role" 이라는 이름으로 빈 RoleDto 객체를 view에 전달한다.
@GetMapping(value="/admin/roles/register")
public String viewCreateRole(Model model) throws Exception {
model.addAttribute("role", new RoleDto());
return "admin/role/register";
}
Controller-View 간 정보 전달의 기본이다. 잘 기억하자.
1. DB 연동 인가처리의 의미
이 때까지의 권한 처리는 antMatcher("/user").hasRole("USER") 와 같이 하드 코딩으로 설정 파일에 직접 권한 관련 코드를 작성하여 처리했었다. 그러나 실제 서비스에서는 수많은 유저와 role, 자원에 대한 권한 처리를 해야되기 때문에 이렇게 직접 코딩을 하는 방식을 적용하는 것은 어렵다. 따라서 동적 권한 처리를 하기 위해서 DB와 연동한 인가처리를 한다.
2. DB 연동 처리하기 : FilterInvocationMeatadataSource - 기초 구현
기본 작동 원리
FilterSecurityInterceptor가 AccessDecisionManager에게 인증, 요청, 권한 정보를 담아서 전달해주어 인증 및 인가 처리를 할 수 있게 된다.
기본적으로 FilterChainProxy에 포함되어 있는 FilterSecurityInterceptor에서 사용자로부터 요청받은 url 정보를 갖고 FilterInvocationSecurityMetadataSource의 RequestMap 객체를 확인하여 url에 매핑되어 있는 권한 정보를 확인한다. 그리고 RequestMap에 권한 정보가 있다면 AccessDecisionManager로 Authentication, FilterInvocation, List<ConfigAttribute>를 넘겨서 인가처리를 하게 된다. 여기서 유의할 점은 RequestMap상에 url에 대한 권한 매핑 정보가 없다면 어떤 권한으로 접근을 하든 접근을 허용해준다.
실습
metadatasource 패키지를 생성하고 UrlFilterInvocationMetadataSource 파일을 만든다. 코드는 아래와 같다. getAttributes() 메서드만 직접 작성하고, 나머지 메서드 코드는 복사해올 것이다.
public class UrlFilterInvocationMetadataSource implements FilterInvocationSecurityMetadataSource {
private LinkedHashMap<RequestMatcher, List<ConfigAttribute>> requestMap = new LinkedHashMap<>();
@Override
public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
HttpServletRequest request = ((FilterInvocation) o).getRequest();
requestMap.put(new AntPathRequestMatcher("/mypage"), Arrays.asList(new SecurityConfig("ROLE_USER"))); //테스트용. 향후 DB에서 삽입
if(!Objects.isNull(requestMap)) {
return requestMap.entrySet().stream().map(entry -> {
RequestMatcher matcher = entry.getKey();
if(matcher.matches(request)){
return entry.getValue(); //권한 정보
} else {
return null;
}
})
.filter(Objects:nonNull)
.flatMap(Collection::stream)
.collect(Collectors.toList());
}
return null;
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
Set<ConfigAttribute> allAttributes = new HashSet();
Iterator var2 = this.requestMap.entrySet().iterator();
while(var2.hasNext()) {
Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry = (Map.Entry)var2.next();
allAttributes.addAll((Collection)entry.getValue());
}
return allAttributes;
}
@Override
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}
FilterInvocationSecurityMetadataSource를 implements 한다. 그러면 3개의 메서드를 Override 하도록 되어있는데, 실습해볼 부분은 getAttributes() 메서드이다. getAllConfigAttributes()와 supports(Class<?> clazz) 부분은 같은 인터페이스를 상속하고 있는 DefaultFilterInvocationSecurityMetadataSource의 내용을 복사한다.
getAttributes()
앞서 살펴본 것처럼 FilterInvocationSecurityMetadataSource가 나중에 FilterSecurityInterceptor에 의해 호출될텐데, 그 때 url-권한간 매핑 정보인 requestMap 객체를 정의한다. 순서를 갖도록 LinkedHashMap으로 정의하고, 타입은 <RequestMatcher, List<ConfigAttribute>>로 정의하였다. 자세한건 나중에.
인자로 받는 object는 FilterInvocation 타입으로 캐스팅 후에 .getRequest() 메서드를 호출하여 HttpServletRequest 객체로 뽑아올 수 있다. FilterInvocation 객체는 Invocation(:무언가를 불러오는 행동이나 양식)이라는 단어의 뜻처럼, Filter를 갖고 있어서 불러올 수 있는 역할을 하는 객체이다. request, response, FilterChain을 갖고 있어서, 앞서 살펴봤던 FilterChainProxy에서도 FilterChain을 불러올 때 이 객체를 활용한다.(FilterChainProxy의 getFilters() 메서드)
-----------------------------------------------
public class FilterInvocation {
static final FilterChain DUMMY_CHAIN = (req, res) -> {
throw new UnsupportedOperationException("Dummy filter chain");
};
private FilterChain chain;
private HttpServletRequest request;
private HttpServletResponse response;
... 중략
-----------------------------------------------
RequestMap.put 메서드를 통해서 /mypage url에 ROLE_USER 권한을 가진 사용자가 접근할 수 있도록 기본 데이터를 삽입해주었다. 나중에는 이렇게 하드 코딩으로 매핑 정보를 넣어주는 것이 아니라 DB 에서 정보를 가져와서 넣어준다.
RequestMap을 null 검사하고, 이후 url에 해당하는 권한 정보를 반환해준다. 이 경우 위 도식에서 살펴본 것처럼 인가처리를 AccessDecisionManager에게 위임하게 되고, RequestMap 객체가 null 인 경우 인가처리를 하지 않아서 모든 사용자에게 해당 페이지에 접근이 가능하도록 해준다.
SecurityConfig 설정처리
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
// .antMatchers("/mypage").hasRole("USER")
// .antMatchers("/messages").hasRole("MANAGER")
// .antMatchers("/config").hasRole("ADMIN")
.antMatchers("/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/login_proc")
.defaultSuccessUrl("/")
// .authenticationDetailsSource(authenticationDetailsSource)
.successHandler(formAuthenticationSuccessHandler)
.failureHandler(formAuthenticationFailureHandler)
.and()
.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler())
//////////////////
.and()
.addFilterBefore(customFilterSecurityInterceptor(), FilterSecurityInterceptor.class)
//////////////////
;
}
configure 메서드 부분의 맨 아래에 .and()와 .addFilterBefore(customFilterSecurityInterceptor(), FilterSecurityInterceptor.class)를 추가한다. customFilterSecurityInterceptor는 FilterSecurityInterceptor의 구현체로 따로 클래스 파일로 작성할 것인데, 다음 부분에서 해당 @Bean의 코드를 살펴볼 것이다. 다만 이렇게 설정함으로써 FilterChainProxy에서 기존 FilterSecurityInterceptor앞 부분에 우리가 작성할 필터가 위치하여 인가처리를 해준다. FilterChainProxy의 doFilter 메서드에 break point를 걸고 서버에 접속 시, this.additionalFilters에 FilterSecurityInterceptor가 추가된 것을 확인할 수 있다.
customFilterSecurityInterceptor는 여태 작성해왔던 설정 파일인 SecurityConfig 파일에 @Bean으로 아래와 같이 등록한다.
@Bean
public FilterSecurityInterceptor customFilterSecurityInterceptor() throws Exception {
FilterSecurityInterceptor filterSecurityInterceptor = new FilterSecurityInterceptor();
//3가지 속성을 설정해주어야 한다.
filterSecurityInterceptor.setSecurityMetadataSource(urlFilterInvocationSecurityMetadataSource());
filterSecurityInterceptor.setAccessDecisionManager(affirmativeBased());
filterSecurityInterceptor.setAuthenticationManager(authenticationManagerBean());
return filterSecurityInterceptor;
}
private AccessDecisionManager affirmativeBased() {
return new AffirmativeBased(getAccessDecisionVoters());
}
private List<AccessDecisionVoter<?>> getAccessDecisionVoters() {
return Arrays.asList(new RoleVoter());
}
@Bean
public FilterInvocationSecurityMetadataSource urlFilterInvocationSecurityMetadataSource() {
return new UrlFilterInvocationMetadataSource();
}
주석문과 같이 3가지를 설정해주어야 한다. SecurityMetadataSource는 앞에서 작성했던 urlFilterInvocationSecurityMetadataSource로 설정한다. AccessDecisionManager는 여러 전략 중 affirmativeBased를 선택하여 설정해준다. url에 매칭되는 권한 리스트 중 1개만 존재해도 인가를 해주는 전략이다. 뒤에서 더 자세하게 배운다. 마지막으로 authenticationManagerBean은 기본적으로 추가되는 AuthenticationManager라고 보면 된다.
참조
1) 인프런 - 스프링 시큐리티 - Spring Boot 기반으로 개발하는 Spring Security - 정수원님 강의
https://www.inflearn.com/course/%EC%BD%94%EC%96%B4-%EC%8A%A4%ED%94%84%EB%A7%81-%EC%8B%9C%ED%81%90%E
'Programming-[Backend] > Spring Security' 카테고리의 다른 글
[스프링 시큐리티]20. 실시간 권한 업데이트, 허용필터 (0) | 2022.02.20 |
---|---|
[스프링 시큐리티]19. DB 연동 인가처리 -FilterInvocationSecurityMetadataSource 구현 (0) | 2022.02.16 |
[스프링 시큐리티] 17. Ajax 인증 구현 - DSL, CSRF (0) | 2022.01.23 |
[스프링 시큐리티] 16. Ajax 인증 구현 - 인증, 인가 처리 (0) | 2022.01.23 |
[스프링 시큐리티] 15. Ajax 인증 구현 - AbstractAuthenticationProcessingFilter, Token, AuthenticationProvider (2) | 2022.01.20 |