인가 정책(hasRole)을 매번 antMatchers에 등록하며 사용할 수 없습니다. 이를 DB로 관리하는 방법을 알아보겠습니다.
DB에서 불러오기를 할라면 몇가지 작업이 필요합니다.
1. Repository 만들기
2. 객체를 담을 FactoryBean 생성
3. 이러한 객체를 담아 관리하는 MetadataSource
4. 최종 Config 세팅에 필요한 작업
FilterInvocationSecurityMetadataSource
의 주요 기능은 다음과 같습니다.
FilterInvocationSecurityMetadataSource
를 상속받는 클래스를 만들고 매서드를 정의 해보겠습니다
public class UrlFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
// LinkedHashMap으로 Url을 담는 RequestMatcher와 권한정보를 가지고있는 ConfigAttribute를 불러옵니다.
private LinkedHashMap<RequestMatcher, List<ConfigAttribute>> requestMap;
// 자원을 담을 수 있게 객체 초기화를 선언해줍니다. resourcesMap는 아래에서 설명하겠습니다.
public UrlFilterInvocationSecurityMetadataSource(LinkedHashMap<RequestMatcher, List<ConfigAttribute>> resourcesMap) {
this.requestMap = resourcesMap;
}
// 여기서는 이 매서드만 잘 만들어주면 됩니다.
@Override
public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
// request타입으로 casting해주고
HttpServletRequest request = ((FilterInvocation) o).getRequest();
// 위에서 받아온 requestMap과 현재 request가 맞는지를 비교하기 시작합니다.
if(request != null){
for(Map.Entry<RequestMatcher, List<ConfigAttribute>> entry : requestMap.entrySet()){
RequestMatcher matcher = entry.getKey();
if(matcher.matches(request)){
return entry.getValue(); // 맞으면 바로 리턴
}
}
}
return null; // 끝까지 없으면 null이겠죠
}
// 위에 선언한 매서드를 제외하고는 여기서는 안쓰이므로,
// DefaultFilterInvocationSecurityMetadataSource에 있는 코드를 그대로 참고합니다.
@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);
}
}
FactoryBean
의<LinkedHashMap<RequestMatcher, List<ConfigAttribute>>>
제너릭 타입을 상속받아 코드작성을 진행하시면 됩니다.
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 boolean isSingleton() {
return FactoryBean.super.isSingleton();
}
@Override
public LinkedHashMap<RequestMatcher, List<ConfigAttribute>> getObject() throws Exception {
// 외부적인 요인으로 값이 들어왔을때를 대비해서 null 일때만 초기화해줍니다.
if(resourceMap == null){
init();
}
return resourceMap;
}
// 여기서 Repository로 연결된 Service를 통해 자료를 가져오게 됩니다.
private void init() {
resourceMap = securityResourceService.getResourceList();
}
// 타입체크를 하는 매서드입니다 최종적으로 리턴되는 타입을 작성하면 됩니다.
@Override
public Class<?> getObjectType() {
return LinkedHashMap.class;
}
}
아래는 SecurityResourceService
에 대한 부분입니다.
public class SecurityResourceService {
public SecurityResourceService(ResourceRepository resourceRepository) {
this.resourceRepository = resourceRepository;
}
private ResourceRepository resourceRepository;
public LinkedHashMap<RequestMatcher, List<ConfigAttribute>> getResourceList(){
LinkedHashMap<RequestMatcher, List<ConfigAttribute>> result = new LinkedHashMap<>();
List<Resource> resourceList = resourceRepository.findAllResources();
resourceList.forEach(resource -> {
List<ConfigAttribute> configAttributes = new ArrayList<>();
resource.getRoleSet().forEach(role -> {
configAttributes.add(new SecurityConfig(role.getRoleName()));
});
result.put(new AntPathRequestMatcher(resource.getResourceName()), configAttributes);
});
return result;
}
}
configAttributes
에는 인가 권한을 넣어주면 됩니다. 여러개가 있을수 있으므로 하나씩 넣어주면 됩니다.
AntPathRequestMatcher
는 Url
의 정보를 담으므로 하나일 수 밖에 없습니다.
또한 기존의 인가정책과 동일하게 불러온 순서에 영향을 받으므로 DB Column을 order할 수있는 장치를 만들어두시면 좋습니다.
찾아오는 부분은 여기까지 작성하면 됩니다. 이제 Configure
에서 설정해주셔야합니다.
위에 작성한 코드를 작동시키기 위해서는 FilterSecurityInterceptor
에 등록해주셔야합니다.
FilterSecurityInterceptor
를 정상적으로 작동시키기 위해서는 setAccessDecisionManager
와 setAuthenticationManager
도 기본으로 설정을 해줘야합니다.
@Bean
public FilterSecurityInterceptor customFilterSecurityInterceptor() throws Exception {
FilterSecurityInterceptor filterSecurityInterceptor = new FilterSecurityInterceptor();
filterSecurityInterceptor.setSecurityMetadataSource(urlFilterInvocationSecurityMetadataSource());
filterSecurityInterceptor.setAccessDecisionManager(affirmativeBased());
filterSecurityInterceptor.setAuthenticationManager(authenticationManagerBean());
return filterSecurityInterceptor;
}
// AccessDecisionManager의 3가지 정책중 하나를 사용합니다.
/*
- AffirmativeBased : 여러 Voter 중에 한명이라도 허용하면 허용, 기본전략
- ConsensusBased : 다수결
- UnanimousBased : 만장일치
*/
private AccessDecisionManager affirmativeBased() {
return new AffirmativeBased(getAccessDecisionVoters());
}
// 권한 리스트를 지정해줍니다. 지금은 따로 권한을 불러온 리스트가 없으므로 새로운 객체를 담아 보내줍니다.
private List<AccessDecisionVoter<?>> getAccessDecisionVoters() {
return Arrays.asList(new RoleVoter());
}
// metaSource를 초기화해주는 작업을 진행합니다.
private FilterInvocationSecurityMetadataSource urlFilterInvocationSecurityMetadataSource() throws Exception {
return new UrlFilterInvocationSecurityMetadataSource(urlResourcesMapFactoryBean().getObject());
}
// 여기는 DB에서 불러오는 작업을 합니다.
private UrlResourcesMapFactoryBean urlResourcesMapFactoryBean() {
UrlResourcesMapFactoryBean urlResourcesMapFactoryBean = new UrlResourcesMapFactoryBean();
urlResourcesMapFactoryBean.setSecurityResourceService(securityResourceService);
return urlResourcesMapFactoryBean;
}
그리고 기존에 설정해놨던 antMatchers()
를 제거 해주시면 됩니다.
// 이제 이런거 다 필요 없습니다.
.antMatchers("/mypage").hasRole("USER")
.antMatchers("/messages").hasRole("MANAGER")
.antMatchers("/config").hasRole("ADMIN")
다만 우리가 DB에 새로 등록할때나 삭제할때도 SecurityResourceService
를 다시 정비해줄 필요가 있습니다.
아래는 Contoller
의 예시입니다.
@PostMapping(value="/admin/resources")
public String createResources(ResourceDto resourcesDto) throws Exception {
ModelMapper modelMapper = new ModelMapper();
Role role = roleRepository.findByRoleName(resourcesDto.getRoleName());
Set<Role> roles = new HashSet<>();
roles.add(role);
Resource resources = modelMapper.map(resourcesDto, Resource.class);
resources.setRoleSet(roles);
// 다음과 같이 서비스에서 resources를 넘겨 DB insert작업을 진행합니다.
resourceService.createResource(resources);
// 등록된 것은 UrlFilterInvocationSecurityMetadataSource에
//reload()란 매서드를 하나 만들어 작동시키면 됩니다.
urlFilterInvocationSecurityMetadataSource.reload();
return "redirect:/admin/resources";
}
@DeleteMapping(value="/admin/resources/delete/{id}")
public String removeResources(@PathVariable String id, Model model) throws Exception {
Resource resources = resourceService.getResource(Long.valueOf(id));
resourceService.deleteResource(Long.valueOf(id));
urlFilterInvocationSecurityMetadataSource.reload();
return "redirect:/admin/resources";
}
reload()
에서는 SecurityREsourceService
를 재실행시키고 requestMap
에 수정사항을 반영하면 됩니다.
//생략...
public void reload(){
LinkedHashMap<RequestMatcher, List<ConfigAttribute>> reloadMap = securityResourceService.getResourceList();
Iterator<Map.Entry<RequestMatcher, List<ConfigAttribute>>> iterator = reloadMap.entrySet().iterator();
requestMap.clear();
while (iterator.hasNext()){
Map.Entry<RequestMatcher, List<ConfigAttribute>> entry = iterator.next();
requestMap.put(entry.getKey(), entry.getValue());
}
}
//생략...