▸Spring Security

스프링 Security_로그인_로그인 실패 대응 로직 [3/9]

코데방 2020. 3. 28.
728x90
- Develop OS : Windows10 Ent, 64bit
- WEB/WAS Server : Tomcat v9.0
- DBMS : MySQL 5.7.29 for Linux (Docker)
- Language : JAVA 1.8 (JDK 1.8)
- Framwork : Spring 3.1.1 Release
- Build Tool : Maven 3.6.3
- ORM : Mybatis 3.2.8

 

커스터마이징 순서대로 총 9개의 포스팅으로 나눠져 있습니다. 순서대로 보면 쉽게 적용할 수 있습니다.

 

[Spring MVC/- 기본 문법] - 스프링 Security_로그인_기본 컨텍스트 설정 [1/9]

[Spring MVC/- 기본 문법] - 스프링 Security_로그인_DB 연동 로직 작성 [2/9]

[Spring MVC/- 기본 문법] - 스프링 Security_로그인_로그인 실패 대응 로직 [3/9]

[Spring MVC/- 기본 문법] - 스프링 Security_로그인_로그인 성공 대응 로직 [4/9]

[Spring MVC/- 기본 문법] - 스프링 Security_로그인_DB 패스워드 암호화 [5/9]

[Spring MVC/- 기본 문법] - 스프링 Security_로그인_암호화된 DB 패스워드로 인증 [6/9]

[Spring MVC/- 기본 문법] - 스프링 Security_로그인_Principal 객체 [7/9]

[Spring MVC/- 기본 문법] - 스프링 Security_로그인_자동 로그인(Remember-me) [8/9]

[Spring MVC/- 기본 문법] - 스프링 Security_로그인_security 태그 라이브러리 [9/9]

 

 

붉은색 네모가 이번 포스팅에서 커스터마이징할 곳

 

 

 

저번 글까지 스프링의 기본적인 인증 로직 구현을 완료했습니다. 가장 중요한 뼈대가 완성되었으므로 이번 글부터는 계속해서 부가 기능들을 추가해나가도록 하겠습니다.

 

이번 글에서 다룰 내용은 로그인 실패에 대한 대응 로직입니다. 가장 기본이 되는 대표적인 로직은 "왜 로그인에 실패했는지 사용자에게 알려주는 것" 입니다.

 

 


 

[ 디폴트 설정일 경우 ]

 

별다른 설정을 하지 않았다면 로그인 실패 시 아래 설정에 등록해둔 URL을 호출할 것입니다. 보통 다시 로그인 화면으로 보내 다시 로그인을 시도하게끔 합니다.

 

* authentication-failure-url="/loginView"

		<!-- 로그인 설정 -->
		<s:form-login	
			username-parameter="userId" 
			password-parameter="userPw"
			login-processing-url="/loginAsk" 
			login-page="/loginView" default-target-url="/"
			authentication-failure-url="/loginView" />

 

 

이 때 로그인 화면에서는 왜 로그인에 실패했는지를 알려줘야 합니다. 개발자가 아무것도 따로 만들지 않았을 때 스프링 Security는 인증 실패 시 발생하는 예외 종류에 따라 만들어진 객체를 Session의 Attribute로 담아줍니다. 해당 객체에서 messege를 꺼내오면 메세지를 받을 수 있습니다. 아래와 같이 JSP 페이지에서 해당 메세지를 꺼내오면 됩니다. 

 

		<!-- 로그인 실패 시 출력할 메세지 -->
		${sessionScope["SPRING_SECURITY_LAST_EXCEPTION"].message}

 

 

위와 같이 세션에 메세지를 담아 View에 전달하는 방법은 그리 좋지 않습니다. 요청 한번에 만들어졌다 소멸되는 request 객체와 달리, session 객체는 일정한 기간 동안 계속 메모리에 상주하고 있기 때문에 로그인 화면 View에서 메세지를 출력한 뒤 매번 Session Attribute의 객체를 지워줘야 합니다.

 

