개요
- SecurityConfig 파일에서 하나하나
mvcMatchers("/admin").hasRole("ADMIN")
이렇게 처리해 주지 않고, 동적으로 처리해주고 싶을 때가 있을것이다. - Spring Security 를 통해서 동적으로 권한을 부여해 줄 수 있도록 커스텀할 수 있다.
인가 처리 방식
- 아래 그림은 인가 처리 방식을 나타낸 그림이다.
- FilterInvocation
- 요청 정보를 담는 객체이다.
- FilterInvocation 객체는 FilterSecurityInterceptor 의 doFilter 메소드 안에 있다.
- List attributes
- 권한 정보를 담는 객체이다.
- this.obtainSecurityMetadataSource().getAttribute(object); 에서 권한 정보를 가져온다.
- Authentication, FilterInvocation, attributes 정보를 AccessDecisionManager 에게 전달한다.
SecurityMetadataSource
- SecurityMetadataSource 는 최상의 인터페이스 로써, getAttributes, getAllConfigAttributes, supports 메소드를 가지고있다. -> 이 메소드를 재정의 해줌으로써 커스텀할 수 있다.
- FilterInvocationSecurityMetadataSource
- Url 권한 정보를 추출하는 인터페이스이다.
- MethodSecurityMetadataSource
- Method 권한 정보를 추출하는 인터페이스이다.
- Method 권한 정보를 추출하는 인터페이스이다.
FilterInvocationSecurityMetadataSource
- Url 권한 정보를 추출하는 인터페이스이다.
- FilterInvocationSecurityMetadataSource 인터페이스를 구현한 클래스를 정의함으로써 커스텀할 수 있다.
- RequestMap 객체를 가지고 있다. 이는
url : 권한목록
의 정보들이 담겨있는 map 이다. - 사용자가 접근하고자 하는 Url 자원에 대한 권한 정보 추출한다.
- AccessDecisionManager 에게 전달하여 인가처리 수행한다.
- DB 로부터 자원 및 권한 정보를 매핑하여 맵으로 관리한다.
- 사용자의 매 요청마다 요청정보에 매핑된 권한 정보 확인한다.
- 사용자에게 요청이 들어오면, FilterInvocationSecurityMetadataSource 에서 RequestMap 에 저장된 권한 정보를 불러와서 AccessDecisionManager 에게 전달해준다.
FilterInvocationSecurityMetadataSource 구현 코드
- UrlFilterInvocationSecurityMetadatsSource 클래스 구현
@Slf4j
public class UrlFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
private LinkedHashMap<RequestMatcher, List<ConfigAttribute>> requestMap ;
private SecurityResourceService securityResourceService;
public UrlFilterInvocationSecurityMetadataSource(LinkedHashMap<RequestMatcher, List<ConfigAttribute>> resourcesMap, SecurityResourceService securityResourceService) {
this.requestMap = resourcesMap;
this.securityResourceService = securityResourceService;
}
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
HttpServletRequest request = ((FilterInvocation) object).getRequest();
if(requestMap != null){
for(Map.Entry<RequestMatcher, List<ConfigAttribute>> entry : requestMap.entrySet()){
RequestMatcher matcher = entry.getKey();
if(matcher.matches(request)){
return entry.getValue();
}
}
}
return null;
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
Set<ConfigAttribute> allAttributes = new HashSet<>();
for (Map.Entry<RequestMatcher, List<ConfigAttribute>> entry : requestMap
.entrySet()) {
allAttributes.addAll(entry.getValue());
}
return allAttributes;
}
@Override
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
public void reload(){
LinkedHashMap<RequestMatcher, List<ConfigAttribute>> reloadedMap = securityResourceService.getResourceList();
Iterator<Map.Entry<RequestMatcher, List<ConfigAttribute>>> iterator = reloadedMap.entrySet().iterator();
requestMap.clear();
while(iterator.hasNext()){
Map.Entry<RequestMatcher, List<ConfigAttribute>> entry = iterator.next();
requestMap.put(entry.getKey(), entry.getValue());
}
}
}
- FilterInvocationSecurityMetadataSource 인터페이스를 구현한 클래스 이다.
- 실제로 구현할 메소드는 getAttributes 이기 때문에 getAllConfigAttributes, supports 는 DefaultFilterInvocationSecurityMetadataSource 클래스에서 가져왔다.
- requestMap 에 권한 정보를 세팅해 준다. -> 그 아래에서 해당 url과 매치 되는 권한을 리턴해 준다.
- reload 메소드를 통해서 실시간으로 변경된 권한정보를 갱신해준다. -> requestMap 비워주고, 최신 정보로 갱신한다.
- SecurityConfig 설정
@Bean
public FilterSecurityInterceptor customFilterSecurityInterceptor() throws Exception {
FilterSecurityInterceptor filterSecurityInterceptor = new FilterSecurityInterceptor();
filterSecurityInterceptor.setSecurityMetadataSource(urlFilterInvocationSecurityMetadataSource());
filterSecurityInterceptor.setAccessDecisionManager(affirmativeBased());
filterSecurityInterceptor.setAuthenticationManager(authenticationManagerBean());
return filterSecurityInterceptor;
}
private AccessDecisionManager affirmativeBased() {
AffirmativeBased affirmativeBased = new AffirmativeBased(getAccessDecistionVoters());
return affirmativeBased;
}
private List<AccessDecisionVoter<?>> getAccessDecistionVoters() {
return Arrays.asList(new RoleVoter());
}
@Bean
public FilterInvocationSecurityMetadataSource urlFilterInvocationSecurityMetadataSource() {
return new UrlFilterInvocationSecurityMetadatsSource();
}
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
- FilterInvocationSecurityMetadataSource, AccessDecisionManager, AuthenticationManager 를 설정해준다.
.and()
.addFilterBefore(customFilterSecurityInterceptor(), FilterSecurityInterceptor.class)
- FilterSecurityInterceptor 앞에 직접 커스텀한 customFilterSecurityInterceptor 를 위치시킨다.
- 그렇다면 FilterSecurityInterceptor 가 두번 실행 되지 않을까? -> 그렇지 않다. -> 한번 인가 처리가 되었으면 다음번엔 통과한다.
UrlResourcesMapFactoryBean
- DB 연동 Map 기반으로 권한/ 자원 정보로 만들때 사용된다.
- UrlResourcesMapFactoryBean
- DB로 부터 얻은 권한/자원 정보를 -> ResourceMap 으로 만들고, 빈으로 생성해서 UrlFilterInvocationSecurityMetadataSource 에 전달한다.
- 코드 예시
public class UrlResourcesMapFactoryBean implements FactoryBean<LinkedHashMap<RequestMatcher, List<ConfigAttribute>>> {
private SecurityResourceService securityResourceService;
private LinkedHashMap<RequestMatcher, List<ConfigAttribute>> resourceMap;
public void setSecurityResourceService(SecurityResourceService securityResourceService) {
this.securityResourceService = securityResourceService;
}
@Override
public LinkedHashMap<RequestMatcher, List<ConfigAttribute>> getObject() throws Exception {
if(resourceMap == null){
init();
}
return resourceMap;
}
private void init() {
resourceMap = securityResourceService.getResourceList();
}
@Override
public Class<?> getObjectType() {
return LinkedHashMap.class;
}
@Override
public boolean isSingleton() {
return true;
}
}
- isSingeton 메소드의 반환값을 true 로 지정해서 싱글톤타입으로 구성할 수 있다.
- resourceMap 이 null 인 경우 resourceMap 을 가져올 수 있는 securityResourceService 를 호출한다.
- securityResourceService 의 getResourceList 메소드에서 resourceMap 정보를 가져오는 코드는 아래와 같다.
public LinkedHashMap<RequestMatcher, List<ConfigAttribute>> getResourceList(){
LinkedHashMap<RequestMatcher, List<ConfigAttribute>> result = new LinkedHashMap<>();
List<Resources> resourcesList = resourcesRepository.findAllResources();
resourcesList.forEach(re ->{
List<ConfigAttribute> configAttributeList = new ArrayList<>();
re.getRoleSet().forEach(role -> {
configAttributeList.add(new SecurityConfig(role.getRoleName()));
});
result.put(new AntPathRequestMatcher(re.getResourceName()),configAttributeList);
});
return result;
}
- resourcesRepository.findAllResources() 는 아래와 같이 구현되어 있다.
@Query("select r from Resources r join fetch r.roleSet where r.resourceType = 'url' order by r.orderNum desc")
List<Resources> findAllResources();
PermitAllFilter 구현
public class PermitAllFilter extends FilterSecurityInterceptor {
private static final String FILTER_APPLIED = "__spring_security_filterSecurityInterceptor_filterApplied";
private List<RequestMatcher> permitAllRequestMatcher = new ArrayList<>();
public PermitAllFilter(String... permitAllPattern) {
createPermitAllPattern(permitAllPattern);
}
@Override
protected InterceptorStatusToken beforeInvocation(Object object) {
boolean permitAll = false;
HttpServletRequest request = ((FilterInvocation) object).getRequest();
for (RequestMatcher requestMatcher : permitAllRequestMatcher) {
if (requestMatcher.matches(request)) {
permitAll = true;
break;
}
}
if (permitAll) return null;
return super.beforeInvocation(object);
}
@Override
public void invoke(FilterInvocation fi) throws IOException, ServletException {
InterceptorStatusToken token = beforeInvocation(fi);
}
private void createPermitAllPattern(String... permitAllPattern) {
for (String pattern : permitAllPattern) {
permitAllRequestMatcher.add(new AntPathRequestMatcher(pattern));
}
}
}
- FilterSecurityInterceptor 를 상속받고 invoke 메소드를 재정의한다.
- invoke() 에서 FilterSecurityInterceptor 클래스의 부모 클래스인 AbstractSecurityInterceptor 의 beforeInvocation 를 아래와 같이 재정의 한다.
- 생성자로 permitAllRequestMatcher 에 요청 정보를 넣어준다.
- permitAllRequestMatcher 와 요청 정보를 비교해서 permitAll 처리를 해줄 요청을 검사한다.
REFERENCES
- 정수원님의 스프링 시큐리티
'Spring Security' 카테고리의 다른 글
ProxyFactory 를 이용한 동적 Method 인가 처리 (0) | 2022.02.28 |
---|---|
Method 시큐리티 프로세스 커스텀 (0) | 2022.02.28 |
권한 계층 적용하기 (0) | 2022.02.28 |
Voter 커스텀 하기 (0) | 2022.02.28 |
@AuthenticationPrincipal (0) | 2022.02.28 |