본문 바로가기
관리자

Programming-[Backend]/Spring Security

[스프링 시큐리티]19. DB 연동 인가처리 -FilterInvocationSecurityMetadataSource 구현

728x90
반응형

1. DB 연동 : requestMap 가져오기

 

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

 

이제 UrlFilterInvocationMetadataSource에 위와 같이 강제로 url과 권한을 삽입하는 것이 아니라 DB에 있는 role-resource 정보를 넣어주는 것을 실습해본다. 우선 유저가 요청한 request url에 따라 해당 url에 접근 가능한 role들을 정의하는 requestMap 객체를 가져오는 코드를 작성해본다.

 

 

 

SecurityConfig 수정

지난 글 맨 마지막 부분에서 SecurityConfig 파일의 customFilterSecurityInterceptor()를 정의하는 부분에서 FilterInvocationMetadataSource를 앞서 작성한 new urlFilterInvocationMetadataSource()로 설정해주었다. 이 부분을 이용해서 role-resource 정보를 넣을 것이므로 SecurityConfig 부분을 아래와 같이 수정한다.

 

@Bean
public FilterInvocationSecurityMetadataSource urlFilterInvocationSecurityMetadataSource() throws Exception {
    return new UrlFilterInvocationMetadataSource(urlResourcesMapFactoryBean().getObject());
}

private UrlResourceMapFactoryBean urlResourcesMapFactoryBean() {
    UrlResourceMapFactoryBean urlResourceMapFactoryBean = new UrlResourceMapFactoryBean();
    urlResourceMapFactoryBean.setSecurityResourceService(securityResourceService);
    return urlResourceMapFactoryBean;
}

 

urlFilterInvocationSecurityMetadataSource()를 불러올 때, urlResourceMapFactoryBean()을 정의하고 securityResourceService를 setter로 주입해줬다. 일단은 그냥 넘어가자. 아래에 관계도를 작성해두었는데, 관계도를 보면 구조 이해가 될 것이다.

 

 

 

Resource-Role 정보 넣기

urlFilterInvocationMetadataSource에 role-resource 정보를 넣어본다.

 

factory 패키지를 만들고 UrlResourceMapFactoryBean을 추가한다.

public class UrlResourceMapFactoryBean 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 true; //1개만 있어서 true로 처리
   }

    @Override
    public LinkedHashMap<RequestMatcher, List<ConfigAttribute>> getObject() throws Exception {
        if(Objects.isNull(resourceMap)) {
            init();
        }
        return resourceMap;
    }

    private void init() {
        resourceMap = securityResourceService.getResourceList();
    }

    @Override
    public Class<?> getObjectType() {
        return LinkedHashMap.class;
    }
}

 

우선 FactoryBean<LinkedHashMap<RequestMatcher, List<ConfigAttribute>>를 implements 한다. 이것은 role-resource 정보를 불러오기 위함이다. 단순하게 생각하면 LinkedHashMap으로 순서가 있는 Map 정보 집합체를 다룬다고 생각하면 된다. 그리고 RequestMatcher는 request에서 받아오는 url 정보, List<ConfigAttribute>는 url과 관련된 role 정보의 list라고 보면 된다. 예를 들면 일단은 지난 시간에 배운 아래와 같은 정보라고 생각하면 된다.

 

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

 

그리고 멤버 변수로 requestMap이 정의되었다. 결국 url(resource) - role의 관계 Map을 FilterInvocationMetadataSource 쪽으로 전달해주는 역할을 하는 것이다. getObject() 메서드에서 resourceMap이 null인지 검사하고 init() 처리를 한다. 그리고 init에서는 securityResourceService를 이용해서 모든 resource를 불러온다.

 

구조를 도식화하면 아래와 같다.

 

setSecurityResourceService

SecurityConfig 파일에서 setter를 이용해서 securityResourceSerivce를 설정해주었다.

 

private UrlResourceMapFactoryBean urlResourcesMapFactoryBean() {
    UrlResourceMapFactoryBean urlResourceMapFactoryBean = new UrlResourceMapFactoryBean();
    urlResourceMapFactoryBean.setSecurityResourceService(securityResourceService);
    return urlResourceMapFactoryBean;
}

 

