잊지 않겠습니다.

'Rest'에 해당되는 글 2건

  1. 2014.05.23 REST API 구성시, Spring Security 구현 6
  2. 2013.09.13 HTTP protocol과 REST

Spring Security를 이용한 REST API를 만들때, 일반적인 Spring Security를 이용하는 경우에는 다Login 성공 또는 실패시에 모두 302번 Redirect가 발생하게 됩니다. 또한 인증되지 않은 사용자가 API에 접근한 경우에도 마찬가지로 302번 Redirect로 Login Page로 이동하게 되는데, 이는 REST API 개발시에는 원하지 않는 결과입니다.

먼저, 개발할 API 서버는 Form 인증을 지원하는 것을 전제로 합니다. Basic/Digest 인증의 경우에는 다른 방법으로 개발이 되어야지 됩니다.

일반적인 Form 인증을 이용하는 경우에 Spring Security의 기본 Form인증은 다음 문제를 해결해야지 됩니다.

  1. 인증되지 않은 Request가 들어오는 경우, 인증 Page로 Redirect
  2. Login 성공시, 200 Response가 되지 않고, Success Page로 Redirect
  3. Login 실패시, 200 Response가 되지 않고, Failure Page로 Redirect

결국은 모두 Redirect문제가 발생하게 됩니다. 이에 조금 추가를 해서, 일반적으로는 Spring Security는 application이 시작할 때, 각각의 URL에 대한 ROLE을 설정하게 됩니다. 이러한 ROLE의 설정 없이, Request가 발생하였을때 인증된 사용자인 경우에 ROLE을 확인하는 기능을 추가해보도록 하겠습니다.

인증되지 않는 Request에 대한 문제해결

Spring Security는 ErrorHandling에 인증되지 않았을 때의 동작을 AuthenticationEntryPoint를 설정하여 제어할 수 있습니다. 기본적으로 Form인증을 사용하는 경우에는 org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint가 설정이 됩니다. Spring code를 보면 다음과 같이 구성되어 있습니다.

public class LoginUrlAuthenticationEntryPoint implements AuthenticationEntryPoint, InitializingBean {
    public LoginUrlAuthenticationEntryPoint(String loginFormUrl) {
        this.loginFormUrl = loginFormUrl;
    }


    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
            throws IOException, ServletException {

        String redirectUrl = null;

        if (useForward) {
            if (forceHttps && "http".equals(request.getScheme())) {
                // First redirect the current request to HTTPS.
                // When that request is received, the forward to the login page will be used.
                redirectUrl = buildHttpsRedirectUrlForRequest(request);
            }

            if (redirectUrl == null) {
                String loginForm = determineUrlToUseForThisRequest(request, response, authException);

                if (logger.isDebugEnabled()) {
                    logger.debug("Server side forward to: " + loginForm);
                }

                RequestDispatcher dispatcher = request.getRequestDispatcher(loginForm);

                dispatcher.forward(request, response);

                return;
            }
        } else {
            // redirect to login page. Use https if forceHttps true

            redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);

        }

        redirectStrategy.sendRedirect(request, response, redirectUrl);
    }
}

보시면 최종적으로는 sendRedirect method를 통해서 login url로 redirect를 시켜주고 있는 것을 볼 수 있습니다. 이 부분에 대한 제어만을 변경시켜준다면 우리가 원하는 조건을 만들어줄 수 있습니다.

새롭게 만들어지는 RestAuthenticationHandler는 매우 단순한 코드를 갖습니다. 인증이 되지 않은 사용자들에게 401 status를 던질뿐이니까요.

@Component
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
    }
}

이제 이 부분을 Spring Security Configuration에 설정을 해줍니다.

@Configuration
@EnableWebSecurity
@ComponentScan(basePackages = "com.daesung.fms.api.securities")
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .exceptionHandling()
                .authenticationEntryPoint(restAuthenticationEntryPoint())
            .formLogin()
                .loginProcessingUrl("/auth/login")
                .usernameParameter("username")
                .passwordParameter("password")
                .and()
            .logout()
                .logoutUrl("/auth/logout");
    }
}

exceptionHandling()에 구성된 RestAuthenticationEntryPoint 객체만을 설정해주는 것으로 이제 인증되지 않은 request에는 401 status를 보내줄 수 있게 되었습니다.

인증 성공시, 200 status 반환

Spring Security를 이용한 Form 인증을 성공하는 경우, 일반적으로 SuccessfulUrl로 Redirect가 됩니다. 그런데 REST API의 경우에는 이와 같은 구성이 문제가 됩니다. 우리는 인증을 정상적으로 했는지에 대한 확인이 필요한 것이지, 인증을 한 후에 특정 Page로의 Redirect를 원하는 것이 아니기 때문입니다.