또한 메세지 외에 로그인 실패에 따른 여러 로직을 처리해야 한다면 문제가 복잡해집니다. 예를 들어 인증이 5번 틀리면 계정이 잠긴다거나 하는 로직을 들 수 있습니다. 이 경우도 JSP에서 로직을 좀 넣어주면 충분히 해결이 가능하긴 하지만 MVC 패턴에서 View에 로직을 넣는 것은 최대한 지양하는 것이 좋습니다. 

 

그리고 무엇보다 스프링 Security에서 대부분 기능들을 커스터마이징해서 처리할 수 있는 방법을 충분히 제공하고 있으므로, 깔끔하게 프레임워크를 이용해 로직을 구현하는 것이 스마트한 방법이 아닐까 합니다. 그래서 어차피 디폴트로 지정된 방법은 딱히 사용하지 않을 것이므로 더 이상의 설명은 생략하겠습니다. ㅎㅎ

 

 


 

 

 

[ 로그인 실패 시 대응 로직 커스터마이징 ]

 

이전 글에서는 UserDetailsService 인터페이스를 상속받은 클래스의 Bean 객체를 authentication-provider의 레퍼런스로 DI(의존성 주입)해서, 우리가 원하는 인증 DB의 데이터를 스프링 Security에게 전달해줬습니다. 아래 설정 부분입니다. DI를 하면 직접 작성한 로직으로, 생략하면 디폴트로 지정된 로직으로 동작했었죠.

	<!-- DB 연동 설정 -->
	<s:authentication-manager>
		<s:authentication-provider user-service-ref="userLoginService">
		</s:authentication-provider>
	</s:authentication-manager>

 

 

다른 커스터마이징 방식도 대부분 동일합니다. 본래 있던 디폴트 클래스를 대체할 클래스를 작성한 뒤, 적절한 부분에 레퍼런스로 DI를 해서 바꿔치기 해주면 됩니다. 약속된 실행 메소드는 인터페이스에서 미리 정의해두기 때문에, 우리는 필요한 기능의 인터페이스를 상속받아 각자 입맛에 맞게 오버라이딩해서 리턴값만 제대로 보내주면 됩니다.

 

 

* AuthenticationFailureHandler : 로그인 실패 처리 핸들러가 상속받는 인터페이스

 

로그인 실패를 처리하는 디폴트 클래스는 위의 인터페이스를 구현해 만들어진 "Exception어쩌고"하는 이름도 아주 긴 클래스입니다. 로그인에 실패하면 이 녀석이 예외 객체를 넘겨받아 세션에다가 메세지를 남기고 다시 지정된 페이지로 리다이렉트 시켜줍니다. 

 

따라서 우리는 이 녀석을 대체할 클래스를 하나 만들어서 컨텍스트 설정에 DI만 해주면 됩니다. 같은 인터페이스를 상속받아 메소드를 오버라이딩 해주면 된다는 의미입니다. 

 

 

 

오버라이딩 해야 하는 메소드는 "request, response와 에러 내용을 넘겨줄테니까 알아서 처리해" 라는 의미입니다. 일반적으로는 필요한 로직을 처리하고 request Attribute에 메세지를 담은 뒤, 최종적으로 다시 로그인 페이지로 포워딩 시켜줍니다. 또는 다른 로직으로 맘대로 구현해도 무방합니다.

 

인증 실패 시 인증을 담당하는 Provider가 던진 예외를 Fail Handler에게 전달해주는데, 이 부분은 Provider를 커스터마이징하면 어떤 상황에서 어떤 예외를 던져줄지 직접 결정해야하므로 어떤 종류가 있는지만 대략적으로 보고 넘어가면 될 것 같습니다.

 

Fail Handler가 받는 예외 디폴트 Provider가 리턴하는 결과 (커스터마이징 가능)
AuthenticationServiceException - null 값을 리턴
BadCredentialException

- UsernameNotFoundException 예외를 throw

- UserDetails 객체를 리턴했으나, 아이디 또는 비밀번호가 틀림

LockedException - UserDetails 객체의 isAccountNonLocked() 메소드의 리턴값이 false
DisabledException - UserDetails 객체의 isEnabled() 메소드의 리턴값이 false
AccountExpiredException - UserDetails 객체의 isAccountNonExpired() 메소드의 리턴값이 false
CredentialExpiredException - UserDetails 객체의 isCredentialsNonExpired() 메소드의 리턴값이 false

 

 


 

 

