본문 바로가기

프로그래밍/Spring Security

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

지난 글에서는 Spring Security의 초간단 셋팅으로는 현업에서 절대 써먹을 수 없기 때문에 이를 어떤 부분에서 커스터마이징을 해서 써먹을 수 있는 버전으로 바꿀지 그 포인트를 잡아보았다. 이제부터의 글은 그런 포인트를 하나하나 적용하는 글이라고 보고 이해하기 바란다.

 

가장 먼저 시작해야 할 것은 로그인 화면이다. Spring Security가 제공하는 초간단 로그인 화면 기억하는가? 기억나지 않는 분도 있을 것 같아서 다시 보여주도록 하겠다

 

 

이제 기억나는가? 정말 너무나도 간단한, 초라하기 그지없는 로그인 화면이다. 하지만 자기 기능은 충실히 하고 있다. 로그인 화면의 기능이란 무엇인가? 로그인 아이디와 패스워드를 입력받아 로그인 기능을 수행하기만 하면 되는 것이다. 하지만 그렇다고 이런식으로 초간단으로 하진 않는다. 디자인도 입히고 검증 로직도 붙이고 그런 과정이 있는 로그인 화면을 많이들 만들것이다. 이 화면은 Spring Security가 제공하는 화면이라 우리 입맛에 맞게 고칠수는 없다. 우리 입맞에 만든 화면을 Spring Security 로그인 화면에 사용하도록 바꾼다는 마인드로 접근하도록 하자

 

먼저 설명해야 할 것은 이 화면이 만드는 html 소스이다. 왜냐면 이 화면의 소스를 알아두어야 우리가 만드는 로그인 화면을 수정할 수 있게 된다.

다음의 html 소스는 이 화면의 html 소스이다.

 

<html>
<head>
	<title>Login Page</title>
</head>
<body onload='document.f.j_username.focus();'>
<h3>Login with Username and Password</h3>
<form name='f' action='/j_spring_security_check' method='POST'>
 <table>
    <tr>
		<td>User:</td>
		<td>
			<input type='text' name='j_username' value=''>
		</td>
	</tr>
    <tr>
		<td>Password:</td>
		<td>
			<input type='password' name='j_password'/>
		</td>
	</tr>
    <tr>
		<td colspan='2'>
			<input name="submit" type="submit" value="Login"/>
		</td>
	</tr>
  </table>
</form>
</body>
</html>

 

화면이 초라하니 html 소스도 초라하기 그지없다. 그러나 이런 소스이기 때문에 우리가 짚어내야 할 부분을 아주 쉽게 짚어낼수 있다. 여기서 우리가 짚어야 할 것은 다음의 내용이다.

 

● 아이디를 입력하는 input 태그의 name은 j_username 이다

 비밀번호를 입력하는 input 태그의 name은 j_password 이다.

 form 태그의 action 속성값으로 주어진 /j_spring_security_check(이 부분은 Context Path에 따라 앞 부분 경로가 달라질수 있다. 그러나 /Context Path/j_spring_security_check 구조로 가는 것에는 변함이 없다) 에서 로그인 처리를 진행한다(POST 방식)

 

왜 이것을 짚어야 하느냐? 우리가 만든 로그인 화면에 적용하기 위함이다. 우리가 따로 로그인 화면을 만들었다면 아이디를 입력하는 input 태그의 name 속성 값을 j_username으로 바꿔주고, 비밀번호를 입력하는 input 태그의 name 속성값을 j_password로 바꿔준뒤에 로그인 form 태그의 action 속성 값을 /j_spring_security 로 적용해주기만 하면 Spring Security를 사용하는 로그인 방식으로 적용이 되기 때문이다. 우리가 여기서 사용하는 j_username이나 j_password 란 값도 맘에 안들면 login_id, login_password로 바꿔서 적용도 가능하다(이렇게 할 경우 Spring Security의 설정을 바꿔야 하는데 그 부분은 조금 이따가 나온다)

 

