WEB/Security

실전프로젝트 - 인가 프로세스 DB 연동 웹 계층 구현

Tony Lim 2023. 1. 16. 15:01
728x90

db와 연동하여 자원 및 권한을 설정하고 제어함으로 동적 권한 관리가 가능하도록 한다.

antMatcher("/user").hasRole("USER") 없앰


웹 게반 인가처리 DB 연동 - 주요 아키텍처 이해

이제 db를 사용하니 http.anMatchers를 사용안할거지만 , 원래는 저렇게 configure해놓으면 Map으로 들고 있다가 AccessDecisionManager에게 비교할 수있게 ref를 제공해준다.

접근을 시도하려는 request's authentication의 authority를 확인하여 허용해줄지 말지를 결정하게 된다.

MapBasedMethodSecurityMetadataSource 를 통해서 db에서 authority를 추출하는 방식을 사용할 수 있다.

위에 3개는 이미 구현완료된 annotation을 처리해주는 클래스이다.


웹 기반 인가처리 DB 연동 - FilterInvocationSecurityMetadataSource - 1

public class UrlFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

    private LinkedHashMap<RequestMatcher, List<ConfigAttribute>> requestMap = new LinkedHashMap<>();

    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {

        HttpServletRequest request = ((FilterInvocation) object).getRequest();

        requestMap.put(new AntPathRequestMatcher("/mypage"), Arrays.asList(new SecurityConfig("ROLE_USER")));

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

사용자가 접근하고자 하는 URL 자원에 대한 권한 정보를 추출하고 AccessDecisionManager에게 전달하여 인가처리 수행

DB로부터 자원 및 권한정보를 매핑하여 맵으로관리하고 매 요청마다 확인하는 용도로 사용됨

FilterSecurityInterceptor를 내것을 하나만들어서 기존 FilterSecurityInterceptor앞에 놓으면 내 필터만 처리하고 2번쨰 기존것으로 넘어갈때는 이미 한번 처리했으니 바로 다음으로 넘어가게 된다.

public class UrlResourcesMapFactoryBean implements FactoryBean<LinkedHashMap<RequestMatcher, List<ConfigAttribute>>> {

    private SecurityResourceService securityResourceService;
    private LinkedHashMap<RequestMatcher, List<ConfigAttribute>> resourceMap;

    public UrlResourcesMapFactoryBean(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;
    }
}

보통 Map 자료구조 자체를 bean으로 등록하지 않는다. requestMap에 채워질 resourceMap을 db에서 가져와야하기 때문에 securityResourceService를 별도로 구성한다.

public class SecurityResourceService {

    private ResourcesRepository resourcesRepository;

    public SecurityResourceService(ResourcesRepository resourcesRepository) {
        this.resourcesRepository = resourcesRepository;
    }

    @Transactional
    public LinkedHashMap<RequestMatcher, List<ConfigAttribute>> getResourceList() {

        LinkedHashMap<RequestMatcher,List<ConfigAttribute>> result = new LinkedHashMap<>();
        List<Resources> resourceList = resourcesRepository.findAllResources();
        resourceList.forEach(re -> {
            List<ConfigAttribute> configAttributesList = new ArrayList<>();
            re.getResourcesRoles().forEach(resourcesRole -> {
                Role role = resourcesRole.getRole();
                configAttributesList.add(new SecurityConfig(role.getRoleName()));
                result.put(new AntPathRequestMatcher(re.getResourceName()),configAttributesList);
            });
        });
        return result;
    }
}

db 에 저장되어있는 resource 에 대한 authority 정보들을 파싱해서 map에 담는 과정이다.

@Bean
public SecurityResourceService securityResourceService(ResourcesRepository resourcesRepository) {
    return new SecurityResourceService(resourcesRepository);
}
http.apply(new CustomFilterSecurityInterceptorDsl(securityResourceService(resourcesRepository)));

강의처럼 AppConfig를 따로만들어서 SecurityResouceService를 생성하려 하니 injection 순서가 꼬이게 된다.

하나의 Config파일에 같이 넣어줬다. 


웹 기반 인가처리 실시간 반영하기

controller 단에서 admin 계정으로 권한 정보 update요청이 들어오면 런타임에 requestMap을 비워주고 새로 반영된 정보를 db에서 다시 읽어온다.


인가처리 허용 필터 - PermitAllFilter 구현

FilterSecurityInterceptor를 상속한 PermitAllFilter를 만들어서 beforeInvcation을 오버라이드 한다.

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

기존에는 super.beforeInvocation으로 부모 클래스로 가서 기존 access처리를 했었지만 해당 필터에서 request matching을 먼저하고 null을 리턴함으로 권한 처리 페이즈 까지 안가게 할 수있다.

FilterSecurityInterceptor 애는 한번만 실행되기 때문이다.

permitall 인 만큼 , "/", "/login", "/user/login/**" 과 같은 url들은 굳이 권한 처리 로직을 거치지 않게 할 수 있는것이다.


계층 권한 적용하기 - RoleHierarchy

원래 권한은 독립적으로 존재하지만 계층형으로도 만들 수 있다.

ROLE_ADMIN > ROLE_MANAGER
ROLE_MANAGER > ROLE_USER

해당 포맷으로 만들고 RoleHierarchyImpl.setHierarchy를 호출하면 계층형을 만들어 준다.

 

@Component
public class SetupDataLoader implements ApplicationListener<ContextRefreshedEvent> {
@Transactional
public void createRoleHierarchyIfNotFound(Role childRole, Role parentRole) {
    RoleHierarchy roleHierarchy = roleHierarchyRepository.findByChildName(parentRole.getRoleName());
    if (roleHierarchy == null) {
        roleHierarchy = RoleHierarchy.builder()
                .childName(parentRole.getRoleName())
                .build();
    }
    RoleHierarchy parentRoleHierarchy = roleHierarchyRepository.save(roleHierarchy);

    roleHierarchy = roleHierarchyRepository.findByChildName(childRole.getRoleName());
    if (roleHierarchy == null) {
        roleHierarchy = RoleHierarchy.builder()
                .childName(childRole.getRoleName())
                .build();
    }
    RoleHierarchy childRoleHierarchy = roleHierarchyRepository.save(roleHierarchy);
    childRoleHierarchy.setParentName(parentRoleHierarchy);
}

해당 클래스에서 ApplicationContext가 초기화될때 = AbstractApplicationContext의 refresh 메소드에서 Bean들을 다 초기화하고 제일 마지막에 finishRefresh를 호출할 때 이벤트를 발생시키는 시점이다.

이때 createRoleHierarchyIfNotFound를 호출하여 db에 저장해둔다.

@RequiredArgsConstructor
@Component
public class SecurityInitializer implements ApplicationRunner {

    private final RoleHierarchyService roleHierarchyService;
    private final RoleHierarchyImpl roleHierarchy;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        String allHierarchy = roleHierarchyService.findAllHierarchy();
        roleHierarchy.setHierarchy(allHierarchy);
    }
}

db에 저장해둔것을 꺼낼때 위 사진의 Format처럼 꺼내와서 setting한다.


아이피 접속 제한하기 - CustomIpAddressVoter

기존의 vote와 다르게 여러 voter들 중에 가장 먼저 심사하게 해야하고 심사를 통과하더라도 ACCESS_ABSTAIN을 주어서 나머지 추가 심사를 진행하도록 해야한다.

@RequiredArgsConstructor
public class IpAddressVoter implements AccessDecisionVoter<Object> {

    private final SecurityResourceService securityResourceService;

    @Override
    public boolean supports(ConfigAttribute attribute) {
        return true;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return true;
    }

    @Override
    public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) {

        WebAuthenticationDetails details = (WebAuthenticationDetails)authentication.getDetails();
        String remoteAddress = details.getRemoteAddress();

        List<String> accessIpList = securityResourceService.getAccessIpList();

        int result = ACCESS_DENIED;

        for (String ipAddress : accessIpList) {
            if (remoteAddress.equals(ipAddress)) {
                return ACCESS_ABSTAIN;
            }
        }

        if (result == ACCESS_DENIED) {
            throw new AccessDeniedException("Invalid IpAddress");
        }

        return result;
    }
}