Form 인증에서, 인증이 성공한 경우에는 Spring Security 는 기본적으로 org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler에서 동작을 담당합니다. code를 보면 다음과 같습니다.

public class SavedRequestAwareAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
            Authentication authentication) throws ServletException, IOException {
        SavedRequest savedRequest = requestCache.getRequest(request, response);

        if (savedRequest == null) {
            super.onAuthenticationSuccess(request, response, authentication);

            return;
        }
        String targetUrlParameter = getTargetUrlParameter();
        if (isAlwaysUseDefaultTargetUrl() || (targetUrlParameter != null && StringUtils.hasText(request.getParameter(targetUrlParameter)))) {
            requestCache.removeRequest(request, response);
            super.onAuthenticationSuccess(request, response, authentication);

            return;
        }

        clearAuthenticationAttributes(request);

        // Use the DefaultSavedRequest URL
        String targetUrl = savedRequest.getRedirectUrl();
        logger.debug("Redirecting to DefaultSavedRequest Url: " + targetUrl);
        getRedirectStrategy().sendRedirect(request, response, targetUrl);
    }

    public void setRequestCache(RequestCache requestCache) {
        this.requestCache = requestCache;
    }
}

매우 단순한 code입니다. 여기서 중요한 것은 onAuthenticationSuccess method의 끝부분에 위치한 getRedirectStraegy()입니다. 이 부분만을 제거하고, 200 status만을 반환할 수 있다면 우리가 원하는 결과를 얻어낼 수 있습니다. 인증이 성공한 다음에 실행되기 때문에 매우 단순하게 만들 수 있습니다.

public class RestLoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

    private RequestCache requestCache = new HttpSessionRequestCache();

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        handle(request, response, authentication);
        clearAuthenticationAttributes(request);
    }

    protected void handle(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
            throws IOException, ServletException {
        SavedRequest savedRequest = requestCache.getRequest(request, response);

        if (savedRequest == null) {
            clearAuthenticationAttributes(request);
            return;
        }
        String targetUrlParam = getTargetUrlParameter();
        if (isAlwaysUseDefaultTargetUrl() ||
                (targetUrlParam != null &&
                        StringUtils.hasText(request.getParameter(targetUrlParam)))) {
            requestCache.removeRequest(request, response);
            clearAuthenticationAttributes(request);
            return;
        }
        clearAuthenticationAttributes(request);
    }
}

기본적으로 매우 단순하게 clearAuthenticationAttribute만 해주고, 다른 처리는 해줄 필요가 없습니다. 아무런 Exception이 발생하지 않고, response에 특별히 status를 설정하지 않는 경우에는 200 status를 반환하게 되니까요.

이제 작성된 AuthenticationSuccessHandler를 설정하면 Spring Security는 다음과 같은 코드가 됩니다.

@Configuration
@EnableWebSecurity
@ComponentScan(basePackages = "com.daesung.fms.api.securities")
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .exceptionHandling()
                .authenticationEntryPoint(restAuthenticationEntryPoint())
            .formLogin()
                .loginProcessingUrl("/auth/login")
                .usernameParameter("username")
                .passwordParameter("password")
                .successHandler(loginSuccessHandler()) //추가된 부분
                .and()
            .logout()
                .logoutUrl("/auth/logout")
            .authorizeRequests().anyRequest().authenticated();
    }
}

인증 실패 및 로그 아웃시 동작 설정

인증 실패시의 동작과 로그 아웃시의 동작 역시 인증 성공시와 동일하게 처리할 수 있습니다. 조금 차이가 있다면 인증 실패시에는 401번 UNAUTHENTICATION status를 보내야지 되고, 로그 아웃시에는 항시 200 status를 보내야지 되는 차이만이 있을 뿐입니다.

인증실패시의 Handler는 다음과 같이 구성될 수 있습니다.

/**
 * Created by ykyoon on 14. 4. 22.
 * when login failed, return http status code (401).
 */
public class RestLoginFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request,
                                        HttpServletResponse response,
                                        AuthenticationException exception) throws IOException, ServletException {
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
    }
}

그리고, 로그아웃시의 Handler 역시 다음과 같은 code로 구성가능합니다.

/**
 * Created by ykyoon on 14. 4. 21.
 * after Logouted, send http status ok code (200).
 */
public class RestLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        String targetUrl = determineTargetUrl(request, response);
        if (response.isCommitted()) {
            logger.debug("Response has already been committed. Unable to redirect to " + targetUrl);
        }
    }
}

이제 구성된 두개의 Handler를 추가하면 Spring Security는 다음 code와 같이 변경됩니다.

