본문 바로가기

프로그래밍/Spring Security

Spring Security에서 로그인 작업 후 부가적인 작업을 설정해보자(로그인 성공시)

이전 글에서는 로그인 한 사용자의 정보를 화면에 보여주고 로그인 한 사용자의 권한에 따른 동적 메뉴를 구성하는 방법, 그리고 로그아웃에 대해 살펴보았다. 이번에는 이렇게 로그인 기능을 구현한 뒤의 추가 작업을 구성하는 방법에 대해 고민해볼 시간을 갖고록 한다.

 

로그인 작업이 성공을 하든, 실패를 하든 부가적인 작업이 필요한 상황이 올 수 있다. 예를 들면 로그인을 성공했으면 이를 하루 방문자수에 더한다거나, 로그인 한 사람의 로그인 횟수를 통계 목적을 위해 기록할 수도 있을것이다. 또 로그인에 실패했으면 관련 예외를 다른 방법으로 보여주고 싶을수도 있을 것이다. 즉 로그인 작업이 성공을 하든 실패를 하든 그냥 넘어가는 일은 아마 거의 없을 것이다. 여기서는 그런 작업을 할때 어떤 방법으로 이런 방법을 지정하는지를 알아보도록 하겠다.

 

로그인 작업이 성공했을때 먼가 부가적인 작업을 하고 싶을 경우 Spring Security에서 제공하는 인터페이스인 org.springframework.security.web.authentication.AuthenticationSuccessHandler를 구현한 클래스를 만든뒤에 이 클래스를 Spring Bean으로 등록한다. 그런 후 <form-login> 태그의 authentication-success-handler-ref 속성에 해당 클래스의 id를 넣어주면 된다. 다음은 그런 설정의 예이다.(미리 말하지만 <form-login> 태그의 상위 태그로 <http> 태그가 있다. <http> 태그를 빼고 사용한다는 오해를 불러일으킬수 있을 것 같아 미리 얘기해둔다. <http> 태그 안에 <form-login> 태그를 넣어서 설정한다)

 

<form-login
	username-parameter="loginid"
	password-parameter="loginpwd" 
	login-page="/login.do"
	default-target-url="/main.do"
	authentication-failure-url="/login.do?fail=true"
	authentication-success-handler-ref="customAuthenticationSuccessHandler"
/>

<beans:bean id="customAuthenticationSuccessHandler" class="com.terry.springsecurity.common.security.handler.CustomAuthenticationSuccessHandler">
</beans:bean>

 

form-login 태그의 authentication-success-handler-ref 속성에 CustomAuthenticationSuccessHandler 클래스를 등록한 bean의 id인 customAcuthenticationSuccessHandler를 줌으로써 로그인을 성공했을때 해당 bean을 타도록 지정하는 것이다. 이제 살펴봐야 할 것은 org.springframework.security.web.authentication.AuthenticationSuccessHandler 인터페이스이다. 이 인터페이스에 정의된 메소드는 다음의 메소드 1개 뿐이다.

 

public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException

 

Spring Security는 로그인이 성공한 뒤에 부가적인 작업을 해야 하는 경우 <form-login> 태그의 authentication-success-handler-ref 속성에 설정된 org.springframework.security.web.authentication.AuthenticationSuccessHandler 인터페이스를 구현한 클래스의 onAuthenticationSuccess 메소드를 실행하는 것으로 로그인 성공 후 작업을 진행하게 되는 것이다. 이 메소드로 넘어오고 있는 것은 HttpServletRequest 객체, HttpServletResponse 객체, Authentication 객체가 넘어오고 있는 것을 알 수 있다. 즉 웹으로 넘어온 Request 값을 가져올 수 있고(request.getParameter 메소드), 출력을 정의할 수 있으며(response.getWriter().println 메소드), 인증을 성공했기 때문에 로그인 한 회원의 회원 정보(authentication.getPrincipal())를 가져올 수도 있다. 지금 나열한 것은 일부 예를 들은것이다. HttpServletRequest 객체, HttpServletResponse 객체, Authentication 객체를 이용해서 가져올 수 있는 정보를 이용해 할 수 있는 일은 다 할 수 있다고 보면 된다. 위에서 언급했던 회원별 방문수 증가 작업을 한다면 Authentication 객체를 이용해 회원 로그인 아이디를 가져온뒤 그걸 이용해 DB에 작업하면 되는 것이다. Spring Security는 org.springframework.security.web.authentication.AuthenticationSuccessHandler 인터페이스를 구현한 클래스 2가지를 제공한다. org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler 클래스와 org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler 클래스이다. 그 중 SavedRequestAwareAuthenticationSuccessHandler 클래스 소스는 한번 봐두길 바란다. 다음부터 설명할 내용도 이 클래스를 기반으로 커스터마이징 한 클래스이기도 하기 때문이다.

 

