본문 바로가기

프로그래밍/Spring Security

Spring Security에서 화면에 사용자 정보, 권한에 따른 동적 메뉴 화면 구성 및 로그아웃 구성하기

저번 글에서는 Spring Security에서 DB를 이용한 인증과 권한 설정을 다루었다. 이번 글에서는 이렇게 인증과 권한을 거치면 이런 정보를 화면에서 어떻게 보여주는지에 대해 알아보도록 하겠다

 

흔히 로그인을 거치면 웹페이지에 **님 반갑습니다..이런 내용의 문구와 로그인 한 사람의 정보(예를 들면 방문한 사이트에 가지고 있는 포인트 점수나 등급)이런 것들을 볼 수가 있다. 지금까지의 글들을 잘 보고 따라했다면 이전 글에서 단독 로그인 화면에서 테스트를 진행해서 거기서 로그인이 성공적으로 되면 메인화면으로 이동되는 것을 볼 수 있었을것이다. 근데 메인화면으로 이동해도 로그인이 되었는지 안되었는지 알 수가 없었다. 이유는 메인화면 왼쪽에 아직도 로그인 폼이 나타나고 있기 때문이다.

 

 

이 화면은 로그인 화면에서 로그인이 정상적으로 이루어진 뒤에 자동으로 메인화면으로 이동했을때의 모습이다. 왼쪽에 아직도 로그인 폼이 나타나고 있기 때문에 로그인이 정상적으로 잘 되었는지 안되었는지 알 수가 없다. 이 부분을 이제 바꾸어보도록 하겠다.

 

먼저 해야 할 것은 다음의 Custom Tag를 등록하는 것이다. Spring Security에서는 jsp 페이지에서 사용할 수 있는 Custom Tag를 제공한다

 

<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>

 

공통 include 파일이든 또는 Spring Security의 기능을 jsp 페이지에서 써야 하는 곳에서든 저 코드를 넣어서 Spring Security가 제공하는 tag를 사용할 수 있게해준다.

 

먼저 Spring Security에서 로그인 정보를 가져오는 방법에 대해 설명하도록 하겠다. 2가지의 방법이 있는데 Spring Security가 자체적으로 제공하는 방법HttpServletRequest 객체에서 가져오는 방법 이렇게 2가지가 있다. Servlet Spec 2.5에서는 인증 과정을 마치고 난 뒤의 로그인 정보를 가져오는 기능만 있었으나 Servlet Spec 3.0 이상에서는 Spring Security가 제공하는 인증 기능을 이용해 로그인 하거나 로그아웃 하는 기능도 사용할 수 있다. Servlet Spec 3.0 이상을 사용하는 WAS는 Tomcat 7.0 이상을 사용하면 된다. 현재 이 블로그 글을 쓰는 시점에서는 Tomcat 8.0이 나온 시점이라 7.0도 이젠 안정화되었다고 생각하고 대중적으로 사용할 수 있겠으나 일단 여기서는 Servlet Spec 2.5 기준으로 HttpServletRequest 객체에서 가져오는 방법만 설명하도록 하겠다(이 부분에 대한 설명은 Spring Security의 Reference 문서에 언급되어 있다. 내용도 어렵지 않으니 한번 도전해보길 바란다)

 

Spring Security가 자체적으로 제공하는 방법은 다음과 같은 형식으로 한다

 

<%@ page import="org.springframework.security.core.context.SecurityContextHolder" %>
<%@ page import="org.springframework.security.core.Authentication" %>
<%@ page import="com.terry.springsecurity.vo.MemberInfo" %>
<%
Authentication auth = SecurityContextHolder.getContext().getAuthentication();

Object principal = auth.getPrincipal();	
String name = "";
if(principal != null && principal instanceof MemberInfo){
	name = ((MemberInfo)principal).getName();
}
%>

 

auth.getPrincipal() 함수를 통해 return 되는 것은 2가지중 하나이다. 인증을 하지 않은 상태에서 이 코드가 실행되면 anonymousUser란 문자열이 있는 String 객체가 이 코드에서 return이 된다. 그러나 인증에 성공하면 우리가 이전 글에서 로그인 한 사용자 정보가 들어있는 객체인 MemberInfo 객체가 return이 된다. auth.getPrincipal() 함수는 Object 객체가 return이 되기 때문에 String이나 MemberInfo 클래스 객체 모두 받을 수 있다. 이것을 구분하기 위한 것이 if 문에 있는 조건이다. return 된 객체가 null 이 아니고 MemberInfo 객체일 경우에 return 된 객체를 MemberInfo 클래스 객체로 캐스팅 해서 필요한 정보를 꺼내오는 것이다. 여기서는 로그인 한 사용자의 이름을 가져오고 있다. 이 코드는 jsp에서 사용했지만 jsp가 아닌 자바 코드에서도 사용자 정보가 필요한 부분이 있다면 사용이 가능하다. 이제껏 누누히 얘기했지만 Spring Security가 직접적으로 이용하지 않는 정보 또한 이런 방법으로 가져올 수 있는 것이다.

 

