저작권 안내: 저작권자표시 Yes 상업적이용 No 컨텐츠변경 No

스프링5 입문

JSP 2.3

JPA 입문

DDD Start

인프런 객체 지향 입문 강의
사용자 인증 정보를 보관하는 가장 손쉬운 방법은 세션을 사용하는 것인데, 가용성 향상이나 부하 증가 대처를 위해 웹 서버를 옆으로 늘릴 경우 세셔 클러스터링을 해 주어야 한다. 하지만, 세션 클러스터링을 하려면 별도의 장비 구성이 필요하거나 WAS에 의존적일 수 있기 때문에, 세션 대신에 쿠키에 인증 정보를 암호화해서 저장하고(인증 쿠키라고 부르자) 웹 서버는 인증 쿠키를 파싱해서 사용자를 인증하는 방식으로 아키텍처를 단순화시키곤 한다. 이 경우, 로그인과 로그아웃을 처리하는 서버는 인증 쿠키를 생성하거나 삭제하는 역할을 수행하게 되고, 나머지 웹 어플리케이션들은 인증 쿠키를 이용해서 사용자의 인증 여부 및 아이디 등의 정보를 조회하게 된다.

Apache Shiro는 기본적으로 세션에 사용자 정보를 저장하기 때문에, 인증 쿠키를 이용해서 인증하도록 처리하려면 몇 가지 커스텀 구현을 제공해 주어야 한다. 최근에 필자가 시작한 프로젝트에서 인증 쿠키와 Shiro를 엮을 필요가 있었으며, 이를 위해 시도한 방법을 이 글을 통해 정리해보고자 한다.

쿠키를 이용한 인증을 처리하기 위해 다음과 같은 작업을 하였다.
  • 인증 쿠키가 존재할 경우 인증 쿠키 값을 이용해서 사용자 인증을 수행하는 Filter 구현
  • 인증 쿠키의 값을 이용해서 사용자를 인증해주는 Authenticator 커스텀 구현
  • SecurityManager 설정
    • 세션에 Subject를 보관하지 않도록 설정
    • 커스텀 Authenticator를 사용하도록 설정
  • Shiro 필터를 이용해서 알맞은 필터 체인 형성
한 가지씩 차례대로 살펴보도록 하자.

인증 쿠키가 존재할 경우, 인증 쿠키 값을 이용해서 인증을 수행하는 Filter
인증 쿠키가 존재할 경우 해당 인증 쿠키로부터 인증을 수행하도록 처리하는 코드는 간단한다. 아래는 구현 코드 예이다.

public class AuthenticationByUserAuthCookieFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {
        try {
            Cookie authCookie = getAuthCookie((HttpServletRequest) request);
            if (authCookie != null) {
                try {
                    authenticate(authCookie);
                } catch (AuthenticationException ex) {
                    // 인증 쿠키가 잘못되었으므로, 쿠키를 제거
                    removeInvalidAuthCookie((HttpServletResponse) response);
                }
            }
            chain.doFilter(request, response);
        } finally {
        }
    }

    private void removeInvalidAuthCookie(HttpServletResponse response) {
        // 쿠키 삭제 처리 코드 위치
    }

    private Cookie getAuthCookie(HttpServletRequest request) {
        // 인증 쿠키 구하는 코드 위치
    }

    private void authenticate(Cookie authCookie) {
        try {
            SecurityUtils.getSubject().login(
                    new UserAuthValueAuthenticationToken(
                            URLDecoder.decode(authCookie.getValue(), "UTF-8")));
        } catch (UnsupportedEncodingException e) {
            // TODO 잘못된 쿠키 값이므로 쿠키 삭제 필요
        }
    }
    ... // init(), destroy() 메서드
}


위 코드는 인증 쿠키가 존재할 경우, 인증 쿠키의 값을 이용해서 UserAuthValueAuthenticationToken 객체를 생성하고, 그 객체를 이용해서 로그인 요청을 수행한다. 이 필터를 적용하게 되면, 사용자의 웹 요청이 발생할 때 마다 매번 인증 처리를 수행하게 된다.

인증 쿠키 값을 이용하여 인증을 처리해주는 Authenticator 구현
인증 요청을 수행할 때 사용한 인증 토큰이 UserAuthValueAuthenticationToken 이므로, 이 토큰을 이용해서 인증을 처리해주는 Authenticator를 만들어주어야 한다. 아래 코드는 구현 예이다.