그럼 이제부터 우리가 만들 이 CustomAuthenticationSuccessHandler 클래스에서 무엇을 할 것인가? 여기서는 우리가 로그인 할 때 로그인 후 이동할 URL을 지정하면 해당 URL로 이동하는 기능을 넣을려고 한다. 이 기능을 하는데 있어 몇가지 우선순위를 잡을 것이다. 다음이 그 우선순위이다.

 

● 지정된 Request Parameter(loginRedirect)에 로그인 작업을 마친 뒤 redirect 할 URL을 지정했다면 이 URL로 redirect 하도록 한다.

● 만약 지정된 Request Parameter에 지정된 URL이 없다면 세션에 저장된 URL로 redirect 하도록 한다.

● 세션에 저장된 URL도 없다면 Request의 REFERER 헤더값을 읽어서 로그인 페이지를 방문하기 전 페이지의 URL을 읽어서 거기로 이동하도록 한다.(REFERER 기능 사용 여부는 설정 가능하도록 한다. 이 기능 설정을 해야 하는 이유는 밑에서 별도로 설명하도록 하겠다)

● 위의 3가지 경우 모두 만족하는게 없으면 CustomAuthenticationSuccessHandler 클래스에 있는 defaultUrl 속성에 지정된 URL로  이동하도록 한다.

 

 

이런 우선 순위를 생각해두고 이 기능을 구현해보도록 하자. 먼저 첫번째 우선 순위 작업을 위해 작업을 해야 할 부분은 어렵진 않다. onAuthenticationSuccess 메소드에서 HttpServletRequest 객체를 가져오고 있기 때문에 URL이 들어가 있을 파라미터 이름으로 getParameter(파라미터 이름) 메소드를 실행시키면 되기 때문이다.

 

첫번째 우선 순위 작업을 만족하지 못했을 경우 진행할 두번째 우선 순위, 세션에 있는 URL로의 Redirect를 설명하겠다. Spring Security는 인증을 하지 않은 상태에서 권한이 필요한 화면을 접근할려고 할 경우 로그인 화면을 띄운다고 이전 글에서 언급한 적이 있다. 이때 로그인 화면을 띄우기 전에 필요한 정보를 세션에 저장하게 되는데 이 정보 중에 저장되는 것으로 Spring Security가 띄우는 로그인 화면을 보기 이전의 화면 URL도 저장하고 있다. 바로 이 URL을 꺼낸다는 것이다. 이 부분에 대해 설명할려면 먼저 Spring Security가 제공하는 RequestCache 인터페이스에 대한 이해가 필요하다.

 

org.springframework.security.web.savedrequest.RequestCache 인터페이스는 로그인 화면을 보여주기 전에 사용자 요청을 저장하고 이를 꺼내오는 메카니즘을 정의하는 인터페이스이다. 사용자의 요청은 org.springframework.security.web.savedrequest.SavedRequest 인터페이스를 구현한 클래스 단위로 저장된다. 즉 사용자가 요청했던 request 파라미터 값들, 그 당시의 헤더값들 등이 SavedRequest 인터페이스를 구현한 클래스에 담겨지게 되는 것이다. Spring Security는 SavedRequest 인터페이스를 구현한 클래스인 org.springframework.security.web.savedrequest.DefaultSavedRequest 클래스를 제공하는데 