그러면 우리 로그인 화면에서 input 태그와 form 태그의 속성 값들을 바꿔서 끝나느냐? 아니다. 우리가 만든 화면으로 로그인 화면을 사용하겠다..라고 지정하는 부분이 남아있다. 이 화면을 왜 지정하느냐? 흔히 우리가 로그인 화면을 보면 전용 화면도 있지만 메인 화면의 오른쪽 상단에 조그맣게 로그인 form 만들어서 로그인 하는 경우가 많은데? 이런 경우를 가정해보자. 로그인을 하지 않은 상태에서 어떤 특정 URL을 가겠다고 하자. 그때 로그인을 하라고 유도하기 위해 메인화면으로 이동시켜서 오른쪽 상단에서 로그인 하도록 유도할 것인가? 아니다. 별로 로그인 화면을 하나 만들어둬서 이동을 시켜야 한다. 그런 관점에서의 로그인 화면이다. (이 설명이 이해가 안된다면 포탈 사이트에 로그인 하지 않은 상태에서 로그인 해야 이용할 수 있는 화면을 가보도록 해보자. 그러면 덩그러니 로그인만 하도록 되어 있는 화면으로 이동할 것이다) 즉 인증을 하지 않은 상태에서 인증을 받아야 이용할 수 있는 화면을 사용하려 할 경우 인증을 받기 위한 로그인 화면을 지정하는 것이다. 또한 위에서 방금 언급했지만 메인 화면의 오른쪽 상단에 조그맣게 로그인 form 만든 것도 위의 Rule을 따라야 Spring Security를 이용한 로그인이 된다.

 

위의 이런 내용을 이해한뒤 다음의 아주 썰렁한 샘플 메인 화면을 보도록 하겠다

 

 

작게 캡춰해서 그렇지 이 화면은 메인 화면이다. 메인화면 컨텐츠로 보여줄것이 따로 없어서 작게 캡춰한 것이다. 절대 팝업창이 아니니 오해 없길 바란다. 아무튼 이 메인 화면의 구조는 왼쪽에 로그인 화면(응? 요즘 트랜드엔 맞지 않는 구조인데..넘어가자..)을 구성하고 있다. 지금까지의 내용을 정리하자면 로그인을 하는 곳은 방금 설명한 메인화면 왼쪽에 로그인 하는 그런 UI가 하나 있고 또 인증을 받지 않은 상태에서 권한이 필요한 화면을 접근한 경우 인증을 받기 위해 나타나는 로그인 단독 화면 이렇게 2군데가 있게 되는 것이다.(독립적인 로그인 화면과 프레임의 왼쪽에 있는 조그만 로그인 영역)

 

Custom 로그인 화면 UI에 대해서는 이 정도로 설명해두고 본격적으로 Spring Security 에서 이를 이용하는 방법에 대해 알아보도록 하자. Spring Security에서 위에서 언급한 로그인 화면, 즉 form 방식의 로그인을 하기 위해서는 <http> 태그 안에 <form-login> 태그를 넣어야 한다. 다음과 같이 말이다.

 

<http auto-config="true">
	<intercept-url pattern="/admin/**" access="ROLE_ADMIN" />
	<intercept-url pattern="/main.do" access="ROLE_ANONYMOUS" />
	<intercept-url pattern="/**" access="ROLE_USER"/>
	<form-login
		username-parameter="loginid"
		password-parameter="loginpwd" 
		login-page="/login.do"
		default-target-url="/main.do"
                authentication-failure-url="/login.do?fail=true"
	/>
</http>

 

<form-login> 태그 또한 여러가지 유용한 속성들을 많이 제공해주는데 일단 위에 언급한 5가지만 설명하기로 하고 차후 필요한 속성이 생기면 그때 다시 속성에 대해 설명하도록 하겠다(Spring Security의 문서 부록을 보면 XML Namespace에 대한 설명이 나오는데 거기서 <form-login> 태그 설명을 보길 추천한다)

