본문 바로가기

프로그래밍/Spring Security

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

지난글에서는 로그인 작업 후 로그인이 성공했을때 부가적인 작업을 어떤식으로 설정하는지 설명했다. 한줄로 요약하면 org.springframework.security.web.authentication.AuthenticationSuccessHandler 인터페이스를 구현한 클래스를 만든 뒤 이를 <bean> 태그를 이용하여 등록한 뒤 <http> 태그의 authentication-success-handler-ref 속성에 해당 bean 클래스를 설정하는 것으로 구현할 수가 있다.(이 한 줄 요약을 풀어서 살을 붙여가며 설명하면 저번 글 처럼 엄청난 양의 글이 된다..물에 불린 라면도 아닌 것이 머 그리 불어나는건지..ㅠㅠ..) 로그인이 성공했을때의 부가작업을 다루었으니 이번엔 로그인이 실패했을때의 부가작업을 설명할 차례가 됐다.

 

이 작업을 하기에 앞서 무엇을 해야 할지를 먼저 생각해야 한다. 로그인을 실패했다고 가정할 경우 무엇을 해야 할까? 최종 화면을 로그인 페이지를 보여줘야 하는 것은 당연함이다(로그인 실패했다고 로그인 실패했습니다..란 식의 메시지 뿌리고 로그인 화면 링크를 걸어 사용자가 수동으로 로그인 화면을 이동하게 할 수는 없잖은가) 로그인 페이지를 보여줄때 무슨 이유로 로그인을 실패했는지를 보여줘야 할 것이다. 

 

Spring Security에서는 로그인 실패했을때 부가적인 작업을 org.springframework.security.web.authentication.AuthenticationFailureHandler 인터페이스를 구현한 클래스에서 하도록 되어 있다. 방금 위에서 언급했던 한줄 요약 표현의 방법을 사용한다면 org.springframework.security.web.authentication.AuthenticationFailureHandler 인터페이스를 구현한 클래스를 만든 뒤 이를 <bean> 태그를 이용하여 등록한 뒤 <http> 태그의 authentication-failure-handler-ref 속성에 해당 bean 클래스를 설정하는 것으로 구현할 수가 있다. 다음은 그런 설정의 예이다.

 

<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"
        authentication-failure-handler-ref="customAuthenticationFailureHandler"
/>

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

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

 

이전글에서의 보여준 예시에 추가한 것이다(어차피 일반적으로 구현하게 될 경우 로그인 성공과 실패 쌍으로 구현하는 경우가 다반사이기 때문에 로그인 성공시의 예시에 추가로 달아놓았다) 그러면 이제 org.springframework.security.web.authentication.AuthenticationFailureHandler 인터페이스에 대해 살펴볼 차례이다. 이전글에서 설명했던 org.springframework.security.web.authentication.AuthenticationSuccessHandler 인터페이스와 마찬가지로 이 인터페이스 또한 다음의 메소드 1개뿐이다.

 

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

 

이전글에서 설명했던 AuthenticationSuccessHandler 인터페이스의 onAuthenticationSuccess와 한가지만 빼고 동일한 구성으로 되어 있다. 눈치 빠른 사람은 바로 파악했겠지만 AuthenticationSuccessHandler 인터페이스의 경우 로그인을 성공한 뒤의 작업이기 때문에 onAuthenticationSuccess 메소드의 세번째 인자로 인증 정보 객체인 Authentication 인터페이스를 구현한 객체가 오지만 AuthenticationFailureHandler 인터페이스의 경우 로그인이 실패한 뒤의 작업이기 때문에 로그인이 어떤 이유로 실패했는지의 정보를 가지고 있는 AuthenticationException 클래스 객체가 오게 된다.

 