@Configuration
@EnableWebSecurity
@ComponentScan(basePackages = "com.daesung.fms.api.securities")
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .exceptionHandling()
                .authenticationEntryPoint(restAuthenticationEntryPoint())
            .formLogin()
                .loginProcessingUrl("/auth/login")
                .usernameParameter("username")
                .passwordParameter("password")
                .successHandler(loginSuccessHandler()) 
                .failureHandler(loginFailureHandler()) //추가2
                .and()
            .logout()
                .logoutUrl("/auth/logout")
                .logoutSuccessHandler(logoutSuccessHandler()) //추가2
            .authorizeRequests().anyRequest().authenticated();
    }
}

이렇게 구성하면 이제 Spring Security로 구성되는 REST API를 구성할 수 있습니다.

API URL별 권한 설정

REST API를 개발하면, 각 URL에 따른 사용 권한을 넣어주는 것이 일반적입니다. 또는 URL 단위로 권한을 묶어서 관리하게 되는데, 그럼 API가 하나하나 추가 될때마다 security의 설정에 매번 URL을 hard coding으로 넣어주게 된다면 이는 매우 큰 비용으로 발생되게 됩니다. 사용자에게 권한만 넘겨주고, 그 권한과 URL을 Request마다 체크를 해서 권한을 확인하면 Security에 대한 권한 코드를 좀더 동적으로 사용할 수 있습니다. (DB 값만을 변경시켜주면 URL에 따른 권한을 변경할 수 있습니다.)

먼저, Spring Security는 각각의 URL과 ROLE간의 Matching을 어떻게 하고 있는지에 대한 구조에 대한 이해가 필요합니다. Spring Security의 URL-ROLE간의 matching은 다음과 같이 진행됩니다.

  1. FilterInvocationSecurityMetadataSource을 통해 URL/METHOD를 이용한 접근권한이 어떻게 되는지 확인합니다. 이 과정이 매우 중요합니다. 우리가 Spring Security에 각각의 URL별 권한을 hard coding으로 넣어주는 경우에는, 이 MetadataSource가 hard coding된 것과 같은 역활을 하게 됩니다.
  2. FilterInvocationSecurityMetadataSource에서 얻어온 ROLE들과 사용자가 갖은 ROLE을 이용해서 AccessVote를 진행합니다.

이러한 두과정을 거치는 Filter가 존재를 하는데, 이 필터가 아래 그림의 최종적인 Filter인 FilterSecurityInterceptor입니다.

이러한 Filter Chain에서 새로운 Filter를 만들어서 Spring Security에 추가를 할 예정입니다. Spring Security의 FilterSecurityInterceptor에는 모든 인증된 Request만이 접근할 수 있도록 설정을 하고, 그 뒤에 우리가 새롭게 만든 Filter를 통해서 인증을 처리할 계획입니다.

먼저, 새로운 FilterInvocationSecurityMetaSource가 필요합니다. 이는 FilterInvocationSecurityMetadataSource interface를 상속하면 매우 간단하게 구성 가능합니다.

/**
 * Created by ykyoon on 14. 4. 21.
 * DB 기반의 인증 관리 시스템.
 */
public class FmsFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

    public FmsFilterInvocationSecurityMetadataSource(MenuRoleService menuRoleService) {
        this.menuRoleService = menuRoleService;
        parser = new FmsUrlParser();
        permissions = new Hashtable<>();
    }
    private final MenuRoleService menuRoleService;
    private final FmsUrlParser parser;
    private final Map<String, Permission> permissions;

    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        final HttpServletRequest request = ((FilterInvocation) object).getRequest();
        final String httpMethod = request.getMethod().toUpperCase();
        final String url = parser.parse(request.getRequestURI());
        final String key = String.format("%s %s", httpMethod, url);

        final Permission permission;
        if(permissions.containsKey(key)) {
            permission = permissions.get(key);
        } else {
            permission = menuRoleService.findByMethodAndUrl(httpMethod, url);
            if(permission != null) {
                permissions.put(key, permission);
            }
        }

        String[] roles;
        if(permission == null) {
            roles = new String[] { "ROLE_ADMIN" };
        } else {
            roles = new String[] { "ROLE_ADMIN", permission.getName() };
        }
        return SecurityConfig.createList(roles);
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return FilterInvocation.class.isAssignableFrom(clazz);
    }

    /**
     * url - permission hashmap clear
     */
    public void clear() {
        permissions.clear();
    }
}

작성된 code의 menuRoleService는 request URL과 http method를 이용해서 접근 가능한 ROLE을 반환합니다. 약간의 tip으로, DB를 계속해서 읽는 것보다는 Hashtable을 이용해서 한번 읽어들인 ROLE은 또다시 DB 조회를 하지 않도록 MAP을 가지고 있도록 구성하였습니다.

이제 AccessVote를 구현할 차례입니다. AccessVote는 FilterInvocationSecurityMetadataSource에서 넘겨준 ROLE과 사용자의 ROLE간의 비교를 해주게 됩니다.

/**
 * Created by ykyoon on 14. 4. 22.
 * Authorized Name 기반의 인증 Vote class
 */