username-parameter 속성은 로그인 화면에서 사용자 아이디를 입력받는 text 태그의 기본 name 속성값인 j_username을 다른 값으로 사용하려 할 때 이 속성을 사용한다. password-parameter 속성은 로그인 화면에서 사용자 패스워드를 입력받는 text 태그의 기본 name 속성값인 j_password를 다른 값으로 사용하려 할때 이 속성을 사용한다. 여기서는 loginid와 loginpwd로 정의했기 때문에 위에 보여준 우리가 사용하고자 하는 로그인 화면에서 다음과 같이 작성해주면 된다는 얘기다(아래 html 코드는 메인 화면의 왼쪽 로그인 양식의 코드를 보여주고 있다)

 

<form id="loginfrm" name="loginfrm" method="POST" action="${ctx}/j_spring_security">
<table>
	<tr>
		<td style="width:50px;">id</td>
		<td style="width:150px;">
			<input style="width:145px;" type="text" id="loginid" name="loginid" value="" />
		</td>
		
	</tr>
	<tr>
		<td>pwd</td>
		<td>
			<input style="width:145px;" type="text" id="loginpwd" name="loginpwd" value="" />
		</td>
	</tr>
	<tr>
		<td colspan="2">
			<input type="submit" id="loginbtn" value="로그인" />
		</td>
	</tr>
</table>
</form>

 

username-parameter 속성과 password-parameter 속성을 지정하지 않았다면 위의 html 소스 코드에서 text 타입의 name 속성이 loginid와 loginpwd로 지정된 것을 j_username과 j_password로 지정했어야 했다. 그러나 이 속성을 지정함으로써 우리가 원하는 name 속성 값을 줄 수가 있게 된다.

 

login-page 속성은 로그인 화면 URL을 지정한다. 인증을 받지 않은 상태에서 권한이 필요한 화면을 접근할 경우 인증을 받기 위한 로그인 화면을 띄운다고 위에서 얘기했다(찐한 색으로 강조도 했다^^) 그때 보여지는 화면 URL이 바로 이 속성이다. 이 화면에서도 로그인 id를 입력받는 text 태그와 로그인 password를 입력받는 text 태그의 name 속성값을 위에서 방금 언급한 username-parameter 속성과 password-parameter 속성값으로 주어진 값으로 각각 셋팅해주어야 한다(어떤 화면에서든 간에 Spring Security에서 로그인 id를 입력받는 부분과 로그인 password를 입력받는 부분의 html text 태그 name 속성 값을 username-parameter 속성과 password-parameter 속성에 준 값으로 셋팅한다고 생각해두면 된다)

 

default-target-url 속성은 로그인 인증을 성공하면 어떤 페이지를 보여줄지를 결정하는 속성이다. 위에서 설정한 내용대로라면 로그인 인증을 마치면 메인 화면으로 이동할 것이다. 이 내용에 대해서는 혼선이 있을것 같아서 부가적인 설명을 하도록 하겠다. 우리가 로그인 화면을 보는 경우는 2가지가 있다.

 

● 로그인 화면을 보여주는 링크를 클릭하여 로그인 화면을 보거나 또는 메인화면과 같이 바로 로그인 화면을 보는 경우(이것은 우리가 로그인 화면을 보고자 하는 의지의 표현으로써 로그인 화면을 보는 경우이다)

● 현재 인증을 받지 않은 상태에서 권한이 필요한 화면의 링크를 클릭하거나 또는 그 URL을 직접 입력하였을 때 인증을 받기 위한 로그인 화면(로그인 기능만 있는 단독화면)을 보는 경우(이것은 우리가 로그인 화면을 볼려고 하는 의지가 없는 상태에서 시스템적으로 인증을 받기 위해 자동으로 보여주는 경우이다)

 