RequestCache 인터페이스를 구현한 클래스는 이 DefaultSavedRequest 클래스 객체를 저장하게 되는것이다. Spring Security는 RequestCache 인터페이스를 구현한 클래스로 org.springframework.security.web.savedrequest.HttpSessionRequestCache 클래스를 제공하는데 앞에서 사용자 요청을 세션에 저장한다 함은 바로 이 HttpSessionRequestCache 클래스를 이용해서 사용자 요청 정보들이 들어있는 DefaultSavedRequest 클래스 객체를 세션에 저장한다는 뜻이다.

 

그럼 우리가 만드는 CustomAuthenticationSuccessHandler 클래스에서 세션에 저장되어 있는 사용자가 로그인 화면을 보기 전에 방문했던 URL은 어떻게 가져올까? HttpSessionRequestCache 클래스 객체를 생성해서 거기서 DefaultSavedRequest 클래스 객체를 가져온뒤 거기서 getRedirectURL 메소드를 호출하면 된다. 설명은 클래스로 얘기했지만 이 클래스들이 인터페이스를 구현한것이기 때문에 실제 코드는 인터페이스로 코딩할 것이다.

 

이렇게 두번째 우선 순위에서도 URL을 가져오지 못할 수 있다. 즉 세션에 DefaultSavedRequest 클래스 객체로 저장되지 않았을 경우이다. 그러면 그런 경우는 어떤 경우가 있을 수 있을까? Spring Security가 시스템적으로(자동으로) 로그인 화면을 띄워서 보여주는 것이 아니라 사용자가 직접 로그인 URL로 이동한 경우가 그것이다. Spring Security가 시스템적으로 로그인 화면을 띄울때는 DefaultSavedRequest 객체를 만들어서 로그인 화면을 보기 전의 화면에 대한 URL과 헤더 정보들을 저장해놓지만, 사용자가 링크를 타고 로그인 화면으로 이동했거나 직접 URL을 입력하여 로그인 화면을 이동했을 경우엔 이런 DefaultSavedRequest 객체를 저장하지를 않기 때문에 세션에 저장되어 있지도 않고 그렇기 땜에 당연 URL도 가져오지 못하는 것이다. 그런 경우를 대비해서 HttpServletRequest 객체의 getHeader 메소드를 이용해 REFERER 헤더 값을 읽어올 수 있다. 위에서 이 기능에 대한 얘기를 했을때 이 기능은 사용 여부를 설정 할 수 있게끔 하도록 한다고 했다. 이 REFERER 헤더를 이용하는 방법은 사실상 비추이기 때문이다. 

 

REFERER 헤더값을 이용하는 방법은 이전 페이지 URL을 얻어오는데 있어 가장 보편적으로 쓰이는 방법이긴 하다. 그러나 로그인의 경우는 이 방법이 비추일수 밖에 없는 상황이 존재한다. 다음의 상황을 생각해 보자. 설명을 위해 다음의 화면 전개를 보자.

 

메인 화면 -> 로그인 화면 링크 클릭 -> 로그인 화면 -> 아이디와 비밀번호 입력한 뒤 로그인 버튼 클릭 -> 인증 처리 -> 메인 화면

 

위의 글박스에 언급한 내용은 메인 화면을 보고 있는 상태에서 로그인 화면 보여주는 링크를 클릭해서 로그인 화면을 보는 상황에서 아이디와 비밀번호를 입력한뒤 로그인 과정을 거쳐 메인 화면을 보는 것을 절차로 보여주는 것이다. 위에서 인증 처리라고 얘기했지만 이 부분도 사실 URL이 엄연히 존재한다. 우리가 로그인 화면에서 <form> 태그 작성시 action을 지정하지 않았는가? j_spring_security_check 라고 엄연히 action을 주었다. 그러면 여기서 로그인 과정을 처리할텐데 이 j_spring_security_check 입장에서 REFERER 헤더값을 읽는다면 어디를 가리키겠는가? 바로 로그인 화면 URL을 가리키게 되는 것이다. 그래서 REFERER로 이동하게끔 지정하면 로그인을 성공해도 다시 로그인 화면을 보여주기 때문에 REFERER 헤더값을 이용하는 방법은 사실상 의미가 없게 된다. 그래서 이 기능에 대해 사용 여부를 설정할 수 있게 한 것이다. 그러나 여기서 굳이 REFERER를 구현한 이유는 Spring Security에서 Redirect URL 선정 기준을 REFERER 헤더값을 읽어서 가도록 하는 방법이 존재하기에 구현은 해보았다.추측엔 웹서비스와 같이 로그인 아이디와 패스워드를 같이 주는 방법에서는 REFERER 헤더값을 해도 지장이 없을것 같아 그렇지 않을까 싶다.

 