위에서 설명한 방법이 Spring Security에서 제공하는 방법이라면 지금부터 설명하는 방법은 Spring Security가 아닌 Servlet Spec을 통해 가져오는 방법을 설명하도록 하겠다. 다음의 코드는 Servlet Spec을 이용해서 가져오는 방법이다.

 

<%@ page import="org.springframework.security.core.context.SecurityContextHolder" %>
<%@ page import="org.springframework.security.core.Authentication" %>
<%@ page import="com.terry.springsecurity.vo.MemberInfo" %>
<%
Authentication auth = (Authentication)request.getUserPrincipal();
String name = "";
if(auth == null){
	
}else{
	Object principal = auth.getPrincipal();
	if(principal != null && principal instanceof MemberInfo){
		name = ((MemberInfo)principal).getName();
	}
}
%>

 

위의 코드에서 보듯이 HttpServletRequest 클래스의 getUserPrincipal() 메소드를 이용해서 인증 정보를 가져오고 있다. 두 가지 방법의 코드를 비교해보면 대부분 비슷하지만 한가지 차이점이 존재하는 부분이 있다. Spring Security를 이용해서 인증 정보를 가져오는 경우(Authentication auth = SecurityContextHolder.getContext().getAuthentication();)와 HttpServletRequest를 이용해서 인증 정보를 가져오는 경우(Authentication auth = (Authentication)request.getUserPrincipal();)에서 로그인이 성공했을 경우엔 두 방법 모두 org.springframework.security.authentication.UsernamePasswordAuthenticationToken 객체를 받아온다. 그러나 로그인을 하지 않은, 즉 Anonymous 사용자인 경우엔 두 가지 방법에 차이점이 존재한다. Anonymous 사용자인 경우 Spring Security를 이용하는 방법에서는 org.springframework.security.authentication.AnonymousAuthenticationToken 객체가 return 되지만 HttpServletRequest를 이용하는 방법에서는 null이 return 되기 때문이다. 그래서 Spring Security를 이용하는 방법에서는 인증 정보를 받는 변수인 auth에 대한 null 체크를 하지 않지만 HttpServletRequest를 이용하는 방법에서는 인증 정보를 받는 변수인 auth에 대한 null 체크를 하게 되는 것이다.

 

개인적으로는 Spring Security를 사용하는 방법이 좋다고 생각한다. jsp 페이지 쪽에서는 이미 Custom Tag를 이용해서 Spring Security가 제공하는 기능을 사용하는 마당에 이 부분만 HttpServletRequest 객체를 이용하는 것도 좀 쌩뚱맞아 보이고, 그리고 Spring MVC쪽에서 인증 정보를 가져온다고 할 경우 HttpServletContext 객체를 이용할 경우 Spring MVC의 Service나 Repository 계층에서 사용할 수 없기 때문에(물론 불가능하진 않다. Controller에서 Service로 HttpServletRequest 객체를 던져주든가 아니면 Controller에서 인증 정보를 받아 이 정보를 Service쪽에 파라미터로 전달해줘도 된다. 그러나 Spring Security를 사용할 경우 파라미터로 전달하지 않아도 가능한데 파라미터로 일일이 전달하는 식으로 일처리를 할 필요가 있겠는가) Spring Security를 사용하는 방법이 좋다고 본다(이 부분은 퍼포먼스를 고려한 의견이 아닌 개인적인 의견임을 밝혀둔다. 절대적으로 Spring Security를 사용해야 한다 이런 의미는 아니니 오해는 없길 바란다)

 

기존 메인 화면은 왼쪽에 로그인 기능만 있었다. 그러나 지금부터는 이 부분을 약간 고쳐보도록 하겠다. 로그인을 하기 전에는 로그인 폼이 보이지만 로그인을 하면 로그인 한 사용자의 정보를 보여주고 로그아웃 기능을 제공하며 로그인 한 사용자의 권한에 따른 동적 메뉴 구성하는 내용을 보여주도록 하겠다. 위와 같이 사용자 인증 정보를 가져오면 다음엔 이를 이용해서 사용자 정보와 권한에 따른 동적 메뉴를 보여주어야 한다.

 

