본문 바로가기

프로그래밍/Spring Security

Spring Security에서 설정하는 Access Denied 페이지

지난 2개의 글로 Spring Security에서 권한을 어떤식으로 이용하여 자원(URL) 접근을 제어하는지를 살펴보았다. 이번에는 권한과 관련된 마지막 얘기로 자원을 이용하는데 있어 권한이 충분치 않은 경우 어떻게 진행하는지를 알아보자.

 

우리가 자주 보는 웹페이지는 아니지만 다음과 같은 유형의 웹페이지를 보는 경우가 있다.

 

 

이 웹 페이지는 Tomcat에서 띄우는 것으로 Http Status Code가 403인 경우, 즉 서버가 접근을 허용하지 않는 자원을 접근할려고 시도했을때 보여지는 페이지이다. 이런 성격의 페이지는 모든 Web Server와 WAS 모두 가지고 있다. 모양은 약간약간씩 다르지만 보여지는 내용을 읽어보면 동일함을 알 수가 있다.

 

이 페이지를 왜 설명하는가? Spring Security에서 인증을 거진 사용자가 접근하고자 하는 자원(여기서는 웹페이지라고 생각해보자)에 대해 충분한 권한을 가지고 있지 않은 경우 이 Http 403 에러 페이지를 보여주기 때문이다. 쉬운말로 풀어보자면 로그인 한 사람이 관리자 권한을 가지고 있는사람이 아닌데 관리자만 이용할 수 있는 페이지를 접근할려고 시도한 경우 Http 403 에러 페이지를 보여준다는 얘기이다.

 

이런 웹페이지를 보여주는것은 사실 당연한 것이다. 특정 권한을 갖지 않은 상태에서 특정 권한을 갖고 있어야 접근할 수 있는 페이지를 그냥 접근하게 할 수는 없지않은가? 그렇기때문에 에러 처리하고 이를 적절한 표현방법(여기서는 Http 403 에러 페이지)로 표현하는 것이다. 그러나 이걸 실제 웹사이트 개발시에 나오게 할 순 없다. 생각해보라. 잘 이용하다가 에러가 생겼을 경우 디자인 된 웹페이지가 아닌 일반 Web Server나 WAS가 제공하는 전혀 디자인이 안된 페이지를 보여줄 수는 없지 않은가? 

 

Spring Security에서는 <http> 태그의 하위 태그로 <access-denied-handler> 라는 태그가 있는데 이 태그의 error-page 속성에 특정 페이지 URL을 입력하여 접근 권한이 없을 경우 사용자가 지정한 페이지를 보여줄 수가 있다.

 

<http auto-config="true" use-expressions="true">
	<access-denied-handler error-page="/common/access_denied.do"/>
</http>

 

이렇게 설정하면 기존 제공하는 Http 403 에러 페이지를 보여주는 것이 아니라 /common/access_denied.do 페이지를 보여주게 된다. 그러면 이 페이지에서는 무엇을 보여줘야 할까? 에러 메시지를 보여줘야 할 것이다. 다음은 /common/access_denied.do의 소스이다.

 

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page isErrorPage="true"%>
<%@ page import="org.springframework.security.core.context.SecurityContextHolder" %>
<%@ page import="org.springframework.security.core.Authentication" %> 
<%@ page import="org.springframework.security.core.userdetails.UserDetails" %>
<%@ page import="org.springframework.security.core.userdetails.UserDetailsService" %>
<html>
<head>
<title></title>
<style>
table{
	width:800px;
}
table, th, td
{
	border-collapse:collapse; 
	border:1px solid gray;
}
</style>
<jsp:include page="/WEB-INF/views/include/jsInclude.jsp"></jsp:include> 
<script type="text/javascript">
$(document).ready(function () {
	
});
</script>    
</head>
<body>
<div style="display:inline-block;">
    <jsp:include page="/WEB-INF/views/include/leftmenu.jsp"></jsp:include> 
    <div style="float:right;">
    	접근권한이 없습니다.<br> 담당자에게 문의하여 주시기 바랍니다. <br>
		${SPRING_SECURITY_403_EXCEPTION}							
		<br>
		<%
		Authentication auth = SecurityContextHolder.getContext().getAuthentication();
		Object principal = auth.getPrincipal();
		if (principal instanceof UserDetails) {
			String username = ((UserDetails) principal).getUsername();
			String password = ((UserDetails) principal).getPassword();
			out.println("Account : " + username.toString() + "<br>");
		}
		%>
		<a href="<c:url value='/main.do'/>">확인</a>	
    </div>
