본문 바로가기

프로그래밍/Spring Security

Spring Security에서 DB를 이용한 자원 접근 권한 설정 및 판단 (1)

지난 글에서는 Spring Security의 FilterSecurityInterceptor 클래스를 통해 어떤 식으로 인증 정보를 이용하여 자원 접근 권한을 제어하는지에 대한 설명을 진행해봤다. 오늘은 이를 응용하여 자원 접근 권한을 XML이 아닌 DB에 설정한 뒤 이를 이용해서 자원 접근 권한을 제어해보도록 하자. 지금부터 설명하는 내용은 전자정부 프레임워크 세미나 중 Spring Security 기능소개 및 활용방법 세미나 동영상을 OLC 사이트에서 보고 이를 적용한 것임을 밝혀둔다(이 부분은 내가 직접 생각해서 만든 부분이 아니다. 혹여 내가 생각한 거라고 오해하는 분이 있으실 듯도 하여 미리 밝혀둔다)

 

이전 글을 다시 복습하는 차원에서 한번 기억을 떠올려보도록 하자. Spring Security의 FilterSecurityInterceptor 클래스에 설정되는 것은 3가지가 있다고 했었다. 인증 정보(Authentication Manager), 대상 정보(Security MetadataSource), 판단 주체(AccessManager) 이렇게 3가지다. 그리고 기존의 tag를 이용한 설정에서 기본적으로 셋팅이 되는 FilterSecurityInterceptor 클래스에 대해서도 알아보았다. 저번 글을 회상했을때 DB를 이용한 자원 접근 권한 설정 및 판단을 할려면 기존의 셋팅으로는 할 수가 없다는 것을 알 수가 있다. 인증 정보야 <authentication-manager> 태그로 만들어지고 FilterSecurityInterceptor 클래스에 설정이 되기 때문에 별 문제가 없지만, 대상 정보를 <intercept-url> 태그에서 읽은 것이 아닌 DB에서 읽은 값으로 구성이 되어져야 한다. 또 판단 주체를 만들때도 설정되어지는 Voter가 DB에서 읽은 값으로 구성된 대상 정보로 판단해야 하는 부분이 생기기 땜에 커스터마이징이 없이는 할 수가 없다. 즉 커스터마이징 셋팅이 된 FilterSecurityInterceptor 클래스의 필요성이 생기게 된다. 

 

그러면 DB에서 읽은 대상 정보를 만드는 방법을 고민해보도록 하자. 이전 글에서 기본 셋팅에서 만들어지는 대상 정보 클래스는 org.springframework.security.web.access.expression.ExpressionBasedFilterInvocationSecurityMetadataSource 라고 언급했었다. 그리고 이 클래스는 org.springframework.security.web.access.expression.DefaultFilterInvocationSecurityMetadataSource 클래스를 상속받고 있으며 이 DefaultFilterInvocationSecurityMetadataSource 클래스는 내부에 Map<RequestMatcher, Collection<ConfigAttribute>> 타입의 멤버변수 requestMap이 있고 실제로는 LinkedHashMap이 들어간다고 했었다. ExpressionBasedFilterInvocationSecurityMetadataSource 클래스가 <intercept-url> 태그에 spel 표현식을 사용한 권한 설정을 읽어서 Spring Security에 설정하기 위해 기존의 DefaultFilterInvocationSecurityMetadataSource 클래스를 상속받은 것을 감안한다면 우리는 다음과 같은 확장 포인트를 잡을수 있다.

 

● DefaultFilterInvocationSecurityMetadataSource 클래스와 같은 구조 또는 이 클래스를 상속받아 클래스를 만든다.

● 이때 requestMap에 해당하는 멤버변수를 만들어주고 여기에는 DB에서 조회한 내용으로 셋팅해준다.

● 이것을 셋팅하는 시점은 bean이 올라가는 시점에 셋팅해준다.

 

이렇게 잡아볼수가 있다. 여기서 또 하나의 커스터마이징 포인트가 존재하는데 그것은 관리자 페이지를 통해 이런 자원별 접근 권한을 수정했을 경우 이 내용이 바로 반영되어야 한다는 점이다. 관리자 페이지에서 자원별 접근 권한을 수정했는데 그것을 반영하기 위해 WAS를 내렸다가 올릴수는 없다. 그렇기땜에 수정된 내용을 반영하는 서비스를 하나 만들어야 한다. 그래서 DB에서 자원별 접근 권한을 조회하는 서비스를 만들어야 한다. 이 서비스는 여기뿐만 아니라 위에서 언급했던 requestMap 멤버변수 셋팅시에도 사용되어질것이다.

 

여기서는 DefaultFilterInvocationSecurityMetadataSource 클래스와 같은 구조를 갖는 클래스를 만들어보도록 하자. DefaultFilterInvocationSecuityMetadataSource 클래스의 정의는 다음과 같다.

 

public class DefaultFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

    protected final Log logger = LogFactory.getLog(getClass());

    private final Map<RequestMatcher, Collection<ConfigAttribute>> requestMap;

    public DefaultFilterInvocationSecurityMetadataSource(
            LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> requestMap) {

        this.requestMap = requestMap;
    }

    ...
}

 