AccessDecsionVoter를 구현하고 허용 가능한 ip가 존재할 경우에만 ACCESS_ABSTAIN을 return하여 추가적인 Voter에게 검사를 받을 수 있도록한다.

private List<AccessDecisionVoter<?>> getAccessDecisionVoters() {

    List<AccessDecisionVoter<? extends Object>> accessDecisionVoters = new ArrayList<>();
    accessDecisionVoters.add(new IpAddressVoter(securityResourceService));
    accessDecisionVoters.add(roleVoter());

    return accessDecisionVoters;
}
@Override
public void configure(HttpSecurity http) throws Exception {
    ApplicationContext context = http.getSharedObject(ApplicationContext.class);

    // here we lookup from the ApplicationContext. You can also just create a new instance.
    FilterSecurityInterceptor filterSecurityInterceptor = new PermitAllFilter(permitAllResources);
    filterSecurityInterceptor.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
    filterSecurityInterceptor.setSecurityMetadataSource(new UrlFilterInvocationSecurityMetadataSource(urlResourcesMapFactoryBean().getObject(), securityResourceService));
    filterSecurityInterceptor.setAccessDecisionManager(new AffirmativeBased(getAccessDecisionVoters()));

    http.addFilterBefore(filterSecurityInterceptor, FilterSecurityInterceptor.class);
}

security dsl에서 (init,configure 해주는 클래스)  , 예를들면 filter설정을 할때 setAccessDecisionManager 에서 해당 voter를 추가한 다음 넣어준다.

 

 

728x90