주요글: 도커 시작하기
반응형
사용자 인증 정보를 보관하는 가장 손쉬운 방법은 세션을 사용하는 것인데, 가용성 향상이나 부하 증가 대처를 위해 웹 서버를 옆으로 늘릴 경우 세셔 클러스터링을 해 주어야 한다. 하지만, 세션 클러스터링을 하려면 별도의 장비 구성이 필요하거나 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>


+ Recent posts