public class AuthValueAuthenticator extends AbstractAuthenticator {

    @Override
    protected AuthenticationInfo doAuthenticate(AuthenticationToken token)
            throws AuthenticationException {
        if (token instanceof UserAuthValueAuthenticationToken) {
            return getUserAuthenticationInfo(token.getPrincipal().toString());
        }
        return null;
    }

    private AuthenticationInfo getUserAuthenticationInfo(String authToken) {
        String[] authInfo = AuthValueCryptor.decrypt(authToken);
        return new SimpleAuthenticationInfo(new UserPrincipal(authInfo[0],
                authInfo[1]), "", "AuthValueAuthenticator");
    }

}

토큰 타입이 UserAuthValueAuthenticationToken 이면, 해당 토큰으로부터 인증 값을 가져오고, 그 인증값으로 사용자 인증 정보(getUserAuthenticationInfo() 메서드)를 생성한다. 위 코드는 인증 쿠키 값에 사용자 ID 정보가 암호화되어 들어가 있는 경우의 구현 코드를 보여준 것이며, 인증 쿠키 값이 DB에 보관된 사용자 정보를 조회하기 위한 고유키 값이라면 그 키 값을 이용해서 정보를 조회한 뒤 AuthenticationInfo를 생성하도록 구현하면 될 것이다.

참고로, 위 코드에서 UserPrincipal은 커스텀 구현 Principal 클래스로서 사용자 ID와 이름을 보관하기 위해 만들었다.

스프링 설정을 이용한 SecurityManager 설정
Authenticator 구현 클래스를 작성했으므로 SecurityManager가 해당 Authenticator를 사용하도록 설정해 주어야 한다. 아래 코드는 스프링 설정의 예를 보여주고 있다.

<bean id="authValueAuthenticator"
    class="com.scgs.racon.infra.shiro.AuthValueAuthenticator">
</bean>

<!-- Shiro Security Manager -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    <property name="subjectDAO.sessionStorageEvaluator.sessionStorageEnabled"
        value="false" />
    <property name="realm" ref="realm" />
    <property name="authenticator" ref="authValueAuthenticator" />
</bean>

<bean id="realm" class="com.scgs.racon.infra.shiro.OpUserJdbcRealm">
    <property name="dataSource" ref="dataSource" />
    <property name="userRolesQuery"
        value="select ROLE_CODE from OP_USER_ROLE where OP_USER_ID = ?" />
    <property name="permissionsQuery"
        value="select PERMISSION_CODE from OP_USER_ROLE_PERMISSIONS where ROLE_CODE = ?" />
    <property name="permissionsLookupEnabled" value="true" />
</bean>

위 코드에서는 두 가지를 하고 있다.
  • Subject 정보가 세션에 보관되지 않도록 설정 (subjectDAO.sessionStorageEvaluator.sessionStorageEnabled 프로퍼티를 false로 설정)
  • 앞서 구현한 Authenticator를 사용하도록 설정

(참고로, realm의 경우에도 커스텀 Realm 구현 클래스이다.)


ShiroFilter 설정을 이용한 필터 설정
이제 남은 작업은 ShiroFilter를 이용해서 앞서 작성한 필터를 적용하는 것이다.

먼저 스프링 설정 파일에 아래와 같이 앞서 작성했던 필터를 생성하고, ShiroFilter에서 해당 필터를 사용하도록 설정한다.

<bean id="userAuthFilter"
    class="com.scgs.racon.infra.auth.AuthenticationByUserAuthCookieFilter">
</bean>

<bean id="userAuthCheckFilter" class="com.scgs.racon.infra.auth.AuthCheckFilter">
    <property name="loginUrl" value="/login" />
</bean>

