본문 바로가기

학원/복기

[Spring] Spring Security 로그인 커스터마이징

LoginController

@Controller
public class LoginController { 
	@RequestMapping(value = "/loginPage", method = RequestMethod.GET)
	public String login() {
		return "login_page";
	}
}

 

login_page.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>    
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>SPRING</title>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
</head>
<body>
	<h1>로그인</h1>
	<hr>
	<form action="<c:url value="/loginPage"/>" method="post" id="loginForm">
	<table>
		<tr>
			<td>아이디</td>
			<td><input type="text" name="userid" id="userid"></td>
		</tr>
		<tr>
			<td>비밀번호</td>
			<td><input type="password" name="passwd" id="passwd"></td>
		</tr>
		<tr>
			<td colspan="2"><button type="submit">로그인</button></td>
		</tr>		
	</table>		
	<%-- CSRF 공격을 방어하기 위해 Spring Security에 의해 발급된 CSRF Token을 hidden 타입으로 전달 --%>
	<input type="hidden" name="${_csrf.parameterName }" value="${_csrf.token }">
	</form>
</body>
</html>

 

security-context.xml

...	
    <http auto-config="true" use-expressions="true">
    	...
        ...
	<form-login login-page="/loginPage" login-processing-url="/loginPage" 
	username-parameter="userid" password-parameter="passwd" default-target-url="/"/>
...

 

  • form-login : form 방식의 로그인 페이를 설정하기 위한 엘리먼트 
    •   속성을 사용하지 않은 경우 Spring Security의 로그인 페이지를 제공한다. 
  • login-page : 로그인 페이지를 요청하는 URL 주소를 속성값으로 설정한다.
  • login-processing-url : 아이디와 비밀번호를 전달받아 로그인 처리하는 페이지의 URL 주소  설정한다.
  • username-parameter : 아이디를 전달하기 위한 이름을 속성값으로 설정한다.
  • default-target-url : 로그인 성공시 호출된 페이지의 URL 주소를 속성값으로 설정한다. 

 


home.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!-- Security 태그 라이브러리를 JSP 문서에 포함 - Spring Security 태그 사용 가능 -->
<%@taglib prefix="sec" uri="http://www.springframework.org/security/tags"%>    
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>SPRING</title>
</head>
<body>
	<h1>메인페이지</h1>
	<hr>
	<h3><a href="<c:url value="/guest/page"/>">Guest</a></h3>
	<h3><a href="<c:url value="/user/page"/>">User</a></h3>
	<h3><a href="<c:url value="/manager/page"/>">Manager</a></h3>
	<h3><a href="<c:url value="/admin/page"/>">Administrator</a></h3>
	<hr>
	<!-- authorize : 권한을 비교하여 비교하여 태그의 포함 여부를 설정하기 위한 태그 -->
	<!-- access 속성 : 권한(Role)을 속성값으로 설정 - SpEL 사용 가능 -->
	<!-- 인증을 받지 않은 사용자인 경우 태그 포함되도록 설정 -->
	<sec:authorize access="isAnonymous()">
		<h3><a href="<c:url value="/login"/>">로그인</a></h3>
	</sec:authorize>
</body>
</html>

 

 

CSRF 공격을 방어하기 위해 Spring Security에 의해 발급된 CSRF Token을 hidden 타입으로 전달해야 한다. 

  • 서버에 전달된 요청이 실제 서버에서 허용된 요청이 맞는지를 확인하기 위해 CSRF Token을 발행하는 것이다.
  • 서버에서는 뷰페이지를 생성할 때마다 랜덤으로 토큰을 발행하여 세션에 저장하고 사용자가 서버에 페이지를 요청할 때 Hidden 타입으로 토큰을 서버에 전달하여 세션의 저장된 토큰과 비교히여 사용자를 확인한다.
  • 일치 여부를 확인한 토큰은 삭제하고 새로운 뷰에 대한 토큰을 다시 발행한다.

* CSRF(Cross-Site Request Forgery) 공격 : 사이트간 요청을 위조하는 공격

 

 

login_page.jsp

<input type="hidden" name="${_csrf.parameterName }" value="${_csrf.token }">

 