로그인을 실패했을때의 작업은 로그인 실패 이유를 보여주는 로그인 화면을 보여주기 때문에 이전글에서 했던 로그인 성공했을때와는 달리 이동해야 할 URL의 우선순위를 따지는 그런 작업도 없는 단순 작업으로 갈 것이다. 다만 여기서는 로그인 실패했을때 메시지 보여주는 부분을 다르게 갈 것이다. 예전 글이 생각날지 모르겠지만 인증(로그인) 실패했을때  관련 에러 메시지를 세션에 저장한 뒤 그것을 웹페이지에서 세션에 접근해서 보여줬던 것을 기억할 것이다. 에러 메시지를 세션에 저장하는건 우리가 그렇게 설정을 한게 아니라 Spring Security가 자동으로 그렇게 하고 있다. 이 부분을 세션을 이용하지 않고 request의 Attribute에 저장할 것이다. request의 Attribute에 저장하여 jsp 페이지에서는 request의 해당 Attribute에 접근해서 관련 메시지를 가져와서 화면에 출력할 것이다.

 

그렇게 구현한 코드가 다음의 코드이다.

 

import java.io.IOException;

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

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;

/**
 * 인증 실패 핸들러
 * @author TerryChang
 *
 */
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {

	private String loginidname;			// 로그인 id값이 들어오는 input 태그 name
	private String loginpasswdname;		// 로그인 password 값이 들어오는 input 태그 name
	private String loginredirectname;		// 로그인 성공시 redirect 할 URL이 지정되어 있는 input 태그 name
	private String exceptionmsgname;		// 예외 메시지를 request의 Attribute에 저장할 때 사용될 key 값
	private String defaultFailureUrl;		// 화면에 보여줄 URL(로그인 화면)
	
	public CustomAuthenticationFailureHandler(){
		this.loginidname = "j_username";
		this.loginpasswdname = "j_password";
		this.loginredirectname = "loginRedirect";
		this.exceptionmsgname = "securityexceptionmsg";
		this.defaultFailureUrl = "/login.do";
	}
	
	
	public String getLoginidname() {
		return loginidname;
	}


	public void setLoginidname(String loginidname) {
		this.loginidname = loginidname;
	}


	public String getLoginpasswdname() {
		return loginpasswdname;
	}


	public void setLoginpasswdname(String loginpasswdname) {
		this.loginpasswdname = loginpasswdname;
	}

	public String getExceptionmsgname() {
		return exceptionmsgname;
	}

	public String getLoginredirectname() {
		return loginredirectname;
	}


	public void setLoginredirectname(String loginredirectname) {
		this.loginredirectname = loginredirectname;
	}


	public void setExceptionmsgname(String exceptionmsgname) {
		this.exceptionmsgname = exceptionmsgname;
	}

	public String getDefaultFailureUrl() {
		return defaultFailureUrl;
	}


	public void setDefaultFailureUrl(String defaultFailureUrl) {
		this.defaultFailureUrl = defaultFailureUrl;
	}


	@Override
	public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
		// TODO Auto-generated method stub
		
		// Request 객체의 Attribute에 사용자가 실패시 입력했던 로그인 ID와 비밀번호를 저장해두어 로그인 페이지에서 이를 접근하도록 한다
		String loginid = request.getParameter(loginidname);
		String loginpasswd = request.getParameter(loginpasswdname);
		String loginRedirect = request.getParameter(loginredirectname);
		
		request.setAttribute(loginidname, loginid);
		request.setAttribute(loginpasswdname, loginpasswd);
		request.setAttribute(loginredirectname, loginRedirect);
		
		
		// Request 객체의 Attribute에 예외 메시지 저장
		request.setAttribute(exceptionmsgname, exception.getMessage());
		
		request.getRequestDispatcher(defaultFailureUrl).forward(request, response);
	}

}

 

로그인 성공 했을때는 설명해야 할 내용이 많아서 코드를 나눠서 설명했지만 로그인 실패했을때는 하는 기능이 별로 없어서 한꺼번에 보여주고 설명하도록 하겠다. 이 클래스가 받아야 할 파라미터는 5개가 있다. 물론 생성자에서 기본 값으로 셋팅해주고 있지만 기본값을 사용하지 않을 경우엔 바꿀수가 있다. 먼저 설정해야 할 5개의 파라미터는 HttpServletRequest에서 로그인 아이디가 저장되어 있는 파라미터 이름, HttpServletRequest에서 로그인 패스워드가 저장되어 있는 파라미터의 이름, 로그인 성공시 redirect 해야 할 URL이 저장되어 있는 파라미터 이름, 로그인 페이지에서 jstl을 이용하여 에러 메시지를 가져올때 사용해야 할 변수 이름, 화면에 보여줘야 할 페이지의 URL(일반적으로 재로그인을 유도해야 하기 때문에 로그인 페이지 URL을 사용할 것이다) 이렇게 5가지이다. 각 변수에 대한 getter/setter 메소드가 대부분이니 핵심인 onAuthenticationFailure 메소드를 살펴보도록 하자

 