그리고 위에서 언급한 3가지 방법으로도 URL을 구할 수 없으면 지정된 화면으로 이동해야 할 것이다. 그래서 defaultURL 속성을 두어 지정된 화면을 설정하도록 했다. 이런 우선순위를 생각하고 이제 코드를 보도록 하자. 먼저 위에서 언급했던 우선순위 중 어느 것을 사용해야 할지를 결정하는 메소드이다.

 

/**
 * 인증 성공후 어떤 URL로 redirect 할지를 결정한다
 * 판단 기준은 targetUrlParameter 값을 읽은 URL이 존재할 경우 그것을 1순위
 * 1순위 URL이 없을 경우 Spring Security가 세션에 저장한 URL을 2순위
 * 2순위 URL이 없을 경우 Request의 REFERER를 사용하고 그 REFERER URL이 존재할 경우 그 URL을 3순위
 * 3순위 URL이 없을 경우 Default URL을 4순위로 한다
 * @param request
 * @param response
 * @return   1 : targetUrlParameter 값을 읽은 URL
 *            2 : Session에 저장되어 있는 URL
 *            3 : referer 헤더에 있는 url
 *            0 : default url
 */
private int decideRedirectStrategy(HttpServletRequest request, HttpServletResponse response){
	int result = 0;
	
	SavedRequest savedRequest = requestCache.getRequest(request, response);
	
	if(!"".equals(targetUrlParameter)){
		String targetUrl = request.getParameter(targetUrlParameter);
		if(StringUtils.hasText(targetUrl)){
			result = 1;
		}else{
			if(savedRequest != null){
				result = 2;
			}else{
				String refererUrl = request.getHeader("REFERER");
				if(useReferer && StringUtils.hasText(refererUrl)){
					result = 3;
				}else{
					result = 0;
				}
			}
		}
		
		return result;
	}
	
	if(savedRequest != null){
		result = 2;
		return result;
	}
	
	String refererUrl = request.getHeader("REFERER");
	if(useReferer && StringUtils.hasText(refererUrl)){
		result = 3;
	}else{
		result = 0;
	}
	
	return result;
}

 

코드를 보면 먼저 Spring Security가 세션에 저장하고 있는 SavedRequest 객체를 가져오도록 한다. 그리고 이동해야 할 URL이 지정될 파라미터 이름이 정해져 있는 상태면 그 파라미터 이름으로 request 객체에서 값을 읽어서 URL이 존재하는지 확인하여 그 URL이 존재하면 1을 return 한다(이것이 1순위). 그러나 이 값이 없을 경우 앞에서 가져온 SavedRequest 객체가 null인지 확인한 뒤 null이 아니면 SavedRequest 객체가 있다는 의미이기 때문에 2를 return 한다(이것이 2순위) 그러나 SavedRequest 객체가 null이라면 REFERER 헤더값을 사용하도록 설정했는지 그리고 사용하는 것으로 설정했으면 REFERER 헤더값을 읽어서 URL이 존재하면 3을 return 한다(이것이 3순위) 그리고 지금까지의 모든 조건을 다 만족하지 않으면 0을 return 하도록 한다(이것이 4순위). 4순위일때 0을 return 하는것 빼고는 순위값을 그대로 return 하도록 했다. 그래서 이 return 된 순위값에 따라 적절한 메소드를 호출하여 URL을 얻어오면 되는데 그것이 다음의 코드이다.

 

private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
	// TODO Auto-generated method stub
	
        clearAuthenticationAttributes(request);

	int intRedirectStrategy = decideRedirectStrategy(request, response);
	switch(intRedirectStrategy){
	case 1:
		useTargetUrl(request, response);
		break;
	case 2:
		useSessionUrl(request, response);
		break;
	case 3:
		useRefererUrl(request, response);
		break;
	default:
		useDefaultUrl(request, response);
	}
}