</div>
</body>
</html>

 

이 페이지의 소스는 특별히 하는 일은 없다. 즉 Spring Security가 설정한 예외 메시지를 화면에 보여주고 있다. 그리고 계정 정보를 조회해서 계정 아이디를 보여주고 있다. 다음은 관리자 권한이 없는 계정으로 로그인 한 상황에서 관리자 권한이 필요한 웹페이지를 접근할려고 시도했을때 위의 소스로 보여준 웹페이지가 실제 브라우저에서 어떤식으로 나타나고 있는지 보여주고 있다.

 

 

 

그러면 이런 화면이 어떤 식으로 나타나게 되는지 원리를 알아봐야 할 것이다. 이전 글에서 우리는 로그인 한 사용자가 가지고 있는 권한을 가지고 해당 자원을 접근할 수 있는지 판단하는 판단 주체(Access Manager)에 대해 설명한 적이 있다. 이 판단 주체에 등록된 거수기 역할을 하는 Voter들의 결과를 이용해 결정을 내리게 되는데, 우리는 이전 글에서 이 판단 주체를 등록된 Voter 중 단 1개라도 접근에 대해 허가해주면 이를 허가해주는 클래스인 org.springframework.security.access.vote.AffirmativeBased를 사용했다. 이 AffirmativeBased 클래스의 decide 메소드에서 등록된 Voter들의 판단 결과를 이용해서 최종 판단을 하게 되는데 이 decide 메소드에서 허가를 하지 않을 경우 org.springframework.security.access.AccessDeniedException을 생성해서 던지게 된다. AccessDeniedException을 AffirmativeBased 클래스가 던지면 이를 FilterSecurityInterceptor Filter가 받아 던지게 될 것이고, 이렇게 던져진 예외는 다시 FilterSecurityInterceptor Filter 전에 있는 Filter는 ExceptionTranslationFilter가 받게 될 것이다. ExceptionTranslationFilter는 두 가지의 예외를 전문으로 처리하게 되는데 하나는 org.springframework.security.core.AuthenticationException이고 다른 하나가 위에서 언급했던 AccessDeniedException 이다. ExceptionTranslationFilter는 AccessDeniedException을 받았을 경우 Anonymous 사용자(로그인 과정을 거치지 않은 사용자)인지 파악해서 Anomymous 사용자인 경우 로그인 페이지를 보여주도록 작업하고, 로그인을 거쳐서 권한을 가지고 있는 사용자인 경우 ExceptionTranslationFilter 에 등록된 handler에서 이 예외를 처리하도록 한다. 이 handler는 org.springframework.security.web.access.AccessDeniedHandler 인터페이스를 구현한 클래스가 handler로 등록이 된다. ExceptionTranslationFilter는 이 인터페이스에 있는 handle 메소드를 실행하여 AccessDeniedException 예외에 대한 처리를 하게 된다. 기본적으로 등록되는 handler는 AccessDeniedHandler 인터페이스를 구현한 org.springframework.security.web.access.AccessDeniedHandlerImpl 클래스가 등록된다. 즉 위에서 보여준 화면은 이 AccessDeniedHandlerImpl 클래스의 handle 메소드에서 보여준다. handle 메소드는 AccessDeniedHandler 인터페이스에 다음과 같이 정의되어 있다.

 

public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException

 

handle 메소드를 보면 HttpServletRequest와 HttpServletResponse, AccessDeniedException 객체를 파라미터로 받고 있기 때문에 사용자가 입력한 값과 출력할 부분을 별도로 정의할 수가 있게 되는 것이다.

 

예외 메시지는 handle 메소드에서 SPRING_SECURITY_403_EXCEPTION을 HttpServletRequest 객체의 key로 해서 저장하게 된다. HttpServletResponse 객체에 상태 코드를 403으로 셋팅한 뒤 사용자가 설정한 페이지가 있을 경우 해당 페이지로 forward를 시켜준다. 만약 설정한 페이지가 없을 경우엔 HttpServletResponse 객체에 상태 코드를 403으로 셋팅했기 때문에 웹서버나 WAS가 가지고 있는 페이지를 보여주게 된다. 위에서 <access-denied-handler> 태그 설정시 error-page 속성에 페이지 URL을 지정했었는데 error-page 속성에 지정한 페이지 URL이 바로 forward 시켜주는 사용자 설정 페이지 URL이다.

