본문 바로가기
Programming/>> Spring

[Spring] Spring기반 웹 사이트 템플릿 만들기 - 3. Spring Security 리소스 DB 연동

by 니키ᕕ( ᐛ )ᕗ 2018. 1. 23.

이 내용을 간략하게 저장하자면 configure 메소드내 authorizeRequests의 설정을 DB에 넣어버린다고 보면 될 듯.

 

URL에 설정된 Role정보에 따라 접근을 허가할지 말지를 판단하는 기능을 넣기 위해 FilterSecurityInterceptor 설정을 한다.

Egov에서는 메소드나 AOP 정보도 DB에 저장하여 연동할 수 있지만 그 수준은 일반 사용자가 제어할 수 있는 범위는 아니라고 생각하기 때문에 굳이 내 프로젝트에서는 할 이유가 없다고 생각한다.

 

 

 

FilterSecurityIntercepter는 SecurityFilterChain중 가장 마지막에 오고 인증이 완료된 뒤 자원에 대한 접근 여부를 판단하게 된다.

@Bean
public FilterSecurityInterceptor filterSecurityInterceptor() throws Exception {
	FilterSecurityInterceptor filterSecurityInterceptor = new FilterSecurityInterceptor();
	filterSecurityInterceptor.setAuthenticationManager(authenticationManagerBean());
	filterSecurityInterceptor.setSecurityMetadataSource(reloadableFilterInvocationSecurityMetadataSource());
	filterSecurityInterceptor.setAccessDecisionManager(affirmativeBased());
	return filterSecurityInterceptor;
}

위에 보이는 바와 같이 FilterSecurityInterceptor에는 3가지 정보가 필요하다.

 

1. 인증정보 -> 여러 Filter들을 거치며 인증된 정보.

2. 대상정보 -> 리소스, 자원, URL 등.

3. 판단주체 -> 해당 인증 정보가 리소스에 접근이 가능한지.

 

 

1. 인증 정보

@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
}

AuthenticationManager에 저장된 인증정보를 가져온다.

 

 

2. 대상정보

@Bean public FilterInvocationSecurityMetadataSource reloadableFilterInvocationSecurityMetadataSource() {
	return new ReloadableFilterInvocationSecurityMetadataSource(roleAndUrlResourcesMapLoader());
}

@Bean public RoleAndUrlResourcesMapLoader roleAndUrlResourcesMapLoader() {
	return new RoleAndUrlResourcesMapLoader();
}

public class RoleAndUrlResourcesMapLoader {
	
	@Autowired
	SecurityService securityService;
	
	/**
	 * ROLE의 계층 리스트를 가져온다.
	 * 
	 * @return URL과 ROLE의 매핑 리스트를 가져온다.
	 */
	public LinkedHashMap<RequestMatcher, List<ConfigAttribute>> getRoleAndUrlResourcesMap() {
		List<Map<String, Object>> resultList = securityService.getRolesAndUrl();
		return patchRequestMatcher(makeResourceMap(resultList));
	}
	
	
	/**
	 * makeLinkedHashMap의 결과 데이터에서 Object key를 RequestMatcher로 캐스팅한다.
	 * 
	 * @param data LinkedHashMap으로 변환된 데이터.
	 * @return 리턴값 설명.
	 */
	private LinkedHashMap<RequestMatcher, List<ConfigAttribute>> patchRequestMatcher(LinkedHashMap<Object, List<ConfigAttribute>> data) {
		LinkedHashMap<RequestMatcher, List<ConfigAttribute>> ret = new LinkedHashMap<RequestMatcher, List<ConfigAttribute>>();
		Set<Object> keys = data.keySet();
		
		for (Object key : keys) {
			ret.put((RequestMatcher) key, data.get(key));
		}
		return ret;
	}
	
