본문 바로가기

프로그래밍/Spring Security

Spring Security의 로그인 화면 커스터마이징 (2)

지난 글에서는 Spring Security에서 제공하는 초간단 로그인 화면의 이해와 이를 기반으로 한 로그인 화면 구성 방법을 살펴보고 <intercept-url> 태그에서 Spring EL을 이용한 권한 설정을 알아보았다. 이번 글에서는 로그인 화면 커스터마이징의 마무리 글로 인증 실패시 메시지가 나오는 부분과 이를 커스터마이징 하는 부분과 화면에 인증 정보를 꺼내는 부분에 대해 알아보도록 하겠다.

 

이전 글을 보면 인증에 실패할 경우 로그인 기능만 제공하는 단독 로그인 화면이 있다는 것은 알고 있을 것이다(<form-login> 태그의 login-page 속성에 정의하는 URL로 보여지는 화면) 이 화면의 소스를 가지고 설명하도록 하겠다(메인 화면의 로그인에도 같은 내용으로 적용이 가능하지만 표현해야 하는 방법이 다를수 있기 때문에 여기서는 언급하지 않겠다. 메인 화면의 로그인 화면의 경우 ajax 로그인에 javascript alert 로 보여지는 방법이 좋은 방법일수도 있다. 이렇게 하는 방법은 나중에 별도로 다루겠다)

 

<%@ 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>
    </form>
</div>
</body>
</html>

 

밑에서 이 화면이 실제로 나타나는 모습을 보여주겠지만 로그인 기능에만 충실한 썰렁한 로그인 화면이다. 로그인 화면을 만들어 본 사람이라면 javascript쪽 코드 내용은 값의 여부를 판단하기 위한 코드임을 알 수 있을 것이다. 그리고 로그인 아이디와 비밀번호 넣는 부분의 name 속성을 각각 loginid와 loginpwd로 주었는데 이 부분은 이전 글에서 <form-login> 태그의 username-parameter 속성과 password-parameter 속성에 지정한 값이었다. 

 

위의 로그인 페이지 소스에서 다음과 같은 이제껏 못봤던 부분이 있을것이다.

 

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

 

이 부분은 인증을 실패했을 경우 실패 메시지를 보여주는 부분이다. 첫줄을 보면 jstl 태그로 param.fail이 값이 없는지를 확인하는 부분이 나온다. 이전 글에서 보면 <form-login> 태그의 authentication-failure-url 속성 값으로 /login.do?fail=true라고 준 부분이 있다. 인증이 실패했을 경우 fail이란 파라미터에 true로 값을 셋팅해서 로그인 페이지를 보여주라는 의미이다. 바로 이 fail 파라미터를 체크하는 부분이 이 부분이다. 그냥 로그인 페이지를 보여주는 경우에서는 fail 파라미터가 셋팅이 아예 안되있기 때문에 <c:if> 태그의 조건을 만족시키지 못하지만 인증이 실패해서 fail 파라미터에 true가 셋팅될 경우 <c:if> 태그 조건을 만족하기 때문에 <c:if> 태그의 조건을 만족하게 되어 <c:if> 태그 안쪽의 내용을 보여주게 되는 것이다.

 

Spring Security는 작업을 하면서 예외가 발생할 경우 해당 예외에 대한 객체를 만든뒤 이를 세션에 저장하게 되는데 이때 세션에 저장될때 사용되는 key의 이름이 SPRING_SECURITY_LAST_EXCEPTION이다. 그래서 세션에서 이 키를 이용해 꺼내온 뒤에 여기서 message 변수 값을 읽어와서 출력하게 되는 것이다.

이런식으로 에러 메시지를 출력할 수가 있다. 다음은 이렇게 셋팅한 상태에서 인증에 실패했을 경우 보여지는 화면이다.

 

 

 

Bad credentials라고 나오는 부분..이 부분이 $sessionScope["SPRING_SECURITY_LAST_EXCEPTION"].message로 인해 나오는 부분이다.

 

● 일반적으로 jsp 페이지를 코딩하다보면 jsp 페이지에서 세션을 생성하지 못하도록 <%@ page session="false" %> 로 셋팅하는 경우가 있다. 근데 이렇게 셋팅할 경우 세션에 대한 이용도 할수가 없기 때문에 위와 같은 세션으로 제공되는 Spring Security 에러 메시지를 볼 수가 없게 된다. 똑같이 따라 했는데도 안나올 경우 <%@ page session="false" %>로 지정했는지 확인하자

 