&nbap;

이런 원리를 왜 이리 장황하게 설명했는가 하면 이렇게 동작하는 것 또한 우리가 임의로 지정할 수 있기 때문이다. 위에서 handler로 사용되는 클래스는 AccessDeniedHandler 인터페이스를 구현했다고 말했다. 즉 우리가 AccessDeniedHandler 인터페이스를 구현한 클래스를 만들어 접근 권한 예외 발생시 어떤 식으로 process를 흐르게 할 지 지정할 수가 있는 것이다. 기존 제공되는 클래스인 AccessDeniedHandlerImpl 클래스는 예외 메시지만 보여주고 있는데 예외 메시지 뿐만 아니라 기타 특정 작업을 해야 할 경우 AccessDeniedHandler 인터페이스를 구현한 클래스를 하나 만들어서 handler로 등록하면 된다. <access-denied-handler> 태그에는 ref 속성이 있는데 이 ref 속성에 AccessDeniedHandler 인터페이스를 구현한 클래스를 등록하면 ExceptionTranslationFilter에서 이 ref 속성에 등록한 bean 을 AccessDeniedandlerImpl 클래스 대신 사용하게 된다.

&nbap;

이렇게 사용자 정의 AccessDeniedHandler 인터페이스를 구현하는 예를 하나 들어보자. 우리가 어떤 페이지를 접근할 때 일반적인 방법이 아닌 ajax로 접근하는 경우가 있다고 가정해보자. ajax로 접근하게 되면 callback 함수로 그 결과를 받게되는데 문제는 이 결과가 자바스크립트에서 해석이 되어야 한다는 것이다. 사이트의 모든 페이지가 ajax로 접근하는 것이 아님을 감안한다면 일반적인 페이지 접근시에는 위에서 보여준 저런 화면 스타일로 접근 권한 에러 화면을 보여주면 되겠지만 ajax로 접근할 경우엔 저런 화면으로 보여줄 수가 없다. 즉 callback 함수에서 ajax 결과를 받아 이를 자바스크립트로 해석해서 alert로 보여주는 식이어야 한다는 것이다. 이 두가지 상황을 모두 소화해야 하는 AccessDeniedHandler 인터페이스를 구현한 클래스를 정의해보자.

 

이걸 구현할려면 한가지 해야 할 작업이 있다. 그것은 일반적인 접근과 ajax로 접근한 것을 구분해야 할 방법을 정의해야 하는 것이다. 일반적인 접근과 ajax로 접근한 것을 어떻게 구분할까? 여기서는 ajax로 접근할 때 특정 헤더값을 넣어주는 것으로 이것을 구분하도록 하겠다. 즉 특정 헤더값을 넣은뒤에 handle 메소드에서 넘겨주는 HttpServletRequest 객체를 이용해 특정 헤더값이 있는지의 여부를 판단하여 구분할 수가 있다. ajax를 호출할때 다음과 같이 호출한다고 보자.

 

$.ajax({
	url : "${ctx}/admin/ajaxTest",
	data : {title : title, content : content},
	contentType: "application/x-www-form-urlencoded; charset=UTF-8",
	type : "POST",
	dataType : "json",
	beforeSend: function (xhr) {
		xhr.setRequestHeader("X-Ajax-call", "true");
	},
	success:function(data, textStatus, jqXHR){
		if(data.result == "OK"){
			alert("ok");
		}else{
			alert("fail");
		}
	},
	error:function(jqXHR, textStatus, errorThrown){
		if(jqXHR.status == 403){
			var response = $.parseJSON(jqXHR.responseText);
			alert("result : " + response.result + "\nmessage : " + response.message);
		}else{
			alert(errorThrown);	
		}
		
	}
});

 