위의 클래스 소스를 보자. 이 클래스는 org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource 인터페이스를 구현하고 있다. 그리고 생성자로 LinkedHashMap<RequestMatcher, Collection<ConfigAttributes>> 객체를 받아 이를 requestMap 멤버변수에 셋팅해주고 있다. DefaultFilterInvocationSecurityMetadataSource 클래스가 가장 기본이 되는 대상정보 클래스(이 클래스를 상속받아 다른 클래스를 만들어서 사용하고 있었다)임을 감안한다면 우리가 만들 클래스는 FilterInvocationSecurityMetadataSource 인터페이스를 구현한 클래스로 대성정보 클래스를 사용하면 된다는 것을 알 수 있다. 그리고 생성자에 LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> 타입의 객체를 requestMap 멤버변수에 셋팅해주면 되겠다.

 

그러면 이제 LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> 타입 객체를 만들때 어떤 내용을 채워야 하는가를 고민할 차례다. 이전 글에서는 <intercept-url> 태그에 설정된 Ant 방식의 url pattern과 spel로 설정된 권한을 넣었다고 설명했다. 그럼 이런 패턴과 권한이 어디있을까? 바로 DB에 있다. 즉 우리는 DB에서 이 url 패턴과 이 패턴이 접근할 권한을 DB에서 조회한뒤 이를 org.springframework.security.web.util.matcher.AntPathRequestMatcher 클래스 객체를 key로 하고 Spring에서 제공하는 권한 클래스 객체들이 들어있는 List 객체를 value로 넣는 LinkedHashMap을 만들면 되는 것이다. 이를 위해 Dao 및 Service를 다음과 같이 만든다

 

SecuredObjectDao 클래스

 

import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import javax.sql.DataSource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

public class SecuredObjectDao {

	private Logger logger = LoggerFactory.getLogger(this.getClass());

	/**
     * url 형식인 보호자원 - Role 맵핑정보를 조회하는 default 쿼리이다.
     */
    public static final String DEF_ROLES_AND_URL_QUERY =
    		"SELECT A.RESOURCE_PATTERN AS URL, B.AUTHORITY AS AUTHORITY "
    			+ "FROM SECURED_RESOURCES A, SECURED_RESOURCES_ROLE B "
    			+ "WHERE A.RESOURCE_ID = B.RESOURCE_ID "
    			+ "AND A.RESOURCE_TYPE = 'url' "
    			+ "ORDER BY A.SORT_ORDER ";

    /**
     * method 형식인 보호자원 - Role 맵핑정보를 조회하는 default 쿼리이다.
     */
    public static final String DEF_ROLES_AND_METHOD_QUERY =
    		"SELECT A.RESOURCE_PATTERN AS METHOD, B.AUTHORITY AS AUTHORITY "
        			+ "FROM SECURED_RESOURCES A, SECURED_RESOURCES_ROLE B "
        			+ "WHERE A.RESOURCE_ID = B.RESOURCE_ID "
        			+ "AND A.RESOURCE_TYPE = 'method' "
        			+ "ORDER BY A.SORT_ORDER ";

    /**
     * pointcut 형식인 보호자원 - Role 맵핑정보를 조회하는 default
     * 쿼리이다.
     */
    public static final String DEF_ROLES_AND_POINTCUT_QUERY =
    		"SELECT A.RESOURCE_PATTERN AS POINTCUT, B.AUTHORITY AS AUTHORITY "
        			+ "FROM SECURED_RESOURCES A, SECURED_RESOURCES_ROLE B "
        			+ "WHERE A.RESOURCE_ID = B.RESOURCE_ID "
        			+ "AND A.RESOURCE_TYPE = 'pointcut' "
        			+ "ORDER BY A.SORT_ORDER ";

    /**
     * 매 request 마다 best matching url 보호자원 - Role 맵핑정보를
     * 얻기위한 default 쿼리이다. (Oracle 10g specific)
     */
    public static final String DEF_REGEX_MATCHED_REQUEST_MAPPING_QUERY_ORACLE10G =
        "SELECT a.resource_pattern uri, b.authority authority "
            + "FROM secured_resources a, secured_resources_role b "
            + "WHERE a.resource_id = b.resource_id "
            + "AND a.resource_id =  "
            + " ( SELECT resource_id FROM "
            + "    ( SELECT resource_id, ROW_NUMBER() OVER (ORDER BY sort_order) resource_order FROM secured_resources c "
            + "      WHERE REGEXP_LIKE ( :url, c.resource_pattern ) "
            + "      AND c.resource_type = 'url' "
            + "      ORDER BY c.sort_order ) "
            + "   WHERE resource_order = 1 ) ";

    /**
     * Role 의 계층(Hierarchy) 관계를 조회하는 default 쿼리이다.
     */
    public static final String DEF_HIERARCHICAL_ROLES_QUERY =
        "SELECT a.child_role child, a.parent_role parent "
            + "FROM ROLES_HIERARCHY a LEFT JOIN ROLES_HIERARCHY b on (a.child_role = b.parent_role) ";

    private String sqlRolesAndUrl;

    private String sqlRolesAndMethod;

    private String sqlRolesAndPointcut;

    private String sqlRegexMatchedRequestMapping;