private void clearAuthenticationAttributes(HttpServletRequest request) {
	HttpSession session = request.getSession(false);

	if (session == null) {
		return;
	}

	session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
}

private void useTargetUrl(HttpServletRequest request, HttpServletResponse response) throws IOException{
	SavedRequest savedRequest = requestCache.getRequest(request, response);
	if(savedRequest != null){
		requestCache.removeRequest(request, response);
	}
	String targetUrl = request.getParameter(targetUrlParameter);
	redirectStrategy.sendRedirect(request, response, targetUrl);
}

private void useSessionUrl(HttpServletRequest request, HttpServletResponse response) throws IOException{
	SavedRequest savedRequest = requestCache.getRequest(request, response);
	String targetUrl = savedRequest.getRedirectUrl();
	redirectStrategy.sendRedirect(request, response, targetUrl);
}

private void useRefererUrl(HttpServletRequest request, HttpServletResponse response) throws IOException{
	String targetUrl = request.getHeader("REFERER");
	redirectStrategy.sendRedirect(request, response, targetUrl);
}

private void useDefaultUrl(HttpServletRequest request, HttpServletResponse response) throws IOException{
	redirectStrategy.sendRedirect(request, response, defaultUrl);
}

 

이 코드에 대한 분석은 그리 어렵지는 않을것이다. AuthenticationHandler 인터페이스에서 정의한 onAuthenticationSuccess 메소드에서 우선순위를 결정짓고 그 우선순위에 따라 이동할 URL을 추출한 뒤 이동하는 것이 전부이다. 상세한 내용은 방금 이 코드를 보여주기 전의 코드에서 우선순위를 결정하는 부분에 대한 설명을 곱씹어보면 해당 순위일때 어떤식으로 URL을 꺼내오는지에 대한 파악이 될 것이다. 여기서는 이전 코드에서 설명하지 않은 부분 위주로 설명을 하겠다.

 

Spring Security에서 로그인하는 과정에서 로그인이 실패한 경우에 세션에 관련 에러를 저장한다고 예전 블로그에서 언급한 바가 있다. 로그인 화면을 만났을때 로그인 시도 첫번째에 성공했다면 이런 에러가 세션에 저장되어 있지 않았겠지만 어디 그게 순탄하게만 가겠는가? 로그인이 실패한 상황이 한번이라도 발생했으면 에러가 세션에 저장되어 있을것이다. 근데 이런 상태에서 로그인이 성공했다고 생각해보자. 그러면 세션에 있는 에러를 지워야 하지 않겠는가? 그 역할을 하는 것이 clearAuthenticationAttributes 메소드이다. 이 메소드를 보면 세션을 받아와서 WebAttributes.AUTHENTICATION_EXCEPTION 변수에 정의된 이름으로 된 세션 값을 지우고 있다. 이 변수에 저장된 값은 SPRING_SECURITY_LAST_EXCEPTION 이란 문자열로써 정리하면 Spring Security는 에러 발생시 SPRING_SECURITY_LAST_EXCEPTION이란 key 값으로 저장함을 알 수가 있다.

 

화면을 이동할때는 org.springframework.security.web.RedirectStrategy 인터페이스를 구현한 객체를 받아서 하고 있다. RedirectStrategy는 Spring Security가 화면 이동에 대한 규칙을 정의하는 부분을 만든 인터페이스로 이 인터페이스를 구현한 객체로 화면 redirect를 하면 된다. 이 인터페이스는 다음의 메소드가 정의되어 있다.

 

void sendRedirect(HttpServletRequest request, HttpServletResponse response, String url) throws IOException;

 