onAuthenticationFailure 메소드에서는 HttpServletRequest 객체를 인자값으로 받고 있기 때문에 우리가 이 bean 클래스에서 받는 파라미터 중 로그인 아이디가 저장되어 있는 파라미터 이름, 로그인 패스워드가 저장되어 있는 파라미터 이름, 로그인 성공시 redirect 해야 할 URL이 저장되어 있는 파라미터 이름을 이용해서 각각의 값을 가져올 수 있다. HttpServletRequest 클래스의 getParameter 메소드를 이용해서 가져온 것들이 바로 그것들이다. 이 값들을 HttpServletRequest 객체의 setAttribute 메소드를 이용해 가져온 값을 그대로 셋팅해줌으로써 나중에 보여줄 페이지에서 jstl을 이용해서 그대로 가져와 사용할 수가 있다. 인증을 실패했기 때문에 왜 실패했는지 메시지를 보여줘야 하는데 이 메시지 또한 setAttribute 메소드를 이용해서 메시지를 셋팅해주어 나중에 보여줄 페이지에서 jstl을 이용해 그대로 보여줄 수 있다. 마지막으로 HttpServletRequest 클래스의 getRequestDispatcher 메소드를 이용해서 보여줘야 할 화면으로 forward를 해주면 된다. forward로 해줘야 jstl을 이용해서 setAttribute로 저장한 값을 가져올 수 있다. HttpServletResponse 클래스의 sendRedirect 메소드를 이용해서 redirect를 하지 말기 바란다(그 이유에 대해서는 여기서는 설명하지 않겠다. forward와 redirect의 차이에 대해 공부하면 알 수 있는 내용이다)

 

이렇게 onAuthenticationFailure 메소드에서 작업을 하면 다음의 로그인 페이지 소스에서 관련 에러 메시지를 보여줄 수 있게 된다.

 

<%@ 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="${loginid}" />
    		</td>
    		<td rowspan="2">
    			<input type="button" id="loginbtn" value="확인" />
    		</td>
    	</tr>
    	<tr>
    		<td>비밀번호</td>
    		<td>
    			<input type="text" id="loginpwd" name="loginpwd" value="${loginpwd}" />
    		</td>
    	</tr>
    	<c:if test="${not empty securityexceptionmsg}">
    	<tr>
    		<td colspan="2">
				<font color="red">
				<p>Your login attempt was not successful, try again.</p>
				<p>${securityexceptionmsg}</p>
				</font>
    		</td>
    	</tr>
    	</c:if>
    </table>
    <input type="hidden" name="loginRedirect" value="${loginRedirect}" />
    </form>
</div>
</body>
</html>

 

이전에 설명했던 로그인 화면 커스터마이징때 올렸던 로그인 화면의 소스와 위에 올린 소스를 보면 다른 부분이 몇가지 있다. 예전 소스의 경우는 로그인이 실패했더라도 사용자가 입력했던 로그인 아이디와 패스워드를 보여주질 못했다. 그러나 지금은 사용자가 입력한 값을 jstl을 이용해 다시 가져올 수가 있다. 그래서 로그인 아이디와 패스워드를 입력했던 <input> 태그를 보면 value부분에 jstl을 이용해서 값을 가져와서 화면에 다시 보여주게 되었다. 그리고 에러 메시지를 보여주는 부분에서 변화가 있다. 예전엔 세션에 에러메시지가 저장되었기 때문에 이를 세션에서 가져왔지만 지금은 세션에 저장하지 않기 때문에 jstl을 이용해서 바로 출력했다. 또 세션에 저장하지 않기 때문에 <c:remove>를 이용해서 세션에 저장된 에러메시지를 세션에서 삭제하는 부분도 없어졌다. 마지막으로 로그인 성공시 이동할 URL을 저장해 놓는 hidden 태그인 loginRedirect의 value로 기존 로그인 페이지에서 있던 값을 jstl을 이용해서 그대로 셋팅해주게 했다.

 