    private String sqlHierarchicalRoles;

    public SecuredObjectDao() {
        this.sqlRolesAndUrl = DEF_ROLES_AND_URL_QUERY;
        this.sqlRolesAndMethod = DEF_ROLES_AND_METHOD_QUERY;
        this.sqlRolesAndPointcut = DEF_ROLES_AND_POINTCUT_QUERY;
        this.sqlRegexMatchedRequestMapping =
            DEF_REGEX_MATCHED_REQUEST_MAPPING_QUERY_ORACLE10G;
        this.sqlHierarchicalRoles = DEF_HIERARCHICAL_ROLES_QUERY;
    }

    private NamedParameterJdbcTemplate namedParameterJdbcTemplate;

    public void setDataSource(DataSource dataSource) {
        this.namedParameterJdbcTemplate =
            new NamedParameterJdbcTemplate(dataSource);
    }

    /**
     * 롤에 대한 URL 정보를 가져오는 SQL을 얻어온다.
     * @return
     */
    public String getSqlRolesAndUrl() {
        return sqlRolesAndUrl;
    }

    /**
     * 롤에대한 URL 정보를 가져오는 SQL을 설정한다.
     * @param sqlRolesAndUrl
     */
    public void setSqlRolesAndUrl(String sqlRolesAndUrl) {
        this.sqlRolesAndUrl = sqlRolesAndUrl;
    }

    public String getSqlRolesAndMethod() {
        return sqlRolesAndMethod;
    }

    public void setSqlRolesAndMethod(String sqlRolesAndMethod) {
        this.sqlRolesAndMethod = sqlRolesAndMethod;
    }

    public String getSqlRolesAndPointcut() {
        return sqlRolesAndPointcut;
    }

    public void setSqlRolesAndPointcut(String sqlRolesAndPointcut) {
        this.sqlRolesAndPointcut = sqlRolesAndPointcut;
    }

    public String getSqlRegexMatchedRequestMapping() {
        return sqlRegexMatchedRequestMapping;
    }

    public void setSqlRegexMatchedRequestMapping(
            String sqlRegexMatchedRequestMapping) {
        this.sqlRegexMatchedRequestMapping = sqlRegexMatchedRequestMapping;
    }

    public String getSqlHierarchicalRoles() {
        return sqlHierarchicalRoles;
    }

    public void setSqlHierarchicalRoles(String sqlHierarchicalRoles) {
        this.sqlHierarchicalRoles = sqlHierarchicalRoles;
    }

    public LinkedHashMap<Object, List<ConfigAttribute>> getRolesAndResources(String resourceType) throws Exception {

        LinkedHashMap<Object, List<ConfigAttribute>> resourcesMap = new LinkedHashMap<Object, List<ConfigAttribute>>();

        String sqlRolesAndResources;
        boolean isResourcesUrl = true;
        if ("method".equals(resourceType)) {
            sqlRolesAndResources = getSqlRolesAndMethod();
            isResourcesUrl = false;
        } else if ("pointcut".equals(resourceType)) {
            sqlRolesAndResources = getSqlRolesAndPointcut();
            isResourcesUrl = false;
        } else {
            sqlRolesAndResources = getSqlRolesAndUrl();
        }

        List<Map<String, Object>> resultList = this.namedParameterJdbcTemplate.queryForList(sqlRolesAndResources, new HashMap<String, String>());

        Iterator<Map<String, Object>> itr = resultList.iterator();
        Map<String, Object> tempMap;
        String preResource = null;
        String presentResourceStr;
        Object presentResource;
        while (itr.hasNext()) {
            tempMap = itr.next();

            presentResourceStr = (String) tempMap.get(resourceType);
            // url 인 경우 RequestKey 형식의 key를 Map에 담아야 함
            presentResource = isResourcesUrl ? new AntPathRequestMatcher(presentResourceStr) : 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("authority")));
            
            // 만약 동일한 Resource 에 대해 한개 이상의 Role 맵핑 추가인 경우 
            // 이전 resourceKey 에 현재 새로 계산된 Role 맵핑 리스트로 덮어쓰게 됨.
            resourcesMap.put(presentResource, configList);