다음의 코드는 jQuery를 이용한 ajax 사용 코드이다. jQuery를 이용해서 ajax를 사용해 본 경험이 있는 사람이라면 이 코드가 어떤식으로 동작하게 될지 이해가 갈 것이다. 여기서 중점적으로 봐야 할 부분은 beforeSend 부분과 error 를 정의한 부분이다. beforeSend는 지정된 URL로 요청을 보내기 전에 선행되는 작업을 하는 부분을 정의하게 되는데 여기서는 Request Header로 X-Ajax-call이란 이름의 헤더에 true란 값을 설정하고 있다. 바로 이 부분이 우리가 구현할 AccessDeniedHandler 인터페이스를 구현한 handler 클래스에서 Ajax로 호출했는지의 여부를 알아낼 수 있는 구분값이 된다. 즉 handler 클래스에서 X-Ajax-call이란 헤더값을 읽어서 그 값이 true이면 Ajax로 호출되었음을 알 수가 있는 것이다. 그리고 error 는 ajax를 호출했을때 http status code가 200(OK)가 아닌 다른 code가 return 되었을때 처리하게 되는 callback 함수를 지정하게 되는데 여기서 status code 값이 403인지를 확인하여 그에 따라 작업을 하고 있다. 여기서는 던지는 문자열을 json 객체로 변환한뒤 이를 alert 함수로 내용을 보여주고 있다.

 

그러면 이렇게 ajax를 호출할 때 해당 URL에 대한 접근 권한이 없어서 예외가 발생했을 경우 이를 처리하게 될 AccessDeniedHandler 인터페이스를 구현한 클래스를 보도록 하자

 

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.access.AccessDeniedHandler;

public class CustomAccessDeniedHandler implements AccessDeniedHandler {

	private String errorPage;
	
	@Override
	public void handle(HttpServletRequest request,	HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
		// TODO Auto-generated method stub
		// Ajax를 통해 들어온것인지 파악한다
		String ajaxHeader = request.getHeader("X-Ajax-call");
		String result = "";
		
		response.setStatus(HttpServletResponse.SC_FORBIDDEN);
		response.setCharacterEncoding("UTF-8");
		
		if(ajaxHeader == null){					// null로 받은 경우는 X-Ajax-call 헤더 변수가 없다는 의미이기 때문에 ajax가 아닌 일반적인 방법으로 접근했음을 의미한다
			Authentication auth = SecurityContextHolder.getContext().getAuthentication();
			Object principal = auth.getPrincipal();
			if (principal instanceof UserDetails) {
				String username = ((UserDetails) principal).getUsername();
				request.setAttribute("username", username);
			}
			request.setAttribute("errormsg", accessDeniedException);
			request.getRequestDispatcher(errorPage).forward(request, response);
		}else{
			if("true".equals(ajaxHeader)){		// true로 값을 받았다는 것은 ajax로 접근했음을 의미한다
				result = "{\"result\" : \"fail\", \"message\" : \"" + accessDeniedException.getMessage() + "\"}";
			}else{								// 헤더 변수는 있으나 값이 틀린 경우이므로 헤더값이 틀렸다는 의미로 돌려준다
				result = "{\"result\" : \"fail\", \"message\" : \"Access Denied(Header Value Mismatch)\"}";
			}
			response.getWriter().print(result);
			response.getWriter().flush();
		}
	}
	
	public void setErrorPage(String errorPage) {
        if ((errorPage != null) && !errorPage.startsWith("/")) {
            throw new IllegalArgumentException("errorPage must begin with '/'");
        }

        this.errorPage = errorPage;
    }

}

 

이해를 돕기 위해 소스에 주석을 달아놓았으나 그래도 설명을 하자면..

이 클래스에는 errorPage란 멤버변수가 있는데 이 변수가 하는 역할은 위에서 설명했던 접근 권한 에러가 발생했을시 보여줘야 할 페이지 URL을 여기에 설정하게 된다.그리고 handle 메소드에서 본격적인 작업을 하게되는데 먼저 X-Ajax-call이란 request header 값을 읽고 response.setStatus(HttpServletResponse.SC_FORBIDDEN);을 해서 http status code값을 403으로 셋팅을 해주도록 한다. 그리고 읽어들인 X-Ajax-call header 값이 null 일 경우 이런 header를 설정하지 않았음을 의미하기 때문에 이는 ajax를 이용한 호출이 아닌 일반적인 web page 호출 방식을 거쳤다고 보고 예외 메시지와 사용자가 로그인 한 계정 정보를 조회하고 이를 이 handler에 지정된 error page(위에서 언급했던 errorPage 멤버변수)에 관련 정보를 request attribute에 셋팅해준뒤 forward 하게 된다.