대신 crsfInput 태그를 사용해도 된다.

<sec:csrfInput/>

* csrfInput : CSRF Token을 hidden 타입으로 서버에 전달하기 위한 태그

 

 


 

에러메시지 출력하기

 

login_page.jsp

...
<c:if test="${not empty SPRING_SECURITY_LAST_EXCEPTION}">
	<h3 style="color: red">아이디 또는 비밀번호가 맞지 않습니다.</h3>
	<c:remove var="SPRING_SECURITY_LAST_EXCEPTION"/>
</c:if>
...

SPRING_SECURITY_LAST_EXCEPTION : Spring Security에 의해 마지막에 발생된 예외가 세션의 속성값으로 저장된 세션의 속성명

 

 

 

 

Srping Security의 메세지를 가지고 오는것도 가능하다.

 

 

security_message.properties

AbstractUserDetailsAuthenticationProvider.badCredentials = \uC544\uC774\uB514 \uB610\uB294 \uBE44\uBC00\uBC88\uD638\uAC00 \uB9DE\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. \uD655\uC778\uD574 \uC8FC\uC138\uC694.
AbstractUserDetailsAuthenticationProvider.credentialsExpired = \uBE44\uBC00\uBC88\uD638\uC758 \uC720\uD6A8\uAE30\uAC04\uC774 \uB9CC\uB8CC \uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uAD00\uB9AC\uC790\uC5D0\uAC8C \uBB38\uC758\uD574 \uC8FC\uC138\uC694.
AbstractUserDetailsAuthenticationProvider.disabled = \uACC4\uC815\uC774 \uBE44\uD65C\uC131\uD654 \uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uAD00\uB9AC\uC790\uC5D0\uAC8C \uBB38\uC758\uD574 \uC8FC\uC138\uC694.
AbstractUserDetailsAuthenticationProvider.expired = \uACC4\uC815\uC774 \uB9CC\uB8CC \uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uAD00\uB9AC\uC790\uC5D0\uAC8C \uBB38\uC758\uD574 \uC8FC\uC138\uC694.
AbstractUserDetailsAuthenticationProvider.locked = \uACC4\uC815\uC774 \uC7A0\uACA8 \uC788\uC2B5\uB2C8\uB2E4. \uAD00\uB9AC\uC790\uC5D0\uAC8C \uBB38\uC758\uD574 \uC8FC\uC138\uC694.

 

 

root-context.xml 

    <bean class="org.springframework.context.support.ReloadableResourceBundleMessageSource" id="messageSource">
		<property name="basenames">
			<list>
				<value>/WEB-INF/message/security_message</value>
			</list>
		</property>
		<property name="cacheSeconds" value="60"/>
		<property name="defaultEncoding" value="utf-8"/>
	</bean>

 

login_page.jsp

<c:if test="${not empty SPRING_SECURITY_LAST_EXCEPTION}">
	<h3 style="color: red;">${sessionScope.SPRING_SECURITY_LAST_EXCEPTION.message }</h3>
	<!-- 예외가 저장된 세션의 속성값 삭제 -->
	<c:remove var="SPRING_SECURITY_LAST_EXCEPTION"/>
</c:if>

 


 

AccessDeniedException이 발생된 경우 access-denied-handler 엘리먼트를 이용해 403 에러코드 대신 응답 결과를 제공할 수 있다.

 

 

security-context.xml

<access-denied-handler error-page="/accessDenied"/>

 

LoginController

@RequestMapping(value = "/accessDenied", method = RequestMethod.GET)
public String accessDenied() {
	return "access_denied";
}

 

access_denied.jsp

<body>
	<h1>에러페이지</h1>
	<hr>
	<h3 style="color: red;">권한이 없어 페이지에 접근 불가능합니다. 관리자에게 문의해 주세요.</h3>
</body>

 


 

커스터마이징

 

단순한 페이지 이동 뿐만 아니라, 접근 제한에 대한 다양한 처리를 위해서 AccessDeniedHandler 인터페이스를 상속받은 자식클래스를 이용할 수 있다. 