	/**
     * 리소스 유형에 대한 할당된 롤 정보를 가져온다.
     *
     * @param resourceType
     * @return
     * @throws Exception
     */
	private LinkedHashMap<Object, List<ConfigAttribute>> makeResourceMap(List<Map<String, Object>> list) {

		String resourceType = "url";
		String authorityStr = "accessRole";
		
		Map<String, Object> tempMap;
        String preResource = null;
        String presentResourceStr;
        Object presentResource;
	
		LinkedHashMap<Object, List<ConfigAttribute>> resourcesMap = new LinkedHashMap<Object, List<ConfigAttribute>>();
      
		Iterator<Map<String, Object>> itr = list.iterator();
        while (itr.hasNext()) {
            tempMap = itr.next();

            presentResourceStr = (String) tempMap.get(resourceType);
            
            presentResource = new AntPathRequestMatcher(presentResourceStr);
            
            List<ConfigAttribute> configList = new LinkedList<ConfigAttribute>();

            // 이미 requestMap 에 해당 Resource 에 대한 Role 이 하나 이상 맵핑되어 있었던 경우,
            // sort_order 는 resource(Resource) 에 대해 매겨지므로 같은 Resource 에 대한 Role 맵핑은 연속으로 조회됨.
            // 해당 맵핑 Role List (SecurityConfig) 의 데이터를 재활용하여 새롭게 데이터 구축
            if (preResource != null && presentResourceStr.equals(preResource)) {
                List<ConfigAttribute> preAuthList = resourcesMap.get(presentResource);
                Iterator<ConfigAttribute> preAuthItr = preAuthList.iterator();
                while (preAuthItr.hasNext()) {
                    SecurityConfig tempConfig = (SecurityConfig) preAuthItr.next();
                    configList.add(tempConfig);
                }
            }

            configList.add(new SecurityConfig((String) tempMap.get(authorityStr)));

            // 만약 동일한 Resource 에 대해 한개 이상의 Role 맵핑 추가인 경우 이전 resourceKey 에 현재 새로 계산된 Role 맵핑 리스트로 덮어쓰게 됨.
            resourcesMap.put(presentResource, configList);

            // 이전 resource 비교위해 저장.
            preResource = presentResourceStr;
        }
        return resourcesMap;
	}
}
public class ReloadableFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

	private RoleAndUrlResourcesMapLoader roleAndUrlResourcesMapLoader;
	
	private LinkedHashMap<RequestMatcher, List<ConfigAttribute>> requestMap;

	public ReloadableFilterInvocationSecurityMetadataSource(RoleAndUrlResourcesMapLoader roleAndUrlResourcesMapLoader) {
        this.roleAndUrlResourcesMapLoader = roleAndUrlResourcesMapLoader;
    }
	
	public LinkedHashMap<RequestMatcher, List<ConfigAttribute>> getRoleAndUrlResourcesMap() {
		return roleAndUrlResourcesMapLoader.getRoleAndUrlResourcesMap();
	}

    public Collection<ConfigAttribute> getAllConfigAttributes() {
    	if(this.requestMap == null) this.requestMap = getRoleAndUrlResourcesMap();
    	
        Set<ConfigAttribute> allAttributes = new HashSet<ConfigAttribute>();
        for (Map.Entry<RequestMatcher, List<ConfigAttribute>> entry : requestMap.entrySet()) {
            allAttributes.addAll(entry.getValue());
        }
        return allAttributes;
    }

    public Collection<ConfigAttribute> getAttributes(Object object) {
    	if(this.requestMap == null) this.requestMap = getRoleAndUrlResourcesMap();
    	
        final HttpServletRequest request = ((FilterInvocation) object).getRequest();
        for (Map.Entry<RequestMatcher, List<ConfigAttribute>> entry : requestMap.entrySet()) {
            if (entry.getKey().matches(request)) {
                return entry.getValue();
            }
        }
        return null;
    }

    public boolean supports(Class<?> clazz) {
        return FilterInvocation.class.isAssignableFrom(clazz);
    }

	public void reload() throws Exception {

		LinkedHashMap<RequestMatcher, List<ConfigAttribute>> reloadedMap = getRoleAndUrlResourcesMap();

        Iterator<Entry<RequestMatcher, List<ConfigAttribute>>> iterator = reloadedMap.entrySet().iterator();

        // 이전 데이터 삭제
        requestMap.clear();

        while (iterator.hasNext()) {
        	Entry<RequestMatcher, List<ConfigAttribute>> entry = iterator.next();

            requestMap.put(entry.getKey(), entry.getValue());
        }
    }
}

+위의 소스코드는 Egovframework를 참조하였다. 

ReloadableFilterInvocationSecurityMetadataSource : egovframework.rte.fdl.security.intercept.EgovReloadableFilterInvocationSecurityMetadataSource

RoleAndUrlResourcesMapLoader : egovframework.rte.fdl.security.securedobject.impl.SecuredObjectServiceImpl

 

egov에선 ciregex나 regex도 지원하지만 정규식에 좀 문외한이라 ant 패턴의 URL만 가져왔다.

 

ReloadableFilterInvocationSecurityMetadataSource에서 가져오게 되는 LinkedHashMap<RequestMatcher, List<ConfigAttribute>> 타입의 requestMap의 RequestMatcher에는 ant 패턴의 URL이, List<ConfigAttribute>에는 권한정보가 들어가 있다. Ant 패턴의 URL이 Key로, 권한정보 Value를 불러와 이를 판단주체가 판단하게 된다.

 