그래서 UrlResourceMapFactoryBean에서도 setter를 정의해주었다. 만약 SecurityConfig에서 아래와 같이 정의한다면 굳이 setter를 호출하지 않고 생성자 처리를 해도 됐을 것이다. 아마 다양한 주입 방식을 연습해보는게 아닐까 추측한다.

 

private UrlResourceMapFactoryBean urlResourcesMapFactoryBean() {
    UrlResourceMapFactoryBean urlResourceMapFactoryBean = new UrlResourceMapFactoryBean(securityResourceService);
    return urlResourceMapFactoryBean;
}

 

 

 

Resource-Role 정보 찾아오기

securityResourceSerivce를 통해서 Resource-Role 정보를 찾아온다.

 

@RequiredArgsConstructor
public class SecurityResourceService {

    private final ResourcesRepository resourcesRepository;
    private final RoleResourceRepository roleResourceRepository;

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

        LinkedHashMap<RequestMatcher, List<ConfigAttribute>> result = new LinkedHashMap<>();
        List<Resource> resources = resourcesRepository.findAll();
        resources.forEach(resource -> {
            List<ConfigAttribute> configAttributes = new ArrayList<>();
            Long resourceId = resource.getId();
            List<RoleResource> roleResources = roleResourceRepository.findAllByResourceId(resourceId);
            roleResources.forEach(roleResource -> {
                configAttributes.add(new SecurityConfig(roleResource.getRole().getRoleName())); //ConfigAttribute 타입의 구현체인 SecurityConfig를 넣어준다.
                result.put(new AntPathRequestMatcher(resource.getResourceName()), configAttributes);
            });
        });
        return result;
    }
}

 

DB 및 엔티티 구조가 강의 내용과 다르기 때문에, 강의 내용대로 이해하면 안된다. 그러나 원리는 같다.

 

목표로하는 resource-role 정보를 LinkedHashMap<RequestMatcher, List<ConfigAttribute>> 형태로 찾아오는 getResourceList 메서드를 정의한다. resources.forEach 문을 통해서 DB에 존재하는 모든 resource에 대해서 관련된 role을 List<ConfigAttributes> 형태로 찾아오는 것이라고 이해하면 된다.

 

 

 


 

 

 

2. DB 연동 : requestMap 조회 확인하기

 

DB 정보

이제 정상적으로 작동되는지 확인해본다. 우선 테스트를 위한 DB 정보는 다음과 같다.

 

Account

 

Role

 

Account_Roles

각 계정이 각 이름과 매칭되는 권한만 갖고 있는 형태이다.

 

Resources

 

실제 페이지 매핑

 

마이페이지 : /mypage

메시지 : /messages

환경설정 : /config

-> /admin이 아니라 /config이다. 이것은 모든 유저가 /config에 들어갈 수 있다는 것을 테스트해보기 위함이다. 이는 지난 글에서 배운 바대로, 만약 requestMap에 resource-role 관계가 없다면 어떤 유저든지 해당 resource에 접근할 수 있다는 것을 확인해보기 위함이다. 즉 admin, user, manager 모두 환경설정 페이지에 접근가능하다.

 

Resource_Roles

 

admin은 /admin, user는 /mypage, manager는 /messages에 매핑된다.

 

 

 

 

 

 

실습 결과

 

user로 로그인하여 접근

 

user로 로그인하여 권한이 있는 /mypage에 접근하는 경우 debugging을 해보자. 로그인 후 UrlFilterInvocationMetadataSource 파일의 getAttributes 메서드에 break point를 잡는다.

 

 

그리고 마이페이지에 접근하면, request - request - ... 객체 내부에 /mypage로 접근한다는 정보를 확인할 수 있다.

 

 