<div style="width:200px;float:left;">
	<sec:authorize access="isAnonymous()">
	<form id="loginfrm" name="loginfrm" method="POST" action="${ctx}/j_spring_security_check">
	<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>
	</sec:authorize>
	<sec:authorize access="isAuthenticated()">
	<%=name%>님 반갑습니다<br/>
	<a href="${ctx}/j_spring_security_logout">로그아웃</a>
	</sec:authorize>
	<ul>
    	<sec:authorize access="hasRole('ROLE_ADMIN')">
        <li>관리자 화면</li>
        </sec:authorize>
        <sec:authorize access="permitAll">
        <li>비회원 게시판</li>
        </sec:authorize>
        <sec:authorize access="isAuthenticated()">
        <li>준회원 게시판</li>
        </sec:authorize>
        <sec:authorize access="hasAnyRole('ROLE_MEMBER2', 'ROLE_ADMIN')">
        <li>정회원 게시판</li>
        </sec:authorize>
    </ul>
</div>

 

위의 html 코드를 보면 <sec:authorize> 태그가 많이 보인다. 이 태그를 이용해서 사용자의 권한을 검사하게 된다. <sec:authorize> 태그 사용된 것들 중 처음 사용된 것을 보면 <sec:authorize access="isAnonymous()"> 로 사용된 것을 볼 수 있다. isAnonymous()라는거..어디서 본 적 있다고 생각들지 않는가? 예전 블로그 글에서 Spring EL 태그를 사용한 권한 설정 설명 부분에서 설명한 내용이다. isAnonymous()는 로그인 하지 않은 Anonymous User인지 확인하여 Anonymous User인 경우 true를 return 한다. 즉 <sec:authorize access="isAnonymous()">는 로그인 하지 않은 Anonymous User인 경우 <sec:authorize> 태그 사이에 있는 내용을 출력하는 것이다. <sec:authorize> 태그 사이에 무슨 태그가 있는가? 바로 로그인 폼이 있다. 즉 로그인 하지 않은 사용자인 경우 로그인 폼을 보여주기 위해 <sec:authorize access="isAnonymous()"> 태그 사이에 로그인 폼을 보여주는 태그를 넣은 것이다.

 

로그인 폼 태그를 보여주고 나면 <sec:authorize access="isAuthenticated()">가 나온다. isAuthenticated()는 인증을 받은 사용자일 경우 true를 return한다. 즉 <sec:authorize access="isAuthenticated()">는 인증을 받은 사용자일 경우 보여줘야 할 내용이 <sec:authorize> 태그 사이에 들어가게 되는 것이다. 무엇을 보여주고 있는가? 로그인 한 사용자의 이름(name)과 로그아웃 링크를 제공하고 있다.

 

그 다음에 나오는 <sec:authorize> 태그는 사용자의 권한에 따라 접근할 수 있는 메뉴를 동적으로 보여주고 있다. 관리자 화면을 생각해보자. 관리자 화면은 관리자 권한(ROLE_ADMIN)을 가지고 있는 사람만 접근이 가능하다. 그렇기땜에 hasRole('ROLE_ADMIN')으로 관리자 권한이 있을 경우에만 관리자 화면 메뉴를 보여준다. 비회원 게시판을 생각해보자. 비회원 게시판은 누구나(로그인을 하지 않은 사용자나 로그인을 한 사용자 모두) 접근이 가능하다. 그래서 permitAll로 로그인을 하지 않은 사람이든 로그인을 한 사람이든 모두 나타나도록 한다. 준회원 게시판의 경우는 로그인을 한 사람은 볼 수가 있다(권한이 준회원, 정회원, 관리자가 있다고 하면 정회원 또한 게시판을 볼 수 있고, 관리자 또한 게시판을 볼 수 있다) 그래서 isAuthenticated()로 로그인 한 사람이면 모두 볼 수 있도록 한다. 정회원 게시판의 경우는 정회원 권한(ROLE_MEMBER2)와 관리자 권한이 접근 가능하기 때문에 hasAnyRole('ROLE_MEMBER2', 'ROLE_ADMIN')로 정회원 권한이나 관리자 권한일 경우 정회원 게시판으로 메뉴가 나타나도록 한다.

 

지금까지 설명한 내용을 모두 반영한 코드는 다음과 같다.

 

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ page import="org.springframework.security.core.context.SecurityContextHolder" %>
<%@ page import="org.springframework.security.core.Authentication" %>
<%@ page import="com.terry.springsecurity.vo.MemberInfo" %>
<%
Authentication auth = SecurityContextHolder.getContext().getAuthentication();