DB에서 저장되도 서버상에선 최신의 권한정보를 가져올 수 있도록 권한정보를 Reload 하는 소스코드이다. 어떤분은 cache에 저장해두었다가 cache event manager를 써서 정보가 수정될 때만 cache의 데이터를 변경하는 방법을 쓰던.. 자바에서 cache를 사용해본적이 없어서 참고하지 않았음 다음기회에..

 

 

3. 판단 주체

@Bean
public AffirmativeBased affirmativeBased() {
  List<AccessDecisionVoter<? extends Object>> accessDecisionVoters = new ArrayList<>();
  accessDecisionVoters.add(roleVoter());
  AffirmativeBased affirmativeBased = new AffirmativeBased(accessDecisionVoters);
  return affirmativeBased;
}

@Bean
public RoleHierarchyVoter roleVoter() {
	return new RoleHierarchyVoter(roleHierarchy());
}

 

판단 주체는 인증정보와 대상정보를 가지고 리소스로의 접근을 허용할지 말지를 판단하게 되는데, 판단할 참고 자료를(roleVoter에 포함되는 Voter 클래스의 권한정보) 판단 주체(accessDecisionVoter)에게 쥐어주고 어떤 기준(affirmativeBased)으로 판단할지를 정해준다.

 

판단 주체는 참고 자료에 따라 아래의 세가지의 결과를 리턴한다.

- ACCESS_GRANTED(허가)

- ACCESS_DENIED(거절)

- ACCESS_ABSTAIN(보류)

 

판단 기준이 되는 org.springframework.security.access.AbstractAccessDecisionManager를 상속한 3가지의 클래스가 있다.

- AffirmativeBased : 등록된 Voter 클래스 객체 중 단 하나라도 ACCESS_GRANTED의 리턴값을 받으면 최종적으로 접근을 허가한다.

- ConsensusBased : 등록된 Voter 클래스 객체들의 리턴값 중 ACCESS_GRANTED의 리턴값이 ACCESS_DENIED보다 많아야 접근을 허가한다. 즉 다수결.

- UnanimousBased : 등록된 Voter 클래스 객체들 모두가 ACCESS_GRANTED의 리턴값을 받아야 최종적으로 접근을 허가한다.

 

위의 코드를 기준으로 얘기하자면. 

아래의 표는 각각의 권한이 가지게 되는 권한 계층구조이다.

admin

admin 

manager 

user

manager

 

manager 

user

user

 

 

user

admin 권한을 가진 사용자만 /admin에 접근이 가능하다고 할 때,

 

1. AffirmativeBased

- admin : /admin에 접근할 수 있는 권한 admin을 가지고 있으므로 접근 가능.

- manager, user : admin 권한을 가지고 있지 않으므로 접근 불가능.

 

2. ConsensusBased 

- admin : /admin에 접근할 수 있는 권한 admin을 가지고 있으나 manager, user는 deny이기 때문에 1:2의 결론이 나므로 접근 불가능.

- manager, user : admin 권한을 전혀 가지고 있지 않으므로 접근 불가능.

 

3. UnanimousBased 

- admin, manager, user : deny되는 권한을 한 개 이상 가지고 있으므로 접근 불가능.

@Bean public RoleHierarchyLoader roleHierarchyLoader() {
	return new RoleHierarchyLoader();
}

@Bean public RoleHierarchyImpl roleHierarchy() {
	return roleHierarchyLoader().getRoleHierarchyImpl();
}
public class RoleHierarchyLoader {
	@Autowired
	SecurityService securityService;
	
	private RoleHierarchyImpl roleHierarchyImpl;

	public RoleHierarchyLoader() {
		super();
		roleHierarchyImpl = new RoleHierarchyImpl();
	}

	public RoleHierarchyImpl getRoleHierarchyImpl() {
		getRoleHierarchy();
		return roleHierarchyImpl;
	}
	
	public void getRoleHierarchy() {
		roleHierarchyImpl.setHierarchy(securityService.getRolesHierarchy());
	}
	
	public void setRoleHierarchy(String roleHierarchyString) {
		roleHierarchyImpl.setHierarchy(roleHierarchyString);
	}
	
}

+ UserDetail뿐만 아니라 FilterSecurityInterceptor에서도 roleHierarchy 정보가 필요할 때마다 DB에서 호출할 수있는 RoleHierarchyLoder Bean을 따로 등록함 .

 

+ ㅠㅠ 드디어 미루고 미루던 Security 설정 포스팅을 마무리함.

 

참조 : http://zgundam.tistory.com/57

댓글