그러나 X-Ajax-call 이란 request header 값이 null이 아닐 경우(이 경우는 위에서 jquery ajax 메소드 설명시 beforeSend 에서 request header에 X-Ajax-call 헤더에 값을 셋팅한 경우가 되겠다) 이 값이 true 일 경우 관련 예외 메시지를 response stream에 write 하고 있다. 이 때 json 형식을 사용하고 있기 때문에 위에서 jquery ajax 메소드 설명시 error callback 함수에서 jquery의 parseJSON 메소드를 사용하여 json 객체로 변환을 할 수가 있게 되는 것이다. true가 아니라면 헤더값 설정을 잘못했다고 간주하고 그에 따른 에러메시지를 셋팅해서 response stream에 write 한다.

 

이렇게 만든 AccessDeniedHandler 인터페이스 구현 클래스는 다음과 같이 <bean> 태그에 선언한뒤 이를 <access-denied-handler> 태그에서 사용해주면 된다.

 

<access-denied-handler ref="accessDenied"/>

<beans:bean id="accessDenied" class="com.terry.springsecurity.common.security.handler.CustomAccessDeniedHandler">
	<beans:property name="errorPage" value="/common/access_denied2.do" />
</beans:bean>

 

errorPage 란 property에 보여줘야 할 에러 페이지 URL을 지정한뒤 이를 <access-denied-handler>의 ref 속성에 지정하면 끝난다. 이제 /common/access_denied2.do의 소스를 보자. 위에서 살폈던 /common/access_denied.do 소스와는 다소의 차이가 있다. 왜냐면 CustomAccessDeniedHandler 클래스에서 에러 메시지를 Spring Security가 지정한 상수를 request attribute의 key로 사용하지 않고 별도 key(erromsg)를 사용했고 사용자 계정 정보를 jsp 단에서 조사해서 내려주는 것이 아니라 CustomAccessDeniedHandler 클래스에서 조회해서 이를 request attribute에 셋팅해서 내려주고 있기 때문이다. 그래서 예전 /common/access_denied.do 소스보다는 더욱 간결해졌다. 보여지는 화면 내용은 예전 /common/access_denied.do와 같은 화면을 보여주고 있어서 이 화면은 생략하도록 하겠다.

 

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page isErrorPage="true"%>
<html>
<head>
<title></title>
<style>
table{
	width:800px;
}
table, th, td
{
	border-collapse:collapse; 
	border:1px solid gray;
}
</style>
<jsp:include page="/WEB-INF/views/include/jsInclude.jsp"></jsp:include> 
<script type="text/javascript">
$(document).ready(function () {
	
});
</script>    
</head>
<body>
<div style="display:inline-block;">
    <jsp:include page="/WEB-INF/views/include/leftmenu.jsp"></jsp:include> 
    <div style="float:right;">
    	접근권한이 없습니다.<br> 담당자에게 문의하여 주시기 바랍니다. <br>
		${errormsg}					
		<br>
		<c:if test="${not empty username}">
			${username}<br/>
		</c:if>
		<a href="<c:url value='/main.do'/>">확인</a>	
    </div>
</div>
</body>
</html>

 