            // 이전 resource 비교위해 저장
            preResource = presentResourceStr;
        }
                
        return resourcesMap;
    }

    public LinkedHashMap<Object, List<ConfigAttribute>> getRolesAndUrl() throws Exception {
        return getRolesAndResources("url");
    }

    public LinkedHashMap<Object, List<ConfigAttribute>> getRolesAndMethod() throws Exception {
        return getRolesAndResources("method");
    }

    public LinkedHashMap<Object, List<ConfigAttribute>> getRolesAndPointcut() throws Exception {
        return getRolesAndResources("pointcut");
    }

    public List<ConfigAttribute> getRegexMatchedRequestMapping(String url) throws Exception {

        // best regex matching - best 매칭된 Uri 에 따른 Role List 조회, 
    	// DB 차원의 정규식 지원이 있는 경우 사용 (ex. hsqldb custom function, Oracle 10g regexp_like 등)
        Map<String, String> paramMap = new HashMap<String, String>();
        paramMap.put("url", url);
        List<Map<String, Object>> resultList = this.namedParameterJdbcTemplate.queryForList(getSqlRegexMatchedRequestMapping(), paramMap);

        Iterator<Map<String, Object>> itr = resultList.iterator();
        Map<String, Object> tempMap;
        List<ConfigAttribute> configList = new LinkedList<ConfigAttribute>();
        // 같은 Uri 에 대한 Role 맵핑이므로 무조건 configList 에 add 함
        while (itr.hasNext()) {
            tempMap = itr.next();
            configList.add(new SecurityConfig((String)tempMap.get("authority")));
        }

        if (configList.size() > 0) {
        	logger.debug("Request Uri : {}, matched Uri : {}, mapping Roles : {}", url, resultList.get(0).get("uri"), configList);
            
        }
        return configList;
    }

    public String getHierarchicalRoles() throws Exception {

    	List<Map<String, Object>> resultList = this.namedParameterJdbcTemplate.queryForList(getSqlHierarchicalRoles(), new HashMap<String, String>());

        Iterator<Map<String, Object>> itr = resultList.iterator();
        StringBuffer concatedRoles = new StringBuffer();
        Map<String, Object> tempMap;
        while (itr.hasNext()) {
            tempMap = itr.next();
            concatedRoles.append(tempMap.get("child"));
            concatedRoles.append(" > ");
            concatedRoles.append(tempMap.get("parent"));
            concatedRoles.append("\n");
        }

        return concatedRoles.toString();
    }
}

 