//접근이 제한된 페이지를 요청한 경우 실행될 기능을 제공하기 위한 클래스
// => AccessDeniedHandler 인터페이스를 상속 받아 작성
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
	@Override
	public void handle(HttpServletRequest request, HttpServletResponse response,
			AccessDeniedException accessDeniedException) throws IOException, ServletException {
		//접근 제한에 대한 명령 실행 - 계정 잠금 기능 활성화 등의 명령 작성
		response.sendRedirect(request.getContextPath()+"/accessDenied");
	}
}

 

Spring Security 관련 클래스를 Spring Bean으로 등록해준다.

 

security-context.xml

<beans:bean class="xyz.itwill.security.CustomAccessDeniedHandler" id="customAccessDeniedHandler"/>

 

ref 속성에는 AccessDeniedHandler 인터페이스를 상속받은 클래스에 대한 Spring Bean의 식별자(beanName)를 속성값으로 설정해준다.

<access-denied-handler ref="customAccessDeniedHandler"/>

 


 

Spring Security는 인증 성공 후 기본적으로 SaveRequestAwareAuthenticationSuccessHandler 클래스를 이용하여 사용자가 원래 요청한 페이지의 정보를 유지하여 요청 페이지로 이동되도록 처리한다.

 

public class CustomLoginSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {}

 

 

내가 원하는대로 커스터마이징한 핸들러 클래스를 작성하기 위해서는 AuthenticationSuccessHandler 인터페이스를 상속받아서 작성하면 된다.

 

 

로그인이 성공했을 때 동작하는 핸들러 클래스 만들기

 

로그인 계정의 권한을 확인하여 특정 페이지를 무조건 요청되도록 설정해보자.

 

 

CustomLoginSuccessHandler

//로그인 성공 후에 실행될 기능을 제공하기 위한 클래스
// => AuthenticationSuccessHandler 인터페이스를 상속받아 작성
// => 사용자의 마지막 로그인 날짜를 변경 처리하는 기능 구현
public class CustomLoginSuccessHandler implements AuthenticationSuccessHandler {

	@Override
	//로그인 계정의 권한을 확인하여 특정 페이지를 무조건 요청되도록 설정
	//Authentication 객체 : 인증 및 인가(권한)와 관련된 모든 정보를 저장하기 위한 객체
	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
			Authentication authentication) throws IOException, ServletException {
		//로그인 사용자의 권한이 저장되기 위한 List 객체 생성
		List<String> roleNames=new ArrayList<String>();
		
		//Authentication.getAuthorities() : 인증된 계정된 모든 권한(GrantedAuthority 객체)을 
		//List 객체로 반환하는 메소드
		//GrantedAuthority 객체 : 사용자에게 부여된 권한에 대한 정보를 저장한 객체
		for(GrantedAuthority authority : authentication.getAuthorities()) {
			//GrantedAuthority.getAuthority() : GrantedAuthority 객체에 저장된 권한을 반환하는 메소드 
			roleNames.add(authority.getAuthority());
		}
		
		//Collection<T>.contains(List 객체에 저장된 요소를 비교
		if(roleNames.contains("ROLE_ADMIN")) {
			response.sendRedirect(request.getContextPath()+"/admin/page");	
			return;
		}
		
		if(roleNames.contains("ROLE_MANAGER")) {
			response.sendRedirect(request.getContextPath()+"/manager/page");	
			return;
		}
		
		if(roleNames.contains("ROLE_USER")) {
			response.sendRedirect(request.getContextPath()+"/user/page");	
			return;
		}
		
		response.sendRedirect(request.getContextPath()+"/guest/page");
	}

}

 

 

security-context.xml

 

 

authentication-success-handler-ref 속성

: AuthenticationSuccessHandler 인터페이스를 상속받은 클래스에 대한 Spring Bean의 식별자(beanName)를 속성값으로 설정

<form-login login-page="/loginPage" login-processing-url="/loginPage" 
	username-parameter="userid" password-parameter="passwd" default-target-url="/"
	authentication-success-handler-ref="customLoginSuccesshandler"
/>
...