글박스의 내용을 보면 아마 어떤 상황에서 로그인 화면을 보여줄지를 이해할것이다. 이 부분을 설명한 이유는 default-target-url 속성 위에서 언급한 두가지 로그인 화면의 경우에서 첫번째 화면의 경우에만 해당됨을 설명하기 위함이었다. 두번째의 경우는 내가 이동하고자 하는 URL(권한이 필요한 화면의 링크나 직접 입력한 URL)이 있기 땜에 로그인 인증을 성공할 경우 그 곳으로 이동하지만 첫번째의 경우는 그런 URL이 없기 때문이 이렇게 default-target-url 속성에 입력한 URL로 이동하게 되는 것이다. 또한 두번째 경우에서 보여지는 로그인 화면은 <form-login> 태그에서 login-page 속성에 지정해둔 로그인 화면을 보여주는 것이다. 그런데 이런 요청도 있을 수 있다. 두번째의 경우도 내가 이동하고자 하는 URL이 아니라 default-target-url 속성에 지정해둔 화면을 보도록 하고 싶을수도 있다. 즉 첫번째 경우든 두번째 경우든 어떤 상황에서 보여지는 login 화면이라도 로그인 인증을 성공하면 무조건 default-target-url 속성 값을 보여주도록 하고 싶을 수도 있다. 이럴 경우 <form-login> 태그에서 always-use-default-target 속성을 true로 주면 된다(이 속성은 기본값으로 false로 되어 있다)

 

authentication-failure-url 속성은 인증이 실패했을 경우 보여주는 화면의 URL을 지정하는 부분이다. 여기서는 인증에 실패했을 경우 로그인 화면을 다시 보여주어 재로그인을 유도하도록 했다. 다만 파라미터 fail에 true 값을 지정함으로써 인증을 실패하여 보여지는 로그인 화면이라는 것을 구분하도록 했다.(이 파라미터 값을 나중에 로그인 화면에서 사용하는 상황이 있는데 그 내용에 대해서는 다음 글에서 다루도록 하겠다)

 

위와 같이 속성값들을 정의하고 한번 메인 화면으로 이동해서 로그인 과정을 거쳐보자. 이전에 설명했던 초간단 셋팅에서 등록한 사용자 계정을 이용해서 로그인을 시도해보자. 로그인이 되어 메인화면이 짜잔..하고 나타났는가? 아니다. 메인화면이 나타났다면 지금까지 따라했던 셋팅이 잘못된 부분이 있다는 것이다. 메인화면이 나타나지 않고 계속 리다이렉션만 하다가 결국 리다이렉션 순환 오류가 발생할 것이다. 지금까지 한 셋팅으로 리다이렉션 순환 오류가 나타난다면 여러분은 올바르게 셋팅한 것이다. 그러면 왜 리다이렉션 순환 오류가 발생하는가에 대해 고민해보았나? 그것에 대해 답을 냈다면 여러분들은 정말 이해가 빠른 것이다. 그러나 모든 사람들이 이해가 빠른건 아니기 땜에 설명을 하도록 하겠다. 

 

다음은 위에 셋팅한 <http> 태그에서 <intercept-url> 부분만 발췌한 부분이다.

 

<intercept-url pattern="/admin/**" access="ROLE_ADMIN" />
<intercept-url pattern="/main.do" access="ROLE_ANONYMOUS" />
<intercept-url pattern="/**" access="ROLE_USER"/>

 

이것을 보고..아..하고 이해하신 분이 있다면 그래도 대단하신 분이다^^.. 그러나 나는 처음에 이런 상황을 닥쳤을때 이것을 보고 바로 이해는 못했다. 이제 왜 이런 증상이 있는지 설명하도록 하겠다.

 