SecuredObjectDao 클래스는 예전 전자정부 Spring Security 세미나때 사용했던 코드를 그대로 사용했다. 이 코드에서는 url 타입 뿐만 아니라 메소드나 포인트컷도 할수 있도록 했기 때문에 여기서 사용되지 않는 코드도 붙어 있는 상황이다. 그러나 그렇다고 이걸 빼버리면 오히려 혼선이 있을것 같아서 일단은 넣어두었다. 소스를 보면 DEF_ROLES_AND_URL_QUERY란 final String 변수가 있는데 이 쿼리를 만들때는 Ant 패턴으로 등록된 URL과 이 URL이 접근할 수 있는 권한을 넣어주면 된다. 즉 쿼리를 실행하면 다음과 같은 스타일의 결과가 나오게끔 한다면 된다는 얘기다.

 

 /notmember/**

 ANONYMOUS

 /notmember/**

 ADMIN_BOARD_VIEW

 /admin/**

 ADMIN_BOARD_VIEW

 

첫번째 컬럼엔 Ant Pattern 방식의 URL 표현식이 나와야 하고 두번째 컬럼엔 권한 이름이 나와야 한다. 이 검색 결과 형태를 보자. 우리가 <intercept-url> 태그 설정시 썼던 데이터 표현과 같다. 다만 여기서는 SPEL을 사용할 수 없기 때문에 permitAll, denyAll, hasRole 같은 함수를 사용하면 안된다. 권한 명칭을 써야 한다. 그래서 로그인 하지 않은 권한도 ANONYMOUS라고 명시해준 것이다. 이 부분이 사실 단점이 되는 부분이기도 하다. /notmember/** 부분을 보자. 모든 사람이 접근하도록 하겠다면 permitAll 하나만 넣어도 가능한 것이었는데 DB로 바뀌면서 접근할 수 있는 모든 권한을 일일이 명시해줘야 한다. DEF_ROLES_AND_URL_QUERY 변수는 기본적으로 설정되는 쿼리를 지정하는 것으로 만약 이 쿼리를 바꾸고 싶다면 sqlRolesAndUrl 멤버변수에 셋팅해주면 된다. setter 메소드가 있기 때문에 <property> 태그를 이용해서 설정할 수가 있다. 그리고 실제로 사용되는 변수도 sqlRolesAndUrl 이다. DEF_ROLES_AND_URL_QUERY 변수에 있는 내용이 생성자에서 sqlRolesAndUrl에 들어가고 있다.

 

이번 글을 시작하는 시점에서 설명했던 내용 중에 DefaultFilterInvocationSecurityMetadataSource 클래스는 내부에 Map<RequestMatcher, Collection<ConfigAttribute>> 타입의 멤버변수 requestMap이 있고 실제로는 LinkedHashMap이 들어간다고 설명한 부분이 있다. 위에서 DefaultFilterInvocationSecurityMetadataSource와 같은 구조의 클래스를 만든다고 했기 때문에 우리가 만들 클래스에도 Map<RequestMatcher, Collection<ConfigAttribute>> 타입의 멤버변수 requestMap이 있을 것이다. 여기에 들어가야 하는 내용이 URL과 거길 접근하는 권한이 들어가야 한다는 것은 이전 글에서 설명한 바가 있다. 그렇기 때문에 우리가 만든 쿼리를 실행하여 나온 결과가 requestMap에 들어가게끔 조작을 해줘야 한다. 즉 LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> 타입으로 결과를 만들어야 한다는 것이다. 그런 작업을 해주는 메소드가 getRolesAndUrl() 메소드이다. 이 메소드는 내부에서 getRolesAndResources('url')을 호출하고 있기 때문에 실제 LinkedHashMap을 만들어주는 작업은 getRolesAndResources 메소드가 하게 된다.

 

getRolesAndResources 메소드는 url과 메소드, 포인트컷 모두 LinkedHashMap을 만들기 때문에 혼선이 생길수 있으나 3개 모두 만들어지는 스타일은 동일하기 때문에 분석하는데 있어서 그리 어렵지는 않다. this.namedParameterJdbcTemplate.queryForList 를 통해 쿼리를 실행하여 위에서 언급한 형태의 결과를 얻으면 이를 Map 형태로 검색된 쿼리 결과의 레코드를 꺼낸뒤 첫번째 컬럼값인 url 패턴을 이용하여 AntPathRequestMatcher 객체로 생성하고 두번째 컬럼값인 권한 이름을 이용해서 SecurityConfig 객체를 만든뒤 이렇게 만든 SecurityConfig 객체를 LinkedList 객체에 넣어둔다. 그래서 이렇게 만든 AntPathRequestMatcher 객체를 key로 해서 LinkedHashMap에 AntPathRequestMatcher 객체와 LinekedList 객체를 넣어둔다. 그리고 다음 레코드 값을 꺼냈을때 첫번째 컬럼값인 url 패턴이 이전 레코드것과 동일하면 LinkedHashMap에서 방금 넣은 것을 꺼낸뒤에 LinkedList 객체를 다음 레코드값에 있는 권한을넣은 것으로 재구성해서 다시 LinkedHashMap에 넣는다. 이런 식으로 getRolesAndResources 메소드는 LinkedHashMap을 만들게 된다.

 

이렇게 LinkedHashMap을 만들때 주의해야 할 사항이 있다. 이전 글에서 LinkedHashMap은 넣은 순서대로 key 목록을 가져온다고 언급한 적이 있다. 즉 Spring Securiry는 이 순서를 이용해서 url의 적용 우선 순위를 잡게 되는데 이 부분을 어디서 설정할까? DEF_ROLES_AND_URL_QUERY 변수에 설정된 쿼리를 다시 보자. 쿼리를 보면 SECURED_RESOURCES 테이블의 SORT_ORDER 컬럼 값으로 정렬을 하고 있다. SORT_ORDER 컬럼..이 컬럼은 무슨 역할을 하는걸까?

 

<intercept-url> 태그 설명시 상세한 URL 선언을 먼저하고 러프한 URL 선언을 나중에 해야 한다고 말한것을 기억할 것이다. 예를 들어 다음과 같이 /notmember/board.do와 /notmember/** 이렇게 2가지의 url 패턴이 있다고 하자. /notmember/board.do는 /notmember/**의 특정 부분이다. 이 특정 부분을 먼저 선언하고 /notmember/**를 선언해줘야 /notmember/board.do 접근시 /notmember/board.do 패턴으로 적용한 것을 먼저 만나도록 할 수가 있는것이다. SORT_ORDER 컬럼은 바로 이 순서를 정하는 것이다. DB에서 /notmember/board.do 에 대한 SORT_ORDER 컬럼의 값(예를 들어 이 값을 10으로 했다고 가정해보자)이 /notmember/** 에 대한 SORT_ORDER 컬럼의 값(예를 들어 이 값을 100으로 했다고 가정해보자)보다 작아야 쿼리 결과에서 먼저 올라오게 될 것이다. 그리고 먼저 올라왔기 때문에 LinkedHashMap에 먼저 들어가게 될것이다. 이런식으로 SORT_ORDER 값을 설정할때는 이런 주의점을 기억하고 설정해야 한다.

 

이렇게 SecuredObjectDao 클래스에서 LinkedHashMap을 만들었다면 이제 이 DAO 를 Injection 받아서 LinkedHashMap을 가져오는 서비스 bean이 있어야 할것이다. 그것이 다음에 나오는 SecuredObjectService 인터페이스를 구현한 SecuredObjectServiceImpl 클래스이다 

 

SecuredObjectService 인터페이스

 

import java.util.LinkedHashMap;
import java.util.List;

import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.web.util.matcher.RequestMatcher;

public interface SecuredObjectService {

	/**
     * 롤에 대한 URL의 매핑 정보를 얻어온다.
     * @return
     * @throws Exception
     */
    public LinkedHashMap<RequestMatcher, List<ConfigAttribute>> getRolesAndUrl() throws Exception;

    /**
     * 롤에 대한 메소드의 매핑 정보를 얻어온다.
     * @return
     * @throws Exception
     */
    public LinkedHashMap<String, List<ConfigAttribute>> getRolesAndMethod() throws Exception;

    /**
     * 롤에 대한 AOP pointcut 메핑 정보를 얻어온다.
     * @return
     * @throws Exception
     */
    public LinkedHashMap<String, List<ConfigAttribute>> getRolesAndPointcut() throws Exception;

    /**
     * Best 매칭 정보를 얻어온다.
     * @param url
     * @return
     * @throws Exception
     */
    public List<ConfigAttribute> getMatchedRequestMapping(String url) throws Exception;

    /**
     * 롤의 계층적 구조를 얻어온다.
     * @return
     * @throws Exception
     */
    public String getHierarchicalRoles() throws Exception;
}

 

SecuredObjectServiceImpl 클래스

 

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Set;

import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;

import com.terry.springsecurity.common.security.dao.SecuredObjectDao;
import com.terry.springsecurity.common.security.service.SecuredObjectService;