@Slf4j
public class FmsAccessDecisionVoter implements AccessDecisionVoter<FilterInvocation> {

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

    @Override
    public boolean supports(Class<?> clazz) {
        return clazz != null && clazz.isAssignableFrom(FilterInvocation.class);
    }

    @Override
    public int vote(Authentication authentication, FilterInvocation fi, Collection<ConfigAttribute> attributes) {
        assert authentication != null;
        assert fi != null;
        assert attributes != null;

        SecurityConfig securityConfig = null;

        boolean containAuthority = false;
        for(final ConfigAttribute configAttribute : attributes) {
            if(configAttribute instanceof SecurityConfig) {
                securityConfig = (SecurityConfig) configAttribute;
                for(GrantedAuthority grantedAuthority : authentication.getAuthorities()) {
                    containAuthority = securityConfig.getAttribute().equals(grantedAuthority.getAuthority());
                    if(containAuthority) {
                        break;
                    }
                }
                if(containAuthority) {
                    break;
                }
            }
        }
        return containAuthority ? ACCESS_GRANTED : ACCESS_DENIED;
    }
}

AccessDecisionVoter는 하는 일이 매우 단순합니다. Authentication이 넘어왔고, 이에 대한 규칙을 이용해서 ROLE을 matching이 되는 것이 있는지 없는지를 확인하는 과정을 거치게 됩니다. 이 코드를 조금 더 수정을 한다면 권한 1, 권한 2, 권한 3 모두를 갖는 사용자만이 접근할 수 있는 규칙 역시 만드는 것이 가능합니다. 이건 Security에 대한 운영원칙과 BL에 따라 달라질 것입니다.

이제 구성된 FilterInvocationSecurityMetadataSource와 AccessDecisionVoter를 Spring Security에 추가해보도록 하겠습니다.

먼저 Bean들을 등록시켜줍니다.

    /**
     * FMS API 권한 Filter.
     * @return securityMetadataSource() 가 적용된 데이터.
     * @throws Exception
     */
    @Bean
    public FilterSecurityInterceptor filterSecurityInterceptor() throws Exception {
        FilterSecurityInterceptor filterSecurityInterceptor = new FilterSecurityInterceptor();
        filterSecurityInterceptor.setSecurityMetadataSource(securityMetadataSource());
        filterSecurityInterceptor.setAuthenticationManager(authenticationManager());
        filterSecurityInterceptor.setAccessDecisionManager(affirmativeBased());
        return filterSecurityInterceptor;
    }

    //여러 Voter를 추가하는 것이 가능. List로 index가 작은 Voter가 먼저 투표. 투표 포기(ACCESS_ABSTAIN)
    //가 되는 경우, 다음 Voter에게 넘어감.
    @Bean
    public AffirmativeBased affirmativeBased() {
        FmsAccessDecisionVoter voter = new FmsAccessDecisionVoter();
        List<AccessDecisionVoter> voters = new ArrayList<>();
        voters.add(voter);
        return new AffirmativeBased(voters);
    }

    @Bean
    public FilterInvocationSecurityMetadataSource securityMetadataSource() {
        return new FmsFilterInvocationSecurityMetadataSource(menuRoleService);
    }

그리고, FilterSecurityInterceptor 뒤에 우리가 만들어준 Filter를 등록시켜주면 됩니다.

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .exceptionHandling()
                .authenticationEntryPoint(restAuthenticationEntryPoint())
                .and()
            .formLogin()
                .loginProcessingUrl("/auth/login")
                .usernameParameter("username")
                .passwordParameter("password")
                .successHandler(loginSuccessHandler())
                .failureHandler(loginFailureHandler())
                .and()
            .logout()
                .logoutUrl("/auth/logout")
                .logoutSuccessHandler(logoutSuccessHandler())
                .and()
            .authorizeRequests().anyRequest().authenticated()
                .and()
            .addFilterAfter(filterSecurityInterceptor(), FilterSecurityInterceptor.class);
    }

Summary

REST API 서비스 구현시에, Spring Security를 이용한 인증처리를 만들었습니다. 기본적인 Form 인증과 구성된 Security 다음 동작이 다릅니다.

동작기본 Spring Security FormREST Security Form
인증성공시설정된 인증 성공 URL로 302 Redirect200 status return
인증실패시설정된 인증 실패 URL로 302 Redirect401 unauthentication status return
로그아웃시설정된 로그아웃 성공 URL로 302 Redirect200 status return

또한, API의 추가 또는 권한변경시 코드의 변경 없이, DB값을 조회해와서 동적인 ROLE을 설정하는 것이 가능합니다.

인증은 매우 중요하고, 인증은 회사의 운영원칙에 많은 변경이 오게 되는 것이 사실입니다. 특히 REST API를 구성한다면 이 인증에 대한 많은 고민들이 필요하실 것이라고 생각하며 이만 글을 줄입니다. 모두들 Happy Coding~