<!-- Spring Security 관련 클래스를 Spring Bean으로 등록 -->
<beans:bean class="xyz.itwill.security.CustomLoginSuccessHandler" id="customLoginSuccesshandler"/>

 


CustomLoginFailuerHandler

//로그인 성공 후에 실행될 기능을 제공하기 위한 클래스
//=> 로그인 실패 횟수 누적 등의 기능을 구현
public class CustomLoginFailuerHandler extends SimpleUrlAuthenticationFailureHandler {
	@Override
	public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException exception) throws IOException, ServletException {
		request.getSession().setAttribute("userid", request.getParameter("userid"));
		setDefaultFailureUrl("/loginPage");//로그인 실패시 로그인 페이지로 
		super.onAuthenticationFailure(request, response, exception);
	}
}

 

 

security-context.xml

 

authentication-failure-handler-ref 속성 : AuthenticationFailuerHandler 인터페이스를 상속받은 클래스에 대한 Spring Bean의 식별자(beanName)를 속성값으로 설정

<form-login login-page="/loginPage" login-processing-url="/loginPage" 
	username-parameter="userid" password-parameter="passwd" default-target-url="/"
	...
	authentication-failure-handler-ref="customLoginFailuerHandler"
/>
...
<beans:bean class="xyz.itwill.security.CustomLoginFailuerHandler" id="customLoginFailuerHandler"/>

 


로그아웃 구현

 

home.jsp

 

 

로그아웃할 때는 무조건 form 태그를 이용해서 post 방식으로 토큰을 전달해주어야 한다.

=> CSRF 토큰을 전달하여 처리되도록 설정해야 하기 때문

	<!-- 인증된 사용자인 경우 태그가 포함되도록 설정 -->
	<sec:authorize access="isAuthenticated">
		<!-- 로그아웃 처리 기능을 제공하는 페이지는 반드시 form 태그를 사용하여 요청해야 한다. 
			=> CSRF 토큰을 전달하여 처리되도록 설정해야 하기 때문 -->
		<form action="<c:url value="/logout"/>" method="post">
			<sec:csrfInput/>
			<button type="submit">로그아웃</button>
		</form>
	</sec:authorize>

 

 

security-context.xml

<logout logout-url="/logout" logout-success-url="/" invalidate-session="true" delete-cookies="JSESSIONID"/>

 

logout : 로그아웃 처리 기능을 제공하는 엘리먼트

  • logout-url 속성 : 로그아웃 처리를 위해 요청하기 위한 URL 주소를 속성값으로 설정
  • logout-success-url 속성 : 로그아웃 처리 후 요청할 페이지의 URL 주소를 속성값으로 설정
  • invalidate-session 속성 : 세션을 무효화 처리하기 위한 논리값(false 또는 true)을 속성값으로 설정
  • delete-cookies 속성 : 삭제할 쿠키에 대한 이름을 속성값으로 설정 

 

 

 

authentication 태그 : 인증된 사용자에 대한 정보를 제공하기 위한 태그

=> 인증된 사용자에게만 필요한 정보를 제공 가능 

 

property 속성 : 인증 사용자(UserDetails 객체)에 정보를 제공받기 위한 속성명(필드명)을 속성값으로 설정 

  • principal 속성값 : UserDetails 인터페이스를 상속받은 User 객체가 저장된 속성
    • User 객체의 username 필드 : 사용자의 식별자(아이디)가 저장된 필드
    • User 객체의 accountNonExpired 필드 : 사용자의 유효날짜 관련 논리값이 저장된 필드 
    • User 객체의 AccountNonLocked 필드 : 사용자 잠금 관련 논리값이 저장된 필드 
    • User 객체의 grantedAuthorities 필드 : 사용자의 권한 정보를 요소로 가진 List 객체가 저장된 필드 
    • User 객체의 credentialsNonExpired 필드 : 비밀번호의 유효날짜 관련 논리값이 저장된 필드 
    • User 객체의 enabled 필드 : 사용자의 활성 관련 논리값이 저장된 필드

 

home.jsp

<sec:authorize access="isAuthenticated()">
	<h3><sec:authentication property="principal.username"/></h3>
</sec:authorize>