그리고 stream을 돌면서 resource가 순차적으로 불려오고, RequestMatcher에 의해서 requestMap을 구성해나가는 것을 확인할 수 있다. 아래 그림은 맨 첫 값인 /admin/**의 resource에 대한 loop이며, request와 일치하지 않기 때문에 else 문에 걸렸지만, /mypage를 불러오는 부분에서는 if 문에 걸려서 requestMap으로 정보가 저장되는 것을 확인할 수 있다.

 

 

 

user로 로그인하여 /messages로 접근

user는 requestMap상 /messages에 manager 권한이 없기 때문에 인가 거부된다. 위와 같이 requestMap을 만드는 과정은 똑같이 진행되지만, break point 이후를 살펴보면 아래 그림과 같이 AbstractSecurityInterceptor에서  accessDecisionManager.decide에 의해 AccessDeniedException이 발생하는 것을 확인할 수 있다.

 

 

 

드디어 일반적인 실무에서 사용하는 DB를 연동한 자원-권한 관리에 대해 기초를 알게 되었다!

 

 

 


추가 내용1 : orderNo

 

지난 글([스프링 시큐리티] [작성중] 6. 권한 설정, 표현식) 에서 antPathMatcher를 이용한 방식은 하위 계층의 구체적인 url 정보가 먼저 와야한다 고 배웠다. 그런데 위와 같이 그냥 LinkedHashMap 형태로 requestMap을 구성하면 상위 계층의 url 정보가 먼저 처리되어 버릴 수 있다.

 

상위 자원 허용 시, 하위 자원까지 모두 허용됨

예를 들어 "/admin/view/**", "/admin/view/config" 라는 resource가 2개 있고 각각 {"ROLE_ADMIN", "ROLE_USER"}, "ROLE_ADMIN" 의 Role과 매핑되어 있는 형태라고 가정하자. 상위 resource인 view까지는 user도 접근이 가능하지만, 설정을 하는 /view/config부터는 ROLE_ADMIN 권한을 가진 계정만 접근할 수 있는 상황이다. 이 때 requestMap에서 "/admin/**"이 먼저 불려와지면 ROLE_USER 권한의 계정도 /view/config에 접근할 수 있게 된다.

 

그래서 강의에서는 resources 테이블에서 정보를 불러올 때, 각 resources 객체에 orderNo라는 컬럼을 두어 순번을 매기고 이 번호대로만 order by로 resources를 불러오도록 했다.

 

향후 계층 권한(RoleHierarchy)을 사용하면 문제가 되지 않을 듯 한데, 일단은 이런 내용이 있었다는 것을 기록해둔다.


 

추가 내용2 : Exception message encoding

 

SecurityConfig에서 AccessDeniedHandler()를 통해서 접근 거부 처리를 하는데, 올바른 에러 메시지가 나오지 않는 문제가 발생했다. 

AccessDeniedHandler에서 한글로 message가 출력되는 것을 확인했다. encoding 문제라고 직감은 했다만, 정확한 답변은 인프런 강의 질문과 답변 페이지에서 찾을 수 있었다. (참조2)

 

 

결론적으로는 다음과 같이 URLEncoder를 사용해주면 된다.

 

String deniedUrl = errorPage + "?exception=" + URLEncoder.encode(e.getMessage(), "UTF-8");

 

이것은 URL에는 원칙적으로 ASCII가 아닌 문자를 사용할 수 없기 때문에 무조건 encoding을 해줘야 하기 때문이다. 위와 같이 AccessDeniedHandler에서 직접 response.sendRedirect()를 하는 경우 파라미터를 직접 인코딩 해줘야만 한다.


 

 

참조

 

1) 인프런 - 스프링 시큐리티 - Spring Boot 기반으로 개발하는 Spring Security - 정수원님 강의

https://www.inflearn.com/course/%EC%BD%94%EC%96%B4-%EC%8A%A4%ED%94%84%EB%A7%81-%EC%8B%9C%ED%81%90%E

 

 

2) 인프런 - 스프링 시큐리티 - Spring Boot 기반으로 개발하는 Spring Security - 정수원님 강의- 질문과 답변

https://www.inflearn.com/questions/52731

 

728x90
반응형