public class SecuredObjectServiceImpl implements SecuredObjectService {

private SecuredObjectDao securedObjectDao;
	
	public SecuredObjectDao getSecuredObjectDao() {
		return securedObjectDao;
	}

	public void setSecureObjectDao(SecuredObjectDao secureObjectDao) {
		this.securedObjectDao = secureObjectDao;
	}

	@Override
	public LinkedHashMap<RequestMatcher, List<ConfigAttribute>> getRolesAndUrl() throws Exception {
		// TODO Auto-generated method stub
		LinkedHashMap<RequestMatcher, List<ConfigAttribute>> ret = new LinkedHashMap<RequestMatcher, List<ConfigAttribute>>();
		LinkedHashMap<Object, List<ConfigAttribute>> data = securedObjectDao.getRolesAndUrl();
		Set<Object> keys = data.keySet();
		for(Object key : keys){
			ret.put((AntPathRequestMatcher)key, data.get(key));
		}
		return ret;
	}

	@Override
	public LinkedHashMap<String, List<ConfigAttribute>> getRolesAndMethod()
			throws Exception {
		// TODO Auto-generated method stub
		LinkedHashMap<String, List<ConfigAttribute>> ret = new LinkedHashMap<String, List<ConfigAttribute>>();
		LinkedHashMap<Object, List<ConfigAttribute>> data = securedObjectDao.getRolesAndMethod();
		Set<Object> keys = data.keySet();
		for(Object key : keys){
			ret.put((String)key, data.get(key));
		}
		return ret;
	}

	@Override
	public LinkedHashMap<String, List<ConfigAttribute>> getRolesAndPointcut()
			throws Exception {
		// TODO Auto-generated method stub
		LinkedHashMap<String, List<ConfigAttribute>> ret = new LinkedHashMap<String, List<ConfigAttribute>>();
		LinkedHashMap<Object, List<ConfigAttribute>> data = securedObjectDao.getRolesAndPointcut();
		Set<Object> keys = data.keySet();
		for(Object key : keys){
			ret.put((String)key, data.get(key));
		}
		return ret;
	}

	@Override
	public List<ConfigAttribute> getMatchedRequestMapping(String url) throws Exception {
		// TODO Auto-generated method stub
		return securedObjectDao.getRegexMatchedRequestMapping(url);
	}

	@Override
	public String getHierarchicalRoles() throws Exception {
		// TODO Auto-generated method stub
		return securedObjectDao.getHierarchicalRoles();
	}

}

 

SecuredObjectService 인터페이스에는 5개의 메소드가 정의되어 있는데 우리는 URL에 따른 권한 작업을 하는 것이기 때문에 getRolesAndUrl() 메소드만 살펴보도록 하겠다. 그러면 SecuredObjectService 인터페이스를 구현한 SecuredObjectServiceImpl 클래스를 보자. 이 클래스에는 방금 만들었던 SecuredObjectDao bean 객체를 setter 메소드를 이용해서 Injection 받을수가 있다. 그리고 SecuredObjectService 인터페이스에서 정의한 getRolesAndUrl 메소드에서 SecurityObjectDao bean 객체의 getRolesAndUrl() 메소드를 호출해서 그 결과를 return 해주고 있다(SecuredObjectServiceImpl 클래스의 getRolesAndUrl 메소드에서 SecuredObjectDao bean 객체의 getRolesAndUrl() 메소드 결과를 재가공해서 이를 return 하는 이유를 잘 모르겠다. Set 객체를 사용하는 이유는 중복제거를 하기 위해서 하는건데 Map이란게 key는 중복이 될 수 있는 것이 아니어서 저렇게 할 필요가 없다고 생각되는데..)

 

