Java Spring Security DB연동

떡ol·2023년 5월 28일
0

인가 정책(hasRole)을 매번 antMatchers에 등록하며 사용할 수 없습니다. 이를 DB로 관리하는 방법을 알아보겠습니다.

DB에서 불러오기

DB에서 불러오기를 할라면 몇가지 작업이 필요합니다.
1. Repository 만들기
2. 객체를 담을 FactoryBean 생성
3. 이러한 객체를 담아 관리하는 MetadataSource
4. 최종 Config 세팅에 필요한 작업

FilterInvocationSecurityMetadataSource의 주요 기능은 다음과 같습니다.

  • 사용자가 접근하고자 하는 Url 자원에 대한 권한 정보 추출
  • AccessDecisionManager 에게 전달하여 인가처리 수행
  • DB 로부터 자원 및 권한 정보를 매핑하여 맵으로 관리
  • 사용자의 매 요청마다 요청정보에 매핑된 권한 정보 확인

FilterInvocationSecurityMetadataSource를 상속받는 클래스를 만들고 매서드를 정의 해보겠습니다

UrlFilterInvocationSecurityMetadataSource

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);
    }
}

UrlResourcesMapFactoryBean

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에는 인가 권한을 넣어주면 됩니다. 여러개가 있을수 있으므로 하나씩 넣어주면 됩니다.
AntPathRequestMatcherUrl의 정보를 담으므로 하나일 수 밖에 없습니다.
또한 기존의 인가정책과 동일하게 불러온 순서에 영향을 받으므로 DB Column을 order할 수있는 장치를 만들어두시면 좋습니다.

찾아오는 부분은 여기까지 작성하면 됩니다. 이제 Configure에서 설정해주셔야합니다.

WebSecurityConfigurerAdapter

위에 작성한 코드를 작동시키기 위해서는 FilterSecurityInterceptor 에 등록해주셔야합니다.
FilterSecurityInterceptor를 정상적으로 작동시키기 위해서는 setAccessDecisionManagersetAuthenticationManager도 기본으로 설정을 해줘야합니다.

    @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 변경지점 적용하기

다만 우리가 DB에 새로 등록할때나 삭제할때도 SecurityResourceService를 다시 정비해줄 필요가 있습니다.
아래는 Contoller의 예시입니다.

MainController

    @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";
    }

UrlFilterInvocationSecurityMetadataSource

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());
        }
    }
    //생략...
profile
하이

0개의 댓글