로그인 실패를 하게 되면 “loginURL?error”라는 url로 바뀔 뿐, 사용자 입장에서는 무엇이 일어났는지 아무것도 알 수 없다.
따라서 로그인을 실패할 경우, 아래 html(mustache)처럼 실패 이유를 출력해주고 싶다.
...
<div class="loginFail">
{{#error}}
{{exception}}
{{/error}}
</div>
...
로그인 실패와 관련된 예외들(비밀번호가 맞지 않는다던지...)들은 WebSecurityConfigurerAdapter 상속 클래스의 configure(HttpSecurity http) 메서드에 failureHandler()를 추가해야 예외를 캐치해낼 수 있다.
이 failureHandler() 메서드는 파라미터로 AuthenticationFailureHandler
인터페이스 구현체를 받는다. LoginFailHandler
를 빈으로 주입하고 있는데, 이는 AuthenticationFailureHandler
를 구현한 커스텀 클래스다.
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeRequests()
....
.and()
....
.failureHandler(loginFailHandler())//로그인 실패 시 처리하는 핸들러 등록.
.and()
...
}
@Bean
public LoginFailHandler loginFailHandler(){
return new LoginFailHandler();
}
}
LoginFailHandler
는 로그인 실패를 핸들링하는 커스텀 핸들러다. 위에서 언급한대로 AuthenticationFailureHandler
를 implements해야 한다.
그런데 SimpleUrlAuthenticationFailureHandler
를 extends하면 AuthenticationFailureHandler
를 implements 한 것과 같게 된다.
그 이유는 SimpleUrlAuthenticationFailureHandler
가 해당 인터페이스를 구현한 것이기 때문에, 이를 상속하면 자동으로 해당 인터페이스를 구현한 것이 된다.
그렇다면 굳이 SimpleUrlAuthenticationFailureHandler
를 사용한 이유는 무엇일까?
바로 해당 클래스가 제공해주는 setDefaultFailureUrl() 메서드를 활용하기 위해서이다. 해당 메서드를 활용하면 로그인 실패 시 url을 지정해준다.
단, 해당 url은 컨트롤러에 매핑되어 있어야 된다.
public class LoginFailHandler extends SimpleUrlAuthenticationFailureHandler {
@Override
//httpServletRequest -> request 에 대한 정보 , httpServletResponse -> response 에 대해 설정할 수 있는 변수
//AuthenticationException e -> 로그인 실패 시 예외에 대한 정보를 담고 있음.
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
logger.info("login fail handler");
String errorMessage;
if (e instanceof BadCredentialsException || e instanceof InternalAuthenticationServiceException){
errorMessage="아이디 또는 비밀번호가 맞지 않습니다.";
}else if (e instanceof UsernameNotFoundException){
errorMessage="존재하지 않는 아이디 입니다.";
}
else{
errorMessage="알 수 없는 이유로 로그인이 안되고 있습니다.";
}
errorMessage= URLEncoder.encode(errorMessage,"UTF-8");//한글 인코딩 깨지는 문제 방지
setDefaultFailureUrl("/loginForm?error=true&exception=" + errorMessage);
super.onAuthenticationFailure(httpServletRequest,httpServletResponse,e);
}
}
setDefaultFailureUrl()이 호출되었을 때 지정되있는 url을 매핑해줄 컨트롤러 코드가 필요하다.
해당 코드에는 RequestParam 으로 넘겨진 error와 exeption을 모델에 담은 후, 뷰 리졸브해주고 있다.
@Controller
public class LoginController {
@GetMapping("/loginForm")
public String loginForm(@RequestParam(value = "error", required = false) String error,
@RequestParam(value = "exception", required = false) String exception, Model model) {
model.addAttribute("error", error);
model.addAttribute("exception", exception);
logger.info("loginForm view resolve");
return "/login/loginForm";
}
}
SimpleUrlAuthenticationFailureHandler 참고
https://docs.spring.io/spring-security/site/docs/4.2.6.RELEASE/apidocs/org/springframework/security/web/authentication/SimpleUrlAuthenticationFailureHandler.html