그러면 이렇게 URL에 따른 권한 정보를 조회할 수 있는 기능을 가지고 있는 서비스를 어떻게 활용할 수 있을까? 일단 우리가 이걸 만든 목적은 Spring Security의 FilterSecurityInterceptor에 대상 정보(Security MetaDataSource)로 사용하기 위한 DefaultFilterInvocationSecurityMetadataSource와 같은 구조의 클래스에서 이 서비스를 사용해서 requestMap을 얻기 위함이었다. 그러면 이 requestMap이란걸 어떤식으로 설정해주는게 좋을까? Spring Security가 처음 올라가는 시점에 바로 DB를 조회해서 requestMap을 꾸며주는것이 가장 좋다. 근데 문제는 이렇게 URL에 따른 권한이 운영하는 도중에 바뀌는 상황이 없을까? 아니다. 운영하는 도중에 얼마든지 바뀔수 있는 사항이 발생한다. 예를 들어보자. 어떤 특정 페이지가 문제(여기서 문제라 함은 WAS에서 보여주는 jsp 500 에러 페이지라고 생각해보자)가 생겼다. 그러면 일단은 그 페이지에 대한 접근 권한 설정을 관리자 권한으로 바꾸어서 일반인은 못들어오게 한뒤에 다른 페이지(권한을 만족하지 못할것이기 때문에 별도로 만든 에러페이지로 가게 될 것이다)로 유도하는 방법이 있을것이다. 이렇게 URL에 따른 권한 설정은 운영중에서도 바뀔수가 있는 사항이다. 문제는 이런 작업을 할때 관리자 페이지에서 권한을 수정한 뒤에 WAS를 재가동시켜서 requestMap을 다시 재생성해야 하느냐이다. 그렇게 할 수는 없다. 그래서 특정 액션이 발생했을때 이 requestMap을 다시 만들어주는 기능도 필요하게 된다. 다시 만들어주는거야 어렵지는 않다. 우리가 방금 만든 SecuredObjectServiceImpl 클래스의 getRolesAndUrl() 메소드를 다시 호출해서 requestMap을 다시 가져오면된다. 그럼 이런 부분을 어떻게 구현하는것이 좋을까? 우리가 앞으로 만들 DefaultFilterInvocationSecurityMetadataSource와 같은 구조의 클래스에 이런 기능을 넣으면 된다. 그리고 아예 requestMap을 전문으로 만드는 bean(결국 SecuredObjectServiceImpl 클래스가 만들기는 하지만..)을 하나 제작해보도록 하자. 일종의 wrapper를 덧씌우는 것인데 이렇게 하면 URL 별 권한 뿐만 아니라 메소드나 포인트컷에 대한 조회도 가능하게끔 확장성을 가져갈 수 있다(실제 전자정부 프레임워크 세미나에서는 확장성을 가지고 있으나 여기서는 메소드나 포인트컷에 대한 설명을 하질 않을 것이라 이 wrapper에 대해서는 확장성을 빼버렸다. 관심있는 사람은 OLC에서 전자정부 프레임워크 세미나 중 Spring Security 세미나를 보면 이 부분을 알 수 있다)

 

UrlResourcesMapFactoryBean 클래스

 

import java.util.LinkedHashMap;
import java.util.List;

import org.springframework.beans.factory.FactoryBean;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.web.util.matcher.RequestMatcher;

import com.terry.springsecurity.common.security.service.SecuredObjectService;

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

	private SecuredObjectService securedObjectService;
	
	private LinkedHashMap<RequestMatcher, List<ConfigAttribute>> requestMap;
	
	public void setSecuredObjectService(SecuredObjectService securedObjectService) {
		this.securedObjectService = securedObjectService;
	}

	public void init() throws Exception {
		requestMap = securedObjectService.getRolesAndUrl();
	}
	
	@Override
	public LinkedHashMap<RequestMatcher, List<ConfigAttribute>> getObject() throws Exception {
		// TODO Auto-generated method stub
		if(requestMap == null){
			requestMap = securedObjectService.getRolesAndUrl();
		}
		return requestMap;
	}

	@Override
	public Class<?> getObjectType() {
		// TODO Auto-generated method stub
		return LinkedHashMap.class;
	}

	@Override
	public boolean isSingleton() {
		// TODO Auto-generated method stub
		return true;
	}
}

 

UrlResourcesMapFactoryBean 클래스는 FactoryBean이다. 바꿔 말해서 이 클래스를 <bean> 태그를 이용해 설정한 뒤 다른 <bean> 태그에서 이것을 참조하게 되면 UrlResourcesMapFactoryBean 클래스가 참조가 되는 것이 아니라 이 클래스의 getObject() 메소드를 통해 return 되는 객체가 셋팅되는 것이다. 이 클래스를 참조하게 되면 getObject() 메소드가 우리가 requestMap 타입으로 잡고 있는 LinkedHashMap<RequestMatcher, List<ConfigAttribute>> 타입의 객체가 참조되게 되는 것이다.  UrlResourceMapFactoryBean 클래스는 SecuredObjectService 인터페이스를 구현한 객체를 Injection 받을수 있기 때문에 우리가 위에서 만든 SecuredObjectServiceImpl 클래스를 Injection 시킬수가 있다. 그리고 getObject() 메소드에서 Injection 받은 SeciredObjectService 인터페이스 구현 bean의 getRolesAndUrl() 메소드를 호출함으로써 DB에서 URL에 따른 권한을 조회한 결과인 LinkedHashMap 클래스 객체를 return 하게 되는 것이다.

 

이제 대상 정보 클래스를 만들기 위해 먼저 선작업해야 할 것들을 다 만들었다. 이제는 대상 정보 클래스를 만들도록 하자. 위에서도 언급했지만 DefaultFilterInvocationSecurityMetadataSource와 같은 구조의 클래스를 만든다고 했었다. 이제 이 클래스를 만들어보도록 하자.

 

ReloadableFilterInvocationSecurityMetadataSource 클래스

 

import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import javax.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.security.web.util.matcher.RequestMatcher;

import com.terry.springsecurity.common.security.service.SecuredObjectService;