Posted by Y2K
,

* 사내 강의용으로 사용한 자료를 Blog에 공유합니다. Spring을 이용한 Web 개발에 대한 전반적인 내용에 대해서 다루고 있습니다.



WebApplication은 HTTP Protocol로 동작하는 네트워크 프로그래밍의 일종입니다. 

이 정의가 조금은 의문스러워보이실지 모르겠지만, 저희는 지금 네트워크 프로그래밍을 하고 있는것입니다. 다만 이 네트워크를 처리해주는 것이 Servlet Container가 됩니다. 네트워크 프로그래밍은 매우 어려운 작업이지만, 지금 우리가 할 수 있는 가장 큰 이유는 Servlet Container가 이 일을 처리해주고 있기 때문입니다. 이는 매우 큰 의미를 갖습니다. 더이상 개발자들은 이 어려운 네트워크 문제를 다루지 않고, 처리하고자 하는 BL에만 집중할 수 있다는 점이, 오늘날의 성공적인 web application 환경을 만들어주게 되었습니다.  네트워크 처리 부분만을 의미하는 것으로 web application server 라는 표현을 사용하기도 합니다. 

위에서 보시는것처럼 web application server는 web으로 동작하는 application만을 의미하게 됩니다. 또한 Java EE(java enterprise edition)의 명세 및 구현된 web application 기술에 대한 구현체가 servlet container라고 할 수 있습니다. 이에 따라 servlet container는 다음과 같은 정의를 내릴 수 있습니다. 

servlet container = web application server + Java EE web application 기술 구현체

가장 널리, 그리고 무료로 사용될 수 있는 servlet container는 다음과 같습니다. 

tomcat : 가장 오랫동안 servlet 기술 명세에 대한 reference 구현체였으며, 가장 잘 알려진 servlet container입니다. 
jetty : 실험적 시도를 가장 과감하게 도입하는 것으로 유명합니다. 또한 속도가 가장 빠르고 가벼운 servlet container로 유명합니다.
grizzly : 현 servlet 기술 명세에 대한 reference인 glassfish의 servlet framework입니다. 제한적 무료라서 그런지, tomcat에 비해서 대중성이 조금 떨어지고 있다는 느낌이 들긴 합니다. 


HTTP Protocol

HTTP는 다른 Protocol과는 조금 다른 특징을 가지게 됩니다. 일반적으로 network protocol은 size나 해석의 문제에 의하여 압축되고, 특정 문자로 해석되도록 데이터를 만드는 것이 일반적입니다. 주소번지 1번지부터 12번지까지는 특정 어떤값을 이용하는 식으로요. 그렇지만 HTTP Protocol은 String을 그대로 사용하고 있습니다.  만약에 daum 사이트에 갔을 때, http는 어떤 통신을 하는지 한번 간단하게 알아보도록 하겠습니다. 

먼저 request는 다음과 같이 발생됩니다.

GET / HTTP/1.1
Host: www.naver.com:8000
Connection: keep-alive
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.64 Safari/537.31
DNT: 1
Accept-Encoding: gzip,deflate,sdch
Accept-Language: ko-KR,ko;q=0.8,en-US;q=0.6,en;q=0.4
Accept-Charset: UTF-8,*;q=0.5
Cookie: JSESSIONID=1q5bdgf2p3b22nz1n133l3qd

모든 HTTP Protocol은 기본적으로 CR + LF에 의하여 한개의 항목이 끝나는 것을 지정합니다. 그리고 데이터는 {Name}: {Value} 형태로 구성이 됩니다. 이 형태는 매우 중요합니다. 공백과 :의 위치까지 정확하게 잡혀있는 Protocol format이 됩니다. 만약에 우리가 HTTP Protocol을 직접 만들어서 보내주고 싶다면 이와 같은 형태로 데이터를 구성해서 보내주면 됩니다. 

지금까지 보신 위 코드가 HTTP Header 가 됩니다.  개발시에 자주 이야기가 나오던 HTTP Header에 특정 데이터를 넣어서 보낸다던지, 특정 데이터를 얻어오는 일들이 모두 위의 Text로 구성이 됩니다. 

다음은 response입니다. 

HTTP/1.1 200 OK
Server: nginx
Date: Tue, 07 May 2013 01:24:54 GMT
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: close
Cache-Control: no-cache, no-store, must-revalidate
Pragma: no-cache
P3P: CP="CAO DSP CURa ADMa TAIa PSAa OUR LAW STP PHY ONL UNI PUR FIN COM NAV INT DEM STA PRE"
Vary: Accept-Encoding,User-Agent
Content-Encoding: gzip

404