[ Fail Handler 클래스 작성 ]

 

사족이 좀 길었는데 이제 원리를 이해했다면 로그인 실패를 처리할 클래스를 작성만 하면 됩니다. 먼저 인터페이스를 상속받아줍니다.

 

package hs.spring.hsweb.service.user;

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;
import org.springframework.stereotype.Service;

@Service
/* 로그인 실패 대응 로직 */
public class UserLoginFailHandler implements AuthenticationFailureHandler {

	@Override
	public void onAuthenticationFailure(HttpServletRequest request,
			HttpServletResponse response, AuthenticationException exception)
			throws IOException, ServletException {

	}
}

 

 

 

그리고 필요한 로직을 입맛대로 요리해주면 됩니다. 아래와 같은 방식으로 로그인 실패의 종류를 확인한 뒤 처리해줍니다. 나중에 Provider를 직접 커스터마이징하면 거기서 던져주는 예외와 같이 맞춰주면 됩니다.

 

		<!-- 로그인 실패 시 출력할 메세지 -->
		${requestScope.loginFailMsg}
package hs.spring.hsweb.service.user;

import java.io.IOException;

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

import org.springframework.security.authentication.AccountExpiredException;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Service;

@Service
/* 로그인 실패 대응 로직 */
public class UserLoginFailHandler implements AuthenticationFailureHandler {

	@Override
	public void onAuthenticationFailure(HttpServletRequest request,
			HttpServletResponse response, AuthenticationException exception)
			throws IOException, ServletException {
		
		
		if (exception instanceof AuthenticationServiceException) {
			request.setAttribute("loginFailMsg", "존재하지 않는 사용자입니다.");
		
		} else if(exception instanceof BadCredentialsException) {
			request.setAttribute("loginFailMsg", "아이디 또는 비밀번호가 틀립니다.");
			
		} else if(exception instanceof LockedException) {
			request.setAttribute("loginFailMsg", "잠긴 계정입니다..");
			
		} else if(exception instanceof DisabledException) {
			request.setAttribute("loginFailMsg", "비활성화된 계정입니다..");
			
		} else if(exception instanceof AccountExpiredException) {
			request.setAttribute("loginFailMsg", "만료된 계정입니다..");
			
		} else if(exception instanceof CredentialsExpiredException) {
			request.setAttribute("loginFailMsg", "비밀번호가 만료되었습니다.");
		}
		
		// 로그인 페이지로 다시 포워딩
		RequestDispatcher dispatcher = request.getRequestDispatcher("/loginView");
		dispatcher.forward(request, response);
	}
}

 

 

 


 

 

 

[ 컨텍스트 설정 - 디폴트로 등록된 Fail Handler 바꿔치기 ]

 

이제 모든 준비가 완료되었으므로 컨텍스트 설정 파일만 수정해주면 됩니다. 위에서 말했듯이 디폴트로 설정된 녀석말고 우리가 만든 클래스의 Bean 객체를 의존 주입해서 바꿔주는 부분입니다.

 

로그인 설정 부분을 아래와 같이 바꿔줍니다.

 

* 기존 설정 삭제 : authentication-failure-url="/loginView" 

* 추가 설정 부여 : authentication-failure-handler-ref="핸들러 클래스의 Bean ID"

 

		<!-- 로그인 설정 -->
		<s:form-login	
			username-parameter="userId" 
			password-parameter="userPw"
			login-processing-url="/loginAsk" 
			login-page="/loginView" default-target-url="/"

			authentication-failure-handler-ref="userLoginFailHandler" />
		
		<!-- 설정 제거 -->
		<!-- authentication-failure-url="/loginView" /> -->

 

 

위 설정을 통해 인증 실패의 경우 핸들러로 지정된 Bean 객체에 처리를 요청하도록 합니다. @Service 어노테이션을 클래스에 붙여줬으므로 클래스는 자동으로 루트 컨테이너의 Bean으로 등록되며, 이름을 따로 지정해주지 않았다면 클래스명에서 앞글자만 소문자로 써주면 됩니다. 

 

이것으로 로그인 실패 대응 로직은 완료되었습니다.

728x90

댓글

💲 추천 글