RedirectStrategy 인터페이스를 구현한 클래스를 만들어 거기에서 sendRedirect를 재정의 해주면 되는 것이다. HttpServletRequeet 객체, HttpServletResponse 객체, 이동할 URL을 받아오기 때문에 화면 이동을 구현하는데 아무 지장이 없을 것이다.(redirect가 아니라 forward로 할려고 할 경우도 HttpServletRequest 객체를 받아오기 때문에 가능하다) Spring Security는 이 RedirectStrategy 인터페이스를 구현한 org.springframework.security.web.DefaultRedirectStrategy 클래스를 제공한다. 여기서는 이 클래스를 이용해서 화면을 이동했다. 이 클래스 소스를 보면 내부적으로 HttpServletResponse의 sendRedirect 메소드를 사용해서 화면을 이동하고 있다.

 

이제 이렇게 설명한 내용이 모두 반영된 CustomAuthenticationSuccessHandler 클래스의 소스를 보면 다음과 같다

 

import java.io.IOException;

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

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.WebAttributes;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.savedrequest.SavedRequest;
import org.springframework.util.StringUtils;

public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private Logger logger = LoggerFactory.getLogger(this.getClass());
	
	private RequestCache requestCache = new HttpSessionRequestCache();
	
	private String targetUrlParameter;
	
	private String defaultUrl;
	
	private boolean useReferer;
	
	private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
	
	public CustomAuthenticationSuccessHandler(){
		targetUrlParameter = "";
		defaultUrl = "/";
		useReferer = false;
	}
	
	public String getTargetUrlParameter() {
		return targetUrlParameter;
	}



	public void setTargetUrlParameter(String targetUrlParameter) {
		this.targetUrlParameter = targetUrlParameter;
	}



	public String getDefaultUrl() {
		return defaultUrl;
	}



	public void setDefaultUrl(String defaultUrl) {
		this.defaultUrl = defaultUrl;
	}



	public boolean isUseReferer() {
		return useReferer;
	}



	public void setUseReferer(boolean useReferer) {
		this.useReferer = useReferer;
	}



	@Override
	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
		// TODO Auto-generated method stub
		
		clearAuthenticationAttributes(request);
		
		int intRedirectStrategy = decideRedirectStrategy(request, response);
		switch(intRedirectStrategy){
		case 1:
			useTargetUrl(request, response);
			break;
		case 2:
			useSessionUrl(request, response);
			break;
		case 3:
			useRefererUrl(request, response);
			break;
		default:
			useDefaultUrl(request, response);
		}
	}
	
	private void clearAuthenticationAttributes(HttpServletRequest request) {
        HttpSession session = request.getSession(false);

        if (session == null) {
            return;
        }

        session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
    }
	
	private void useTargetUrl(HttpServletRequest request, HttpServletResponse response) throws IOException{
		SavedRequest savedRequest = requestCache.getRequest(request, response);
		if(savedRequest != null){
			requestCache.removeRequest(request, response);
		}
		String targetUrl = request.getParameter(targetUrlParameter);
		redirectStrategy.sendRedirect(request, response, targetUrl);
	}
	
	private void useSessionUrl(HttpServletRequest request, HttpServletResponse response) throws IOException{
		SavedRequest savedRequest = requestCache.getRequest(request, response);
		String targetUrl = savedRequest.getRedirectUrl();
		redirectStrategy.sendRedirect(request, response, targetUrl);
	}
	
	private void useRefererUrl(HttpServletRequest request, HttpServletResponse response) throws IOException{
		String targetUrl = request.getHeader("REFERER");
		redirectStrategy.sendRedirect(request, response, targetUrl);
	}
	
	private void useDefaultUrl(HttpServletRequest request, HttpServletResponse response) throws IOException{
		redirectStrategy.sendRedirect(request, response, defaultUrl);
	}
	
	/**
	 * 인증 성공후 어떤 URL로 redirect 할지를 결정한다
	 * 판단 기준은 targetUrlParameter 값을 읽은 URL이 존재할 경우 그것을 1순위
	 * 1순위 URL이 없을 경우 Spring Security가 세션에 저장한 URL을 2순위
	 * 2순위 URL이 없을 경우 Request의 REFERER를 사용하고 그 REFERER URL이 존재할 경우 그 URL을 3순위
	 * 3순위 URL이 없을 경우 Default URL을 4순위로 한다
	 * @param request
	 * @param response
	 * @return   1 : targetUrlParameter 값을 읽은 URL
	 *            2 : Session에 저장되어 있는 URL
	 *            3 : referer 헤더에 있는 url
	 *            0 : default url
	 */
	private int decideRedirectStrategy(HttpServletRequest request, HttpServletResponse response){
		int result = 0;
		
		SavedRequest savedRequest = requestCache.getRequest(request, response);
		
		if(!"".equals(targetUrlParameter)){
			String targetUrl = request.getParameter(targetUrlParameter);
			if(StringUtils.hasText(targetUrl)){
				result = 1;
			}else{
				if(savedRequest != null){
					result = 2;
				}else{
					String refererUrl = request.getHeader("REFERER");
					if(useReferer && StringUtils.hasText(refererUrl)){
						result = 3;
					}else{
						result = 0;
					}
				}
			}
			
			return result;
		}
		
		if(savedRequest != null){
			result = 2;
			return result;
		}
		
		String refererUrl = request.getHeader("REFERER");
		if(useReferer && StringUtils.hasText(refererUrl)){
			result = 3;
		}else{
			result = 0;
		}
		
		return result;
	}
}

 