보시면 request와 비슷한 내용이 return되는 것을 알수 있는데, 아래 부분에 약간 다른 내용이 있습니다. 이 부분이 Body입니다. Body는 Header다음에 CR + LF + CR + LF 후에 나오는 text 영역이 됩니다. 우리가 만드는 HTML 부분이 되는 것이 일반적입니다. 

HTTP Protocol의 request type

request의 첫줄에 나오는 GET / HTTP/1.1에 주목할 필요가 있습니다. 이 부분은 지금까지 사용되는 부분과 조금 다릅니다. 이 부분을 http method라고 불리우는 영역입니다. HTTP method에 따라서 url의 parameter를 server에서 달리 해석하게 됩니다. 

1. GET
우리가 가장 자주 보는 형태입니다. parameter는 ?로 시작되고, =로 key와 value가 설정이 됩니다. 그리고 다른 key가 있는 경우에는 &로 연결이 됩니다. 따라서 형태는 다음과 같이 구성이 될 수 있습니다. 


가장 주로 사용되는 형태입니다. HTTP Protocol의 정의상으로는 GET는 정보의 조회시에 사용하는 형태입니다. <a> tag나 browser의 url로 접근하는 경우에는 모두 GET으로 동작하게 됩니다. 

2. POST
역시 자주보이는 형태입니다. parameter의 구성방법역시 GET과 동일합니다. 다만 parameter를 보내는 방법에 큰 차이를 가지고 있습니다. GET에서 parameter를 이용해서 구성을 했지만, POST는 body에 GET에서 보내줬던 paramter를 넣어서 보내주게 됩니다. 이 방법은 사용자에게 parameter를 노출시키지 않는 장점을 가지고 있고, GET에서 불가능한 대용량 대이터를 보내는 것이 가능합니다. 그리고 file upload 시에도 POST 방식을 사용하게 됩니다. HTTP protocol에서는 기본적으로 update의 의미를 가지고 있습니다. 

위 GET/POST는 가장 많이 사용되고 있는 HTTP method입니다. 그리고 Browser에서 기본적으로 지원을 하고 있습니다. 그렇지만, 아래 나오는 method들은 구형 browser에서는 지원되지 않고 있습니다. 그리고 GET/POST 이외에는 parameter를 보내지 못합니다. 이 부분은 RFC 문서에 정의된 내용으로 HTTP 1.1 이상의 버젼에서는 추후 지원할 수도 있습니다.  

3. DELETE
조금은 생소한 method입니다. HTTP protocol에서는 이름 그대로 delete의 의미를 가지고 있습니다. 

4. PUT
Create/Insert의 의미를 갖는 method 입니다. 

5. TRACE
보낸 request를 그대로 다시 보내주길 원하는 method입니다. echo server나 server의 상태가 원활한지를 알아보는 방법으로 사용됩니다.  이 method는 매우 심각한 문제를 가지고 있습니다. XST(Cross-Site Tracing)이라는 악의적 공격방법을 이용해서 사용자의 request에 들어있는 인증정보를 빼돌릴때 사용이 됩니다. 그래서 TRACE를 지원하는 web server들은 모두 심각한 오류를 가지는 것으로 보고되며, 모든 web server는 TRACE method를 무효할 것을 권장하고 있습니다. 

6. OPTIONS
Web Server에서 HTTP Method중 어떤 것들을 지원하는지 알아보는데 사용됩니다. 

7. CONNECT
Proxy에서 사용됩니다. Http TLS(Transport Layer Security) Tunnelling을 요청할 때 사용됩니다. CONNECT로 보내지는 method는 그 서버를 통해서 다른 서버에 접속하게 되는 것을 요청하게 됩니다. 

8. HEAD
GET과 동일한 정보를 return합니다. 다만 차이를 갖는 것이 GET은 Body에 데이터를 넣어서 보내지지만, HEAD는 Header에 Message-Body에 body를 모두 넣어서 보내게 됩니다. HEAD method는 browser에서 cache를 사용할 때 주로 이용합니다. 


위 method를 이용한 결과는 HTTP result라는 숫자로 표시가 되는데요. 이는 response의 첫줄에 나오는 숫자가 Return에 대한 결과입니다. 우리가 자주 보는 HTTP result의 결과가 바로 이것입니다. 

1. 2xx - Success
# 200 : OK
# 201 : Created - POST나 PUT에 의해서 새로운 Resource가 생성되었음을 나타내는 result code입니다.
# 202 : Accepted - async http request가 들어왔을 때, 그 request가 수용되었음을 나타냅니다. 
# 203 : Partial Information - return되는 정보는 cached된 것이거나, 내부 정보임을 의미합니다. 
# 204 : No Response - Response가 정상적으로 처리되었지만, Output의 내용이 없음을 의미합니다. 주로 Body가 비어있는 경우에 204를 return 합니다.