<!-- Shiro Filter를 이용한 필터 체인 처리 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
    <property name="securityManager" ref="securityManager" />
    <property name="filterChainDefinitions">
        <value>
            /home=userAuthFilter
            /login=userAuthFilter
            /my/**=userAuthFilter,userAuthCheckFilter
        </value>
    </property>
</bean>

ShiroFilter의 필터 체인 정의를 사용해서 필터가 적용될 URL의 가장 첫 번째에 인증 필터를 적용한다. 그리고, 로그인을 안 한 경우 로그인 페이지로 이동시키고 싶다면, 해당 기능을 제공하는 필터를 만들어 추가해주면 된다. 예를 들어, 위 코드의 경우 /my/** 로 오는 모든 요청에 대해 먼저 userAuthFilter를 적용하고, 그 뒤에 userAuthCheckFilter가 적용되도록 했다. userAuthCheckFilter는 사용자가 인증되지 않은 경우 로그인 페이지로 이동시키는 기능을 제공한다고 할 경우, 위 설정은 /my/**로 요청이 들어올 경우 먼저 userAuthFilter로 사용자 인증을 처리하고(쿠키가 있는 경우에 한해 인증 처리), userAuthCheckFilter를 이용해서 인증을 하지 않은 사용자인 경우 로그인 페이지로 리다이렉트 시킨다.

참고로, userAuthCheckFilter는 아래와 같이 Subject.isAuthenticated()를 이용해서 인증 여부를 확인한다.

public class AuthCheckFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {
        if (!SecurityUtils.getSubject().isAuthenticated()) {
            sendRedirectLoginPage((HttpServletRequest) request,
                    (HttpServletResponse) response);
            return;
        }
        chain.doFilter(request, response);
    }
    ...
}

web.xml에서 ShiroFilter를 사용하도록 설정
이제 남은 작업은 ShiroFilter가 적용되록 web.xml에 설정하는 것이다. ShiroFilter가 서블릿 필터로 사용되록 하기 위해 DelegatingFilterProxy를 사용하면 된다. 아래는 설정 예이다.

<filter>
    <filter-name>shiroFilter</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    <init-param>
        <param-name>targetFilterLifecycle</param-name>
        <param-value>true</param-value>
    </init-param>
    <init-param>
        <param-name>contextAttribute</param-name>
        <param-value>org.springframework.web.servlet.FrameworkServlet.CONTEXT.dispatcher</param-value>
    </init-param>
</filter>

<filter-mapping>
    <filter-name>shiroFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>


Posted by 최범균 madvirus

댓글을 달아 주세요

쿠키를 좀더 쉽게 다룰 수 있도록 해 주는 쿠키 유틸티리 클래스를 작성해본다.

쿠키 유틸리티 클래스 CookieBox 만들기

쿠키는 웹 어플리케이션에서 클라이언트의 정보를 임시로 저장하기 위해 많이 사용된다. 또한, 클라이언트의 상태를 유지할 때 사용되는 세션을 구현하기 위해 쿠키를 사용하기도 한다. 쿠키는 약방의 감초와 같은 존재로서, 쿠키를 사용함으로써 좀더 쉽고 간결한 방법으로 웹 어플리케이션을 구현할 수 있게 되는 경우가 많다.

쿠키가 사용되는 부분은 많은데, 서블릿 API의 쿠키 지원 클래스는 2.4 버전이 나올 때 까지 여전히 빈약하다. 서블릿 API의 javax.servlet.http.HttpServletRequest 인터페이스가 제공하는 쿠키 관련 메소드는 아래에 표시한 한개뿐이다.

   public Cookie[] getCookies()

HttpServletRequest 인터페이스가 쿠키 관련된 메소드를 빈약하게 제공하기 때문에(진짜 심하게 빈약함이 느껴진다!!), JSP나 서블릿 등에서 쿠키를 사용할 때에는 쿠키를 다루기 위한 보조 클래스를 작성해서 작업하는 것이 좋다. 본 글에서는 편리하게 쿠키를 처리할 수 있도록 해 주는 CookieBox 라는 클래스를 작성해볼 것이다.

CookieBox 클래스의 소스 코드

말보다는 소스 코드를 직접 보면서 설명하는 것이 이해가 빠를 것 같으므로, CookieBox 클래스의 소스 코드부터 살펴보도록 하자.

    package jsp.util;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.Cookie;
    import java.util.Map;
    import java.net.URLEncoder; 
    import java.net.URLDecoder; 
    import java.io.IOException; 
    
    public class CookieBox {
        
        private Map cookieMap = new java.util.HashMap();
        
        public CookieBox(HttpServletRequest request) {
            Cookie[] cookies = request.getCookies();
            if (cookies != null) {
                for (int i = 0 ; i < cookies.length ; i++) {
                    cookieMap.put(cookies[i].getName(), cookies[i]);
                }
            }
        }
        
        public static Cookie createCookie(String name, String value)
        throws IOException {
            return new Cookie(name, URLEncoder.encode(value, "euc-kr"));
        }
    
        public static Cookie createCookie(
                String name, String value, String path, int maxAge) 
        throws IOException {
            Cookie cookie = new Cookie(name, 
                                    URLEncoder.encode(value, "euc-kr"));
            cookie.setPath(path);
            cookie.setMaxAge(maxAge);
            return cookie;
        }
        
        public static Cookie createCookie(
                String name, String value,  
                String domain, String path, int maxAge) 
        throws IOException {
            Cookie cookie = new Cookie(name, 
                      URLEncoder.encode(value, "euc-kr"));
            cookie.setDomain(domain);
            cookie.setPath(path);
            cookie.setMaxAge(maxAge);
            return cookie;
        }
        
        public Cookie getCookie(String name) {
            return (Cookie)cookieMap.get(name); 
        }
        
        public String getValue(String name) throws IOException {
            Cookie cookie = (Cookie)cookieMap.get(name);
            if (cookie == null) return null;
            return URLDecoder.decode(cookie.getValue(), "euc-kr");
        }
        
        public boolean exists(String name) {
            return cookieMap.get(name) != null;
        }
    }

CookieBox 클래스는 다음과 같이 두 가지 종류의 메소드를 제공한다.

  • Cookie 객체를 생성할 때 사용할 수 있는 static 메소드인 createCookie()
  • HttpServletRequest의 Cookie 객체 및 쿠키값을 읽어올 수 있는 메소드
CookieBox 클래스를 이용한 쿠키값 읽기

먼저, CookieBox 클래스를 사용하면 손쉽게 쿠키를 사용할 수 있다. CookieBox는 다음과 같은 형태로 사용할 수 있다.

    
    // CookieBox 클래스의 생성자는 request로부터 쿠키 정보를 추출
    CookieBox cookieBox = new CookieBox(request);
    
    Cookie idCookie = cookieBox.getCookie("id"); // 쿠키가 존재하지 않으면 null 리턴
    
    // 지정한 이름의 쿠키가 존재하는지의 여부
    if (cookieBox.exists("name")) {
        ...
    }
    
    // 지정한 이름의 쿠키가 존재하지 않으면 값으로 null 리턴
    String value = cookieBox.getValue("ROLE");

일단 CookieBox 객체를 생성한 이후에는 세 개의 메소드(getCookie(), exists(), getValue())를 사용해서 손쉽게 Cookie 객체 및 쿠키값을 사용할 수 있게 된다. 별도의 유틸리티 클래스를 사용하지 않고 쿠키를 사용할 때는 다음과 같은 방식을 사용하게 되는데, 아래 코드와 비교하면 얼마나 위 코드가 간단한 형태인지를 알 수 있을 것이다.

    Cookie[] cookies = request.getCookies();
    Cookie idCookie = null;
    
    if (cookies != null) {
        for (int i = 0 ; i < cookies.length ; i++) {
            if (cookies[i].getName().compareTo("id") == 0) {
                idCookie = cookies[i];
            }
        }
    }

CookieBox 클래스를 이용한 Cookie 생성

javax.servlet.http.Cookie 클래스가 제공하는 생성자는 다음과 같이 한개 뿐이기 때문에,

    Cookie(java.lang.String name, java.lang.String value)

쿠키에 대한 도메인, 경로, 유효시간 등을 설정하기 위해서는 다음과 같은 코드를 사용해야 한다.

    Cookie cookie = new Cookie(name, URLEncoder.encode(value, "euc-kr"));
    cookie.setDomain(domain);
    cookie.setPath(path);
    cookie.setMaxAge(maxAge);

CookieBox 클래스는 static 메소드인 createCookie() 메소드를 통해서 적은 코딩량으로 손쉽게 Cookie 객체를 생성할 수 있도록 지원한다. 예를 들어, CookieBox.createCookie() 메소드를 사용하면 위 코드를 다음과 같이 한줄로 변경할 수 있다.

    Cookie cookie = CookieBox.createCookie(name, value, domain, path, maxAge);

CookieBox.createCookie() 메소드가 세 가지 형태로 존재하기 때문에, 그때 그때 알맞은 메소드를 사용해서 Cookie 객체를 생성할 수 있을 것이다.

관련링크:
Posted by 최범균 madvirus

댓글을 달아 주세요