위의 설명과 샘플 소스들은 먼저 로그인을 해서 인증을 얻은 뒤에 권한을 체크한다는 전제로 만들어진 것들이다. 그러면 만약 인증을 하지 않은 상태에서 접근 권한을 체크하게 된다면 어떻게 될까? 즉 Anonymous 권한이 있는 상태에서 Anonymous 권한으로는 접근할 수 없는 페이지를 접근할려고 시도할때 어떻게 될까? 기존 방법대로라면 당연 로그인 페이지로 이동을 한다. 그러나 위와 같이 ajax로 접근한다면 어떻게 될까? 이 부분을 이해할려면 ExceptionTranslationFilter의 이해가 필요하다. 위에서 설명했을때 ExceptionTranslationFilter는 두가지의 예외를 처리한다고 했다. AuthenticationException과 AccessDeniedException이 그것이었다. 만약 로그인을 하지 않은 상황이어서 Anonymous 권한을 가지고 있는 상태에서 Anonymous 권한으로는 접근할 수 없는 페이지를 접근할려고 시도한다고 생각해보자. 그러면 FilterSecurtyInterceptor 클래스에서는 권한을 만족시키지 못하기 때문에 AccessDeniedException을 던지게 되고 이것을 ExceptionTranslationFilter 클래스가 받게 될 것이다. 그러면 이 클래스는 이 예외에 대한 처리를 handleSpringSecurityException 메소드가 처리하도록 하게 하는데 이 메소드의 소스를 살펴보면 AccessDeniedException을 받았다 해도 그것이 로그인을 하지 않은 상태에서 받은 AccessDeniedException이라면 sendStartAuthentication 메소드에서 차리하도록 하고 로그인을 한 상태에서 받은 AccessDeniedException이라면 우리가 등록한 AccessDeniedHandler 에서 처리하도록 한다(정확하게는 등록한 AccessDeniedHandler 인터페이스의 handle 메소드를 실행한다) sendStartAuthentication 메소드에서 하는 일은 권한 정보를 null로 셋팅하고, 관련 정보를 세션에 저장한뒤 org.springframework.security.web.AuthenticationEntryPoint 인터페이스를 구현한 클래스의 commence 메소드를 실행한다. 우리가 <form-login> 태그를 설정하면 ExceptionTranslationFilter가 만들어질때 AuthenticationEntryPoint 인터페이스를 구현한 클래스인 org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint 클래스 객체가 셋팅이 되어서 들어가게 되며 이 클래스의 commence 메소드에서 로그인 페이지로 이동하는 작업을 하게 된다. 그러나 commence 메소드에서는 위에서 사용한 request header 값을 이용하는 작업을 하는 것이 아니기 때문에 Anonymous 권한으로 ajax를 통해 접근을 시도하려 할 경우 오동작을 하게 된다. 이 부분을 해결할려면 LoginUrlAuthenticationEntryPoint 클래스를 상속받은 새로운 클래스에서 commence 메소드를 위의 AccessDeniedHandler 코드 스타일로 헤더값을 조회해서 적절하게 처리해주는 작업을 해준뒤 이 클래스를 <bean> 태그를 이용해서 등록하고 <http> 태그의 entry-point-ref 속성에 해당 bean으로 설정해주면 된다. 그러나 entry-point-ref 속성을 지정할 경우 <form-login> 태그와 같이 사용하는 것은 문제가 있을 소지가 있어서(관련 샘플을 보면 entry-point-ref 속성과 <form-login> 태그를 같이 사용한 샘플을 본적이 없어서 같이 사용해도 문제가 없을지 장담을 할수가 없다) 아예 이런 상황일 경우 spring security가 제공하는 태그를 이용해서 화면에서 접근을 못하게 동적 화면을 구성하는 편이 좋을듯 하다

 

지금까지 Spring Security에서 접근 권한 예외 발생시 어떻게 처리하는지 알아보고 이를 어떻게 커스터마이징하는지 설명했다. 이번 글을 끝으로 해서 예전글에서 설명했던 커스터마이징 포인트에 대한 모든 설명을 다 마쳤다. Spring Security에 대해 몇가지 다루지 않은 주제가 있기는 하다. 세션 관련 관리나 SSL 관련쪽이 있는데, 세션 관련 내용은 구글링하면 설명이 잘 되어 있어서 별도로 언급을 할 필요성이 없었고 SSL 관련쪽은 아직 내공이 부족해서 글로써 설명하기에는 시점이 아직 이르다. 이거는 차후에 내공이 쌓이면 설명할 기회가 있을꺼라 생각한다. 

 

지금까지 설명했던 Spring Security 관련 샘플 소스를 github에 올려놓았다(https://github.com/TerryChang/spring_security_blog_sample). 사실 소스가 제대로 정리된 소스가 아니다(몇가지 시험삼아 구현해볼 기능을 할려고 템플릿을 잡아놓은것에 Spring Security를 적용한거라 불필요한 구조가 있다). 그래도 보는데는 큰 지장은 없을거라 생각한다. 소스를 받은뒤 README.txt를 읽어서 셋팅할때 참조하길 바란다. 궁금한 점은 언제든 댓글을 남겨주면 답글을 남기도록 하겠다.