2. 3xx - Redirect
# 301 : Moved - 요청된 URL의 페이지가 다른 곳으로 이동되었음을 나타냅니다.
# 302 : Found - 요청된 URL의 페이지에서 다른 곳으로 이동하는 것을 원하는 것을 나타냅니다. 301과 비슷하지만, 302의 경우에는 Form을 POST로 넘겼을 때, 그 결과에 대한 Accept의 의미로 사용됩니다.
# 303 : Method - Found와 같은 의미로 사용되지만, Found는 URL로 이동됨을 나타냅니다. Method는 Body에 있는 Document를 이용해서 표시하는 것을 의미하게 됩니다.
# 304 : Not modified - 변경된 상황이 없기 때문에 Cache에 있는 내용을 이용해서 표시하라는 것을 의미합니다.

3. 4xx - Client Error
# 400 : Bad Request - HTTP Protocol에 어긋난 request가 입력되었습니다.
# 401 : Unauthorized - 인증되지 않은 HTTP Request가 들어왔습니다.
# 402 : Payment Required - Http Head에 있는 정보를 변경해서 다시 보내주는 것을 요청할때 사용됩니다.
# 403 : Forbidden - 인증되었지만, 권한이 없음을 나타냅니다.
# 404 : Not Found - Resource가 없는 URL에 요청되었음을 나타냅니다. 우리가 Spring으로 web을 개발할때, 처음에 자주 볼 수 있는 에러입니다. Controller 뿐 아니라 View File이 없는 경우에도 404 Not Found가 표시됨을 유의해주시길 바랍니다. 

4. 5xx - Server Error
# 500 : Internal Error - 서버 내부의 에러입니다. java나 .net에서 exception이 발생했을 때, 이것을 처리하지 않을때 나오는 에러입니다.
# 501 : Not Implemented - 아직 구현되지 않은 URL을 호출했습니다. 이는 404와는 다른 에러입니다. 404의 경우에는 아애 없는 URL이 호출된 상황이고, 501의 경우에는 구현되지 않았지만, URL resource는 존재할 때 사용됩니다.
# 502 : Timeout - RFC에 의해서 정의된 에러는 아닙니다. 그렇지만 몇몇 WebServer들은 구현되어있는 에러코드로, Server의 BL로직이 너무 오래 걸릴때, 에러로 return 해줍니다. 


웹의 구조적 아키텍쳐

apache httpd를 시작한 Roy Fielding은 web은 주요 제약점에 의해서 확장성이 좌우되는것을 알게 되고, 그에 대한 구조적 스타일을 다음과 같이 정의했습니다. 

1. Client / Server
웹은 client/server 기반 system으로 client/server 규약의 핵심은 관심의 분리입니다. 웹의 일괄된 인터페이스를 따른다는 가정하에, Client와 Server는 각자의 언어 및 기술을 이용해서 독립적으로 구현되고 배포가 가능하게 됩니다. 

2. Uniform interface
웹을 구성하는 Client/Server/Network 간의 interface는 일관성에 기반하고 있습니다. 이러한 구조가 붕괴가 되는 경우, 현 웹 커뮤니케이션체계는 붕괴가 되어버립니다. 
이 인터페이스는 다음과 같은 정의들을 보일 수 있습니다. 

# 리소스 식별 : URI로 구분될 수 있는 resource는 unique하기 때문에, 고유 식별자로 사용될 수 있습니다.
# 표현을 통한 Resource 처리 : HTML, JSON과 같은 web resource는 표현 방법이 정해져있지 않습니다. HTML을 그냥 text로 보여줘도 되고, web browser에서 보이듯이 rendering해서 표시를 해도 괜찮다는 뜻입니다. 이는 Document-View 구조와 같이 Resource에 대한 처리는 Client에게 맡긴다는 정의로도 볼 수 있습니다.
# 자기 서술적 메세지 : HTTP protocol의 header는 자신에 대한 서술적 메세지를 포함합니다. 그리고, 요청에 대한 처리는 전적으로 요청에 대한 응답자에서 결정하게 됩니다. 
# Application 상태 엔진으로서의 Hyper-media : resource의 상태표현은 resource의 link를 포함합니다. 따라서 모든 resource는 실타래처럼 연결되기 때문에 사용자들은 정보와 application을 직접적인 방식으로 훝어보는 것이 가능하게 됩니다. 이는 HTML의 표준적인 특징중 하나입니다.

3. Layered System
우리가 구성하는 MVC와 같은 Layered System이 아닌 Browser-proxy-gateway-switch-web server 와같은 Layer가 구성되는 network 기반의 중간계층을 사용할 수 있는 구조적 특징을 갖습니다. network 기반의 중간계층의 경우에는 보안의 강화 또는 응답 캐싱, 부하를 분산하는 용도로 주로 사용됩니다. 