그럼 이제는 이렇게 만든 CustomAuthenticationSuccessHandler 클래스를 Spring Security에서 사용하겠다고 적용해야 하지 않겠는가? 그것은 다음과 같이 적용한다.

 

<form-login
	username-parameter="loginid"
	password-parameter="loginpwd" 
	login-page="/login.do"
	default-target-url="/main.do"
	authentication-failure-url="/login.do?fail=true"
	authentication-success-handler-ref="customAuthenticationSuccessHandler"
/>

<beans:bean id="customAuthenticationSuccessHandler" class="com.terry.springsecurity.common.security.handler.CustomAuthenticationSuccessHandler">
	<beans:property name="targetUrlParameter" value="loginRedirect" />
	<beans:property name="useReferer" value="false" />
	<beans:property name="defaultUrl" value="/main.do" />
</beans:bean>

 

CustomAuthenticationSuccessHandler 클래스를 bean으로 등록한 뒤에 <form-login> 태그에 authentication-success-handler-ref 속성에 bean으로 등록한 CustomAuthenticationSuccessHandler 클래스의 id를 넣으면 된다. CustomAuthenticationSuccessHandler 클래스에 보면 targetUrlParameter 속성와 useReferer 속성과 defaultUrl 속성을 지정한 부분이 있다. 로그인 성공한 뒤의 이동할 화면 URL이 들어있는 파라미터 이름을 targetUrlParameter 속성에 넣는다(이 부분은 밑에서 변경된 로그인 페이지와 같이 보면 이해할 수 있을 것이다) useReferer 는 REFERER 헤더 값을 사용할 것인지의 여부를 결정하는 것으로 원래 기본값은 false로 되어 있는 속성이긴 하지만 예시를 위해 지정도 해보았다. true를 할 경우 REFERER 헤더 값을 사용하겠다는 의미이다. 마지막으로 defaultUrl 속성은 위에서 언급했던 우선순위에 모두 만족되지 않을 경우 마지막으로 정해지는 default URL을 설정하는 것으로 여기서는 main 화면 URL을 설정했다.

 