우리가 로그인 하기 위해 메인 화면인 main.do를 갔을 경우 우리는 로그인을 하지 않은 ANONYMOUS USER이기 때문에 access="ROLE_ANONYMOUS" 설정을 만족하여 main.do 화면을 볼 수 있을 것이다. 그러나 우리가 초간단 설정에서 지정한 사용자(설명을 하기 위해 user1으로 하겠다)으로 로그인 했다고 생각해보자. 사용자 user1으로 로그인을 성공하면 우리는 ROLE_USER 권한을 갖게 된다. 그리고 ROLE_USER 권한을 갖고 <form-login> 태그에 지정한 default-target-url 속성에 지정한 main.do로 이동할 것이다. 근데 여기에서 문제가 생긴다. 무슨 문제냐면 main.do는 ROLE_ANONYMOUS 권한, 즉 로그인 하지 않은 사람만 접근이 가능하기 땜에 로그인 한 사람은 접근을 할 수 없게 된다. 이로 인해 아이디와 패스워드를 올바르게 입력하여 로그인을 해도 이동하고자 하는 화면에 올바른 권한을 갖고 있지 않다고 여겨서 로그인을 실패했다고 생각한다. 그래서 Spring Security는 <form-login> 태그의 login-page 속성에 지정한 로그인 화면 URL인 login.do로 이동할 것이다. 근데 login.do에서 이런 문제를 또 부딪치게 된다. 위에서 지정한 login.do가 속하는 패턴은 특정 URL을 지정한 패턴에 login.do가 없기 때문에 /**에서 처리하려 할테고 /**는 ROLE_USER 권한을 갖고 있어야 접근이 가능하다. 그러나 현재 상태는 로그인을 실패한 상태이기 때문에 가지고 있는 권한은 ROLE_ANONYMOUS 권한이다. 그렇기 땜에 화면에 맞지 않은 권한이어서 다시 login.do로 이동하려 할테고 이 login.do로 이동하는 것이 계속 이런 문제를 안고 리다이렉션을 무한루프 돌듯 계속 하다보니 리다이렉션 오류가 발생하는 것이다.

 

이런 상황땜에 URL에 따른 권한 설계가 중요한 것이다. 이 부분을 해결해보도록 하자. 메인 화면(main.do)은 로그인을 하여 권한을 갖고 있는 사람(ROLE_USER, ROLE_ADMIN)이나 로그인을 하지 않거나 로그인을 실패해서 권한을 갖고 있지 않은 사람(ROLE_ANONYMOUS) 모두가 방문 가능하다. 또 로그인 화면은 권한이 없는 사람만 접근 가능하도록 하자. 로그인을 성공하여 권한을 갖고 있는 사람이 로그인 화면을 이동할 상황이 없기 때문이다. 그리고 /** 패턴, 즉  대부분의 URL이 속하는 패턴에서는 권한이 있는 사람이나 권한이 없는 사람 모두 접근이 가능하도록 하자. 차후에 화면이 하나하나 늘어가면서 세세하게 분리되어야 할 것이 있다면 디테일한 패턴과 그에 따른 권한을 /** 패턴 위에 지정해주면 된다. 그래서 정리된 패턴은 다음과 같다

 

<intercept-url pattern="/admin/**" access="ROLE_ADMIN" />
<intercept-url pattern="/login.do" access="ROLE_ANONYMOUS" />
<intercept-url pattern="/main.do" access="ROLE_ANONYMOUS,ROLE_USER,ROLE_ADMIN" />
<intercept-url pattern="/**" access="ROLE_ANONYMOUS,ROLE_USER,ROLE_ADMIN"/>

 

이렇게 정리한 뒤에 로그인을 시도해보자. 메인 화면에서 사용자 user1으로 올바르게 로그인을 해보면 <form-login> 태그에서 default-target-url 속성에 지정한 main.do로 이동할 것이며 user1 로그인이 실패할 경우 <form-login> 태그에서 login-page 속성에서 지정한 login.do로 이동할 것이다. 근데 로그인을 성공하여 main.do로 이동해도 로그인이 제대로 잘 되었는지 알 길이 없다. 왜냐면 우리는 아직 로그인을 성공했을 경우 로그인 한 사람의 정보를 보여주는 부분을 구현하지 않았기 때문이다. 이 부분에 대해서는 나중에 수정하는 내용을 언급할 것이다. 일단은 메인 화면에서 로그인을 해서 메인 화면으로 이동했으면 로그인을 성공한것이고, 로그인 화면으로 이동하면 로그인을 실패한 것이라고 생각해두자.

 

이제 마지막으로 한가지 더 설명할 부분이 있다. <intercept-url> 태그에서 access 속성에 권한을 지정할때 권한이 늘거나 줄때마다 일일이 집어넣어야 한다. 인증을 받지 않은 권한은 ROLE_ANONYMOUS라 해도 인증을 받은 권한은 여러가지가 존재하게 된다. main.do를 지정한 부분을 다시 보자. 메인 화면은 모두가 접근 가능하도록 해야 한다. 하지만 이 모두..란 권한은 이분화가 가능한데, 그것은 권한을 갖고 있지 않은 ANONYMOUS 사용자와 어떤 권한이든 권한을 갖고 있는 사용자 이렇게 이분화가 가능하다. 근데 위와 같은 표기로 모든 권한을 다 쓴다고 가정해보자. 10개의 권한이 있다면 10개를 모두 써야한다. 그뿐만 아니라 권한이 추가되거나 삭제될때마다 이에 대한 관리도 이루어져야 한다. 이 부분을 좀더 쉽게 하기 위해 Spring Security는 Spring EL 태그를 사용하여 이런 권한 설정을 할 수 있다. 몇 가지 자주 사용하는 표현을 나열하면 다음과 같다.

 

 

표현식

설명

 hasRole(role1)

 인자로 들어간 권한(role1)을 가지고 있을 경우 true를 리턴한다

 hasAnyRole(role1, role2)

 인자로 들어간 권한들(role1, role2) 하나라도 가지고 있을 경우 true를 리턴한다(인자로 들어가는 권한의 갯수는 제한이 없다. 예시를 보이기 위해 2개만 넣은 것이지 실제로 들어가는 권한의 갯수는 제한이 없다)

 permitAll

 권한이 있든 없든 모두 접근 가능하다

 denyAll

 권한이 있든 없든 모두 접근 불가능하다

 isAnonymous()

 Anonymous 사용자일 경우 true를 리턴한다

 isRememberMe()

 Spring Security의 Remember-me 기능으로 로그인 한 사용자일 경우 true를 리턴한다

 isAuthenticated()

 Anonymous 사용자가 아닐 경우(로그인을 성공해서 인증에 성공한 경우) true를 리턴한다

 isFullyAuthenticated()

 Anonymous 사용자가 아니고 Remember-me 기능으로 로그인 하지 않은 사용자 일 경우 true를 리턴한다

위에서 설명했던 main.do를 보자. main.do는 인증을 받아 권한을 갖고 있든, 인증을 받지 않아 권한을 갖고 있지 않든 모두 접근이 가능해야 한다. 지금까지 설정한 방법으로 한다면 <intercept-url> 태그의 access 속성에 모든 권한(ROLE_ANONYMOUS 포함)을 다 나열해줘야 한다. 그러나 위의 표현식을 사용하게 되면 permitAll 을 써주는 것 만으로도 적용이 되는 것이다. 또한 권한을 관리하면서 새로이 권한이 추가되거나 기존의 권한이 삭제되는 상황이 발생하더라도 건드릴 필요가 없게 된다. 이렇게 <intercept-url> 태그에 표현식을 적용한 뒤 최종 <http> 태그 셋팅을 보면 다음과 같다.

 

<http auto-config="true" use-expressions="true">
	<intercept-url pattern="/admin/**" access="hasRole('ROLE_ADMIN')" />
	<intercept-url pattern="/login.do" access="isAnonymous()" />
	<intercept-url pattern="/main.do" access="permitAll" />
	<intercept-url pattern="/**" access="permitAll"/>
	<form-login
		username-parameter="loginid"
		password-parameter="loginpwd" 
		login-page="/login.do"
		default-target-url="/main.do"
	/>
</http>

 

<http> 태그의 use-expressions 속성을 true로 주어야 위에서 설명한 표현식 사용이 가능하다. 지금까지 로그인 화면 커스터마이징과 로그인 화면을 보기 위한 경로별 권한 설정에 대해 설명했다. 다음에는 로그인 화면에서의 메시지를 보여주는 부분과 로그인 정보를 보여주는 부분을 설명하도록 하겠다.