4. Cache
cache는 웹구조의 중요한 제약조건중 하나입니다. 캐시는 웹자체의 전체적인 비용을 줄일 수 있는 기술적 요건이며 server, network, client 모두에 위치가 가능하게 됩니다. 

5. Stateless
웹의 가장 큰 특징중 하나입니다. web server가 client의 상태를 직접 관리할 필요가 전혀 없다는 것입니다. 따라서 client는 server와 상호작용하는 관련 상황정보를 직접 관리를 해야지 됩니다. 이는 web server가 client와의 복잡한 연결을 위해 필요한 상태 관리를 전혀 하지 않는다는 점입니다. 이를 다른 말로 FF(fire and forget)이라고도 합니다. 이는 장점도 단점도 아닙니다. 비용상의 trade-off라고도 할 수 있습니다. 

6. Code-on-demand
웹은 주문형 코드를 많이 사용합니다. 이 제약조건은 script나 plugin과 같은 실행가능한 program을 일시적으로 program에 전송하여 client가 실행할 수 있도록 합니다. 그렇지만 이는 client와 server간의 강한 결합을 가지고 오게 되는데. 이는 web에 대한 확장성을 막아버릴 수 있기때문에 최대한 지양해야지 되는 구조라고 할 수 있습니다.

이러한 구조적 스타일에 기반한 웹의 확장은 2000년 Roy Fielding에 의해서 'Representational State Transfer'라고 이름을 붙여 발표하게 되었습니다. 그리고 이 논문의 제목은 지금 REST라는 용어로 더욱더 많이 사용되고 있습니다. 

REST는 어느날 갑자기 나온것이 아니라, 웹의 구조적인 특징을 이용하고, 그에 대한 안정화로서 나온 기술입니다. 어찌보면 기술이라기보다는 'Style'이라고 할 수 있는 방법입니다. REST에 충실한 구조를 RESTful 하다는 용어로 사용하고 있고, 이는 다음과 같은 특징을 가지고 있습니다. 

1. URI 식별자 설계
# '/'는 계층 관계를 나타내는데 사용됩니다.
# URI의 마지막 문자로 '/'는 사용하지 않습니다.
# '-'는 URI의 가독성을 높이는데 사용합니다.
# '_'은 URI에서 사용하지 않습니다.
# URI는 소문자로 구성을 합니다.
# 파일 확장자는 URI에 포함하지 않습니다. 파일 확장자는 URI를 통해 얻어질 수 있는 media-type에 따라 결정이 됩니다.

2. URI 디자인
# Document/Object의 이름은 단수를 이용한다 (ex: http://www.daum.net/leagures/teams/players/report)
# Collection이 표시되는 경우, 복수를 이용한다 (ex: http://www.daum.net/leagures/teams/players)
# 제어가 발생되는 URI는 동사나 동사구를 이용한다 (ex : http://www.daum.net/students/morgan/register)
# CRUD 기능을 나타내는 것은 URI에 사용하지 않는다. 
- 이는 REST API를 구성할 때 사용되는 규칙입니다. 
- 위에서 살펴본 GET/POST/DELETE/PUT을 이용한 URI를 구성해서 CRUD 기능을 만들어주는 것이 권장됩니다.

3. input parameter
# parameter의 경우에는 URI query라는 이름으로 사용됩니다. 
# URI query의 경우에는 검색기준으로 사용되는 것이 일반적입니다. 이는 HTTP GET method의 사용방법과 연관있습니다.

4. http method
# GET method는 리소스의 상태표현을 얻는데 사용됩니다.
# HEAD method는 응답에 대한 header만을 얻어올 때 사용합니다.
# PUT method는 리소스를 생성하거나 갱신하는데 사용합니다.
# POST method는 리소스를 생성하거나 갱신하는데 사용합니다. POST와 PUT는 일반적으로 같은 목적으로 사용됩니다. 다만 사람들끼리의 암시적인 약속으로 Create시에는 PUT을, Update에서는 POST를 사용하는 것이 일반적입니다.
# Control이 발생되는 URI는 반드시 POST에 의해서 실행됩니다.
# DELETE는 Resource를 삭제할 때 사용됩니다.
# OPTIONS는 resource의 사용 가능한 action method가 무엇인지를 알기위해서 구성됩니다.


Summary 

이번에는 HTTP에 대한 기본적인 이론에 대해서 알아봤습니다. HTTP는 우리가 만드는 모든 web application의 핵심기술입니다. 다만 기술의 구현은 servlet container 또는 web server에서 담당하지만, 그에 대한 동작을 알아보는 것은 개발시 디버그등에 매우 유리하게 만들 수 있습니다. 그리고 URI의 디자인은 URI 자체의 문서화 뿐 아니라 Google에서의 검색의 가장 큰 기준이 됩니다. 여기서 나온 내용들은 꼭 알아두시길 바랍니다.


Posted by Y2K
,