그런데 여기서 주의할 점이 있다. 사실 세션을 통한 에러 메시지 출력은 바람직하진 않다. 인증과 관련된 페이지(로그인 페이지)가 해킹으로 동시다발로 공격받을 경우 인증 실패가 계속해서 발생할것이고 이로 인해 세션에 예외 객체를 계속 저장할 것이다. 이러다보면 WAS의 메모리가 가득차는 상황이 발생하여 Full GC가 발생하게 된다. 이런 Full GC가 지속적으로 발생하게 되면 전반적으로 웹페이지의 응답이 늦어질수 밖에 없다. 인증을 성공했을 경우에 인증 정보도 세션에 저장하지만 이것은 정상적인 경로로 접근해서 인증한 것이고(만약 로그인 아이디와 패스워드가 유출되어서 그걸 이용한 공격이라면..그건 어쩔수 없다..유출된 걸로 공격하는 것까지 막을수는 없는거니까..) 인증 에러 메시지가 세션에 저장되는 경우는 비정상적인 방법(올바르지 않은 아이디와 비밀번호를 이용하는..)을 통한 인증을 한다는 것으로도 볼 수가 있다. 그래서 이렇게 예외가 발생할 경우 세션에 저장하지 않도록 해주는 것이 좋다. 이 부분에 대한 설정은 <form-login> 태그의 authentication-success-handler-ref 속성과 authentication-failure-handler-ref 속성을 이용하는 부분을 설명할 때 이 에러 메시지 보이는 부분을 세션을 이용하지 않도록 하는 부분으로 설명할 것이다.

 

여기까지 진행하면 에러메시지까지 잘 나오니 써먹을만 하다..라고 생각이 드는 수도 있겠지만 아직 불만인 점이 있다. 그것은 한글로 메시지가 나오지 않는 것과 이 메시지를 우리 입맛에 맞게 수정을 하지 못하는 부분이 불만이다. 지금부터는 이 부분에 대한 설명을 진행하겠다.

 

Maven으로 Spring Security 관련 라이브러리를 사용하게 되면 반드시 들어가야 하는 것이 spring-security-core-버전.jar이다. 이 jar 파일 안을 살펴보면 

org.springframework.security 패키지에 messages로 시작하는 프로퍼티 파일들이 있다. 이 프로퍼티 파일들이 Spring Security에서 사용되는 각종 메시지들이 들어가 있는 프로퍼티 파일들이다. 여러개가 있는 것은 각 언어별로 로칼라이징이 되어 있기 때문이다. messages.properties 파일과 messages_ko_KR.properties 파일을 열어보자. 그러면 messages.properties 파일에 사용된 key가 그대로 messages_ko_KR.properties 파일에 사용되고 있으며 key에 대한 value값 또한 영어의 내용을 한글로 옮긴 것임을 알 수 있다.(messages_ko_KR.properties 파일을 이클립스에서 더블 클릭했을때 한글이 깨져서 나온다면 이클립스에 propedit 같은 프로퍼티 파일 전용 플러그인이 설치되지 않아서 그렇다. 구글에서 propedit으로 검색하면 설치 방법이 나온다) 위에 보여준 로그인 실패 메시지의 경우는 message.properties 파일에서 key가 AbstractUserDetailsAuthenticationProvider.badCredentials 인 것을 읽어서 표현 한 것이다.

 

그런데 이상한 것은 spring security에서는 한글화된 message 프로퍼티 파일(표현하고자 하는 메시지 내용은 우리가 원하는 내용은 아닐수도 있겠지만..)을 제공해주는데도 왜 영어로 된 메시지를 표현할까? 사실 이 부분은 아직 나도 발견하진 못했다. 그러나 이 부분은 왜 이렇게 동작할까..란 부분에 대해서는 고민하진 않았다(고민할 필요성이 없다는 뜻은 아니니 오해하지 말았음 한다). 왜냐면 이 메시지를 그대로 쓰지는 않을것이기에 외부에서 우리가 따로 정의해서 쓸 것이기 때문이다. message 프로퍼티란 것은 key에 따라 우리가 원하는 메시지를 표현하는 것이다. 그렇기 때문에 spring security message 프로퍼티 파일에서 key 이름을 동일하게 사용하는, 내용물만 달리하는 메시지 파일을 사용하면 된다. 예를 들어 spring-security-core jar 파일에 있는 messages_ko_KR.properties 파일에 다음의 내용이 있다면..

 

AbstractUserDetailsAuthenticationProvider.badCredentials=비밀번호(credential)가 맞지 않습니다.

AbstractUserDetailsAuthenticationProvider.credentialsExpired=비밀번호(credential)의 유효 기간이 만료되었습니다.

AbstractUserDetailsAuthenticationProvider.disabled=존재하지 않는 사용자 입니다.

AbstractUserDetailsAuthenticationProvider.expired=사용자 계정의 유효 기간이 만료 되었습니다.

AbstractUserDetailsAuthenticationProvider.locked=사용자 계정이 잠겨 있습니다.

 

우리가 앞으로 사용할 spring_security_messages.properties 파일엔 다음과 같이 하면 된다는 얘기다

 

AbstractUserDetailsAuthenticationProvider.badCredentials=비밀번호가 맞지 않습니다. 관리자에게 문의해주세요.