Object principal = auth.getPrincipal();
String name = "";
if(principal != null && principal instanceof MemberInfo){
	name = ((MemberInfo)principal).getName();
}
%>
<div style="width:200px;float:left;">
	<sec:authorize access="isAnonymous()">
	<form id="loginfrm" name="loginfrm" method="POST" action="${ctx}/j_spring_security_check">
	<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>
	</sec:authorize>
	<sec:authorize access="isAuthenticated()">
	<%=name%>님 반갑습니다<br/>
	<a href="${ctx}/j_spring_security_logout">로그아웃</a>
	</sec:authorize>
	<ul>
    	<sec:authorize access="hasRole('ROLE_ADMIN')">
        <li>관리자 화면</li>
        </sec:authorize>
        <sec:authorize access="permitAll">
        <li>비회원 게시판</li>
        </sec:authorize>
        <sec:authorize access="isAuthenticated()">
        <li>준회원 게시판</li>
        </sec:authorize>
        <sec:authorize access="hasAnyRole('ROLE_MEMBER2', 'ROLE_ADMIN')">
        <li>정회원 게시판</li>
        </sec:authorize>
    </ul>
</div>

 

이렇게까지 만들고 다시 메인화면을 띄운후 로그인을 하면 다음과 같은 모습으로 바뀌게 된다.

 

 

 

로그인 id test1에 준회원 권한을 DB에서 설정해 둔 뒤에 로그인을 한 모습이다. 로그인 한 사람의 이름(테스트1)을 보여주고 있고 로그아웃 링크를 보여주고 있다. 또 준회원 권한(ROLE_MEMBER1)을 가지고 있기 때문에 비회원 게시판과 준회원 게시판 메뉴를 보여주고 있다. 관리자 권한(ROLE_ADMIN)을 주면 정회원 게시판과 관리자 화면 메뉴도 나타날 것이다. 이렇게 권한을 이용한 동적 메뉴 화면 모습을 보여주고 있다. 잘 이해가 안되면 <sec:authorize> 태그 설명한 부분을 다시 한번 읽어보면서 화면과 매핑을 시켜보면 이해가 될 것이라고 생각한다.

 

이제는 로그아웃을 설명할 차례이다. 지금까지 블로그에서 설명하면서 화면상에서 로그아웃을 보여주는 부분이 없어서 로그아웃에 대한 설정을 설명할 일이 없었으나 이제는 로그아웃이 언급되다보니 로그아웃 설정에 대한 설명을 할 시점이 되었다. 로그아웃을 실행시킬려면 j_spring_security_logout 으로 링크를 걸면 로그아웃 기능을 수행한다(j_spring_security_logout 이란 문자열이 길어서 거부감이 있다면 이것도 바꿀수 있다. 곧 이어 설명할 logout 태그의 logout-url 속성에 원하는 값을 지정해주면 된다. 이 속성의 default 값이 j_spring_security_logout 이다). 또한 로그아웃에 대한 설정을 <http> 태그 안에 다음과 같은 내용으로 넣도록 한다.

 

<logout 
	logout-success-url="/main.do"
	delete-cookies="JSESSIONID"
/>

 

logout-success-url 속성은 로그아웃 작업을 마친 뒤에 보여지는 화면 URL을 설정한다. 여기서는 메인 화면 URL을 설정함으로써 로그아웃을 한 뒤에 메인 화면으로 이동하도록 한다. delete-cookies는 로그아웃을 진행하면서 지워야 할 쿠키를 지정하는 속성이다. ,를 구분자로 하여 여러개의 쿠키 이름을 지정하면 된다. WAS를 통해 웹브라우저로 웹페이지를 보면 JSESSIONID란 이름의 쿠키가 내려오는데 로그아웃을 하면서 이 쿠키도 같이 정리하기 위해 delete-cookies 속성에 지정하는 것이다. 만약 다른 이름의 쿠키가 내려오거나 또는 사이트에서 로그인을 하면 내부적으로 특정 쿠키를 생성해서 작업하고 있다면 이 delete-cookies 속성을 이용해서 그런 부류의 쿠키를 지울수가 있는 것이다.

 

이번 글에서는 사용자 정보를 화면에 보여주고 사용자가 가지고 있는 권한에 따른 동적 메뉴 구성, 그리고 로그아웃에 대해 설명했다. 다음 글에서는 로그인을 성공했을때, 또는 로그인을 실패했을때 먼가 부가적인 작업을 하고 싶을 경우가 있을 것이다. 즉 로그인 작업을 마친 뒤 부가작업을 수행하는 그런 경우를 얘기한다. 그럴 경우를 위한 작업을 진행해보도록 하겠다.