targetUrlParameter 속성을 넣었기 때문에 우리는 로그인 화면을 약산 수정할 필요가 있다. 왜냐면 로그인 화면에서 아이디와 비밀번호뿐만 아니라 로그인 성공시 이동해야 할 URL도 같이 넘겨줘야 하기 때문이다(물론 안념겨줄수도 있다. 그럴 경우는 정해진 우선순위에 따라 이동할 URL을 결정할 것이다. 다음의 로그인 화면 html 소스를 보자

 

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title></title>
<jsp:include page="/WEB-INF/views/include/jsInclude.jsp"></jsp:include> 
<script type="text/javascript">
$(document).ready(function (){
	
	$("#loginbtn").click(function(){
		if($("#loginid").val() == ""){
			alert("로그인 아이디를 입력해주세요");
			$("#loginid").focus();
		}else if($("#loginpwd").val() == ""){
			alert("로그인 비밀번호를 입력해주세요");
			$("#loginpwd").focus();
		}else{
			$("#loginfrm").attr("action", "<c:url value='/j_spring_security_check'/>");
			$("#loginfrm").submit();
		}
	});
		
});
</script>    
</head>
<body>
<div style="display:inline-block;">
    로그인 화면
    <form id="loginfrm" name="loginfrm" action="<c:url value='${ctx}/j_spring_security_check'/>" method="POST">
    <table>
    	<tr>
    		<td>아이디</td>
    		<td>
    			<input type="text" id="loginid" name="loginid" value="" />
    		</td>
    		<td rowspan="2">
    			<input type="button" id="loginbtn" value="확인" />
    		</td>
    	</tr>
    	<tr>
    		<td>비밀번호</td>
    		<td>
    			<input type="text" id="loginpwd" name="loginpwd" value="" />
    		</td>
    	</tr>
    	<c:if test="${not empty param.fail}">
    	<tr>
    		<td colspan="2">
				<font color="red">
				<p>Your login attempt was not successful, try again.</p>
				<p>Reason: ${sessionScope["SPRING_SECURITY_LAST_EXCEPTION"].message}</p>
				</font>
				<c:remove scope="session" var="SPRING_SECURITY_LAST_EXCEPTION"/>
    		</td>
    	</tr>
    	</c:if>
    </table>
	<input type="hidden" name="loginRedirect" value="${loginRedirect}" />
    </form>
</div>
</body>
</html>

 

로그인 화면 커스터마이징 글에서 로그인 화면 소스를 보여준 적이 있다. 여기서 추가된 부분이 있는데 </table>과 </form> 태그 사이에 있는 다음의 내용이다.

 

<input type="hidden" name="loginRedirect" value="${loginRedirect}" />

 

hidden type으로 name이 loginRedirect에 Spring MVC Model 객체에서 loginRedirect 로 넣은 값을 value로 셋팅 해주고 있다. 바로 name에 설정한 loginRedirect란 값이 위에서 CustomAuthenticationHandler 클래스를 bean으로 등록할때 targetUrlParameter 속성에 설정한 값인 loginRedirect인 것이다. 즉 name이 loginRedirect란 hidden 태그에 이동해야 할 URL을 설정하면 로그인 성공시 request.getPameter("loginRedirect")를 함으로써 로그인 성공후 이동해야 할 URL을 읽어오게 되는 것이다.

 

그러면 Spring MVC Model 객체에 loginRedirect 값은 누가 넣는가? 그건 다음 글에서 설명할 로그인 작업이 실패했을때의 별도 작업을 하는 클래스인 CustomAuthenticationFailureHandler 클래스에서 하게 될 것이다. 원래는 이 글에서 다 설명할려고 했는데 두 글로 나눠야겠다. 설명을 너무 생략하면 내용을 이해하기가 어렵고..어쩔수가 없다. 이번에는 로그인 성공했을 때 부가 작업을 수행하는 방법에 대해 설명했다. 다음에서는 로그인 실패했을때 부가 작업을 수행하는 방법에 대해 설명하겠다.

 

loginRedirect로 인해 혼선이 있을 수 있을것 같아 설명해 둘 것이 있다. 로그인 하지 않은 상태에서 권한이 필요한 화면으로 가려고 할 경우 로그인 화면을 먼저 보게 되고 이 로그인 화면에서 로그인을 성공하면 권한이 필요한 화면(충분한 권한이 있지 않을 경우 접근 에러 화면이 대신 나온다)으로 이동하게 된다. 근데 이럴 경우를 보면 hidden 태그인 loginRedirect에는 아무런 값도 들어가지 않는데도 권한이 필요한 화면으로 이동을 할 수가 있다. 이것이 가능한 이유는 Spring Security에서 로그인 화면을 보여주기 전에 위에서 설명했던 DefaultSavedRequest 클래스 객체를 만든뒤 거기에 사용자 요청과 이동 URL을 넣고 이를 세션에 저장한뒤 로그인 화면을 보여주기 때문이다. 그래서 loginRedirect에 아무런 값이 없어도 2순위인 세션 검사에서 만족하기 때문에 여기서 해당 URL로 이동하게 되는 것이다.