AbstractUserDetailsAuthenticationProvider.credentialsExpired=비밀번호의 유효 기간이 만료되었습니다. 관리자에게 문의해주세요.

AbstractUserDetailsAuthenticationProvider.disabled=존재하지 않는 사용자 입니다.관리자에게 문의해주세요.

AbstractUserDetailsAuthenticationProvider.expired=사용자 계정의 유효 기간이 만료 되었습니다. 관리자에게 문의해주세요.

AbstractUserDetailsAuthenticationProvider.locked=사용자 계정이 잠겨 있습니다. 관리자에게 문의해주세요.

 

위와 아래의 차이는 메시지 내용만 차이가 있을 뿐이지 메시지에 대한 key는 그대로 사용했다. messages_ko_KR.properties 파일의 내용을 그대로 복사한 뒤에 /WEB-INF/messages에 spring_security_messages.properties 파일을 새로 만들고 이 파일에 복사한 내용을 붙여넣은 뒤 우리가 원하는 문구로 수정한다.(이 문구 수정은 사실 금방 할수는 없다. 어떤 상황에서 어떤 key값에 사용되는 메시지를 보여줄지는 지금은 알 수가 없다. 테스트 하면서 나오는 문구를 보고 그 문구가 어느 key를 사용하는지 확인한 뒤에 문구를 바꿔보며 확인해야 한다. 또한 key가 달라도 같은 내용의 메시지를 사용하는 경우가 있기 땜에 꼼꼼히 테스트 해볼 필요성은 있다)

 

그럼 위와 같이 만든 spring_security_messages.properties 파일을 어떻게 설정해야 우리가 이용할 수 있을까? 일반적인 Spring에서의 MessageSource 설정 방식을 따르면 된다. 여기서는 Spring에서 일반적으로 사용되는 MessageSource 관련 클래스 중 하나인 org.springframework.context.support.ReloadableResourceBundleMessageSource를 사용했다.

 

<bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
   <property name="basenames">
	   <list>
		   <value>/WEB-INF/messages/spring_security_messages</value>
	   </list>
   </property>
   <property name="defaultEncoding" value="UTF-8"/>
   <property name="cacheSeconds" value="5"/>
</bean>

 

이것을 설정하는데 있어서 주의할 사항이 하나 있다. Spring에서의 Message 처리에 대해서는 View쪽과 밀접한 연관을 가지기 때문에 Servlet Context에 messageSource 설정을 하는 경우가 일반적이다. 그러나 여기서는 그렇게 해서는 안된다. Spring Security에서 사용할 메시지를 결정하는 주체는 Spring Security이기 때문에 Spring Security가 지금 설정해 놓은 messageSource에 접근할 수 있어야 한다. 이전의 설정을 보면 우리는 Spring Security를 Root Context에 설정해 놓았다. 문제는 Root Context는 Servlet Context에 설정되어 있는 bean을 접근 할 수 없다는데 있다. 즉 Servlet Context 설정 파일에 위의 messageSource 설정을 할 경우 Root Context에 있는 Spring Security가 Servlet Context에 있는 messageSource에 접근할 수가 없다. 또한 messageSource는 Spring Security만 이용하는 자원이 아니다. Spring에 설정된 messageSource를 Spring Security가 이용하는 개념으로 보아야 한다.(이런 이유로 message properties 파일도 여러개 설정이 가능하게 <list> 태그를 사용한 것이다.) 그래서 messageSource를 Root Context에 설정해놓으면 Spring Security 뿐만 아니라 Servlet Context에 설정해놓은 Spring MVC쪽에서도 messageSource 접근이 가능하다.(물론 Spring Security를 Servlet Context에 설정할 수도 있다. 그러나 이럴 경우 여러개의 Servlet Context가 발생하여 추가 확장을 할려고 할때마다 Spring Security를 또 설정해주어야 한다. Servlet Context 별로 보안 설정을 한다는 것인데 보안이란걸 그런식으로 설정 하지는 않잖은가? 그럴바엔 Root Context에 설정하여 공통 개념으로 보안을 똑같이 적용한다는 컨셉으로 가는게 좋다고 본다)

 

이렇게 Root Context에 messageSource를 설정하고 실행해보면 다음과 같이 한글로 메시지가 나오며 메시지 내용이 커스터마이징이 된 것을 확인할 수 있다.

 

 

지난번 글과 이번 글로 Spring Security의 커스터마이징 포인트였던 로그인 화면 커스터마이징에 대한 부분을 마무리짓도록 하겠다(정확하게는 한 부분이 더 남긴하다. 메인화면에 있는 로그인에서 로그인이 성공했을때의 커스터마이징 내용이 남아있다. 근데 이것은 다음에 설명해야 할 내용이 있어야 설명이 쉬어서 차후에 따로 설명하겠다) 다음에는 Spring Security의 커스터마이징 포인트로 잡았던 DB를 이용한 사용자인증 절차를 진행하기 위한 DB 테이블 스키마를 공개하도록 하겠다.