스프링 시큐리티에서 success & failuare handler (이하 handler) 의 경우 AuthenticationProvider
에서 인증에 대하여 성공/실패의 유무에 대한 액션을 취한다.
공식적인 설명은 아래와 같다.
- 성공적인 사용자 인증을 처리하는 데 사용되는 전략입니다.
- 구현은 원하는 대로 할 수 있지만 일반적인 동작은 제어하는 것입니다.
- 후속 목적지로의 탐색 (리디렉션 또는 포워드 사용)
예를 들어, 사용자가 로그인 양식을 제출하여 로그인한 후 애플리케이션에서 결정해야 합니다.
일단 우리가 로그인을 성공했을때 예를들어 유저의 DB에서 count 를 치거나 아니면 유저가 로그인 시간을 log 에 남긴다는 요구사항이 나오면 아래의 핸들러에서 핸들링 하면 된다.
onAuthenticationSuccess
메소드에서 첫번째 메소드의 경우 사용자가 성공적으로 인증되면 호출됩니다
@Component(value = "authenticationSuccessHandler")
public class DomainSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException {
// 유저 성공 로직을 추가 해준다.
AuthenticationSuccessHandler.super.onAuthenticationSuccess(request, response, chain, authentication);
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
}
}
공식적인 설명의 경우 아래와 같습니다
- 실패한 인증 시도를 처리하는 데 사용되는 전략입니다.
- 일반적인 동작은 사용자를 인증 페이지로 리디렉션하는 것일 수 있습니다(이 경우 양식 로그인)을 사용하여 다시 시도할 수 있습니다. 더 정교한 논리는
- 예외 유형에 따라 구현됩니다.
로그인 실패 핸들러의 경우 onAuthenticationFailure
메소드로 인증 시도가 실패할 때 호출됩니다. 메소드의 인자에서 AuthenticationException
예외의 종류로는 아래와 같습니다.
따로 메소드를 만들어서 getExceptionMessage
라는 메소드를 만들어서 예외를 체크해서 메세지를 만들어 줍니다.
writePrintErrorResponse
의 메소드는 응답객체에 메세지를 만들기 위해서 작성된 메소드 입니다.
여기서 핸들링할때 성공 핸들링과 동일하게 로그를 남겨도되고 db작업이나 로그인 실패시 행위를 핸들링하면 됩니다.
@Component(value = "authenticationFailureHandler")
public class DomainFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
// 실패로직 핸들링
exception.printStackTrace();
writePrintErrorResponse(response, exception);
}
private void writePrintErrorResponse(HttpServletResponse response, AuthenticationException exception) {
try {
ObjectMapper objectMapper = new ObjectMapper();
Map<String, Object> responseMap = new HashMap<>();
String message = getExceptionMessage(exception);
responseMap.put("status", 401);
responseMap.put("message", message);
response.getOutputStream().println(objectMapper.writeValueAsString(responseMap));
} catch (IOException e) {
e.printStackTrace();
}
}
private String getExceptionMessage(AuthenticationException exception) {
if (exception instanceof BadCredentialsException) {
return "비밀번호불일치";
} else if (exception instanceof UsernameNotFoundException) {
return "계정없음";
} else if (exception instanceof AccountExpiredException) {
return "계정만료";
} else if (exception instanceof CredentialsExpiredException) {
return "비밀번호만료";
} else if (exception instanceof DisabledException) {
return "계정비활성화";
} else if (exception instanceof LockedException) {
return "계정잠김";
} else {
return "확인된 에러가 없습니다.";
}
}
}
위의 getExceptionMessage
의 경우 if 절이 너무 많이 사용하는 것 같아서
로직을 변경 하였다.
enum
으로 처리하려고 생각을 하였다.
일단 enum을 생성을 하였다.
public enum AuthenticationTypes implements BaseEnumCode<String> {
BadCredentialsException("비밀번호불일치", "비밀번호불일치"),
UsernameNotFoundException("계정없음", "계정없음"),
AccountExpiredException("계정만료", "계정만료"),
CredentialsExpiredException("비밀번호만료", "비밀번호만료"),
DisabledException("계정비활성화", "계정비활성화"),
LockedException("계정잠김", "계정잠김"),
NoneException("알수없는 에러", "알 수 없는 에러 입니다.");
private String desc;
private String value;
AuthenticationTypes(String desc, String value) {
this.desc = desc;
this.value = value;
}
@Override
public String getValue() {
return this.value;
}
private static final Map<String, AuthenticationTypes> descriptions = Collections.unmodifiableMap(Stream.of(values()).collect(Collectors.toMap(AuthenticationTypes::name, Function.identity())));
public static AuthenticationTypes findOf(String findValue) {
return Optional.ofNullable(descriptions.get(findValue)).orElse(NoneException);
}
}
위의 코드 처럼 enum을 작성을 하였다.
value 의 경우 excetpin에서 내려줄 메세지라고 생각하면 된다.
그리고 desc 의 경우 단순한 설명이다.
그리고 getExceptionMessage
의 경우 아래의 코드 처럼 변경을 하였다.
private String getExceptionMessage(AuthenticationException exception) {
AuthenticationTypes authenticationTypes = AuthenticationTypes.findOf(exception.getClass().getSimpleName());
return authenticationTypes.getValue();
}
exception.getClass().getSimpleName()
의 경우 디버깅 해서 확인했더니 단순한 클래스 이름을 string
으로 변환해서 내려준다.
그렇기 때문에 enum에서 name 값과 비교하여서 만들어 주면 될거 같다라는 생각을 하였다.
여기서 findOf
메소드의 경우 Map 을 통하여 값을 enum 타입에 대한 값을 찾아준다
optional 을 통하여 null 일시 알 수 없는 에러 (NoneException)로 정의해서 내려준다.
시큐리티 설정 java 파일에서는 config 메소드에서 HttpSecurity
객체에 아래와 같이 설정을 해주면 빈을 전역변수로 authenticationSuccessHandler,authenticationFailureHandler를 선언(빈생성)을 해주었다면 @Component
어노테이션에서 설정을 해주었기 때문에 자동적으로 custom handler 가 자동으로 빈 등록을 시켜준다.
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/**")
.permitAll()
.and()
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/login-process")
.successHandler(authenticationSuccessHandler)
.failureHandler(authenticationFailureHandler)
.permitAll()
;
}
Note:
- 여기서 만약에
defaultSuccessUrl
을 설정을 해준다면successHandler
가 불러오지 못합니다.- 그렇기 때문에
defaultSuccessUrl
를 세팅 해주지말구 하시면 성공적 핸들링을 사용이 가능합니다
로그인 페이지를 커스텀 하기 위해서는 시큐리티 설정 java 파일에서 아래와 같이 설정을 해줍니다.
http.formLogin()
.loginPage("/login")
.loginProcessingUrl("/login-progress")
.successHandler(authenticationSuccessHandler)
.failureHandler(authenticationFailureHandler)
.permitAll();
loginPage 라는 항목이 있는데 시큐리티에서 제공되는 폼이 아니라 개발자가 만는 폼을 매핑 시켜줄 수 있습니다
그렇게 html 파일을 매핑을 해줄 수 있는데
아래의 화면이 보여집니다. 여기서 webconfiguration 에서 뷰를 매핑을 시켜주는 방법과 controller에서 매핑을 해주는 방법중 선택해서 매핑을 시켜줍니다
그 후에 타임리프를 사용을 하기 때문에 csrf 설정을 넣어준 후 success 되는 것을 볼 수 있을 것입니다.
단, 폼에서 loginProcessingUrl
action 값을 여기 설정되어있는 url 로 설정 하시면 됩니다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Login</title>
</head>
<body>
<form action='/login-progress' method='POST'>
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
<p>
<label for="username" class="sr-only">Username</label>
<input type="text" id="username" name="username" class="form-control" placeholder="Username" required autofocus>
</p>
<p>
<label for="password" class="sr-only">Password</label>
<input type="password" id="password" name="password" class="form-control" placeholder="Password" required>
</p>
<button class="btn btn-lg btn-primary btn-block" type="submit">Login</button>
</form>
</body>
</html>