마지막으로 spring security 설정 xml에서 위의 내용들이 최종 반영된 설정 내용이다.

 

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

<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>

<beans:bean id="customAuthenticationFailureHandler" class="com.terry.springsecurity.common.security.handler.CustomAuthenticationFailureHandler">
	<beans:property name="loginidname" value="loginid" />
	<beans:property name="loginpasswdname" value="loginpwd" />
	<beans:property name="loginredirectname" value="loginRedirect" />
	<beans:property name="exceptionmsgname" value="securityexceptionmsg" />
	<beans:property name="defaultFailureUrl" value="/login.do?fail=true" />
</beans:bean>

 

이 글을 처음 시작할때 먼저 보여주었지만 org.springframework.security.web.authentication.AuthenticationFailureHandler 인터페이스를 구현한 클래스를 태그를 이용해서 등록한 뒤 <form-login> 태그의 authentication-failure-ref 속성에 <bean> 태그로 등록한 클래스의 id 값을 넣어주었다. 그리고 <property> 태그를 이용해서 위에서 언급했던 필요 파라미터 값들을 셋팅해주었다. 여기서는 한가지 변경된 부분이 있는데 CustomAuthenticationFailureHandler 클래스에서 로그인 실패시 화면을 이동하기 때문에 예전에 <form-login> 태그 설정에서 로그인 실패시 보여줘야 할 화면 URL을 설정하는 부분인 authentication-failure-url 속성이 필요없게 되었다. 그래서 여기서 보여주는 <form-login> 태그 설정에서 authentication-failure-url 속성을 뺐다. 대신 authentication-failure-url 속성에 지정했던 값인 /login.do?fail=true 를 CustomAuthenticationFailureHandler 클래스의 defaultFailureUrl 프로퍼티에 사용함으로써 같은 기능을 하도록 했다.

 

이 글을 쓰면서 궁금한 점이 있어서 한번 테스트를 해보고 글을 써서 밝혀두도록 하겠다. 예전 글에서 보면 Spring Security는 에러 메시지를 세션에 저장한다고 언급했었다. 그러면 우리가 CustomAuthenticationFailureHandler 클래스를 만들어서 로그인 실패시 별도로 작업을 지정할 경우 에러 메시지를 세션에 저장한 뒤에 CustomAuthenticationFailureHandler 클래스의 onAuthenticationFailure 메소드를 호출하는 것일까? 아니다. 지금의 글과 같이 Spring Security에 인증 실패시의 핸들러를 별도로 지정한 상황이라면 Spring Security는 인증 과정에서 문제가 생겼을 경우 어떠한 선작업도 하지 않고 바로 인증 실패 핸들러의 onAuthenticationFailure 메소드를 호출하게 된다. 테스트 방법은 onAuthentication 메소드에서 HttpSession 객체를 가져온뒤(HttpServletRequest 객체를 파라미터로 받기 때문에 getSession(false) 메소드를 호출하면 가져올 수 있다) 세션에 있는 key 값중 SPRING_SECURITY_LAST_EXCEPTION 란 key가 있는지 확인하는 방법으로 테스트하면 된다.

 

또한 우리가 <form-login> 태그에서 authentication-success-handler-ref 속성과 authentication-failure-handler-ref 속성에 이러한 bean 클래스들을 정의하지 않았다고 해서 부가작업을 안하는 것이 아니다. 예를 들어 authentication-failure-handler-ref 속성에 bean 클래스를 정의하지 않는다면 Spring Security는 자체적으로 AuthenticationFailureHandler 인터페이스를 구현한 클래스인 org.springframework.security.web.authentication.ExceptionMappingAuthenticationFailureHandler 클래스를 이용해서 로그인 실패시의 부가작업을 하고 있다. 속성에 해당 bean 클래스를 지정하지 않았다고 해서 부가작업을 하지 않는것이 아니라 Spring Security가 자체적으로 만든 클래스를 등록해서 부가작업을 하고 있는 것이다.

 

이번 글에서는 로그인 실패시 부가적인 작업을 설정하는 내용으로 다루었다. 다음엔 로그인 시 비밀번호를 암호화 시켜서 적용하는 방법에 대한 내용을 설명하도록 하겠다.