public class ReloadableFilterInvocationSecurityMetadataSource implements
		FilterInvocationSecurityMetadataSource {

	private Logger logger = LoggerFactory.getLogger(this.getClass());
	
	private final Map<RequestMatcher, Collection<ConfigAttribute>> requestMap;
	
	private SecuredObjectService securedObjectService;
	
	public ReloadableFilterInvocationSecurityMetadataSource(LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> requestMap){
		this.requestMap = requestMap; 
		
	}
	
	public void setSecuredObjectService(SecuredObjectService securedObjectService) {
		this.securedObjectService = securedObjectService;
	}


	@Override
	public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
		// TODO Auto-generated method stub
		HttpServletRequest request = ((FilterInvocation)object).getRequest();
		Collection<ConfigAttribute> result = null;
		for(Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : requestMap.entrySet()){
			if(entry.getKey().matches(request)){
				result = entry.getValue();
				break;
			}
		}
		return result;
	}

	@Override
	public Collection<ConfigAttribute> getAllConfigAttributes() {
		// TODO Auto-generated method stub
		Set<ConfigAttribute> allAttributes = new HashSet<ConfigAttribute>();
		for(Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : requestMap.entrySet()){
			allAttributes.addAll(entry.getValue());
		}
		return allAttributes;
	}

	@Override
	public boolean supports(Class<?> clazz) {
		// TODO Auto-generated method stub
		return FilterInvocation.class.isAssignableFrom(clazz);
	}

	public void reload() throws Exception {
		LinkedHashMap<RequestMatcher, List<ConfigAttribute>> reloadedMap = securedObjectService.getRolesAndUrl();

        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());
        }
        
        if (logger.isInfoEnabled()) {
            logger.info("Secured Url Resources - Role Mappings reloaded at Runtime!");
        }
	}

}

 

ReloadableFilterInvocationSecurityMetadataSource 클래스는 requestMap과 SecuredObjectService 인터페이스를 구현한 클래스를 Injection 받아서 작업하게 된다. requestMap을 Injection 받을때 어떻게 받을까? 방금 설명했던 클래스인  UrlResourcesMapFactoryBean 클래스를 Injection 받아서 requestMap을 받게 된다. UrlResourcesMapFactoryBean 클래스가 FactoryBean 클래스이기 때문에 UrlResourcesMapFactoryBean 타입 객체가 Injection 되는 것이 아니라는 것을 다시한번 강조한다. ReloadableFilterInvocationSecurityMetadataSource는 SecuredObjectService 인터페이스를 구현한 클래스를 Injection 받을 수 있기 때문에 위에서 언급했던 SecuredObjectServiceImpl 클래스 객체를 Injection 시킬수가 있고 이 클래스를 이용해서 DB에서 URL에 따른 권한이 들어있는 requestMap을 조회할 수 있게 된다. ReloadableFilterInvocationSecurityMetadataSource 클래스의 reload() 메소드는 바로 이런 기능을 쓰기 위해 만든것이다. 즉 이 클래스의 reload 메소드를 호출하면 기존에 셋팅된 requestMap을 지운뒤에 Injection 받은 SecuredObjectServiceImpl 클래스에서 다시 requestMap을 조회해서 셋팅하게 된다. 위에서 페이지 에러 발생시 관리자 페이지에서 권한을 수정한다고 했었는데 권한을 수정한뒤 이 bean 클래스의 reload 메소드를 호출하게 되면 DB에서 다시 requestMap을 만들어서 셋팅하게 되는 것이다.

 

ReloadableFilterInvocationSecurityMetadataSource 클래스는  FilterInvocationSecurityMetadataSource 인터페이스를 구현하기 때문에 getAttributes 메소드와 getAllConfigAttributes 메소드를 구현해야한다. getAttributes 메소드의 소스를 보면 파라미터로 받은 Object 객체를 HttpServletRequest 객체로 캐스팅 한 뒤 requestMap에서 캐스팅 된 HttpServletRequest 객체와 Ant 패턴으로 맞는 것을 찾아 그에 따른 권한 목록을 return 한다. 여기서 명심해야 할 부분이 있는데 이 메소드를 보면 loop를 돌면서 Ant 패턴과 맞는 것을 찾다가 발견하면 리턴할 변수에 설정하고 break를 걸어 loop 를 빠져나오고 있다. 이 Ant 패턴은 반드시 한개만 있는 것이 아니다. 예를 들면 HttpServletRequest로 온 것이 /notmember/board.do 라고 가정해보자. 그리고 requestMap에 등록되어 있는 패턴은 /notmember/board.do와 /notmember/** 이렇게 2개가 있다고 가정해보자. 이 패턴 2개 모두 /notmember/board.do를 모두 만족하지만 requestMap에 등록된 /notmember/board.do와 매핑이 되어야 올바른 권한 설정이 될 것이다. 이런점 때문에 requestMap을 만들때 순서가 의미가 있는 것이며 loop를 계속 돌 경우 다음 패턴에서도 만족할 가능성이 있기 때문에 찾으면 break를 걸어서 loop를 빠져나오게 한 것이다. 그리고 getAllConfigAttributes 메소드는 requestMap에 등록된 모든 Ant 패턴에 대한 권한 목록을 return 해주고 있다.

 

이번 글에서는 대상 정보 클래스를 만드는 법에 대해 살펴보았다. 내용이 길었는데, requestMap에 대한 이해가 필수이다. 이 requestMap을 순서에 의미를 두면서 만들어야 한다. 자원에 따른 권한이 requestMap에 설정되고 이 requestMap에 있는 내용을 가져다가 판단한다고 보면 되는 것이다. 다음 글에서는 이렇게 만든 대상 정보 클래스를 어떻게 활용하는지에 대해 다루겠다.